# 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 ./cmd/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/ ├── cmd/ydropbox/main.go # 入口: 仅 flag/env 解析 → app.Run(cfg) ├── go.mod / go.sum ├── internal/ │ ├── app/app.go # 装配: 开库、建路由(server.New)、起 scanner、监听 │ ├── server/ │ │ ├── server.go # New(): 路由注册 │ │ ├── auth.go # 登录/session/锁定 │ │ ├── files.go # 上传/列表/下载/删除 │ │ └── share.go # 分享创建/撤销/公开访问 │ ├── store/store.go # SQLite 层 (modernc.org/sqlite) │ └── scanner/scanner.go # inbox+outbox 目录对账 ├── web/ │ ├── web.go # //go:embed 挂载点,导出 FS │ ├── index.html │ ├── style.css │ ├── app.js │ └── alpine.min.js # vendor └── workspace/ # 运行期生成(gitignore): inbox/, outbox/, .ydropbox/db.sqlite ```