commit b55c5bb6b59ddda81acfc34b4864112f837c2eb0 Author: XingfenD Date: Wed Aug 26 19:59:38 2026 +0800 docs: yDropbox 文件收发站设计规格 diff --git a/docs/superpowers/specs/2026-08-26-ydropbox-design.md b/docs/superpowers/specs/2026-08-26-ydropbox-design.md new file mode 100644 index 0000000..0c5b58f --- /dev/null +++ b/docs/superpowers/specs/2026-08-26-ydropbox-design.md @@ -0,0 +1,158 @@ +# yDropbox — 文件收发站设计规格 + +> 日期: 2026-08-26 +> 状态: 已用户批准 +> 项目名: `yoresee_dropbox`(Go 模块 `yoresee_dropbox`,部署二进制 `yDropbox`) + +## 1. 概述 + +一个自托管单用户文件收发站:用户通过浏览器上传/下载文件,无需 SSH。服务器上的本地程序(自动化脚本、CLI 工具等)通过共享文件系统目录与 Web 端交换文件: + +- 用户 Web 上传 → 落入 `workspace/inbox/`,供本地程序消费。 +- 本地程序产出 → 写入 `workspace/outbox/`,用户在页面下载。 +- 支持生成公开分享链接(可选密码、可选有效期)。 + +**目标** + +- 单 Go 二进制 + `go:embed` 内嵌前端,零外部依赖。 +- 上传、列表、下载、删除、分享链接全流程可用。 +- 服务器本地程序零 API 依赖(直接读写目录即可集成)。 + +**非目标** + +- 不做多用户/权限体系。 +- 不做断点续传、分块上传。 +- 不做在线预览/编辑。 + +## 2. 架构 + +- **进程**: 单个 Go 二进制 `yDropbox`,HTTP 服务默认监听 `127.0.0.1:8999`(仅本机回环),`--addr` 可配。 +- **对外**: 反向代理服务器上由 nginx 将 HTTPS 子域名反代至该端口;证书由 certbot 签发续期。 +- **文件存储**: 文件系统两目录 —— `workspace/inbox/`、`workspace/outbox/`。本地程序与 Web 服务共享此目录。 +- **元数据**: SQLite(`modernc.org/sqlite`,纯 Go 免 CGO),存文件记录、分享链接、密码哈希、有效期。数据库文件位于 `workspace/.ydropbox/db.sqlite`。 +- **前端**: Alpine.js(vendor 单文件 `alpine.min.js` 随仓库提交,`go:embed` 内嵌),声明式响应式绑定(`x-data`/`x-for`/`x-show`),无构建链;不引 CDN,保持零外部依赖。 + +## 3. API 接口定义 + +后端用 Go 标准库 `net/http` + `ServeMux`(Go 1.22+ 方法+路径模式),不引第三方路由。所有 API 返回 JSON(`Content-Type: application/json`);上传与分享下载除外。 + +| 路由 | 方法 | 请求 | 成功响应 | 失败 | +|------|------|------|---------|------| +| `/login` | GET | — | 登录页 | — | +| `/api/login` | POST | `{"token":"..."}` | `200 {}` + Set-Cookie | 401 | +| `/api/logout` | POST | — | `200 {}` 清 cookie | 401 | +| `/api/upload` | POST | multipart `file=@...` | `201 {"file":{...}}` | 401/413/400 | +| `/api/files` | GET | `?dir=inbox\|outbox` 可选过滤 | `200 {"files":[...]}` | 401 | +| `/api/files/{id}/download` | GET | — | 文件流(Content-Disposition 原始名) | 401/404 | +| `/api/files/{id}` | DELETE | — | `204` | 401/404 | +| `/api/files/{id}/share` | POST | `{"password":"可选","expires_in_hours":N 或 "expires_at":ts,均可省略}` | `201 {"url":"/s/",...}` | 401/404/400 | +| `/api/shares` | GET | — | `200 {"shares":[...]}` 活跃分享列表 | 401 | +| `/api/shares/{id}` | DELETE | — | `204` 撤销分享 | 401/404 | +| `/s/{token}` | GET | 密码时先 `POST /s/{token}` `{"password":"..."}` | 文件流 / 密码页 | 403/410/404 | + +**file 对象结构**(列表与上传返回统一): + +```json +{"id":"uuid","name":"report.pdf","dir":"inbox","size":1048576,"created_at":1750000000} +``` + +**说明** + +- 未登录访问页面类路由 → 302 `/login`;未登录访问 `/api/*` → 401 JSON。 +- 上传限 100MB,超限 413。 +- 分享有密码且未验证时,GET 返回密码输入页;密码校验通过后签发短期 share cookie 再放行下载流。 +- `GET /api/shares` 用于页面管理已创建的分享(撤销入口依赖此列表能力)。 + +## 4. 数据模型 + +SQLite 两张表: + +```sql +-- 文件记录 +CREATE TABLE files ( + id TEXT PRIMARY KEY, -- UUIDv4 + original_name TEXT NOT NULL, -- 用户原始文件名(展示/下载用) + storage_name TEXT NOT NULL UNIQUE, -- 落盘名 = UUID(无扩展名) + dir TEXT NOT NULL CHECK (dir IN ('inbox','outbox')), + size INTEGER NOT NULL, + created_at INTEGER NOT NULL -- Unix 秒 +); + +-- 分享链接 +CREATE TABLE shares ( + id TEXT PRIMARY KEY, -- UUIDv4 + file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE, + token TEXT NOT NULL UNIQUE, -- crypto/rand 24 字节 → base64url + password_hash TEXT, -- bcrypt;NULL = 无密码 + expires_at INTEGER, -- Unix 秒;NULL = 永不过期 + created_at INTEGER NOT NULL, + last_accessed_at INTEGER -- 可选审计字段 +); +``` + +**设计要点** + +- **存储与元数据分离**: 落盘文件名用纯 UUID,杜绝 path traversal 与重名冲突;原始文件名只存库,下载时写入 `Content-Disposition`,MIME 类型按 `original_name` 扩展名推断(默认 `application/octet-stream`)。 +- **token 强度**: 24 字节 `crypto/rand` → base64url(32 字符),不可枚举;密码 bcrypt cost 10。 +- **有效期语义**: 未设置则长期有效;访问时校验 `expires_at`,过期返回 410 并删除该分享记录。 +- **级联删除**: 文件删除时其所有分享随 `ON DELETE CASCADE` 失效。 +- **目录同步**: 后台 goroutine 每 10s 扫描 `inbox/` 与 `outbox/` 两个目录(对称扫描——本地程序手动放进 inbox 的文件也能出现在页面),以磁盘为准做增删对账:新文件入库、已消失的文件删记录(级联撤销分享)。 + +## 5. 鉴权与安全 + +**鉴权模型** + +- **登录凭证**: 启动时传入访问 token —— `yDropbox listen --token=` 或环境变量 `YDROPBOX_TOKEN`。token 仅存在内存,用于 `/api/login` 单次校验。 +- **session**: 校验通过后签发 httpOnly cookie(名 `ydropbox_session`,值 = 32 字节随机 id),session 映射存内存,进程重启即全部失效;属性 `HttpOnly; SameSite=Lax; Secure`(Secure 由反代 HTTPS 场景决定)。 +- **session 过期**: 默认 7 天滑动过期,登出即删。 +- **分享密码**: `POST /s/{token}` 校验 bcrypt 后,签发独立短期 cookie(仅绑定该 share id,有效期 1h),后续 GET 凭此放行下载流。 + +**安全措施** + +| 威胁 | 对策 | +|------|------| +| 路径穿越 | 落盘/读取一律用 UUID 文件名,用户输入的文件名永不参与路径拼接 | +| 分享枚举 | 24 字节 CSPRNG token | +| 密码泄露 | bcrypt 哈希存储,日志与响应永不回显 | +| CSRF | API 全部要求 JSON Content-Type + SameSite=Lax cookie;上传 multipart 为例外但需 session cookie 且由同源页面发起 | +| 暴力破解 | `/api/login` 与分享密码校验均加固定延迟 + 连续 5 次失败锁定 60s(内存计数) | +| 大文件打爆内存 | `http.MaxBytesReader` 100MB 流式落盘;nginx 侧 `client_max_body_size 100m` 双保险 | +| 敏感头泄露 | 下载响应带 `X-Content-Type-Options: nosniff`;分享下载带 `Content-Disposition: attachment` | + +**错误码约定**: 401 未登录 / 403 密码错或锁定 / 404 不存在 / 410 分享过期 / 413 超限。 + +## 6. 测试 + +**单元测试**(标准库 `testing` + `httptest`) + +- **store 层**: 文件 CRUD、分享创建/撤销/级联失效、有效期判断(永不过期/时长到期/指定时刻过期)、bcrypt 校验。 +- **handler 层**: 登录成功/失败与锁定、未登录 401、页面 302、上传 201 与超限 413、下载 Content-Disposition 正确性、删除 204 与分享级联、分享密码错 403、过期 410、scanner 对账逻辑(临时目录模拟新增/消失)。 + +**集成测试** + +- 本地起服务,脚本走全链路:登录 → 上传 → 列表 → 下载 → 建分享 → 无密码公开下载 → 撤销后 404。 +- 部署后浏览器人工走查一遍 HTTPS 全流程。 + +## 7. 部署 + +1. **构建**: `CGO_ENABLED=0 go build -o yDropbox .`,纯静态单文件。 +2. **运行**: systemd 服务,`ExecStart=/usr/local/bin/yDropbox listen --addr=127.0.0.1:8999`,`YDROPBOX_TOKEN` 经 systemd 环境或 credentials 注入;数据目录指向服务器上固定路径(如 `/var/lib/ydropbox/workspace`,`--workspace` 可配)。 +3. **反代**: nginx 站点将 HTTPS 子域名反代至 `127.0.0.1:8999`,配置 `client_max_body_size 100m`,certbot 签发续期证书。 +4. **DNS**: 子域名 A 记录指向反代服务器公网 IP(具体域名/IP 由部署时确定,不入仓库文档)。 + +## 8. 目录结构 + +``` +yoresee_dropbox/ +├── main.go # 入口: flag/env 解析、路由注册、启动 scanner +├── go.mod / go.sum +├── internal/ +│ ├── server/ +│ │ ├── auth.go # 登录/session/锁定 +│ │ ├── files.go # 上传/列表/下载/删除 +│ │ └── share.go # 分享创建/撤销/公开访问 +│ ├── store/store.go # SQLite 层 (modernc.org/sqlite) +│ └── scanner/scanner.go # inbox+outbox 目录对账 +├── web/ # go:embed:index.html, style.css, app.js, alpine.min.js(vendor) +└── workspace/ # 运行期生成(gitignore): inbox/, outbox/, .ydropbox/db.sqlite +```