Files
yoresee_dropbox/docs/superpowers/specs/2026-08-26-ydropbox-design.md

9.2 KiB
Raw Permalink Blame History

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/<token>",...} 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 对象结构(列表与上传返回统一):

{"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 两张表:

-- 文件记录
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=<T> 或环境变量 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