From fbac6de97704d55b2bd7c52e63699e08cdb19171 Mon Sep 17 00:00:00 2001 From: cc Date: Wed, 24 Jun 2026 21:52:30 +0800 Subject: [PATCH] docs(special-folders): requirements/design spec for shared access-controlled folders Co-Authored-By: Claude Opus 4.8 --- .../2026-06-24-special-folders-design.md | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-24-special-folders-design.md diff --git a/docs/superpowers/specs/2026-06-24-special-folders-design.md b/docs/superpowers/specs/2026-06-24-special-folders-design.md new file mode 100644 index 0000000..d4c6b8d --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-special-folders-design.md @@ -0,0 +1,153 @@ +# 设计:特殊文件夹(共享公共相册,按文件夹白名单) + +> 在已有照片墙(`#photo` 个人墙,按用户存图)之上,增加一类**共享的、命名的公共相册**: +> 通过地址栏 `#<文件夹名>` 进入,名单内的特定用户登录后可共同上传/浏览/删除, +> 复用照片墙的互动界面,无单文件夹张数上限,但总量超 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 入口门)。 +> +> 状态:需求文档,**待实现**(留作后续 writing-plans 的输入)。 + +## 1. 目标与关键决策 + +| 维度 | 决策 | +|---|---| +| 内容模型 | **共享**:一个特殊文件夹 = 一份公共相册,所有有权用户看到同一份内容 | +| 写权限 | 名单内**任何登录用户**可上传/删除(协作式:谁删大家都没了) | +| 访问控制 | **按文件夹的用户白名单**,名单只存服务器配置文件 | +| 界面 | **复用 `PhotoWallPage`**(拖拽/装饰/导出),去掉 10 张上限 | +| 张数上限 | 无 | +| 配额 | **1 GiB 软阈值**:不拦上传,超了在页面给管理员看警告 | +| 管理员 | 写死用户名 `cc`(可访问所有文件夹 + 看配额警告) | +| 文件夹注册 | 服务器配置文件 `special_folders.txt`,前端不写死,加一行即生效 | +| 登录 | 同一套 `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<用户名>>`。**每次校验时读文件**(文件很小,开销可忽略), + 这样新增/改名单**即时生效、无需重启**——与 `dev_gate` 文件模式同理。 +- 文件夹名合法性:复用用户名级别的字符集白名单 `[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`、随机文件名、路径安全逻辑)。**无张数上限**。 +- 单图大小上限沿用 `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}], totalBytes, overQuota }` | +| POST | `/api/web/folder/{name}/photos` | AuthUser + 白名单 | 上传 `{dataUrl}`,无张数上限 | +| 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`、单图超 8MiB `413`、类型不在白名单 `415`、 +`{id}` 非法/不存在 `404`、IO `500`。上传**不因配额被拒**(软阈值)。 + +**配额字段**(GET list 返回): +- `totalBytes`:该文件夹当前总字节(遍历目录累加文件大小)。 +- `overQuota`:`totalBytes > 1 GiB`(`1024^3`)即 `true`。 + +## 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 上限)。 +- **排版/装饰各人各存浏览器**:图片共享,但每个浏览器的摆放/装饰是各自的。localStorage key + **按文件夹区分**:`photo-wall-state:folder:`(个人墙仍用 `photo-wall-state`), + 互不串。`photoLayout` 仍按服务器图片 id 关联。 +- **删除是共享副作用**:任何有权用户删图 → 服务器文件没了 → 其他人下次加载即不见 + (各自浏览器里该 id 的排版条目成为孤儿,加载时被忽略,无害)。 +- **管理员警告条**:见 §5。 +- **403 处理**:进入后若数据接口返回 403,显示「无权访问该相册」并提供返回。 + +## 7. 部署与配置 + +- 后端改动 → **全量重部署**(同照片墙服务器存图流程)。 +- 新增运行时配置: + - `SPECIAL_DIR`(可选,默认 `special-folders`)。 + - `SPECIAL_FOLDERS_FILE`(可选,默认 `special_folders.txt`)——线上放 + `/opt/internet-project/special_folders.txt`,内容即文件夹→用户名单。 +- 新增一个特殊文件夹的运维动作:在 `special_folders.txt` 加一行(文件夹名 + 允许用户), + 即时生效;首次上传自动建 `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. 待实现时需在计划里钉死的点 + +- `PhotoWallPage` 的"数据源"抽象接口签名(list/upload/get/delete 的统一形状)。 +- 注册表文件解析的容错(坏行跳过、重复文件夹名取首条还是报错)。 +- `totalBytes` 统计的性能(目录文件数大时遍历开销)——当前规模可接受,必要时缓存。 +- 管理员常量的位置与日后可配置化的留口。