# 设计:特殊文件夹(共享公共相册,按文件夹白名单) > 在已有照片墙(`#photo` 个人墙,按用户存图)之上,增加一类**共享的、命名的公共相册**: > 通过地址栏 `#<文件夹名>` 进入,名单内的特定用户登录后可共同上传/浏览/删除, > 复用照片墙的互动界面。特殊文件夹不使用个人 `#photo` 的 10 张上限,但当前实现有 > `MAX_ITEMS = 100` 的总元素上限(图片 + 装饰);总字节超 1 GiB 时给管理员页面提示。 > > 前置阅读:[photo-wall.md](../../photo-wall.md)(照片墙服务器存图)、 > [photo-wall-server-storage-design.md](2026-06-24-photo-wall-server-storage-design.md)、 > [auth-login-gate.md](../../auth-login-gate.md)(登录 + dev_gate 入口门)。 > > 状态:基础特殊文件夹相册已实现;本文件保留设计依据并同步当前约束。视频媒体仍是后续规划, > 见 §10。 ## 1. 目标与关键决策 | 维度 | 决策 | |---|---| | 内容模型 | **共享**:一个特殊文件夹 = 一份公共相册,所有有权用户看到同一份内容 | | 写权限 | 名单内**任何登录用户**可上传/删除(协作式:谁删大家都没了) | | 访问控制 | **按文件夹的用户白名单**,名单只存服务器配置文件 | | 界面 | **复用 `PhotoWallPage`**(拖拽/装饰/导出),去掉 10 张上限 | | 张数上限 | 不使用个人墙 10 张上限;当前受 `MAX_ITEMS = 100` 总元素上限约束 | | 配额 | **1 GiB 软阈值**:不拦上传,超了在页面给管理员看警告 | | 管理员 | 写死用户名 `cc`(可访问所有文件夹 + 看配额警告) | | 文件夹注册 | 服务器配置文件 `special_folders.txt`,前端不写死;当前实现加一行后需重启 Rust 服务 | | 登录 | 同一套 `ccuser`(复用现有登录 + JWT) | 非目标(YAGNI):每文件夹独立配额数值、上传者归属/审计、回收站、文件夹的网页管理后台、 图片压缩。需要时再加。 ## 2. 入口与路由 地址栏 `#<名字>`,前端 `App.tsx` 在 `hashchange` 时依次判断: ``` hash === 'photo' → 个人照片墙(已实现) 否则 POST /api/web/folder {name} → ok? 是 → 特殊文件夹流程 否则 POST /api/web/gate {hash} → ok? 是 → 开发者登录页(已实现) 都不是 → 起始页 ``` - `POST /api/web/folder { name }` 是**登录前**的存在性判断(仿 `/api/web/gate`), 只回 `{ ok: bool }`——该名字是否是注册的特殊文件夹。**不返回白名单**(不泄露成员)。 命中 → 显示登录页(复用 `LoginPage`,`hideStats`,与 `#photo` 一致)。 - 真正的**访问控制在登录后的数据接口**上做(见 §4):不在白名单 → `403`,前端显示 「无权访问该相册」。 - 登录成功 → 渲染特殊文件夹版照片墙(见 §6)。退出 → 清登录态 + 抹掉 hash → 起始页。 > 前端需要记住"当前进入的是哪个特殊文件夹"(hash 即文件夹名),传给数据接口与组件。 ## 3. 注册表与存储 ### 3.1 注册表(配置文件) - 文件 `special_folders.txt`(路径由 `SPECIAL_FOLDERS_FILE` 覆盖,默认仓库/部署目录下), 线上放 `/opt/internet-project/special_folders.txt`。 - 格式:每行 `文件夹名<空白>允许用户(逗号分隔)`;`#` 后为注释;空行忽略。 ``` # 文件夹名 允许用户(逗号分隔) family cc,alice,bob trip2026 cc,carol ``` - 后端解析为 `Map<文件夹名, Set<用户名>>`。当前实现是在服务启动时读取配置文件; 新增或修改名单后需要重启 Rust 服务才会生效。若后续要支持热更新,应改为每次校验读文件 或增加配置缓存刷新机制。 - 文件夹名合法性:复用用户名级别的字符集白名单 `[A-Za-z0-9_-]`、长度 1–64(既作目录名安全, 又避免与 `#photo` 等保留字冲突——`photo` 不应出现在注册表里)。 ### 3.2 图片存储 - 独立根目录 `SPECIAL_DIR`(默认 `special-folders`,线上 `/opt/internet-project/special-folders/`), **与个人墙的 `PHOTO_DIR/<用户>/` 物理隔开**,杜绝文件夹名与用户名撞车。 - 每文件夹一子目录 `SPECIAL_DIR/<文件夹名>/`,每图随机 hex 文件名(复用 `photos.rs` 的 `valid_id`、`parse_data_url`、随机文件名、路径安全逻辑)。不使用个人墙 10 张上限, 但上传会受 `MAX_ITEMS = 100` 总元素上限约束。 - 单图大小上限沿用 `MAX_BYTES = 8 MiB`。 ## 4. 后端接口 复用 §3.2 的存储原语,新增一个"按文件夹(而非按用户)"的存储视图与一组接口。 所有数据接口经 `AuthUser`(已实现的 JWT 提取器)。 | 方法 | 路径 | 鉴权 | 行为 | |---|---|---|---| | POST | `/api/web/folder` | 无(登录前) | body `{name}`;该名是否注册 → `{ok}` | | GET | `/api/web/folder/{name}/photos` | AuthUser + 白名单 | `{ items:[{id}], max, totalBytes, overQuota }` | | POST | `/api/web/folder/{name}/photos` | AuthUser + 白名单 | 上传 `{dataUrl}`,受 `MAX_ITEMS` 总元素上限约束 | | GET | `/api/web/folder/{name}/photos/{id}` | AuthUser + 白名单 | 流式返回字节 | | DELETE | `/api/web/folder/{name}/photos/{id}` | AuthUser + 白名单 | 删除 | **访问控制中间逻辑**(每个 `{name}` 数据接口入口): 1. `name` 不在注册表 → `404`。 2. 登录用户既不在该文件夹白名单、又不是管理员 `cc` → `403`。 3. 通过 → 执行。 **错误码**:未注册 `404`、无权 `403`、总元素达到 `MAX_ITEMS` 时 `409`、单图超 8MiB `413`、类型不在白名单 `415`、`{id}` 非法/不存在 `404`、IO `500`。上传**不因 1 GiB 软配额被拒**(只提示管理员)。 **配额字段**(GET list 返回): - `totalBytes`:该文件夹当前总字节(遍历目录累加文件大小)。 - `overQuota`:`totalBytes > 1 GiB`(`1024^3`)即 `true`。 - `max`:当前总元素上限,现为 `MAX_ITEMS = 100`。 ## 5. 管理员配额提示 - 管理员 = 写死用户名 `cc`(实现上一个常量/小名单,便于日后扩展为可配置)。 - list 接口对所有有权用户都返回 `totalBytes`/`overQuota`;**前端只在登录者是 `cc` 且 `overQuota` 为真时**,于特殊文件夹墙工具栏显示一条警告条: 「该相册已超 1GB(当前 <人类可读大小>),请清理」。其他用户不展示、也不感知。 - 软阈值:超额后仍允许上传,仅提示。 ## 6. 前端:复用照片墙 特殊文件夹版与 `#photo` 个人墙共用 `PhotoWallPage` 的交互(拖拽/双击编辑/右键菜单/ 装饰/导出/撤销重做),差异点: - **数据源**:图片走 `/api/web/folder/{name}/photos` 系列接口(按文件夹),而非按用户。 建议把图片接口抽象成一个"源"参数(个人墙 = 用户源,特殊文件夹 = `folder:` 源), 让 `PhotoWallPage` 通过 props 接收"如何 list/upload/get/delete",避免组件内写死。 - **无个人 10 张上限,但有总元素上限**:个人墙仍是 10 张;特殊文件夹由服务器返回 `max` 并按图片 + 装饰总数限制,当前为 100 条。 - **排版/装饰服务器共享,浏览器保留本地副本**:图片共享,背景、装饰和图片排版也通过 `/api/web/folder/{name}/state` 保存到服务器,所有有权用户看到同一份墙面状态。 浏览器仍写 `localStorage` key `photo-wall-state:folder:` 作为本地副本/兜底; `photoLayout` 按服务器图片 id 关联。 - **删除是共享副作用**:任何有权用户删图 → 服务器文件没了 → 其他人下次加载即不见 (各自浏览器里该 id 的排版条目成为孤儿,加载时被忽略,无害)。 - **管理员警告条**:见 §5。 - **403 处理**:进入后若数据接口返回 403,显示「无权访问该相册」并提供返回。 ## 7. 部署与配置 - 后端改动 → **全量重部署**(同照片墙服务器存图流程)。修改特殊文件夹注册表后,当前实现 也需要重启 Rust 服务重新读取配置。 - 新增运行时配置: - `SPECIAL_DIR`(可选,默认 `special-folders`)。 - `SPECIAL_FOLDERS_FILE`(可选,默认 `special_folders.txt`)——线上放 `/opt/internet-project/special_folders.txt`,内容即文件夹→用户名单。 - 新增一个特殊文件夹的运维动作:在 `special_folders.txt` 加一行(文件夹名 + 允许用户), 重启 Rust 服务后生效;首次上传自动建 `special-folders/<名字>/` 目录。**不改代码、不重部署前端。** ## 8. 复用与新增一览 - **复用**:`auth.rs`(JWT)、`routes/extract.rs`(`AuthUser`)、`photos.rs` 的 `valid_id`/`parse_data_url`/随机文件名/路径安全、前端 `PhotoWallPage`/`api/photos.ts` 思路。 - **新增**(后端):特殊文件夹注册表解析(启动时读 `special_folders.txt`)、按文件夹的存储视图与 配额统计、`/api/web/folder` 存在性接口、4 个 `/api/web/folder/{name}/photos*` 接口、 访问控制(白名单 + 管理员放行)。 - **新增/改动**(前端):`App.tsx` 入口路由加"特殊文件夹"分支、按文件夹的图片 API、 `PhotoWallPage` 接受"数据源 + 是否限额 + 是否管理员 + 配额状态"等 props、管理员警告条、 按文件夹的 localStorage key。 ## 9. 后续优化点 - 注册表热更新:当前启动时读取,若需要运维即时生效,应改为每次校验读文件或增加刷新机制。 - API JSON 命名一致性:设计层建议保持前端友好的 camelCase 字段,例如 `totalBytes`、 `overQuota`、`bgIndex`、`photoLayout`;实现和客户端应保持一致。 - `totalBytes` 统计的性能(目录文件数大时遍历开销)——当前规模可接受,必要时缓存。 - 管理员常量的位置与日后可配置化的留口。 ## 10. 后续扩展:视频媒体 特殊文件夹的视频上传、异步转码、媒体接口和 `mediaLayout` 兼容策略由 [2026-06-29-special-folder-video-media-design.md](2026-06-29-special-folder-video-media-design.md) 接续设计。个人 `#photo` 不在该扩展范围内。