//! File manifests. //! //! This crate defines the [`ManifestEntry`] struct to represent an //! entry in a file manifest. The entry contains data about each file. //! A manifest can be produced of some data, and later, a new manifest //! can be produced and compared with the original. If the manifests //! have changed, the data has changed. If they haven't changed, the //! data has probably not changed. //! //! Such manifests can be used, for example, to verify that data that //! has been restored from a backup is the same as what was backed up. //! //! This crate uses Linux metadata of files. //! //! FIXME: the example needs to be fixed. //! //! ~~~ignore //! let m = std::fs::metadata(".").unwrap(); //! let e = summain::ManifestEntry(m); //! println!("{}", serde_yaml::to_string(e).unwrap()); //! ~~~ //! //! The output is something like: //! //! ~~~yaml //! path: "." //! mode: drwxrwxr-x //! mtime: 1606565867 //! mtime_nsec: 500355545 //! nlink: 6 //! size: ~ //! ~~~ use serde::Serialize; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::{BufReader, Read}; use std::os::linux::fs::MetadataExt; use std::path::{Path, PathBuf}; use tokio::fs::{read_link, symlink_metadata}; const BUF_SIZE: usize = 1024 * 1024; /// An entry in a file manifest. #[derive(Serialize, Debug)] pub struct ManifestEntry { #[serde(skip)] is_regular: bool, // Store the original name in a hidden field, for compute_checksum. #[serde(skip)] filename: PathBuf, // We store pathname as a string so that we can handle non-UTF8 names. path: String, #[serde(with = "mode")] mode: u32, mtime: i64, mtime_nsec: i64, nlink: u64, size: Option, sha256: Option, target: Option, } impl ManifestEntry { /// Create a new manifest entry. /// /// The pathname of the file and the metadata are passed in by the /// caller. This function doesn't query the system for it. /// /// The structure can be serialized using serde. pub async fn new(path: &Path) -> std::io::Result { let m = symlink_metadata(path).await?; let target = if m.file_type().is_symlink() { Some(read_link(path).await?) } else { None }; Ok(Self { is_regular: m.is_file(), filename: path.to_path_buf(), path: path.to_string_lossy().into_owned(), mode: m.st_mode(), mtime: m.st_mtime(), mtime_nsec: m.st_mtime_nsec(), nlink: m.st_nlink(), size: if m.is_dir() { None } else { Some(m.st_size()) }, sha256: None, target, }) } pub fn compute_checksum(&mut self) -> std::io::Result<()> { if self.is_regular { self.sha256 = Some(file_checksum(&self.filename)?); } Ok(()) } } fn file_checksum(path: &Path) -> std::io::Result { let mut hasher = Sha256::new(); let file = File::open(path)?; let mut reader = BufReader::new(file); loop { let mut buf = vec![0; BUF_SIZE]; let n = reader.read(&mut buf)?; if n == 0 { break; } hasher.update(&buf[..n]); } let hash = hasher.finalize(); Ok(format!("{:x}", hash)) } mod mode { use serde::{self, Serializer}; pub fn serialize(mode: &u32, serializer: S) -> Result where S: Serializer, { let s = unix_mode::to_string(*mode); serializer.serialize_str(&s) } }