跳转到内容
天空之翼
选择 · Enter跳转 · Esc关闭 由 Pagefind 提供索引
EN

系统架构

服务设计、数据模型、安全设计与存储提供商能力矩阵。

Picumet 是一个多云对象存储管理平台,全部运行在 Cloudflare 上。前端是 React 单页应用,由 Cloudflare Pages 托管;后端是跑在 Cloudflare Workers 上的 Hono API。API 把关系型元数据存在 D1 里,把短生命周期状态放在 KV 里,对象本身存放在 R2 或任何兼容 S3 协议的对象存储上,比如 AWS S3、Oracle Cloud。

后端按业务领域拆分,而不是按技术层次组织代码。每个服务自包含,自己带着 handlers、校验 schema、类型定义和领域逻辑。集中式的权限服务负责对每个请求做路径规则判定,存储服务把对象操作统一收在一层接口后面。这样跨服务的边界清晰,也不会出现循环依赖。

本文介绍组件拓扑、服务间依赖、关键设计决策、数据模型、存储提供商支持和安全设计。代码以 workers/src/frontend/src/ 为准。

  • 了解 Cloudflare Workers、D1、KV、R2 绑定。
  • 了解 S3 协议和预签名 URL 的基本概念。
  • 熟悉 TypeScript、React 和 Hono 框架。

服务间的依赖关系如下:

组件作用
Cloudflare Pages托管 React 单页应用和静态资源。
Cloudflare Workers承载实现全部业务逻辑的 Hono API。
D1存关系型元数据:用户、挂载点、文件、规则、会话、分享、配额和日志。
KV缓存短期状态:CSRF 令牌、限流计数、自由模式的凭据会话。
R2通过 Workers 绑定存放主提供商的对象。
S3 兼容提供商通过 S3 协议存放 AWS S3 和 Oracle Cloud 的对象。
Hono + Zod提供 HTTP 框架,并对每个 API 请求做运行时校验。
React + TanStack Query渲染界面,管理服务端状态、缓存和变更。
services/业务领域模块:认证、权限、文件、上传、分享、存储、WebDAV 等。
middleware/横切关注点:认证、CSRF、限流、安全响应头、自由模式守卫。
db/数据访问层,通过统一的 Db 接口同时覆盖 D1 和 node:sqlite
shared/公共类型、Zod schema、错误定义和统一响应格式。
utils/路径、加密、SSRF、SMTP、Base64 等工具函数。

上图遵循三条规则:

  • 凡是碰文件数据的服务,都依赖权限服务。
  • 凡是碰对象字节的服务,都依赖存储服务。
  • 认证服务是叶子节点:只有别的服务调用它,它不依赖任何其他服务。

整体结构不允许循环依赖。index.ts 只做路由和中间件的装配,不写业务逻辑。公开路由先注册,兜底的路由文件服务最后注册,负责在 GET /* 上直出文件对象。

决策选择备选方案理由
代码组织按业务领域划分服务按技术层次分层每个领域自包含、可单独测试;跨服务边界明确,循环依赖不会出现。
运行平台只用 Cloudflare Workers + Pages其他边缘运行时或自建服务器D1、R2、KV 绑定免去运维成本,免费额度够用;单一运行时不用再写一层可移植的平台抽象。
数据库后端生产用 D1,测试用 node:sqlite外部 PostgreSQL 或 SupabaseD1 零配置、延迟低;Db 抽象让测试套件在本地 node:sqlite 上运行,不依赖网络。
删除顺序先删元数据,再删对象对象和元数据一起删元数据删除是一个 D1 原子事务,快且可靠;对象删除慢且可能失败,放在事务提交后尽力执行,失败就记成孤儿对象等对账清理。
回收站不做:硬删除,前端二次确认带保留期的回收站或隔离区S3 兼容存储没有统一的回收站语义,做回收站要额外维护墓碑和延迟清理,对当前规模收益不大。
路径边界路径段级边界判断字符串前缀匹配isPathWithinBoundary 要求边界后紧跟 /,防止 /users/alice2 匹配到 /users/alice 这类前缀攻击。
存储协议统一一套 S3 协议凭据模型每个厂商写原生 SDKR2、AWS S3、Oracle 都讲 S3 协议,一套模型全覆盖;拒绝非 S3 协议(OSS、COS),省去维护多套驱动。
下载授权D1 原子消费的一次性令牌长期有效的签名 URLDELETE ... RETURNING 保证令牌只被消费一次,并发请求也复用不了;在网关处计数,下载次数准确。

每个服务目录结构一致:handlers.ts 放 API handlers,schemas.ts 放 Zod 校验,types.ts 放 TypeScript 类型,领域逻辑视需要单独成文件。所有 API 入参都经过 Zod 校验。

职责:用户注册、登录、登出,JWT 签发与验证,会话管理,邮箱验证,密码重置。

services/auth/
├── handlers.ts // 登录、注册、登出 handlers
├── schemas.ts // RegisterSchema、LoginSchema
└── types.ts // JwtPayload 与请求类型
方法路径说明
POST/api/auth/register创建账号。
POST/api/auth/login认证并写入 HttpOnly JWT Cookie。
POST/api/auth/logout清除会话。
GET/api/auth/me返回当前用户信息。
GET/api/auth/csrf-token签发 CSRF 令牌。
GET/api/auth/verify-email/api/auth/verify邮箱验证(别名)。
POST/api/auth/forgot-password发起找回密码。
POST/api/auth/reset-password重置密码。

依赖middleware/auth.tsmiddleware/rate-limit.tsmiddleware/csrf.tsutils/crypto.tsutils/smtp.ts,以及用户、配额、设置、日志等仓库。

职责:核心权限判定算法 checkPermission,路径规则查询与匹配,主体(Principal)构造,权限校验。

services/permissions/
├── check.ts // 核心算法、规则排序、规则加载
├── principal.ts // 从 Hono 上下文构造 Principal,requirePermission、can
└── types.ts // 权限相关类型

判定顺序从高到低:

  1. 管理员特权。
  2. 挂载边界检查。
  3. 用户根路径限制。
  4. API 密钥权限范围,取密钥权限与规则权限的交集。
  5. 路径规则,按主体特异度、路径特异度、显式优先级、effect 排序。
  6. 文件所有者权限回退。
  7. 默认拒绝。

两个关键安全边界:

  • isPathWithinBoundary 按路径段比较,不用 startsWith
  • API 密钥没有匹配规则时直接拒绝,系统不会默认放行。

依赖utils/path.ts、规则仓库、shared/errors.ts

职责:文件/文件夹列表、创建文件夹、文件详情、元数据更新(重命名)、删除、移动(Saga)、批量操作、密码验证、下载链接、多格式复制链接,以及 GET /* 公开路径直服。

services/files/
├── handlers.ts // 列表、文件夹、详情、更新、密码验证、下载链接、复制链接
├── operations.ts // 删除、移动、批量操作、任务状态
├── move.ts // 移动 Saga(复制→校验→原子切换→异步清理源)
├── path-serve.ts // 公开路径直服({origin}{虚拟路径})
├── schemas.ts // Update、CreateFolder、Move、Batch、VerifyPassword
└── types.ts

复制链接:GET /api/files/:id/copy-links 返回 formats: { direct, html, markdown, bbcode }direct 默认就是 {origin}{虚拟路径} 公开直链,由 path-serve.ts 直服;带 ?signed=true&expiresIn={秒} 时改为提供商预签名 URL,拿不到预签名就回退网关令牌。

公开路径直服:GET /* 按虚拟路径流式返回对象。公开挂载无需登录直接可读;私有挂载需要已登录且有下载权限的用户;带密码的文件直接返回 403

移动 Saga:moveWithSaga 先校验权限、冲突和循环,建任务,复制并校验对象,再原子切换元数据,最后异步清理源对象。主文件 API 和 WebDAV 的 MOVE 走同一条路径。

依赖permissions/principal.tsstorage/providers.tsshares/tokens.ts,以及文件、挂载、提供商、日志、任务等仓库。

职责:上传会话管理、单文件直传、分片上传、Worker 代理上传、配额预留/释放、完成校验(HEAD 防伪造)、幂等保证。兼容 PicGo、PicList。

services/uploads/
├── handlers.ts // 会话、代理上传、分片、完成、中止
├── compat.ts // PicGo 兼容上传(Bearer API Key / multipart)
├── schemas.ts // InitUploadSchema
└── types.ts

状态机:

  • 单文件:pending → uploading → verifying → completed,终态含 failedexpiredaborted
  • 分片:pending → uploading → parts_uploaded → completing → completed

依赖permissions/principal.tsstorage/providers.ts、会话/配额/文件/挂载/对账仓库、utils/path.tsutils/crypto.ts

职责:分享链接创建、列表、撤销,公开访问,密码验证,D1 原子消费的下载令牌,分享访问日志,下载网关。

services/shares/
├── handlers.ts // 创建、列表、公开信息、下载、预览、撤销
├── gateway.ts // /api/gateway/download/:token 流式代理
├── tokens.ts // 下载令牌(D1 原子消费)
├── schemas.ts // CreateShareSchema
└── types.ts

下载令牌:consumeDownloadTokenDELETE ... RETURNING 做单次原子消费,一次性令牌在并发下也无法重复使用。

依赖permissions/principal.tsstorage/providers.ts、分享/文件/挂载/提供商/日志仓库、utils/crypto.ts

职责:存储提供商抽象(R2 绑定 / S3 协议),对象操作(HEAD、GET、PUT、DELETE、COPY、List),分片上传,预签名 URL,连通性测试,多提供商切换。

services/storage/
├── providers.ts // 提供商工厂(getProvider、getProviderForMount)
├── r2.ts // R2BindingProvider,基于 env.R2 绑定
├── s3.ts // S3Provider(R2 S3 API / AWS S3 / Oracle)
└── types.ts // StorageProviderInterface 抽象接口

提供商选择:

  • type=r2 且未配置 endpoint 的走 R2BindingProvider(本地或生产 R2 绑定)。
  • 其余一律走 S3Provider,即 S3 协议客户端。

依赖:提供商、挂载仓库,utils/crypto.ts 负责解密密钥。

职责:WebDAV 协议,兼容 PicGo、PicList:PROPFIND、GET/HEAD、PUT、DELETE、MKCOL、MOVE、OPTIONS。Basic 认证用 API 密钥。

services/webdav/
├── handlers.ts // WebDAV 方法 handlers
└── types.ts // WebDAVResource、PropfindRequest

安全要点:

  • 每个方法都走 permissions/principal.ts 做路径级授权(PROPFIND 读、PUT 写、DELETE 删)。
  • MOVE 复用 files/move.ts 的移动 Saga,不直接改 file_metadata
  • 写入目标必须落在密钥的上传根目录内(assertWithinUploadRoot)。
  • XML href 统一经过 escapeXml 转义,防止注入和破坏 XML。
  • Basic 认证格式为 base64(keyId:secret)

依赖middleware/auth.ts(API 密钥认证)、permissions/principal.tsstorage/providers.tsfiles/move.tsutils/path.tsutils/crypto.ts

职责:用户自带对象存储凭据的临时会话(凭据加密写入 KV,短 TTL 自动清理)、文件列表、上传、删除、退出。

services/free-mode/
├── handlers.ts // init、files、upload、object、logout
├── schemas.ts // FreeModeInitSchema
└── types.ts

安全要点:

  • middleware/free-mode.ts 做跨站防护(Origin + Sec-Fetch-Site)、会话级 CSRF、IP + 用户双层 fail-closed 限流。
  • 路径和文件名校验拒绝 ..~、控制字符和反斜杠。
  • validateEndpoint 对 endpoint 做 SSRF 校验。
  • 凭据 AES-GCM 加密后写入 KV,接口不返回 auth_token,只给 fm_token 会话 ID。

依赖middleware/free-mode.tsstorage/providers.tsutils/crypto.tsutils/ssrf.tsutils/path.ts

职责:仪表板和统计、用户管理、全局分享、全部文件、访问日志、系统设置、公告、存储提供商、挂载点、权限规则。

services/admin/
├── handlers.ts // 仪表板、用户、分享、文件、日志、设置、公告
├── storage.ts // 存储提供商、挂载点、权限规则
├── schemas.ts // UserUpdate、Settings、Announcement
├── storage-schemas.ts // Provider、Mount、Rule
└── types.ts

依赖:用户、分享、日志、设置、公告、提供商、挂载、规则等仓库,storage/providers.tsutils/ssrf.ts

职责:个人资料、外观偏好、默认路径、修改密码。

services/users/
├── handlers.ts // /me/settings GET/PUT、/me/password PUT
├── schemas.ts // ProfileSchema、PasswordSchema
└── types.ts

依赖:用户、配额仓库,utils/crypto.ts

职责:API 密钥创建(密钥只显示一次)、列表、撤销、权限规则查询。

services/keys/
├── handlers.ts // POST/GET/DELETE /api/keys、GET /api/keys/rules
├── schemas.ts // CreateKeySchema
└── types.ts

安全要点:

  • uploadPath 规范化后作为密钥上传根边界,拒绝 ..~ 逃逸。
  • 密钥 token 只在创建时显示一次,库里存的是 sha256Hex 哈希。

依赖:API 密钥、规则仓库,utils/crypto.tsutils/path.ts

职责:站点设置、公告、健康检查,无需认证。

services/public/
├── handlers.ts // GET /api/public/settings、/announcements、/health
└── types.ts

依赖:设置、公告仓库。

职责:过期配额释放、移动源对象清理、过期分享标记、配额对账,由定时任务统一触发。

  • releaseExpiredReservations 释放过期上传会话占用的配额预留。
  • cleanupOldObjects 清理移动后遗留的源对象(source_cleanup_pending 标记)。
  • expireDueShares 把到期的分享标记为过期。
  • reconcileQuotas 纠正 used_storageused_files 计数。
  • runScheduledTasks 一次跑完四个任务,返回各自的处理数量。

依赖:会话、分享、文件、挂载、提供商、配额仓库,storage/providers.ts

跨服务的代码集中在四个地方:

  • shared/schemas.ts 提供公共 Zod schema:PathSchemaFileNameSchemaPaginationSchemaUUIDSchemaPasswordSchema
  • shared/types.ts 定义 PrincipalMountFileMetadataConditionsEnv 运行时绑定和 Hono 上下文变量。
  • shared/errors.ts 定义 ApiError,带 badRequestunauthorizedforbiddennotFoundconflicttooManyRequestsinternal 等静态构造。
  • shared/response.ts 提供 okerror,统一成 { success, data | error, timestamp } 响应格式。

中间件层处理横切关注点:

中间件作用
middleware/auth.tsauthMiddlewareoptionalAuthMiddlewareapiKeyAuthMiddlewareadminMiddleware,并暴露 getDbgetClientIp
middleware/csrf.ts对 Cookie 认证的写操作校验 CSRF 令牌。
middleware/rate-limit.tsKV 固定窗口限流。
middleware/free-mode.ts自由模式的跨站、CSRF、限流守卫。
middleware/global.ts初始化请求上下文、CORS、安全响应头。

db/ 是唯一的数据访问层。Db 类要么包 D1 后端(生产环境 Workers),要么包 node:sqlite 后端(本地测试),业务代码不感知用的是哪个。领域仓库在 db/repos/ 下:用户、配额、提供商、挂载、文件、会话、任务、规则、API 密钥、分享、日志、设置、公告、对账。

utils/ 下有 path.ts(规范化、边界判断、模式匹配)、crypto.ts(JWT、bcrypt、AES-GCM)、ssrf.ts(endpoint 校验)、smtp.tsbase64.ts

D1 里存以下核心表:

作用
users账号、角色、状态、默认路径、语言偏好。
user_quotas已用和预留的存储、文件数、上限。
storage_providersS3 协议提供商配置,凭据加密存储。
mounts把提供商映射到虚拟路径,带排序偏好。
file_metadata文件和文件夹:对象键、路径、大小、etag、属主、自定义属性。
upload_sessions记录上传进度、分片和预留配额。
operation_jobs异步的移动、复制、删除任务。
path_rules挂在挂载点上的权限规则。
api_keysAPI 密钥,含权限、协议和上传根目录。
shares分享链接,含密码、过期时间和访问限制。
download_tokens一次性下载令牌,原子消费。
access_logs上传、下载、删除、分享、密码验证等操作日志。
system_settings键值形式的站点设置。
announcements站点公告,以及每用户关闭记录。
reconciliation_reports对象与数据库对账产生的报告。
orphan_objects删除失败的对象,等待重试。

迁移脚本在 workers/migrations/ 下:

  • 0001_initial.sql 建基础表结构。
  • 0002_add_parts_and_download_tokens.sql 加分片字段和 download_tokens 表。
  • 0003_mount_id_and_session_version.sql 加挂载隔离和会话撤销字段。
  • 0004_smtp_and_otp.sql 加 SMTP 和一次性密码(OTP)邮箱验证。
术语定义
Principal(主体)执行操作的实体:用户、角色或 API 密钥。
Mount(挂载点)把存储提供商映射到虚拟路径,如 //backup
Provider(提供商)存储提供商,如 R2、AWS S3、Oracle Cloud。
Path Rule(路径规则)针对特定路径模式的权限策略,挂在挂载点上。
Object Key(对象键)对象在提供商存储桶里的实际存储路径。
Canonical Path(规范化路径)标准化处理后的虚拟路径。
Upload Session(上传会话)记录一次上传的状态和预留配额。
Quota Reserved(预留配额)上传开始时锁定的存储空间,保证配额够用。
Idempotency Key(幂等键)客户端生成的标识,防止重复操作。
Download Token(下载令牌)短期有效的授权令牌,一次下载一个。
Share Link(分享链接)让文件通过公开或密码访问的短链接。

支持的提供商:

提供商状态协议说明
Cloudflare R2支持S3免费额度每月 10GB 加 100 万次 A 类操作,无出站流量费。
AWS S3支持S3标准 S3 协议。
Oracle Cloud支持S3兼容 S3 API;自定义域名需配合 CloudFront 等 CDN。
阿里云 OSS不支持OSS协议不兼容。
腾讯云 COS不支持COS协议不兼容。

能力矩阵:

能力R2S3Oracle
List Objects支持支持支持
Head Object支持支持支持
Get Object支持支持支持
Range Get支持支持支持
Put Object支持支持支持
Multipart Upload支持支持支持
Abort Multipart支持支持支持
Copy Object支持支持支持
Delete Object支持支持支持
预签名 URL支持支持支持
公开 URL支持支持支持
自定义域名支持支持支持(需 CDN)
Checksum(MD5)支持支持支持

凭据模型:所有提供商共用一套 S3 协议配置,不做按提供商的 discriminated union。配置包含 typenameendpointregionbucketaccessKeyIdsecretAccessKey,可选 publicDomainuploadDomainpathPrefix。凭据用 AES-GCM 加密存储,只有构建提供商客户端时才解密。type=r2 且没有 endpoint 的提供商直接用 Workers 的 R2 绑定。

安全模型在请求链路的每一层都做了纵深防御:

  • 认证:JWT 放在 HttpOnly Cookie 里,SameSite=Strict,客户端不写 localStorage
  • 会话撤销users.session_version 写入 JWT。登出、改密、重置密码、管理员禁用账号都会递增版本号,旧 JWT 立即失效。
  • 挂载隔离path_rules.mount_id 把规则绑定到挂载点(NULL 表示全局);权限查询按挂载过滤,同路径不同挂载的规则互不影响。
  • 初始凭据:生产环境必填 ADMIN_PASSWORD(至少 12 位强密码),未配置就 fail-closed 不创建管理员,没有硬编码默认凭据。
  • CSRF:写操作要求 X-CSRF-Token,在 KV 里校验;API 密钥认证可绕过。
  • 限流:KV 固定窗口计数,认证和敏感写接口 fail-closed。
  • 路径遍历normalizePathisPathWithinBoundary,按路径段判断。
  • SSRFvalidateEndpoint 做 scheme、端口白名单,并拦截私网 IPv4/IPv6 段;部署时建议配合 egress 白名单。
  • 对象投毒:完成上传时强制 HEAD 校验,核对 ETag 和大小。
  • XSS:文件名严格校验、React 自动转义、代码预览用 highlight.js 预转义。
  • SQL 注入:全部使用参数化查询。
  • 下载令牌:D1 原子消费,一次性令牌不可复用。
  • 分享下载计数:签发令牌不计数,网关消费令牌时才计一次,超限返回 410
  • 分享密码POST /api/shares/:id/verify 种短期授权 Cookie,密码不进 URL。
  • 大文件内存:PicGo 兼容上传、WebDAV、自由模式都流式转发请求体,不整包进内存。
  • 就绪探针/api/public/health/live/ready 做探针;生产未初始化时业务接口返回 503
  • CSPscript-src 'self' https://challenges.cloudflare.com,不含 unsafe-inline