use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
pub fn short_hash(text: &str) -> String {
let digest = Sha256::digest(text.as_bytes());
hex::encode(digest)[..16].to_string()
}
pub fn app_dir(data_dir: &Path, app_key: &str) -> PathBuf {
data_dir.join(short_hash(app_key))
}
pub fn db_path(data_dir: &Path, app_key: &str, username: &str) -> PathBuf {
app_dir(data_dir, app_key).join(format!("{}.db", short_hash(&username.to_lowercase())))
}
pub fn user_dir(data_dir: &Path, app_key: &str, username: &str) -> PathBuf {
app_dir(data_dir, app_key).join(short_hash(&username.to_lowercase()))
}
pub fn outbox_dir(data_dir: &Path, app_key: &str, username: &str) -> PathBuf {
user_dir(data_dir, app_key, username).join("outbox")
}
pub fn ensure_private_dir(dir: &Path) -> Result<()> {
fs::create_dir_all(dir).map_err(|e| Error::from_io(&e))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(dir, fs::Permissions::from_mode(0o700));
}
Ok(())
}
pub fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
let dir = path.parent().ok_or_else(|| Error::internal("没有上级目录"))?;
fs::create_dir_all(dir).map_err(|e| Error::from_io(&e))?;
let tmp = dir.join(format!(".{}.tmp", path.file_name().and_then(|n| n.to_str()).unwrap_or("file")));
{
let mut file = fs::File::create(&tmp).map_err(|e| Error::from_io(&e))?;
file.write_all(bytes).map_err(|e| Error::from_io(&e))?;
file.sync_all().map_err(|e| Error::from_io(&e))?;
}
fs::rename(&tmp, path).map_err(|e| Error::from_io(&e))
}
#[derive(Clone, Debug)]
pub struct Paths {
pub data: PathBuf,
pub temp: PathBuf,
pub logs: PathBuf,
}
impl Paths {
pub fn users_json(&self, app_key: &str) -> PathBuf {
app_dir(&self.data, app_key).join("users.json")
}
}
pub fn clean_old_files(dir: &Path, max_age: std::time::Duration) {
let Ok(entries) = fs::read_dir(dir) else { return };
let now = std::time::SystemTime::now();
for entry in entries.flatten() {
let path = entry.path();
let Ok(meta) = entry.metadata() else { continue };
let old = meta.modified().ok().and_then(|m| now.duration_since(m).ok()).is_some_and(|age| age > max_age);
if !old {
continue;
}
if meta.is_dir() {
let _ = fs::remove_dir_all(&path);
} else {
let _ = fs::remove_file(&path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_matches_flutter() {
assert_eq!(short_hash("org#app"), &hex::encode(Sha256::digest(b"org#app"))[..16]);
let data = Path::new("/d");
assert_eq!(db_path(data, "org#app", "Alice"), db_path(data, "org#app", "alice"));
assert!(db_path(data, "org#app", "alice").to_string_lossy().ends_with(".db"));
}
}