use std::fs::File as StdFile; use std::io::Write; use std::path::{Path, PathBuf}; use std::str::FromStr; use tempfile::NamedTempFile; use uuid::Uuid; const UPDATE_FILES_PATH: &str = "updates/updates_files"; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Could not parse file name as utf-8")] CouldNotParseFileNameAsUtf8, #[error(transparent)] IoError(#[from] std::io::Error), #[error(transparent)] PersistError(#[from] tempfile::PersistError), #[error(transparent)] UuidError(#[from] uuid::Error), } pub type Result = std::result::Result; #[derive(Clone, Debug)] pub struct FileStore { path: PathBuf, } impl FileStore { pub fn new(path: impl AsRef) -> Result { let path = path.as_ref().to_path_buf(); std::fs::create_dir_all(&path)?; Ok(FileStore { path }) } } impl FileStore { /// Creates a new temporary update file. /// A call to `persist` is needed to persist the file in the database. pub fn new_update(&self) -> Result<(Uuid, File)> { let file = NamedTempFile::new_in(&self.path)?; let uuid = Uuid::new_v4(); let path = self.path.join(uuid.to_string()); let update_file = File { file: Some(file), path }; Ok((uuid, update_file)) } /// Creates a new temporary update file with the given Uuid. /// A call to `persist` is needed to persist the file in the database. pub fn new_update_with_uuid(&self, uuid: u128) -> Result<(Uuid, File)> { let file = NamedTempFile::new_in(&self.path)?; let uuid = Uuid::from_u128(uuid); let path = self.path.join(uuid.to_string()); let update_file = File { file: Some(file), path }; Ok((uuid, update_file)) } /// Returns the file corresponding to the requested uuid. pub fn get_update(&self, uuid: Uuid) -> Result { let path = self.get_update_path(uuid); let file = match StdFile::open(path) { Ok(file) => file, Err(e) => { tracing::error!("Can't access update file {uuid}: {e}"); return Err(e.into()); } }; Ok(file) } /// Returns the path that correspond to this uuid, the path could not exists. pub fn get_update_path(&self, uuid: Uuid) -> PathBuf { self.path.join(uuid.to_string()) } /// Copies the content of the update file pointed to by `uuid` to the `dst` directory. pub fn snapshot(&self, uuid: Uuid, dst: impl AsRef) -> Result<()> { let src = self.path.join(uuid.to_string()); let mut dst = dst.as_ref().join(UPDATE_FILES_PATH); std::fs::create_dir_all(&dst)?; dst.push(uuid.to_string()); std::fs::copy(src, dst)?; Ok(()) } /// Compute the size of all the updates contained in the file store. pub fn compute_total_size(&self) -> Result { let mut total = 0; for uuid in self.all_uuids()? { total += self.compute_size(uuid?).unwrap_or_default(); } Ok(total) } /// Compute the size of one update pub fn compute_size(&self, uuid: Uuid) -> Result { Ok(self.get_update(uuid)?.metadata()?.len()) } pub fn delete(&self, uuid: Uuid) -> Result<()> { let path = self.path.join(uuid.to_string()); if let Err(e) = std::fs::remove_file(path) { tracing::error!("Can't delete file {uuid}: {e}"); Err(e.into()) } else { Ok(()) } } /// List the Uuids of the files in the FileStore pub fn all_uuids(&self) -> Result>> { Ok(self.path.read_dir()?.filter_map(|entry| { let file_name = match entry { Ok(entry) => entry.file_name(), Err(e) => return Some(Err(e.into())), }; let file_name = match file_name.to_str() { Some(file_name) => file_name, None => return Some(Err(Error::CouldNotParseFileNameAsUtf8)), }; if file_name.starts_with('.') { None } else { Some(Uuid::from_str(file_name).map_err(|e| e.into())) } })) } } pub struct File { path: PathBuf, file: Option, } impl File { pub fn dry_file() -> Result { Ok(Self { path: PathBuf::new(), file: None }) } pub fn persist(self) -> Result<()> { if let Some(file) = self.file { file.persist(&self.path)?; } Ok(()) } } impl Write for File { fn write(&mut self, buf: &[u8]) -> std::io::Result { if let Some(file) = self.file.as_mut() { file.write(buf) } else { Ok(buf.len()) } } fn flush(&mut self) -> std::io::Result<()> { if let Some(file) = self.file.as_mut() { file.flush() } else { Ok(()) } } } #[cfg(test)] mod test { use std::io::Write; use tempfile::TempDir; use super::*; #[test] fn all_uuids() { let dir = TempDir::new().unwrap(); let fs = FileStore::new(dir.path()).unwrap(); let (uuid, mut file) = fs.new_update().unwrap(); file.write_all(b"Hello world").unwrap(); file.persist().unwrap(); let all_uuids = fs.all_uuids().unwrap().collect::>>().unwrap(); assert_eq!(all_uuids, vec![uuid]); let (uuid2, file) = fs.new_update().unwrap(); let all_uuids = fs.all_uuids().unwrap().collect::>>().unwrap(); assert_eq!(all_uuids, vec![uuid]); file.persist().unwrap(); let mut all_uuids = fs.all_uuids().unwrap().collect::>>().unwrap(); all_uuids.sort(); let mut expected = vec![uuid, uuid2]; expected.sort(); assert_eq!(all_uuids, expected); } }