fix(photos): serialize save to enforce quota under concurrency; avoid empty-dir leak

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cc 2026-06-24 21:07:05 +08:00
parent 3af4893a81
commit 9d93a33bae

View File

@ -21,20 +21,28 @@ pub enum SaveError {
}
/// 按用户隔离的图片仓库。内部只是一个根目录路径,`Clone` 廉价。
/// `lock` 串行化 save 的 “计数 → 写入” 临界区杜绝并发超额TOCTOU
#[derive(Clone)]
pub struct PhotoStore {
base: PathBuf,
lock: std::sync::Arc<std::sync::Mutex<()>>,
}
impl PhotoStore {
/// 根目录取 `PHOTO_DIR`,默认 `photo-wall`(相对进程 cwd
pub fn from_env() -> Self {
let base = std::env::var("PHOTO_DIR").unwrap_or_else(|_| "photo-wall".to_string());
Self { base: base.into() }
Self {
base: base.into(),
lock: std::sync::Arc::new(std::sync::Mutex::new(())),
}
}
pub fn new(base: impl Into<PathBuf>) -> Self {
Self { base: base.into() }
Self {
base: base.into(),
lock: std::sync::Arc::new(std::sync::Mutex::new(())),
}
}
fn user_dir(&self, user: &str) -> Option<PathBuf> {
@ -48,10 +56,15 @@ impl PhotoStore {
pub fn save(&self, user: &str, data_url: &str) -> Result<String, SaveError> {
let dir = self.user_dir(user).ok_or(SaveError::BadUser)?;
let (ext, bytes) = parse_data_url(data_url)?;
std::fs::create_dir_all(&dir).map_err(|_| SaveError::Io)?;
// 串行化 “计数 → 写入”:防止单用户并发上传同时通过额度检查而超额。
// 中毒锁仅守护一个 (),恢复内部值继续即可,无需 panic。
let _guard = self.lock.lock().unwrap_or_else(|e| e.into_inner());
// 先查额度(`list_in` 对不存在的目录返回空 → 0满了直接返回
// 避免给已满用户白白创建空目录。
if list_in(&dir).len() >= MAX_PHOTOS {
return Err(SaveError::Full);
}
std::fs::create_dir_all(&dir).map_err(|_| SaveError::Io)?;
let id = format!("{}.{}", new_stem(), ext);
std::fs::write(dir.join(&id), &bytes).map_err(|_| SaveError::Io)?;
Ok(id)