原理文档

技术架构、数据模型、分片上传会话、鉴权与安全设计。原生客户端:iOS「文链(手机端)」macOS「DocLinks」

总体架构

┌──────────────────────────────────────────────┐ │ Cloudflare Edge │ 管理员/成员浏览器 ─▶│ Workers (Hono) │ 访客浏览器 ─▶│ / /htmls /files 项目目录与管理台(CSP) │ iOS / Mac 客户端 ─▶│ /api/* 管理 API │ │ /p/* /f/* 公开文件服务 │ │ *.html docs/ 静态文档(assets) │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────┐ ┌──────────┐ │ │ │ D1 │ │ R2 │ │ │ └─────────┘ └──────────┘ │ └──────────────────────────────────────────────┘

D1 存元数据、用户、上传 pending 会话、速率限制;R2 存二进制;Worker 编排鉴权与读写。docs/[assets] 与 Worker 同部署。客户端 App Store 名称:iOS 文链(手机端),macOS DocLinks(详见 原生客户端)。

技术栈

层级技术用途
运行时Cloudflare Workers边缘 HTTP
框架Hono 4.x路由、中间件
数据库D1 (SQLite)12 版迁移 schema
存储R2 + Multipart API对象与分片上传
直传aws4fetchS3 预签名 PUT
管理台Tailwind CDN + 原生 JSSPA,Cookie / Bearer 会话
iOSSwiftUI · App Store 名 文链(手机端)钥匙串 Bearer,与 /files API 对齐
macOSSwiftUI · App Store 名 DocLinks(菜单栏)共用 iOS 业务代码,App Sandbox
静态文档wrangler assetsdocs//*.html

代码结构

src/
├── index.ts
├── types.ts                      # Env, AppVariables
├── middleware/auth.ts            # Cookie/Bearer 鉴权
├── routes/                       # public, api, admin, files
├── controllers/
│   ├── fileController.ts         # HTML + auth/login/logout/token/me
│   ├── shareController.ts        # 分享 + 分片 API + 公开访问
│   ├── userController.ts         # 用户 CRUD、注册、资料
│   ├── indexController.ts        # 公开项目目录 /
│   ├── adminController.ts
│   └── filesController.ts
├── services/
│   ├── fileService.ts
│   ├── projectService.ts
│   ├── shareService.ts
│   ├── userService.ts            # 注册、配额、会话
│   ├── shareMultipartUpload.ts   # pending 会话、abort、complete
│   ├── r2PresignedUpload.ts
│   └── adminTokenService.ts
├── utils/
│   ├── adminAuth.ts              # Cookie、Token 哈希
│   ├── accessPassword.ts         # v2 加盐密码
│   ├── security.ts               # escapeHtml、CSP、timingSafeEqual
│   ├── rateLimit.ts
│   ├── htmlInjector.ts
│   └── contentDisposition.ts
└── views/
    ├── adminPage.ts
    ├── filesPage.ts
    ├── indexPage.ts
    └── shareViewPage.ts 等

数据库设计

核心业务表

用途
filesHTML 托管文件元数据(slug 唯一)
projects项目备注、access_password_hash
share_groups分享组:备注、密码、过期、status、user_id
shared_file_items组内文件,UNIQUE(group_id, relative_path)
users成员/管理员:配额、用量、资料、密码哈希
user_sessions成员 Bearer 会话(token_hash、过期)

系统表(0006–0012)

表 / 配置字段要点用途
app_settingsadmin_tokendefault_user_quota_bytesToken 哈希;默认注册配额 50 MB
pending_share_uploadsupload_id, storage_key, part_count, client_path分片上传会话,complete 校验
rate_limitskey, count, window_startIP 速率限制

Admin 凭证解析:resolveAdminCredentialHash() → D1 哈希 → 回退 SHA-256(env.ADMIN_TOKEN)

R2 存储结构

类型Object Key 格式
HTML 托管files/{slug}
文链(DocLinks)shared/{groupId}/{relativePath}

分享上传使用 R2 Multipart Upload;complete 调用 multipart.complete(parts)

HTML 上传流程

  1. 管理台 XHR POST /api/upload(withCredentials,30min 超时)
  2. authMiddleware 验证 Cookie 或 Bearer
  3. 构建 slug,冲突 409,R2 put + D1 upsert

文链(DocLinks)分片上传流程

  1. init:创建 offline share_group → 每文件 createMultipartUpload → 写入 pending_share_uploads(含 clientPath)
  2. upload:直传(part-url + PUT R2)或代理(chunk 经 Worker);part-url/chunk 校验 pending 会话
  3. complete:对照 pending 校验 uploadId、分片数 → R2 complete → D1 batch 写 items + 置 online + 删 pending
  4. abort:对每个 uploadId 调用 multipart.abort(),删 R2 前缀对象与 D1 记录
  5. init/上传失败时前端或服务端触发 abort,避免 R2 幽灵 multipart

限制:≤500 文件,总大小 ≤50GB,路径 sanitize 后不可重复。上传前校验成员 quota_bytes

HTML 公开访问

  1. GET /p/{path} → resolvePublicSlug → 必要时 302
  2. 检查 online、项目密码 Cookie(timingSafeEqual)
  3. R2 get → HTML 注入 base + viewport

分享公开访问

  1. GET /f/{id} — 过期/下线/密码检查
  2. GET /f/{id}/{path} — Range 视频流,RFC 5987 下载名
  3. 密码 Cookie:SHA-256(share:id:hash:adminSecret)

路径重定向(HTML)

/p/JvF41iat → 解析 JvF41iat/docs/index.html → 302 /p/JvF41iat/docs/index.html → 相对引用 assets/app.js 正确解析

鉴权机制

管理员

环节实现
登录POST /api/auth/login + Admin Token → Cookie htmls_admin_session 或 Bearer
API 鉴权Cookie / Bearer → SHA-256 与 D1/env 哈希比对
在线轮换PATCH /api/auth/token → D1 存新哈希
限流login 10次/5min,check 30次/min(按 IP)

成员用户

环节实现
注册POST /api/auth/register → 默认 50 MB 配额
登录密码验证 → user_sessions + Cookie / Bearer
注销DELETE /api/auth/account → 删除分享(含 R2)→ 删除用户与会话
分享归属share_groups.user_id 关联创建者
配额上传前 assertUserQuota,超额 413

访客密码

静态文档(assets)

仓库 docs/ 目录在 wrangler.toml 中配置为静态资源,与 Worker 同部署:

URL文件用途
/privacy-policy.htmlprivacy-policy.htmlApp Store 隐私政策
/ios-app-guide.htmlios-app-guide.htmliOS App Store 支持 URL
/mac-app-guide.htmlmac-app-guide.htmlMac App Store 支持 URL
/index.htmlindex.html文档中心
/user-guide.htmluser-guide.html使用手册
/architecture.htmlarchitecture.html原理文档

run_worker_first//api/*/htmls/files/p/*/f/* 优先交给 Worker;其余 *.html 由 assets 直接返回。iOS / Mac 应用内链接为 {serverURL}/{path}。根页面 / 页脚亦链接至 /index.html

原生客户端

两客户端共用 Swift 业务层与 REST API;App Store 名称与 Web 品牌「文链 · DocLinks」区分如下:

平台App Store 名称版本Bundle ID代码路径文档
iOS 17+文链(手机端)1.0.2com.chengyusheng.docsshare.uploadios/ShareUpload/ios-app-guide.html
macOS 14+DocLinks1.0.1com.chengyusheng.doclinks-macmac/DocLinksMac/mac-app-guide.html

Bearer Token 存钥匙串;与 Web /files 调用相同 API。应用免费、无 IAP/订阅;账户删除走 DELETE /api/auth/account,服务端级联删除 R2 对象。

iOS 端附加行为(1.0.2+)

HTML 自动增强

htmlInjector.ts 注入 <base target="_blank"> 与 viewport(缺失时)。

安全考量

威胁对策
XSS 窃取 TokenHttpOnly Cookie;管理台 escapeHtml;CSP 头
Token 撞库auth/check、login 限流;恒定时间比较
密码暴力破解unlock 限流;v2 加盐哈希
D1 泄露Admin Token 仅存哈希;密码存哈希
上传滥用文件数/总大小上限;pending 会话校验
R2 幽灵 multipartabort 调用 multipart.abort();失败自动清理
错误信息泄露500 不返回内部 detail
直传越权part-url 校验 pending uploadId + storage_key
成员越权分享 API 按 user_id 过滤;管理员可见全部
配额滥用注册默认 50 MB;上传前校验 used_bytes

wrangler.toml 绑定

[[d1_databases]]
binding = "DB"
database_name = "htmls-lib-db"

[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "htmls-lib-bucket"

[vars]
R2_BUCKET_NAME = "htmls-lib-bucket"

[assets]
directory = "./docs"
html_handling = "none"
run_worker_first = ["/", "/api/*", "/htmls", "/htmls/*", "/files", "/files/*", "/p/*", "/f/*", "/admin"]

# Secrets: ADMIN_TOKEN, R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY