titanit/src/storage.rs
2025-02-22 06:24:42 +02:00

130 lines
4 KiB
Rust

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<Self> {
let s = match url.rfind('/') {
Some(pos) => match url.get(pos + 1..) {
Some(u) => u.parse::<u64>()?,
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<Self> {
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<Self, (Self, Error)> {
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("/", "")
}
}