use anyhow::{anyhow, bail, Error, Result}; use std::{ fs::{create_dir_all, remove_file, rename, File}, path::{PathBuf, MAIN_SEPARATOR}, str::FromStr, thread, time::{Duration, SystemTime, UNIX_EPOCH}, }; const TMP_SUFFIX: &str = ".tmp"; pub struct Item { pub file: File, pub path: PathBuf, // pub id: u64, } impl Item { // Constructors /// Build new `Self` from URL (request) /// * e.g. 123 -> /directory/1/2/3 pub fn from_url(url: &str, directory: &str) -> Result { let s = match url.rfind('/') { Some(pos) => match url.get(pos + 1..) { Some(u) => u.parse::()?, None => bail!("Invalid request"), }, None => bail!("ID not found"), } .to_string(); let mut p = String::with_capacity(s.len()); for (i, d) in s.chars().enumerate() { if i > 0 { p.push(MAIN_SEPARATOR) } p.push(d) } let path = PathBuf::from_str(&format!( "{}{MAIN_SEPARATOR}{p}", directory.trim_end_matches(MAIN_SEPARATOR), ))?; Ok(Self { file: File::open(&path)?, path, }) } /// Create new `Self` with unique pathname in the root `directory` pub fn create(directory: &str) -> Result { loop { // generate file id from current unix time let id = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); // build optimized fs path: // add directory separator after every digit to keep up to 10 files per directory let path = PathBuf::from(format!( "{}{MAIN_SEPARATOR}{}{TMP_SUFFIX}", directory.trim_end_matches(MAIN_SEPARATOR), id.to_string().chars().fold(String::new(), |mut acc, c| { if !acc.is_empty() { acc.push(MAIN_SEPARATOR); } acc.push(c); acc }) )); // recursively create directories // * parent directory is expected create_dir_all(path.parent().unwrap())?; // build `Self` match File::create_new(&path) { // make sure slot is not taken (by another thread) Ok(file) => { return Ok(Self { file, path, // id }); } Err(_) => { println!("[warning] Could not init location: {:?}", path.to_str()); // find free slot after some delay.. thread::sleep(Duration::from_secs(1)); continue; } } } } // Actions /// Commit changes, return permanent Self pub fn commit(mut self) -> Result { match self.path.to_str() { Some(old) => match old.strip_suffix(TMP_SUFFIX) { Some(new) => match rename(old, new) { Ok(()) => { self.path = match PathBuf::from_str(new) { Ok(path) => path, Err(e) => return Err((self, anyhow!(e))), }; Ok(self) } Err(e) => Err((self, anyhow!(e))), }, None => Err((self, anyhow!("Unexpected suffix"))), }, None => Err((self, anyhow!("Unexpected file path"))), } } /// Delete `Self`, cleanup FS pub fn delete(self) -> Result<()> { Ok(remove_file(self.path)?) } // Getters /// Shared helper to build public URI pub fn to_uri(&self, directory: &str) -> String { self.path .to_str() .unwrap() .replace(directory, "") .replace("/", "") } }