docs(special-folders): requirements/design spec for shared access-controlled folders
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f65ef6fb83
commit
fbac6de977
153
docs/superpowers/specs/2026-06-24-special-folders-design.md
Normal file
153
docs/superpowers/specs/2026-06-24-special-folders-design.md
Normal file
@ -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:<name>` 源),
|
||||
让 `PhotoWallPage` 通过 props 接收"如何 list/upload/get/delete",避免组件内写死。
|
||||
- **无 10 张上限**:上传不做客户端张数拦截(个人墙保留 10 上限)。
|
||||
- **排版/装饰各人各存浏览器**:图片共享,但每个浏览器的摆放/装饰是各自的。localStorage key
|
||||
**按文件夹区分**:`photo-wall-state:folder:<name>`(个人墙仍用 `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` 统计的性能(目录文件数大时遍历开销)——当前规模可接受,必要时缓存。
|
||||
- 管理员常量的位置与日后可配置化的留口。
|
||||
Loading…
Reference in New Issue
Block a user