From 9d93a33bae2da035ae63fbffa1115ee379d58310 Mon Sep 17 00:00:00 2001 From: cc Date: Wed, 24 Jun 2026 21:07:05 +0800 Subject: [PATCH] fix(photos): serialize save to enforce quota under concurrency; avoid empty-dir leak Co-Authored-By: Claude Opus 4.8 --- Server/src/photos.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Server/src/photos.rs b/Server/src/photos.rs index 3b9383b..46557f8 100644 --- a/Server/src/photos.rs +++ b/Server/src/photos.rs @@ -21,20 +21,28 @@ pub enum SaveError { } /// 按用户隔离的图片仓库。内部只是一个根目录路径,`Clone` 廉价。 +/// `lock` 串行化 save 的 “计数 → 写入” 临界区,杜绝并发超额(TOCTOU)。 #[derive(Clone)] pub struct PhotoStore { base: PathBuf, + lock: std::sync::Arc>, } 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) -> 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 { @@ -48,10 +56,15 @@ impl PhotoStore { pub fn save(&self, user: &str, data_url: &str) -> Result { 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)