10 KiB
设计:特殊文件夹(共享公共相册,按文件夹白名单)
在已有照片墙(
#photo个人墙,按用户存图)之上,增加一类共享的、命名的公共相册: 通过地址栏#<文件夹名>进入,名单内的特定用户登录后可共同上传/浏览/删除, 复用照片墙的互动界面。特殊文件夹不使用个人#photo的 10 张上限,但当前实现有MAX_ITEMS = 100的总元素上限(图片 + 装饰);总字节超 1 GiB 时给管理员页面提示。前置阅读:photo-wall.md(照片墙服务器存图)、 photo-wall-server-storage-design.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} 数据接口入口):
name不在注册表 →404。- 登录用户既不在该文件夹白名单、又不是管理员
cc→403。 - 通过 → 执行。
错误码:未注册 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:<name>源), 让PhotoWallPage通过 props 接收"如何 list/upload/get/delete",避免组件内写死。 - 无个人 10 张上限,但有总元素上限:个人墙仍是 10 张;特殊文件夹由服务器返回
max并按图片 + 装饰总数限制,当前为 100 条。 - 排版/装饰服务器共享,浏览器保留本地副本:图片共享,背景、装饰和图片排版也通过
/api/web/folder/{name}/state保存到服务器,所有有权用户看到同一份墙面状态。 浏览器仍写localStoragekeyphoto-wall-state:folder:<name>作为本地副本/兜底;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
接续设计。个人 #photo 不在该扩展范围内。