cr4sync
cr4sync 是一个独立的命令行二进制 + 可选的多用户 HTTP API 服务,围绕 Cloudreve v4 实例提供两类核心能力:
- 本地 ↔ Cloudreve 同步——一次性同步 / 增量持续监听 / 双向同步,统一走
sync-core的SyncEngine。 - 第三方网盘 → Cloudreve 单向迁移——目前支持百度网盘,真流式管线 + 断点续传 + 端到端一致性校验。
启用 server cargo feature 还可以以 HTTP API 形式把上述迁移能力暴露给多个 Cloudreve 用户,附带管理员审计能力。
两种使用形态
| 形态 | 适用场景 | 凭证落盘位置 | 数据库 |
|---|---|---|---|
| CLI | 站长本机单人使用 | ./credential/token.json + ./credential/sources/baidu.json | ./credential/migrate.db |
| Server | 主程序插件,向多用户暴露 HTTP API | server.db 内 source_credential 表(行级隔离) | ./credential/server.db |
两种形态完全隔离:CLI 模式绝不读 server.db,server 模式绝不读 token.json(P0 不变量,有静态扫描守护测试)。
文档结构
- 配置:
cr4sync.toml完整示例 + 字段参考 + 凭证目录约定。 - CLI:所有子命令的参数、行为、配置依赖。
- Server:HTTP API 端点、鉴权、envelope、SSE wire format、admin 端点、server.db schema。
设计原则
- 配置一份:CLI 与 server 共用
[serve.cr4]段描述 Cloudreve 后端,差异仅在[serve](监听地址端口)。 - token 隔离(P0):server 路径上传 Cloudreve 必须使用调用方现场注入的 JWT,绝不读取站长
token.json。 - 行级隔离:server.db 所有 Repo 方法签名首参数必带
user_id,admin 跨用户操作走专门的*_admin方法。 - 审计可查:server 所有写操作(POST/PUT/DELETE)落
audit_log表,含 actor / action / target / payload(脱敏)。
文档约定
- 文档中所有 JSON 示例都是 envelope 已脱壳后的形态,即响应 body 是
{ "code": 0, "msg": "ok", "data": <例所示>, "request_id": "..." }。 - 时间戳一律为 Unix 秒 (i64),本机时区由客户端自行换算。
user_id类型恒为字符串(Cloudrevesub字段直透)。
安装与编译
前置
- Rust toolchain (edition 2024)
- 与
sync-core同级目录(如native/cr4sync+native/sync-core),靠path = "../sync-core"复用同步内核
编译
cd path/to/cr4sync
# 仅 CLI(默认), 体积小, 无 axum/tower 依赖
cargo build --release
# CLI + HTTP server 子命令
cargo build --release --features server
# 产物
ls target/release/cr4sync
cr4sync 是独立 cargo workspace(
Cargo.toml自带空[workspace]表),不并入上级 native 工作区,不共享其Cargo.lock。
feature 矩阵
| feature | 包含 | 二进制大小 | 额外依赖 |
|---|---|---|---|
| (默认) | CLI: login/sync/up/dl/del/ls/migrate | 较小 | reqwest, rusqlite, indicatif 等 |
server | 上述 + cr4sync serve | 大约 +30% | axum 0.7, tower-http, moka, ulid, tokio-stream |
CI 矩阵建议两条都跑:
cargo test # CLI only
cargo clippy --all-targets -- -D warnings
cargo test --features server # CLI + server
cargo clippy --all-targets --features server -- -D warnings
安装到系统
install -Dm755 target/release/cr4sync /usr/local/bin/cr4sync
文档构建(本文档本身)
cd path/to/cr4sync/doc
mdbook build # 输出到 ./book/
mdbook serve --open # 本地预览, 自动 reload
需要 mdbook:
cargo install mdbook
完整配置示例
下面是一份带全部已知字段的 cr4sync.toml,未填的行用 # 注释表明缺省值。把它放到工作目录或用 cr4sync -c /path/to/cr4sync.toml 指定。
# cr4sync 配置文件. 注释行可按需取消并填写.
# ============================================================================
# [serve] / [serve.cr4]
# ============================================================================
# - [serve] cr4sync serve 子命令 HTTP 监听 (CLI 用户可全省)
# - [serve.cr4] Cloudreve 后端连接 (CLI 与 server 都强依赖)
[serve]
# address = "127.0.0.1" # serve 监听地址 (缺省 127.0.0.1)
# port = 5731 # serve 监听端口 (缺省 5731)
[serve.cr4]
server_url = "https://demo.cloudreve.org" # 必填, 含 http(s) 协议
# server_port = 443 # 缺省按协议推断 (http=80 / https=443)
# server_api_path = "/api/v4" # 缺省 /api/v4
# admin_group = "Admin" # admin 组名覆盖; 缺省识别 Admin / 管理员
# ============================================================================
# [credential]
# ============================================================================
# 危害提示: 此处明文保存账密有泄露风险 (配置文件常被备份 / 纳入版本控制).
# 推荐留空本段, 直接执行 `cr4sync login --mail x --passwd y` 完成一次性登录,
# 凭证仅落盘到 ./credential/token.json (已 .gitignore).
# 若在此填了 mail/passwd: token.json 不存在时仍需先执行 `cr4sync login`
# (此时无需再带 --mail/--passwd).
[credential]
# mail = ""
# passwd = ""
# ============================================================================
# [db]
# ============================================================================
[db]
type = "sqlite3" # 恒定
# path = "./data" # 缺省 ./data, sync-core sqlite 落到 <path>/sync_core/datas/.sync_db.sqlite3
# ============================================================================
# [conf]
# ============================================================================
[conf]
# retry = 3 # 全局重试次数 (仅 up/dl 子命令使用), 缺省 3
# ============================================================================
# [sync] -- cr4sync sync / SyncEngine 编排
# ============================================================================
[sync]
src = "/home/data" # 必填, 本地源目录, 不存在则终止
target = "/" # 必填, 远端目标 (/ -> cloudreve://my)
# worker = 4 # 缺省 CPU 核数
# parallelism = 3 # 缺省 3, 上限 512
# sync_mode = "single" # single (缺省) / incremental / append / two_way
# sync_conflict = "skip" # skip (缺省, 真 no-op) / overwrite (本地覆盖远程)
# ============================================================================
# [log]
# ============================================================================
[log]
# mode = "cr4sync" # cr4sync (缺省, 仅 cr4sync + sync_core) / full (输出所有依赖)
# level = "info" # trace/debug/info/warn/error
# log_path = "./logs/sync_core.log"
# worker_log_terminal = false # sync/up/dl 是否在终端输出 worker 日志 (缺省 false)
# server_log_path = "./logs/server.log" # serve 子命令日志 (隔离), 仅 server 使用
# ============================================================================
# ===== migrate 子命令 (第三方网盘/对象存储 -> Cloudreve 单向迁移) =====
# ============================================================================
# 仅当使用 `cr4sync migrate` (alias `mgr`) 或 server 的迁移端点时需要.
[migrate]
# 预取下一 chunk 与上传重叠, 消除"下载等上传"停顿. CLI 与 server 均生效.
# auto(默认): chunk_size ≤ 100MB 自动预取; 大 chunk 不预取避免内存翻倍
# always: 强制预取 never: 永不预取
prefetch = "auto"
# 单 chunk 上传/下载失败重试次数, 默认 3, 上限 10. 指数退避 2s -> 4s -> 8s -> 16s -> 32s -> 60s (cap).
# 仅重试网络/服务端瞬时故障 + 锁冲突; 鉴权失败 / 一致性错 / 配置错不重试.
retry = 3
# cloudreve 锁冲突 (错误码 40073) 处置:
# auto(默认): 强制解锁 (DELETE /file/lock) -> 重试; 仍失败则跳过该 task (标 failed)
# skip: 直接跳过 (不解锁, 标 failed)
# force_unlock: 强制解锁 -> 重试; 仍失败则中止整个 job (其他未跑 task 不再发起)
conflict = "auto"
# 全局并发 job 上限 (server 模式), 默认 10, 上限 512; 调大需注意带宽/内存
# max_concurrent_jobs = 10
# 迁移权限申请审批模式: auto(申请即生效) / manual(管理员审批, 缺省)
# approve = "manual"
# PikPak range GET 限流间隔 (秒), 缺省 0.5. 出现 503 时可加大 (如 1.0). 仅 PikPak 生效.
# pikpak_range_optimization = 0.5
[sources.baidu]
# 三个字段缺省走内置 Alist 公开应用 (多人共享配额).
# 用户自己申请的开发者应用可在此覆盖, 独享配额.
# client_id = ""
# client_secret = ""
# redirect_uri = ""
# refresh_token 一次性导入. 留空走 `cr4sync login --baidu` 交互式粘贴 (推荐).
# refresh_token = ""
# is_proxy = false # 是否启用代理 (代理在 source_credential 级)
# proxy = "my-socks5" # 代理名 (引用 [[proxy]].name, is_proxy=true 时必填)
[sources.onedrive]
# OneDrive (Microsoft Graph) 无内置默认应用, 必须自备 Azure 应用.
# server 模式下用户在 login-session 传入, 此处为全局默认.
# client_id = ""
# client_secret = ""
# redirect_uri = ""
# national_cloud = "global" # global / china (21Vianet)
# is_proxy = false # 是否启用代理 (代理在 source_credential 级)
# proxy = "my-socks5" # 代理名 (引用 [[proxy]].name)
[sources.aliyun]
# 阿里云盘支持中转模式(推荐, 免注册)和直连模式(需自备开放平台应用).
# --- 中转模式 (推荐) ---
# 无需自备开放平台应用. 从扫码页拿到 refresh_token 填入即可.
# 扫码页: https://alistgo.com/zh/tool/aliyundrive/request.html
# client_id = "" # 留空则走中转模式
# client_secret = ""
# redirect_uri = ""
# oauth_token_url = "https://api.alistgo.com/alist/ali_open/token" # 中转端点, 缺省 alist 官方中转
# refresh_token = "your-refresh-token-from-scan-page" # 扫码页获取的 refresh_token
# drive_type = "resource" # resource (资源盘, 缺省) / backup (备份盘)
# --- 直连模式 (自备应用) ---
# 需自备阿里云盘开放平台应用, 填 client_id/secret/redirect_uri.
# client_id = "your-app-id"
# client_secret = "your-app-secret"
# redirect_uri = "https://your-callback.com"
# drive_type = "resource" # resource (资源盘, 缺省) / backup (备份盘)
# 直连模式下 oauth_token_url 和 refresh_token 忽略
[sources.s3]
# S3 兼容对象存储 (AWS S3 / MinIO 等), 无 OAuth, 静态凭证.
access_key_id = "AKIAIOSFODNN7EXAMPLE"
secret_access_key = "wJalrXUtFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
region = "us-east-1"
bucket = "my-s3-bucket"
# endpoint = "s3.us-east-1.amazonaws.com" # 缺省 SDK 自动推断
# force_path_style = false # MinIO 需 true
# is_proxy = false # 是否启用代理 (代理在 source_credential 级)
# proxy = "my-socks5" # 代理名 (引用 [[proxy]].name)
[sources.oss]
# 阿里云 OSS, endpoint 自动拼为 https://oss-{region}.aliyuncs.com
access_key_id = "LTAI5t..."
secret_access_key = "xxxxxxxxxxxx"
region = "cn-hangzhou"
bucket = "my-oss-bucket"
# is_proxy = false # 是否启用代理 (代理在 source_credential 级)
# proxy = "my-socks5"
[sources.cos]
# 腾讯云 COS, endpoint 自动拼为 https://cos.{region}.myqcloud.com
# bucket 必须含 APPID: <bucketname>-<APPID>
access_key_id = "AKID..."
secret_access_key = "xxxxxxxxxxxx"
region = "ap-guangzhou"
bucket = "my-bucket-1250000000"
# force_path_style = true # COS 缺省 true
# is_proxy = false # 是否启用代理 (代理在 source_credential 级)
# proxy = "my-socks5"
[sources.pikpak]
# PikPak driver, 账密直接登录 (非 OAuth), 需代理访问.
email = "user@pikpak.com" # PikPak 账号邮箱
password = "your-password" # PikPak 密码
# refresh_token = "" # 已持有的 refresh_token (可选)
# device_id = "" # 设备 ID, 缺省 MD5(email+password)
# captcha_token = "" # 验证码 token (可选, 缺省服务端自动获取)
# is_proxy = true # 是否启用代理 (PikPak 国内访问需代理)
# proxy = "my-socks5" # 代理名
[sources.cloudreve]
# Cloudreve 源 driver (远端 cloudreve 实例作为迁移源). 复用 sync-core ApiClient + AuthClient.
# CLI 登录: cr4sync login --cloudreve (探测验证码, 开启则报错"cli 不支持, 用 serve 模式").
# serve 模式: webui 登录复用 CaptchaWidget (去二维码, 支持验证码/2FA).
# token 存 credential/sources/cloudreve.json (CLI) 或 source_credential 表 (server).
server_url = "https://demo.cloudreve.org" # 必填, 源端 cloudreve 地址 (不带 /api/v4)
# username = "admin@example.com" # 邮箱 (CLI 登录用; serve 模式 webui 输入)
# password = "your-password" # 密码 (CLI 登录用; serve 模式 webui 输入)
# referer = "https://demo.cloudreve.org" # download/login Referer, 缺省 = server_url
# user_agent = "Mozilla/5.0 ..." # User-Agent, 缺省 pikpak web UA; serve 模式前端自动填浏览器 UA
# is_proxy = false
# proxy = "my-socks5"
# ============================================================================
# ===== [[proxy]] 代理配置 (数组段, 可多个) =====
# ============================================================================
# 通用代理, 为第三方网盘 driver 访问提供代理出站能力.
# 代理在 source_credential 级 (driver 登录时绑定), 通过 [sources.xxx] 的 is_proxy + proxy 引用.
# [[proxy]]
# name = "my-socks5" # 必填, 用户内唯一
# scheme = "socks5" # http / https / socks5 / socks5h
# host = "192.168.1.100" # 代理主机地址
# port = 1080 # 代理端口
# username = "user1" # 代理认证用户名 (可选)
# password = "pass123" # 代理认证密码 (可选)
# [[proxy]]
# name = "corp-http"
# scheme = "http"
# host = "proxy.corp.com"
# port = 3128
# 一个 [[storage]] 段 = 一个独立的迁移任务实例.
# 可以重复出现多次, 各自用不同的 name 区分, `cr4sync mgr -s <name>` 选择.
[[storage]]
name = "main-baidu" # 必填, 全局唯一
driver = "baidu" # baidu / onedrive / aliyun / s3 / oss / cos / pikpak
root_path = "/我的资源" # 源端起点 (缺省 /)
target_path = "cloudreve://my/baidu-backup" # 必填, 目标 cloudreve URI
parallelism = 1 # 单实例并发 (缺省 1, 上限 8)
rate_limit = 3 # 源端 API QPS 上限 (缺省 3)
# is_proxy = false # 已废弃, 请在 [sources.xxx] 配置代理; 保留字段兼容旧配置
# proxy = "my-socks5" # 已废弃, 请在 [sources.xxx] 配置代理; 保留字段兼容旧配置
# 再加一个不同根的实例:
# [[storage]]
# name = "music"
# driver = "baidu"
# root_path = "/音乐"
# target_path = "cloudreve://my/music-backup"
# S3 / OSS / COS storage 示例:
# [[storage]]
# name = "main-s3"
# driver = "s3"
# root_path = "/"
# target_path = "cloudreve://my/s3-backup"
# [[storage]]
# name = "main-oss"
# driver = "oss"
# root_path = "/"
# target_path = "cloudreve://my/oss-backup"
# [[storage]]
# name = "main-cos"
# driver = "cos"
# root_path = "/"
# target_path = "cloudreve://my/cos-backup"
# OneDrive / 阿里云盘 storage 示例:
# [[storage]]
# name = "main-onedrive"
# driver = "onedrive"
# root_path = "/Documents"
# target_path = "cloudreve://my/onedrive-backup"
# [[storage]]
# name = "main-aliyun"
# driver = "aliyun"
# root_path = "/文档"
# target_path = "cloudreve://my/aliyun-backup"
# [license]
# path = "./credential/license.lic" # license 文件路径 (缺省 ./credential/license.lic)
server 模式下
[[storage]]段会被忽略——server 的 storage 改由POST /api/v4/cr4sync/source/:driver/storage端点(driver ∈ baidu/onedrive/aliyun/s3/oss/cos/pikpak)写入server.db的storage表,每用户独立。CLI 仍以[[storage]]段为准。
字段参考
下表枚举 cr4sync.toml 所有已知字段的类型、可选性、默认值与校验规则。
[serve]
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
address | string | 否 | "127.0.0.1" | cr4sync serve 监听地址。CLI 用户可全省。必须是合法 IPv4/IPv6 字面值。 |
port | u16 | 否 | 5731 | 监听端口。 |
[serve.cr4]
CLI 与 server 都强依赖的 Cloudreve 后端配置。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
server_url | string | 是 | — | Cloudreve 完整 URL,必须带 http:// 或 https:// 协议头。 |
server_port | u16 | 否 | 按协议推断(http=80 / https=443) | 自定义端口时填写。 |
server_api_path | string | 否 | "/api/v4" | API 前缀。 |
admin_group | string | 否 | 内置识别 "Admin" / "管理员" | server 模式 admin 组名覆盖。自定义 Cloudreve admin 组名时填写;填写后只认该值。 |
旧版本兼容:若
[serve]段直接出现server_url/server_port/server_api_path(旧的扁平结构),loader 会拒绝并提示迁移到嵌套的[serve.cr4]段。
[credential]
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
mail | string | 否 | — | 明文账号。仅作为 cr4sync login 不带 --mail 时的回退源。 |
passwd | string | 否 | — | 明文密码。同上。 |
强烈建议留空:把账密留在配置文件里有泄露风险(备份 / 版本控制)。改用 cr4sync login --mail x --passwd y 一次性登录,凭证只落盘到 ./credential/token.json(已 .gitignore)。
[db]
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
type | string | 否 | "sqlite3" | 数据库类型,目前恒定 sqlite3,预留扩展。 |
path | path | 否 | "./data" | sync-core 数据目录;sqlite 文件落到 <path>/sync_core/datas/.sync_db.sqlite3。 |
[conf]
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
retry | u32 | 否 | 3 | 全局重试次数。仅 up / dl 子命令使用;sync 由 sync-core 自己管。 |
[sync]
cr4sync sync / SyncEngine 编排参数。
| 字段 | 类型 | 必填 | 默认 | 校验 / 说明 |
|---|---|---|---|---|
src | path | 是 | — | 本地源目录,不存在则 cr4sync 启动失败。 |
target | string | 是 | — | 远端目标路径,/ 自动映射为 cloudreve://my。 |
worker | usize | 否 | CPU 核数 | sync-core WorkerPool 容量。 |
parallelism | usize | 否 | 3 | 最大并发传输数,范围 1..=512。 |
sync_mode | string | 否 | "single" | single / incremental / append / two_way 之一。 |
sync_conflict | string | 否 | "skip" | skip(真 no-op) / overwrite(本地覆盖远端) 之一。中文别名 跳过 / 覆盖 也接受。 |
模式映射:
| sync_mode | sync-core SyncMode | 行为 |
|---|---|---|
single | UploadOnly | 仅上传,跑完即退 |
incremental | UploadOnly | 仅上传,初始 + 持续监听 |
append | UploadOnly | 跳过初始,仅持续监听 |
two_way | Full | 双向(上传 + 下载 + 冲突策略) |
[log]
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
mode | string | 否 | "cr4sync" | cr4sync(仅本程序 + sync_core)/ full(所有依赖按 level 输出,调试网络问题用)。 |
level | string | 否 | "info" | trace/debug/info/warn/error。运行期可热更新。 |
log_path | path | 否 | "./logs/sync_core.log" | 主日志文件。 |
worker_log_terminal | bool | 否 | false | sync / up -r / dl -r 是否在终端打 worker 日志(缺省 false,避免大量小文件刷屏)。 |
server_log_path | path | 否 | "./logs/server.log" | 仅 cr4sync serve 使用;HTTP 访问 + 审计的额外 layer 输出。 |
[migrate]
迁移管线调优。CLI 与 server 均生效。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
prefetch | string | 否 | "auto" | 预取策略: auto(chunk_size ≤ 100MB 自动预取) / always(强制) / never(串行) |
retry | u32 | 否 | 3 | 单 chunk 上传/下载失败重试次数, 上限 10. 指数退避 2s→4s→8s→16s→32s→60s(cap). 仅重试可恢复错误 (网络/服务端瞬时故障 + 锁冲突); 鉴权失败 / 一致性错 / 配置错不重试. |
conflict | string | 否 | "auto" | cloudreve 锁冲突 (错误码 40073) 处置: auto(强制解锁+重试→失败则跳过) / skip(直接跳过) / force_unlock(强制解锁+重试→失败则中止整个 job) |
max_concurrent_jobs | u32 | 否 | 10 | server 模式全局并发 job 上限,范围 1..=512。超出上限的 job 在后台排队等待(status = "queued")。调大需注意带宽/内存。 |
approve | string | 否 | "manual" | 迁移权限申请审批模式:"auto"(申请即生效) / "manual"(管理员审批)。仅 server 模式生效。 |
pikpak_range_optimization | f64 | 否 | 0.5 | PikPak range GET 限流间隔(秒)。PikPak CDN 对同一用户的并发 range GET 返 503,限流避免瞬时并发。迁移出现 503 时可加大此值(如 1.0)。仅 PikPak 源生效,其他源不受限。 |
auto模式: 首个 upload 拿到 cloudreve 返回的chunk_size后判断, ≤100MB 则 upload N 与 download N+1 并发 (双缓冲, 消除停顿); 大 chunk 不预取避免内存翻倍 (8 并发 × 100MB × 2 ≈ 1.6GB)。always强制预取 (站长确认内存足够)。never与改造前行为一致。
retry退避序列: 第 1 次重试等 2s, 第 2 次等 4s, 第 3 次等 8s, 第 4 次等 16s, 第 5 次等 32s, 第 6 次及之后等 60s (cap)。max_retry=10意味着最多 1 次首尝试 + 10 次重试 = 11 次。
conflict三种模式:
auto(缺省): 命中锁冲突 → 调DELETE /file/lock强制解锁 → 退避后重试; 重试retry次后仍失败则标 task failed 跳过 (其他 task 继续).skip: 命中锁冲突 → 立即标 task failed 跳过, 不解锁不重试.force_unlock: 命中锁冲突 → 强制解锁 → 重试; 仍失败则中止整个 job (其他未跑 task 不再发起). 用于“宁可整体重跑也不允许跳过“的严格场景.
[sources.baidu]
仅 migrate / server 迁移端点用。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
client_id | string | 否 | 内置 Alist 公开应用 | OAuth client_id,独享配额时填。 |
client_secret | string | 否 | 同上 | OAuth client_secret。 |
redirect_uri | string | 否 | 同上 | OAuth redirect_uri。 |
refresh_token | string | 否 | — | 一次性导入用。强烈建议留空走 cr4sync login --baidu 交互式粘贴。 |
is_proxy | bool | 否 | false | 是否启用代理(代理在 source_credential 级,driver 凭证级) |
proxy | string? | 否 | null | 代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填) |
server 模式百度凭证写哪里:CLI 落盘到
./credential/sources/baidu.json;server 落到server.db的source_credential表(每用户一行)。两者互不干扰。
[sources.onedrive]
仅 server 迁移端点用(CLI 模式 OneDrive 需通过 API 完成 OAuth,不走配置文件)。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
client_id | string | 否 | — | Azure 应用 client_id。server 模式下用户在 login-session 中传入,此处为全局默认。 |
client_secret | string | 否 | — | Azure 应用 client_secret。同上。 |
redirect_uri | string | 否 | — | OAuth 回调地址。同上。 |
national_cloud | string | 否 | "global" | OneDrive 云类型:"global"(国际版)或 "china"(21Vianet 世纪互联版)。 |
is_proxy | bool | 否 | false | 是否启用代理(代理在 source_credential 级,driver 凭证级) |
proxy | string? | 否 | null | 代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填) |
OneDrive 无内置默认应用凭证,用户必须自备 Azure 应用。server 模式下 session 创建时传入的凭证优先级高于配置文件。
[sources.aliyun]
仅 server 迁移端点用。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
client_id | string | 否 | — | 阿里云盘开放平台 app_id。server 模式下用户在 login-session 中传入,此处为全局默认。显式填 client_id 时走直连模式(POST openapi.alipan.com/oauth/refresh_token)。 |
client_secret | string | 否 | — | 开放平台 app_secret。同上。 |
redirect_uri | string | 否 | — | OAuth 回调地址。同上。 |
oauth_token_url | string | 否 | "https://api.alistgo.com/alist/ali_open/token" | 中转 refresh 端点(alist 官方中转)。中转模式用 GET ?refresh_ui=...&server_use=true&driver_txt=alicloud_qr 换 token。显式填 client_id 时走直连,此字段忽略。 |
refresh_token | string | 否 | — | 阿里云盘 refresh_token。中转模式(无 client_id)时直接填入,免自备开放平台应用。 |
drive_type | string | 否 | "resource" | 仅 aliyun driver 使用。"resource"(资源盘,网页端默认)/ "backup"(备份盘)。选择访问资源盘还是备份盘。 |
is_proxy | bool | 否 | false | 是否启用代理(代理在 source_credential 级,driver 凭证级) |
proxy | string? | 否 | null | 代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填) |
阿里云盘支持两种模式:中转模式(推荐,无 client_id 时走 alist 中转端点,无需自备开放平台应用,扫码页拿 refresh_token 即可)和 直连模式(显式填 client_id 时直连 openapi.alipan.com,需自备开放平台应用)。server 模式下 session 创建时传入的凭证优先级高于配置文件。 中转模式扫码页:https://alistgo.com/zh/tool/aliyundrive/request.html。扫码授权后拿到 refresh_token 填入
refresh_token字段即可。is_proxy/proxy可选字段同[sources.onedrive](代理在 source_credential 级,driver 凭证级)。
[sources.s3]
S3 兼容对象存储(AWS S3 / MinIO 等),无 OAuth,静态凭证。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
access_key_id | string | 是 | — | S3 access key ID |
secret_access_key | string | 是 | — | S3 secret access key |
endpoint | string | 否 | 自动推断 | 自定义端点。AWS S3 可省(SDK 自动选);MinIO 等必填。 |
region | string | 是 | — | 区域(如 us-east-1) |
bucket | string | 是 | — | 存储桶名 |
force_path_style | bool | 否 | false | 强制 path-style URL(MinIO 需 true) |
is_proxy | bool | 否 | false | 是否启用代理(代理在 source_credential 级,driver 凭证级) |
proxy | string? | 否 | null | 代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填) |
server 模式:access_key_id 明文存
source_credential.access_token;secret_access_key 经 token_crypto 加密存refresh_token;endpoint/region/bucket/force_path_style 存extra_configJSON 列。
[sources.oss]
阿里云 OSS,内部复用 S3Client,endpoint 默认从 region 自动拼接。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
access_key_id | string | 是 | — | 阿里云 RAM access key ID |
secret_access_key | string | 是 | — | 阿里云 RAM secret access key |
region | string | 是 | — | 区域(如 cn-hangzhou)。误填 oss-cn-hangzhou 会被规范化去掉 oss- 前缀。endpoint 自动拼为 https://oss-{region}.aliyuncs.com |
bucket | string | 是 | — | OSS bucket 名 |
endpoint | string | 否 | 自动拼接 | 覆盖模板 endpoint(自建 OSS 兼容服务时用) |
is_proxy | bool | 否 | false | 是否启用代理(代理在 source_credential 级,driver 凭证级) |
proxy | string? | 否 | null | 代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填) |
force_path_style 固定 false(阿里云原生 API)。region 规范化:填
oss-cn-hangzhou会存为cn-hangzhou,避免 endpoint 重新推导时拼成oss-oss-cn-hangzhou.aliyuncs.com双前缀。
[sources.cos]
腾讯云 COS,内部复用 S3Client,endpoint 默认从 region 自动拼接。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
access_key_id | string | 是 | — | 腾讯云 CAM access key ID(SecretId) |
secret_access_key | string | 是 | — | 腾讯云 CAM secret access key(SecretKey) |
region | string | 是 | — | 区域(如 ap-guangzhou)。误填 cos-ap-guangzhou 会被规范化去掉 cos- 前缀。endpoint 自动拼为 https://cos.{region}.myqcloud.com |
bucket | string | 是 | — | COS bucket 名,必须含 APPID(如 my-bucket-1250000000) |
endpoint | string | 否 | 自动拼接 | 覆盖模板 endpoint |
force_path_style | bool | 否 | true | COS 缺省 true(path-style 兼容) |
is_proxy | bool | 否 | false | 是否启用代理(代理在 source_credential 级,driver 凭证级) |
proxy | string? | 否 | null | 代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填) |
COS bucket 名格式为
<bucketname>-<APPID>,APPID 在腾讯云控制台可查。region 规范化同 OSS:填cos-ap-guangzhou会存为ap-guangzhou。
[sources.pikpak]
PikPak driver,账密直接登录(非 OAuth),需代理访问。
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
email | string | 是 | — | PikPak 账号邮箱 |
password | string | 是 | — | PikPak 密码 |
refresh_token | string? | ✗ | — | 已持有的 refresh_token(可选,提供则跳过账密登录直接刷新) |
device_id | string? | ✗ | MD5(email+password) | 设备 ID,用户可覆盖 |
captcha_token | string? | ✗ | — | 验证码 token(可选,缺省由服务端自动获取) |
is_proxy | bool | 否 | false | 是否启用代理(代理在 source_credential 级,driver 凭证级) |
proxy | string? | 否 | null | 代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填) |
PikPak 国内访问不友好,建议配置代理。PikPakAuth + PikPakClient 都接受 proxy 参数,用
build_proxied_http_client注入。
[sources.webdav]
WebDAV 通用网盘兜底 driver. 支持所有 WebDAV 协议服务 (Nextcloud / Alist / 坚果云等). 匿名访问和认证访问都支持.
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
endpoint | string | 是 | - | WebDAV 服务终结点, 如 http://192.168.1.100:8080/dav/ |
username | string | 否 | - | 用户名, 匿名访问时不填 |
password | string | 否 | - | 密码, 匿名访问时不填 |
auth_scheme | string | 否 | 自动探测 | basic / digest / anonymous; 缺省首次请求时自动探测 |
is_proxy | bool | 否 | false | 代理配置 (source_credential 级) |
proxy | string | 否 | - | 引用 [[proxy]].name |
server 模式:
auth_scheme由 credential 端点自动探测并存库; CLI 模式可手动指定, 不填则探测.
[sources.cloudreve]
Cloudreve 源 driver (源端 cloudreve 实例作为迁移源). 复用 sync-core ApiClient + AuthClient (登录/fs/download). download 带 Referer + User-Agent (防盗链).
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
server_url | string | 是 | - | cloudreve 服务地址, 如 https://demo.cloudreve.org (不带 /api/v4) |
username | string | 否 | - | 邮箱 (CLI 登录用 cr4sync login --cloudreve); serve 模式 webui 登录 |
password | string | 否 | - | 密码 (CLI 登录用); serve 模式不持久化密码 (仅 token) |
referer | string | 否 | server_url | download / login 的 Referer; 缺省 = server_url |
user_agent | string | 否 | 浏览器 UA | User-Agent; 缺省 pikpak web UA; 前端可填 navigator.userAgent |
is_proxy | bool | 否 | false | 代理配置 (source_credential 级) |
proxy | string | 否 | - | 引用 [[proxy]].name |
CLI 登录:
cr4sync login --cloudreve探测验证码, 开启则报错“cli 不支持, 用 serve 模式“; 2FA 支持. serve 模式 webui 登录复用 CaptchaWidget (去二维码, 支持验证码/2FA). token 存credential/sources/cloudreve.json.
[[storage]](数组段,可多个)
仅 CLI migrate 用。server 忽略此段,server 的 storage 在 server.db 里。
| 字段 | 类型 | 必填 | 默认 | 校验 / 说明 |
|---|---|---|---|---|
name | string | 是 | — | 实例名,全局唯一,仅 ASCII 字母数字 / _ / -。 |
driver | string | 是 | — | "baidu" / "onedrive" / "aliyun" / "pikpak" / "webdav" / "cloudreve" / "s3" / "oss" / "cos"。 |
root_path | string | 否 | "/" | 源端起点路径。 |
target_path | string | 是 | — | 目标 cloudreve URI(如 cloudreve://my/x)。 |
parallelism | u32 | 否 | 1 | 单实例并发,上限 8。 |
rate_limit | u32 | 否 | 3 | 源端 API QPS 上限,范围 1..=20。 |
extra_config | string | 否 | — | driver 特定 JSON 配置。aliyun 存 `{“drive_type”:“resource” |
is_proxy | bool | 否 | false | 已废弃,请在 [sources.xxx] 配置代理;保留字段兼容旧配置 |
proxy | string? | 否 | null | 已废弃,请在 [sources.xxx] 配置代理;保留字段兼容旧配置 |
[[proxy]](数组段,可多个)
通用代理配置,为第三方网盘 driver 访问提供代理出站能力。代理在 source_credential 级(driver 登录时绑定),通过 [sources.xxx] 的 is_proxy / proxy 字段引用。
| 字段 | 类型 | 必填 | 默认 | 校验 / 说明 |
|---|---|---|---|---|
name | string | 是 | — | 实例名,用户内唯一,仅 ASCII 字母数字 / _ / -。 |
scheme | string | 是 | — | "http" / "https" / "socks5" / "socks5h"。 |
host | string | 是 | — | 代理主机地址(IP 或域名)。 |
port | u16 | 是 | — | 代理端口,范围 1..=65535。 |
username | string | 否 | — | 代理认证用户名(可选)。 |
password | string | 否 | — | 代理认证密码(可选)。 |
配置校验汇总
config/loader.rs 在解析时强制以下规则:
[serve.cr4].server_url必填且必须以http://或https://开头。- 旧格式扁平
[serve].server_url等字段拒绝并提示迁移。 [serve].address必须是合法 IP 字面值。[sync].src必须存在(fs check)。[sync].worker >= 1。[sync].parallelism in 1..=512。[sync].sync_mode在{single, incremental, append, two_way}内。[sync].sync_conflict在{skip, overwrite, 跳过, 覆盖}内。[log].mode在{cr4sync, full}内。
任何一条不满足,启动即报错,不会写回默认值。
[license]
License 文件路径配置. license 本身是签名后的二进制文件 (.lic), 由 @ReAxis 签发.
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
path | string | 否 | ./credential/license.lic | license 文件默认/指定路径 |
凭证目录
cr4sync 把所有凭证、状态、日志全部约定在工作目录下的相对路径里,不写用户 home 目录。
.
├── cr4sync.toml # 主配置
├── credential/ # ☆ 全部凭证 + 状态在这里
│ ├── token.json # CLI: cloudreve access_token + refresh_token (login 后落盘)
│ ├── sources/
│ │ └── baidu.json # CLI: 百度 OAuth refresh + access (login --baidu 后落盘)
│ ├── migrate.db # CLI: migrate 子命令任务状态机 (sqlite)
│ └── server.db # Server: 全部 server 状态 (sqlite, feature=server 才有)
├── data/ # sync-core 的 sqlite (sync 子命令)
│ └── sync_core/datas/.sync_db.sqlite3
└── logs/
├── sync_core.log # CLI + sync-core 主日志
└── server.log # Server HTTP 访问 + 审计额外 layer (feature=server)
强烈建议把整个 credential/ 加入 .gitignore 并对 server.db 设权限 0600:
chmod 0600 ./credential/server.db
chmod 0700 ./credential
凭证文件用途
| 文件 | 创建者 | 内容 | 备份策略 |
|---|---|---|---|
credential/token.json | cr4sync login (CLI) | Cloudreve access + refresh token | 删了重 login 即可 |
credential/sources/baidu.json | cr4sync login --baidu (CLI) | 百度 OAuth refresh + access | 删了重 login –baidu |
credential/migrate.db | cr4sync mgr 自动建 (CLI) | migrate 任务状态机 | 删了下次跑会从零扫,已 completed 任务不会重传 |
credential/server.db | cr4sync serve 自动建 (Server) | 用户 / 百度凭证 / storage / migrate / audit 全部状态 | 唯一备份点,百度 token 加密存储 |
CLI ↔ Server 隔离不变量
cr4sync 有一条 P0 不变量:
server 路径绝不读取
credential/token.json/credential/sources/baidu.json/credential/migrate.db。
CLI 路径绝不读取credential/server.db。
这条不变量由 server::token_isolation_guard 静态 grep 测试守护,server 子树源码不会出现 prepare_session / prepare_provider / token_store::default_path / credential/token.json 等 CLI 入口。违反或回归 cargo test --features server 单元测试直接失败, 该约束用于维护管理员同时需要使用CLI和Server能力时的隔离能力; 不会泄露管理员信息凭证;
删除用户凭证
CLI
rm -rf ./credential/
rm -rf ./data/
下次启动从零。
Server
通过 admin 端点级联清理:
curl -H "X-CR4-Authorization: Bearer $ADMIN_TOKEN" \
-X DELETE \
"http://127.0.0.1:5731/api/v4/cr4sync/admin/users/u-other"
注意:这只清 cr4sync server.db 中该用户的痕迹——cloudreve 账号本身不受影响,已上传到 cloudreve 的文件不会被删除。cr4sync 是迁移工具,不是 cloudreve 管理工具。
CLI 总览
cr4sync 二进制提供以下子命令:
| 子命令 | alias | 用途 |
|---|---|---|
login | — | Cloudreve 账密 / 扫码登录;百度 OAuth |
sync | — | 按配置文件同步本地 ↔ Cloudreve |
up | upload | 上传文件 / 目录到 Cloudreve |
dl | download | 从 Cloudreve 下载文件 / 目录 |
del | delete | 删除 Cloudreve 文件 / 目录 |
ls | — | 列目录(支持 cloudreve:// / baidu:// / <storage>://) |
mgr | migrate | 第三方网盘 → Cloudreve 单向迁移 |
serve(feature=server) | — | 启动 HTTP API 服务(见 Server 总览) |
全局参数
所有子命令支持:
-c, --config <PATH> 配置文件路径 (缺省 ./cr4sync.toml)
--log_level <LEVEL> 临时覆盖日志级别 (trace/debug/info/warn/error), 不写回配置
配置依赖
| 子命令 | 必读字段 | 凭证依赖 |
|---|---|---|
login(账密) | [serve.cr4] | [credential](可选回退) |
login --qr | [serve.cr4] | — |
login --baidu | [sources.baidu](可选 client_id 等) | — |
sync | [serve.cr4] + [sync] + [db] | credential/token.json |
up / dl | [serve.cr4] + [conf] | credential/token.json |
del | [serve.cr4] | credential/token.json |
ls cloudreve://... | [serve.cr4] | credential/token.json |
ls baidu://... | [sources.baidu] | credential/sources/baidu.json |
ls aliyun://... | [sources.aliyun] | credential/sources/aliyun.json |
ls pikpak://... | [sources.pikpak] | credential/sources/pikpak.json |
ls webdav://... | [sources.webdav] | [sources.webdav] 配置 |
ls <NAME>://... | [[storage]] 中 name=<NAME> 一行 | 对应 driver 的凭证 |
mgr | [serve.cr4] + [[storage]] + [sources.baidu] | credential/token.json + credential/sources/baidu.json |
serve | [serve] + [serve.cr4] | — (鉴权透传, 客户端 token 现场注入) |
退出码
| 退出码 | 含义 |
|---|---|
0 | 成功 |
1 | 通用错误(IO / 解析失败) |
2 | 配置错误 |
3 | 鉴权失败 |
4 | 远端错误 (Cloudreve / 百度 4xx) |
5 | 一致性校验失败 |
具体映射定义在 src/error.rs::CliError::exit_code。
cr4sync login
把 Cloudreve / 第三方网盘的凭证落盘到 ./credential/ 目录。
五种模式
cr4sync login [--mail <EMAIL>] [--passwd <PWD>] # ① 账密 + 2FA
cr4sync login --qr [--device-name <NAME>] # ② 扫码 (需服务端装 qr-login-relay 插件)
cr4sync login --baidu # ③ 百度网盘 OAuth
cr4sync login --aliyun # ④ 阿里云盘中转模式
cr4sync login --pikpak # ⑤ PikPak 账密 / refresh 登录
cr4sync login --cloudreve # ⑥ 源端 cloudreve 账密登录 (迁移源)
五个分支互斥(clap conflicts_with)。
参数
| 参数 | 类型 | 适用模式 | 说明 |
|---|---|---|---|
--mail <EMAIL> | string | ① | 账号。可省,缺省回退 [credential].mail。 |
--passwd <PWD> | string | ① | 密码。可省,缺省回退 [credential].passwd。 |
--qr | flag | ② | 扫码模式开关。 |
--device-name <NAME> | string | ② | 手机端“授权登录“页显示的设备名。缺省 cr4sync-<hostname>。 |
--baidu | flag | ③ | 百度 OAuth 模式。 |
--aliyun | flag | ④ | 阿里云盘中转模式。 |
--pikpak | flag | ⑤ | PikPak 登录。 |
--cloudreve | flag | ⑥ | 源端 cloudreve 登录 (迁移源)。探测验证码, 开启则报错“cli/serve 不支持, 用 webdav 模式“。2FA 支持。token 存 credential/sources/cloudreve.json。 |
① 账密 + 2FA
cr4sync login --mail you@example.com --passwd 'YourPwd'
如果账号开了 2FA,cr4sync 会在终端提示输入 6 位验证码。
落盘 → ./credential/token.json(含 access_token + refresh_token)。
② 扫码登录
cr4sync login --qr
cr4sync login --qr --device-name "我的 NAS"
需要 Cloudreve 服务端安装 qr-login-relay 插件。渲染二维码前会先 GET /qr-login-relay/api/health 健康检查,未部署时拒绝并提示。
流程:
- 生成 X25519 keypair
POST /qr-login-relay/api/session/create拿qr_payload- 终端用 Unicode 半块字符渲染二维码
- 1500ms 轮询
/status,authorized后拿/result - X25519 共享密钥(直接当 AES-256 key,无 HKDF) + AES-256-GCM 解密
- 校验
payload.cloudreve与本机[serve.cr4].server_url同站 - 写
credential/token.json
二维码默认有效期 120 秒,超时需重新运行。Ctrl-C 随时中止。
③ 百度 OAuth
cr4sync login --baidu
流程(oob 授权码方式):
- 终端打印 oob 授权 URL(
redirect_uri=oob) - 用户在浏览器打开授权 → 授权页回显授权码
code - 粘贴
code回终端 - cr4sync 直连百度
oauth/2.0/token?grant_type=authorization_code兑换access_token+ refresh_token - 写
credential/sources/baidu.json
若在
[sources.baidu].refresh_token配置了从别处导入的 refresh_token,--baidu会跳过浏览器授权,直接用它兑换(grant_type=refresh_token)。
可选:在 [sources.baidu] 段覆盖 client_id / client_secret / redirect_uri 用独享配额应用(redirect_uri 仅影响非 oob 场景,oob 流固定 redirect_uri=oob)。
④ 阿里云盘 (中转模式)
cr4sync login --aliyun
流程(中转模式,免注册):
- 终端提示访问
https://alistgo.com/zh/tool/aliyundrive/request.html扫码获取 refresh_token - 用户粘贴 refresh_token
- cr4sync 走 alist 中转端点兑换 access_token
- 写
credential/sources/aliyun.json
若在
[sources.aliyun].refresh_token配置了 refresh_token,--aliyun会跳过交互直接用它兑换。
⑤ PikPak (账密 / refresh 双模式)
cr4sync login --pikpak
优先级 1 — refresh_token + captcha_token (成对, 优先级最高):
- 配置
[sources.pikpak]填了refresh_token+captcha_token - cr4sync 用 refresh_token 兑换 access_token
- 写
credential/sources/pikpak.json
优先级 2 — email + password 账密登录:
- 配置
[sources.pikpak]填了email+password - cr4sync 调 PikPak login API 账密登录
- 写
credential/sources/pikpak.json
风控处理:
- 账密登录失败 (被风控) 时, cr4sync 提示用户:
- 浏览器登录 PikPak (
user.mypikpak.net) - F12 -> Application -> Local Storage 找
captcha_token+refresh_token - 填入
[sources.pikpak]的refresh_token+captcha_token - 重新执行
cr4sync login --pikpak(走优先级 1)
- 浏览器登录 PikPak (
refresh_token+captcha_token永远成对出现, 优先级高于email+passworddevice_id缺省MD5(email+password), 可在[sources.pikpak].device_id覆盖- 代理:
[sources.pikpak].is_proxy+proxy(跟 migrate 一致)
凭证刷新
- Cloudreve
access_token接近过期时,sync / up / dl / mgr 自动用 refresh_token 续期,无感。 - 百度
access_token距过期 < 5 天时,mgr 启动自动直连百度刷新。refresh_token 也过期时报CliError::Auth,需重cr4sync login --baidu。
错误退出
| 现象 | 原因 | 处理 |
|---|---|---|
扫码服务不可用 | qr-login-relay 插件未部署 | 安装插件或改用账密 |
授权结果解密失败 | X25519/AES-GCM 解密失败,通常是 relay 错版 | 升级插件 |
站点不匹配 | 二维码来自其他 Cloudreve 实例 | 确认 [serve.cr4].server_url |
auth: invalid_grant | 百度 refresh_token 过期 | 重 --baidu |
cr4sync sync
按 [sync] 配置同步本地 ↔ Cloudreve。所有 diff / 传输 / 校验由 sync-core SyncEngine 承担,cr4sync 只做编排 + 进度展示。
用法
cr4sync sync # 使用配置文件中的 sync_mode
cr4sync sync --mode single
cr4sync sync --mode incremental
cr4sync sync --mode append
cr4sync sync --mode two_way
参数
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
--mode <MODE> | enum | [sync].sync_mode | 临时覆盖同步模式,不写回配置文件。可选值见下表。 |
模式映射
--mode | sync-core SyncMode | 行为 |
|---|---|---|
single | UploadOnly | 仅上传。run_initial_sync 跑完即退出。 |
incremental | UploadOnly | 仅上传。run_initial_sync 跑完后进入 run_continuous,Ctrl-C 停止。 |
append | UploadOnly | 仅持续监听,跳过 initial。 |
two_way | Full | 双向。run_initial_sync 跑完即退出(上传本地新增 + 下载远端新增 + 冲突按 sync_conflict 解)。 |
配置热更新
incremental / append 模式下运行期支持热更新 cr4sync.toml:
- 软更新(引擎内重读):
sync_conflict,parallelism, 日志level/mode即时生效。 - 硬重建(停旧引擎重建):
src/target/worker/parallelism这几个被 sync-coreWorkerPool构造时快照、运行期不重读的字段。cr4sync 外层 loop 检测到变更后engine.stop()→ 丢旧 Arc → 用新 configSyncEngine::new。
进度展示
indicatif::MultiProgress 渲染:
- 文件级粒度(completed/total + running item 名称)
- 多 worker 并发情况下底部常驻 N 条 spinner(N = 并发数,上限 512)
- log 与 bar 协同:
MultiProgress::suspend让位写日志再重画 bar
已知限制
- UploadOnly 持续阶段冲突一律覆盖:sync-core 在 UploadOnly 的 continuous 阶段把冲突强制映射为 KeepLocal。
sync_conflict = "skip"在持续阶段实际仍上传覆盖。two_way模式下skip/overwrite才完整生效。 - 远端 hash 不可得:sync-core 内容比对依赖
mtime + size,“同大小且 mtime 未变的篡改“理论上可漏判。 - sync 阶段无字节级进度:只显示文件级,单文件字节进度走
up/dl子命令。
配置示例
[serve.cr4]
server_url = "https://demo.cloudreve.org"
[db]
type = "sqlite3"
path = "./data"
[sync]
src = "/home/data"
target = "/"
worker = 4
parallelism = 8
sync_mode = "incremental"
sync_conflict = "skip"
cr4sync up / cr4sync dl
独立的单文件 / 单目录上传 / 下载原语。不走 SyncEngine,行为简单可预测,保留单文件字节级进度。
up(alias upload)— 上传
cr4sync up <src> <target> [-r] [-f] [--conflict <MODE>]
cr4sync upload ./photo.jpg cloudreve://my/backup/photo.jpg
cr4sync up -rf ./photos cloudreve://my/backup/photos
dl(alias download)— 下载
cr4sync dl <src> <target> [-r] [-f] [--conflict <MODE>]
cr4sync download cloudreve://my/data.zip ./data.zip
cr4sync dl -rf cloudreve://my/photos ./photos
公共参数
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
<src> | string | — | 源路径。up:本地路径;dl:远端 URI。 |
<target> | string | — | 目标路径。up:远端 URI;dl:本地路径。 |
-r, --recursive | flag | false | 递归处理目录。 |
-f, --force | flag | false | 覆盖已存在文件。等价于 --conflict overwrite。 |
--conflict <MODE> | enum | skip | skip(跳过)/ overwrite(覆盖)。 |
-r 与 -f 可堆叠:-rf ≡ -r -f。
行为细节
up
- 单文件:直接
POST /file/upload拿 session → 切片chunk_size→ 顺序上传 → cloudreve callback。 - 单文件分片上传:失败重传从 chunk 0 开始(进程内重试,进程重启会丢,仅低效非数据损坏)。
- 目录递归:广度优先扫描,逐个上传单文件流程。
- 重试次数:
[conf].retry(缺省 3)。
dl
- 单文件:cloudreve
download拿带签名直链 → 单 HTTP GET 流式写入<target>.part,下完原子 rename 到<target>。 .part续传:进程重启后 fs 检测到<target>.part存在 → 计算字节数 →Range: bytes=<n>-续传(同进程内重启 OK,跨进程也 OK)。- 目录递归:cloudreve
list拉目录树 → 逐个下载。
target 路径语义
对齐 cp / scp / wget -O:单文件传输时,target 不一定是目录。
| 场景 | 行为 |
|---|---|
up foo.apk /baidu-migrate/foo.apk (target 不存在) | 上传到 /baidu-migrate/foo.apk (target 当文件路径) |
up foo.apk /baidu-migrate/ (target 是已存在目录) | 上传到 /baidu-migrate/foo.apk (传统 join basename) |
up -rf ./photos /backup/photos (目录递归) | target 始终当目录, 内部文件按相对路径 join |
dl /remote/foo.apk foo.apk (target 不存在) | 下载到 ./foo.apk (target 当文件路径) |
dl /remote/foo.apk ./downloads/ (target 是已存在目录) | 下载到 ./downloads/foo.apk (传统 join basename) |
dl -rf /remote/photos ./photos (目录递归) | target 始终当目录 |
判定规则: src 是文件 + target 不是已存在的目录 → target 当作完整文件路径 (不 join basename)。 其他场景 (src 是目录 / target 是已存在目录) → target 视为目录, 后续按相对路径 join。
冲突策略
| 模式 | up 目标存在 | dl 目标存在 |
|---|---|---|
skip | 跳过该文件 | 跳过该文件 |
overwrite | 删除远端再传 | 删除本地 .part + 目标再传 |
进度展示
单文件字节级 bar:
[####################------] 3.2 MiB/4.0 MiB ( 1.2 MiB/s, ETA 1s) ./photo.jpg
目录递归时多文件按顺序处理(无并发),bar 复用。
错误退出
| 现象 | 原因 |
|---|---|
local io: not found | <src> 本地不存在(up)或 <target> 父目录不存在(dl) |
remote: 404 | 远端路径不存在(dl)或目标父目录不存在(up) |
auth: token expired | token.json 过期且 refresh 失败 → cr4sync login |
conflict: target exists | 冲突 skip 模式遇到目标存在(信息性,不是错误) |
cr4sync ls
列目录。支持多 URI scheme:Cloudreve / 百度 / [[storage]] 实例。
用法
cr4sync ls # 列 Cloudreve 根 (cloudreve://my)
cr4sync ls /backup # 列 Cloudreve 路径
cr4sync ls cloudreve://my/backup
cr4sync ls baidu:///我的资源
# 其他 driver scheme
cr4sync ls aliyun:///
cr4sync ls pikpak:///
cr4sync ls webdav:///
cr4sync ls -s main-baidu /我的资源/sub # 用 [[storage]] 实例 (name=main-baidu)
cr4sync ls -l -H ./backup # 长格式 + 人性化大小
cr4sync ls -r=2 cloudreve://my # 递归展开 2 层
参数
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
[target] | string | / | 目标路径或 URI。 |
-s, --source <NAME> | string | — | 用 [[storage]] 实例作入口,target 相对其 root_path。 |
-l, --long | flag | false | 长格式:<size> <mtime> <name>。 |
-H, --human-readable | flag | false | 人性化大小(K/M/G),仅 -l 生效。 |
-r, --recursive[=<DEPTH>] | flag/int | 不递归;指定时缺省深度 1,上限 10 | 递归展开,类似linux的 tree -L |
URI scheme
| scheme | 含义 | 凭证 |
|---|---|---|
无 scheme(如 /x 或 ./y) | 等价 cloudreve://my/x(绝对路径) | credential/token.json |
cloudreve://my/... | Cloudreve 本人空间 | credential/token.json |
cloudreve://share/<id>/... | 分享空间(如果实现) | credential/token.json |
baidu:// / baidu:///x | 百度网盘根 / 路径 /x | credential/sources/baidu.json |
<NAME>://... | [[storage]] 中 name=<NAME> 一行;路径相对 root_path | 对应 driver |
输出格式
默认(短格式)
photos/
videos/
README.md
data.zip
目录追加 /。
-l 长格式
- 2026-06-20 10:23 photos/
- 2026-06-21 14:55 videos/
4096 2026-06-25 09:11 README.md
12345678 2026-06-22 18:30 data.zip
加上 -H:
- 2026-06-20 10:23 photos/
- 2026-06-21 14:55 videos/
4.0K 2026-06-25 09:11 README.md
11.8M 2026-06-22 18:30 data.zip
-r 递归
按层缩进展开。
配置依赖
不同 scheme 需要不同段:
| 用法 | 需要 |
|---|---|
ls / / ls cloudreve://... | [serve.cr4] + credential/token.json |
ls baidu://... | [sources.baidu] + credential/sources/baidu.json |
ls -s <NAME> ... | [[storage]] 中对应 name |
错误退出
| 现象 | 原因 |
|---|---|
storage 'x' not found | -s x 但 [[storage]] 没有该 name |
remote: 404 | 路径不存在 |
auth: ... | 凭证缺失或过期 |
cr4sync del(alias delete)
删除 Cloudreve 远端文件或目录。
用法
cr4sync del cloudreve://my/x/y.zip # 删单文件
cr4sync del -r cloudreve://my/old-dir # 递归删目录 (必须 -r)
cr4sync delete -r -y /backup/2024 # 跳过确认提示
参数
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
<target> | string | — | 远端路径或 cloudreve:// URI。 |
-r, --recursive | flag | false | 递归删除目录。目录必须带 -r,否则报错。 |
-y, --yes | flag | false | 跳过 y/N 确认提示,脚本场景用。 |
行为
- 单文件:
POST /file/delete直接删。 - 目录(带
-r):默认弹 y/N 确认提示(-y跳过)→ 调 Cloudreve 递归删除接口。
路径 scheme
与 ls 类似,缺省 scheme 视为 cloudreve://my/...。不支持 baidu:// 删除源端(tips:cr4sync 是迁移工具,不主动也不具备改源数据能力)。
错误退出
| 现象 | 原因 |
|---|---|
remote: 404 | 目标不存在 |
local: refused | 用户在确认提示输入 n |
bad arg: directory without -r | 目标是目录但未指定 -r |
cr4sync mgr(alias migrate)
第三方网盘 → Cloudreve 单向迁移。支持 baidu / onedrive / aliyun / pikpak / webdav / cloudreve / s3 / oss / cos。真流式(不落盘) + 断点续传 + 端到端一致性校验。
用法
cr4sync mgr -s main-baidu # 跑 [[storage]] 中 name=main-baidu 的实例
cr4sync mgr -s main-baidu /我的资源/sub # 临时覆盖 source_path (只迁子目录)
cr4sync mgr -s main-baidu /a.zip cloudreve://my/a.zip # 单文件
cr4sync mgr -s main-baidu --rate-limit 8 --parallelism 8
cr4sync mgr -s main-baidu --force-rewrite # 把本次扫描命中的任务踩回 pending 重传
cr4sync mgr -s main-baidu --force-reset -y # 清空整个 migrate.db (危险, -y 跳过确认)
参数
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
[source_path] | string | [[storage]].root_path | 源端路径或 URI(如 baidu:///我的资源),覆盖 storage 配置。 |
[target_path] | string | [[storage]].target_path | 目标 Cloudreve URI(如 cloudreve://my/backup)。 |
-s, --source <NAME> | string | 仅一个 baidu storage 时可省 | 选 [[storage]] 实例。 |
--parallelism <N> | u32 | 派生自 rate_limit,上限 8 | 并发数。 |
--rate-limit <QPS> | u32 | [[storage]].rate_limit 或 3 | 源端 API QPS 上限。 |
-y, --yes | flag | false | 跳过预览/确认提示。 |
--force-rewrite | flag | false | 把本次 scan 命中的任务(含 completed)踩回 pending 重传。只动本次涉及行。 |
--force-reset | flag | false | 清空整个 migrate.db。带 y/N 确认(-y 跳过)。与 --force-rewrite 互斥。 |
工作原理
ensure_session → for idx in next_chunk..N {
baidu download_range(offset, len)
→ progress bar inc
→ cloudreve upload_chunk(idx, &bytes)
→ db.update_next_chunk(idx+1)
}
→ [若远程存储策略] complete_upload
→ mark_verifying
→ verify_consistency (size + 头尾 8KB blake3)
→ mark_completed
- 不落盘:每片下载完立即喂上传,drop bytes。内存峰值 =
chunk_size × parallelism,单文件 3T 也能跑。 - 断点续传:进度持久化到
./credential/migrate.db,每片上传后写next_chunk。Ctrl-C 或崩溃重跑从那一片接。 - 一致性校验:cloudreve
/file/info不返 md5,只能 size + 头/尾 8KB blake3 兜底;< 16KB 文件仅信任 size。
状态机
pending ──► uploading ──┬─► verifying ──► completed
│
▼
failed (next run: 复用 next_chunk 续传)
verify_failed (next run: 清空 session + next_chunk=0 重头)
migrate_task 表的 state 字段记录每个任务的当前状态。
进度展示
indicatif::MultiProgress 槽位 bar 池:
[####################------] 3.2 MiB/4.0 MiB ( 1.2 MiB/s, ETA 1s) 上传 chunk 8/16 /我的资源/a.mp4
[######------------------- ] 200.0 KiB/4.0 MiB ( 800 KiB/s, ETA 5s) 下载 chunk 2/16 /我的资源/b.zip
[##########################] 完成 (空闲)
bar 数量 = parallelism,与文件数解耦。空闲槽位 idle_slot_bar 状态等待领取下一文件。
--force-rewrite vs --force-reset
| 操作 | 影响行 | completed 行 | 其他 storage 行 |
|---|---|---|---|
--force-rewrite | 仅本次扫描命中的行 | 一并踩回 pending | 不动 |
--force-reset | 全表 | 一并删除 | 一并删除 |
远端文件被外部删了想重传 → --force-rewrite。
migrate.db 损坏想从零 → --force-reset。
鉴权失败处理
百度 refresh_token 也过期 → CliError::Auth → run_concurrent 立即 JoinSet::shutdown,提示 cr4sync login --baidu。
已知限制
- driver:接受
"baidu"/"onedrive"/"aliyun"/"pikpak"/"webdav"/"cloudreve"/"s3"/"oss"/"cos"。 - 校验粒度:cloudreve v4 不返 md5,中段被篡改理论可漏判。
- dlink 8h 时效:百度单文件 chunk 循环超 8h 由
get_dlink自然续期,调用方无感。 - 校验失败需手动:标记
verify_failed的任务下次跑会清 session 重头传一次;再失败需--force-rewrite或排查源/目标差异。
Server 总览
cr4sync serve 是把第三方网盘 → Cloudreve 迁移能力以 HTTP API 形式暴露给多个 Cloudreve 用户的服务模式。仅在编译时启用 server cargo feature 才包含。
设计原则
- CLI 模式完全不变:CLI 仍走
./credential/token.json+./credential/sources/baidu.json+./credential/migrate.db+ toml[[storage]]。 - Server 全状态收敛到
./credential/server.db:所有 user / 凭证 / storage / migrate / audit 都在这一个 sqlite 文件,与 CLI 完全隔离,备份只需一个文件。 - cloudreve 身份强隔离(P0 不变量):server 路径上传 cloudreve 必须使用调用方 JWT,严禁读取站长
credential/token.json。静态 grep 守护测试兜底。 - 行级隔离:所有 server.db Repo 方法签名首参数必带
user_id,admin 跨用户走专门*_admin方法。 - admin 自动识别:Cloudreve
/user/info响应的data.group.name默认命中"Admin"/"管理员"任一即为 admin;可用[serve.cr4].admin_group覆盖,moka 缓存 60s。 - JWT 不本地验签:cr4sync 不持有 Cloudreve secret,每次 cache miss 走远端
/user/info/{sub}校验。 - 第三方 refresh_token 加密落 server.db:AES-256-GCM 加密, 密钥编译进二进制混淆存储, 不入 config / DB / 日志。站长拿到 DB 只看到密文。
- cloudreve refresh_token 仅内存态:随
POST /migratebody 传入,活在JobTokenState内存里,job 完成即抛弃,不入 db。
路径前缀
server 所有 API 路径前缀固定 /api/v4/cr4sync(与 [serve.cr4].server_api_path 无关——后者是连上游 Cloudreve 用的)。
embed-web 构建下还会在 /extensions/cr4sync 伺服内嵌的前端 SPA(静态资产,不鉴权)。同源部署为 cloudreve 扩展时,nginx 把 /api/v4/cr4sync/ 与 /extensions/cr4sync 两个 location 反代到 server;详见 部署。
API 速览
| Method | Path | 说明 | 鉴权 |
|---|---|---|---|
| GET | /health | 健康检查 | 无 |
| GET | / | meta(version / user_id / is_admin / banned) | ✓ |
| POST | /auth/login | 账密登录 (独立部署) | ✗ |
| POST | /auth/login/2fa | 2FA 验证 | ✗ |
| GET | /auth/captcha/status | 验证码类型与配置 | ✗ |
| GET | /auth/captcha | 图形验证码图片 + ticket | ✗ |
| POST | /auth/token/refresh | 刷新 token | ✗ |
| POST | /auth/qr/create | 创建扫码会话 | ✗ |
| GET | /auth/qr/status/:id | 扫码状态轮询 | ✗ |
| GET | /auth/qr/result/:id | 取扫码结果 | ✗ |
| GET | /sources | 支持的 driver + 用户登录态 | ✓ |
| GET/POST/PUT/DELETE | /source/:driver/storage[/{name}] | storage CRUD, driver ∈ baidu/onedrive/aliyun/pikpak | ✓ |
| POST | /source/baidu/login-session | 百度 OAuth 启动 (含 oob 授权 URL) | ✓ |
| POST | /source/baidu/login-submit[?code=] | 百度 OAuth 提交 code 或 refresh_token | ✓ |
| POST | /source/onedrive/login-session | OneDrive OAuth 启动 | ✓ |
| POST | /source/onedrive/login-submit[?code=] | OneDrive OAuth 提交 code 或 refresh_token | ✓ |
| POST | /source/aliyun/login-session | 阿里云盘 OAuth 启动 | ✓ |
| POST | /source/aliyun/login-submit[?code=] | 阿里云盘 OAuth 提交 code 或 refresh_token | ✓ |
| POST | /source/:driver/credential | S3/OSS/COS 直接凭证 upsert(非 OAuth,driver ∈ s3/oss/cos) | ✓ |
| POST | /source/pikpak/credential | PikPak 账密登录(非 OAuth,直接提交 email/password) | ✓ |
| GET | /browse/storage/{name}?type=&path=&limit=&offset= | storage 双端浏览 (源+目标), 分页 | ✓ |
| GET | /browse/storage?is_create_storage=true&type=&path=&limit=&offset= | 新建 storage 浏览 (无 storage name) | ✓ |
| GET | /source/baidu/info | 百度网盘用户信息 + 容量 | ✓ |
| GET | /source/cloudreve/info | cloudreve 源用户信息 + 容量 | ✓ |
| GET/POST/PUT/DELETE | /proxies[/{name}] | 代理 CRUD (列表/创建/更新/删除) | ✓ |
| POST | /migrate | 异步创建 migrate job | ✓ |
| GET | /migrate/jobs | 枚举当前用户 job | ✓ |
| GET | /migrate/jobs/{job_id} | 查询 job 状态 | ✓ |
| GET | /migrate/jobs/{job_id}/tasks | 分页枚举该 job 的 task | ✓ |
| GET | /migrate/jobs/{job_id}/tasks/{task_id} | 查询该 job 下单 task | ✓ |
| GET | /migrate/sse | SSE 进度推送 | ✓ |
| POST | /migrate/jobs/{job_id}/cancel | 取消 job | ✓ |
| GET | /admin/users[/...] | admin 跨用户查询 | ✓ admin |
| DELETE | /admin/users/{user_id} | 级联清用户 server.db 痕迹 | ✓ admin |
| GET/POST | /admin/jobs[/...] | admin 持久化 job 看板 | ✓ admin |
| GET | /admin/audit | 审计日志查询 | ✓ admin |
| GET | /admin/stats | 全局统计 | ✓ admin |
| GET | /admin/doc/book[/*] | 内嵌 mdbook 文档 (仅 server/*.html 校验权限) | ✗ |
详见 API 接口 各页。
模块布局
cr4sync/src/server/
├── mod.rs # serve(cfg) 入口
├── state.rs # AppState (Arc bundle)
├── error.rs / response.rs # ApiError + Resp<T> envelope
├── db/{mod,user_repo,source_cred_repo,storage_repo,migrate_repo,audit_repo,proxy_repo}.rs
├── middleware/{request_id,auth,require_admin,audit,access_log}.rs
├── auth/{jwt,validator}.rs # base64 JWT 解 + moka cache + lazy upsert
├── providers/token.rs # TokenProvider + JobTokenState
├── jobs/{manager,migrate_job,event}.rs
├── sse/stream.rs # broadcast → SSE 按 user_id 过滤
├── token_isolation_guard.rs # #[cfg(test)] 静态 grep 守护 (P0 不变量)
├── routes/{meta,sources,storage,login_baidu,login_onedrive,login_aliyun,login_pikpak,browse,migrate,admin,proxy}.rs
源端 client 模块布局
cr4sync/src/sources/
├── mod.rs # CloudSource trait
├── local.rs # 本地 fs 扫描
├── baidu_pan/* # 百度网盘 (OAuth)
├── onedrive/* # OneDrive (OAuth)
├── aliyun/* # 阿里云盘 (OAuth)
├── proxy.rs # 代理 reqwest client 构造 helper (build_proxied_http_client)
├── s3/{mod,client,config,fs,download}.rs # S3 基底 (aws-sdk-s3)
├── oss/mod.rs # OSS 薄包装 (复用 S3Client)
└── cos/mod.rs # COS 薄包装 (复用 S3Client)
└── pikpak/{mod,api,auth,fs,download}.rs # PikPak (账密登录, 非 OAuth)
与 CLI 的交叉
- 共享
[serve.cr4]段连 Cloudreve 上游 - 共享
commands/migrate/{pipeline.rs}(通过MigrateProgressSinktrait 抽象),CLI 实现IndicatifSink,server 实现BroadcastSink - 共享
MigrateTaskStoretrait,CLI 实现是MigrateDb(migrate.db),server 实现是 user-scoped 的MigrateRepo(server.db) - 共享
sources/baidu_pan/*、sources/onedrive/*、sources/aliyun/*、sources/s3/*、sources/oss/*、sources/cos/*、sources/pikpak/*第三方网盘/对象存储客户端 - 共享
sources/proxy.rs代理 client 构造 helper:CLI 从[[proxy]]配置段加载,server 从proxy表加载。代理在 source_credential 级(driver 登录时绑定),migrate 时复用 source_credential 的代理。
与 CLI 的隔离
- 不读
token.json/sources/baidu.json(及同目录的 onedrive/aliyun 凭证文件) /migrate.db - 不调
commands::prepare_session/commands::prepare_provider - 不调
auth::token_store::default_path
静态 grep 守护测试 server::token_isolation_guard 在 cargo test --features server 时扫描 server/**/*.rs,命中上述任一禁字立即失败。
构建与启动
构建
# server feature 必须显式开启
cd path/to/cr4sync
cargo build --release --features server
CLI-only 构建(不含 axum / tower-http / moka 等):
cargo build --release
启动
cr4sync serve # 用 [serve] 段配置
cr4sync serve --address 0.0.0.0 # 临时覆盖监听地址
cr4sync serve --port 8080 # 临时覆盖端口
cr4sync -c /etc/cr4sync.toml serve # 指定配置文件
命令行参数
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
--address <IP> | string | [serve].address 或 127.0.0.1 | 临时覆盖监听地址。必须是合法 IPv4/IPv6。 |
--port <PORT> | u16 | [serve].port 或 5731 | 临时覆盖监听端口。 |
必须配置段 (最小启动server所需要的配置)
[serve]
# address = "127.0.0.1" # 未指定缺省回环地址, 自行按需更改, 比如 0.0.0.0
# port = 5731 # 未指定缺省 5731, 可以自行指定端口
[serve.cr4]
# 必填, 你的 Cloudreve 后端地址, 必须填写域名, 需校验 license
# 同内网可将域名在 /etc/hosts 中添加映射到 cloudreve 机器的内网ip
server_url = "https://cloudreve.example.com"
# server_port = 443
# server_api_path = "/api/v4"
[log]
# server_log_path = "./logs/server.log"
[license]
# 缺省路径, 得到license文件将其重命名为 license.lic 放到二进制同路径的 credential 目录下
# 也可以手动指定路径
# path = "./credential/license.lic"
[serve.cr4].server_url 缺失时启动失败。其他全部可省。
启动副作用
- 打开 / 创建
./credential/server.db(缺省路径)。首次启动自动执行 migrations(建表 + 索引)。 - 构造
AppState:含ServerDb/AuthValidator/JobManager/reqwest::Client。 - 组装 router:
/health白名单 + 其余路径强制鉴权 +/admin/*走require_admin中间件。 - bind + serve:
tokio::net::TcpListener::bind+axum::serve+ 优雅关停(Ctrl-C 触发 broadcast cancel,所有运行中的 job 收到CancellationToken::cancel)。
凭证目录权限
强烈建议:
chmod 0700 ./credential
chmod 0600 ./credential/server.db
server.db 含第三方 refresh_token 用户信息等加密密文, 加密多重防护。管理员无法查看子用户的三方driver敏感信息。日志完全脱敏。
systemd 单元示例
[Unit]
Description=cr4sync HTTP API server
After=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/cr4sync
ExecStart=/opt/cr4sync/cr4sync -c /opt/cr4sync/cr4sync.toml serve
Restart=on-failure
RestartSec=5
User=cr4sync
Group=cr4sync
UMask=0077
[Install]
WantedBy=multi-user.target
健康检查
curl http://127.0.0.1:5731/api/v4/cr4sync/health
# {"code":0,"msg":"ok","data":{"ok":true,"service":"cr4sync","version":"..."},"request_id":"01J..."}
日志
- 主日志:
[log].log_path(缺省./logs/sync_core.log),所有模块。 - 额外 layer:
[log].server_log_path(缺省./logs/server.log),过滤target=cr4sync::server::*,HTTP 访问 + 鉴权 + 路由 + 审计写入轨迹专属。 - access_log middleware 每个请求会发一条
info级别记录,含method / path / status / user_id / request_id / elapsed_ms。
部署
cr4sync server 有两种部署形态。
形态 A:作为 cloudreve 扩展(同源部署,推荐)
server 与 cloudreve 走同一域名,由 nginx 反代。前端 SPA embed 进二进制,挂在 /extensions/cr4sync。
路由分工
| 路径 | 内容 | 是否鉴权 |
|---|---|---|
/api/v4/cr4sync/* | cr4sync HTTP API | ✓(白名单 /health 除外) |
/extensions/cr4sync /extensions/cr4sync/* | 内嵌前端 SPA 静态资产 | ✗ |
- UI 恒挂
/extensions/cr4sync(前端 Vitebase与 Routerbasename已对齐该前缀)。 - API 恒
/api/v4/cr4sync(前端用相对路径,从 origin 根算)。
nginx 示例
下例把 cr4sync(127.0.0.1:5731)与 cloudreve 挂到同一域名,是实测可行的配置。
cloudreve 上游按你的实际情况填(下例 location / 走 unix socket、= /sw.js 走 TCP 127.0.0.1:5212,
两者都指向同一个 cloudreve;换成你自己的地址即可)。= /sw.js 与 location / 里的 sub_filter
是 Service Worker 三层防御(见下节),纯同源部署可裁掉。
之所以需要这么多配置, 完全是因为 cloudreve 的
service worker导致, 详见下文
# cr4sync API: 保全完整 /api/v4/cr4sync 前缀 (server 路由带全前缀)
# proxy_pass 必须无尾斜杠, 带斜杠会剥掉前缀导致后端 404
location ^~ /api/v4/cr4sync/ {
proxy_pass http://127.0.0.1:5731;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# SSE: 关缓冲 + 长超时
proxy_buffering off;
proxy_read_timeout 3600s;
}
# cr4sync 前端 SPA (embed-web 内嵌), proxy_pass 同样无尾斜杠
location ^~ /extensions/cr4sync {
proxy_pass http://127.0.0.1:5731;
proxy_set_header Host $host;
}
# Layer 2: 改写 cloudreve 的 sw.js, 往 denylist 注入 /extensions/cr4sync
location = /sw.js {
# cloudreve 上游 原样保全 /sw.js 路径
# cloudreve 上游 (按实际改, 作者此处 unix socket). 注意 socket 末尾 :/ 是 URI 部分,
# 在 location / 里把 / 换成 / 无害; 但勿照搬到精确 location (如 = /sw.js) 否则路径被重写
proxy_pass http://unix:/opt/cloudreve/run/cloudreve.sock:/;
proxy_set_header Host $host;
# 关压缩, 否则 sub_filter 匹配不到
proxy_set_header Accept-Encoding "";
# sw.js 是 JS, 需显式声明类型 (默认只处理 text/html)
sub_filter_types application/javascript text/javascript;
sub_filter_once on;
# 以稳定的正则字面量 /^\/api\/(.+)/ 为锚点, 在其后插入我们的路径
sub_filter '/^\/api\/(.+)/' '/^\/api\/(.+)/,/^\/extensions\/cr4sync/';
}
# 可选: cloudreve 扫码登录插件 qr-login-relay (与 cr4sync 无关, 有就保留)
location = /qr-login-relay {
return 301 /qr-login-relay/;
}
location ^~ /qr-login-relay/ {
proxy_pass http://127.0.0.1:54899/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
# cloudreve 本体 + Layer 1: 往 index.html 注入我们 SW 的注册脚本
# 资产也回落到这里; Accept-Encoding "" 让上游吐未压缩以便 sub_filter 匹配,
# 传输压缩交给 nginx 全局 gzip (需 gzip_types 含 js/css) 重新压
location / {
proxy_set_header HOST $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
# 关压缩, 否则下面的 sub_filter 匹配不到 html
proxy_set_header Accept-Encoding "";
# 不要写 sub_filter_types text/html: 默认已含, 重复声明会报 duplicate MIME type warn
sub_filter_once on;
sub_filter '</head>'
'<script>navigator.serviceWorker&&navigator.serviceWorker.register("/extensions/cr4sync/sw.js",{scope:"/extensions/cr4sync"}).catch(function(){})</script></head>';
# cloudreve 上游 (按实际改, 作者此处 unix socket). 注意 socket 末尾 :/ 是 URI 部分,
# 在 location / 里把 / 换成 / 无害; 但勿照搬到精确 location (如 = /sw.js) 否则路径被重写
proxy_pass http://unix:/opt/cloudreve/run/cloudreve.sock:/;
client_max_body_size 20000m;
}
加入 cloudreve 侧边栏(贴心提示)
配好同源后,可在 cloudreve 管理面板给 cr4sync 加个入口:参数设置 → 外观 → 自定义侧边导航栏, 新增一行:
| Iconify 图标名 | 名称 | 链接 |
|---|---|---|
fluent:extension-24-regular | 数据迁移 | https://你的域名/extensions/cr4sync |
这样用户在 cloudreve 侧边栏点一下就能进 cr4sync 迁移控制台。
同源登录
同源后前端能读 cloudreve 的 localStorage['cloudreve_session'],启动时自动采纳其 access_token /
refresh_token 免手填(见 鉴权 的 Authorization 回退)。cloudreve 轮换
token 后,前端在鉴权失败时会重新采纳。
Service Worker 三层防御(同源必读)
cloudreve v4 前端注册了一个 scope=/ 的 Service Worker(Workbox NavigationRoute,对所有导航
返回 cloudreve 的 index.html,denylist 只放行 /api /f /s /pdfviewer.html)。/extensions/cr4sync
不在 denylist,于是软刷新(Ctrl+R)时该 SW 把我们的导航兜回 cloudreve 的 app shell,只有硬刷新
(Ctrl+F5,绕过 SW)才进得去。
这是 client-side 拦截:SW 命中时直接吐 Cache Storage 里的 index.html,请求根本不到网络,nginx
无法在路由层拦截。故采用三层防御,越靠前越“即时“,越靠后越“持久“:
Layer 1 — 往 cloudreve 的 index.html 注入我们 SW 的注册脚本(nginx sub_filter '</head>' ...)。
用户访问 cloudreve 首页时顺带注册我们的 SW(scope=/extensions/cr4sync),这样等他进 /extensions/cr4sync
时我们的 SW 已就位(scope 更窄者优先)。
- 局限:cloudreve 把自己的 index.html 也 precache 了,存量已装 cloudreve SW 的浏览器访问
/会吃缓存版、拿不到注入脚本。对这些浏览器,在 cloudreve 首页硬刷新一次(Ctrl+F5,绕过 SW 走网络) 即可拿到注入版完成注册;全新/清过缓存的浏览器则首访自动生效。
Layer 2 — 改写 cloudreve 的 /sw.js,把 /extensions/cr4sync 注入它的 denylist(nginx sub_filter)。
注入后 cloudreve 自己的 SW 主动放行我们的导航,不再劫持。
- 覆盖存量用户:SW 脚本在 update check 时从网络重新拉(不吃 precache),故存量浏览器在下次触发更新 检查(导航时,最长约 24h)会自动切到改写版,无需用户操作。
- 局限:绑死 cloudreve 压缩后的
sw.js结构,cloudreve 升级可能使sub_filter失配,需复检。
Layer 3 — cr4sync 前端自带的 sw.js(scope=/extensions/cr4sync,后端发 Service-Worker-Allowed
头放宽到不带尾斜杠)。一旦经 Layer 1/2 或一次硬刷新把我们的 SW 注册上,它就永久控制本前缀,
之后所有软刷新都稳定,不再依赖前两层。
校准提示:部署前先看 cloudreve 实际的
sw.js内容,确认 Layer 2 的匹配串:curl -s https://你的域名/sw.js | grep -o 'denylist[^]]*]'上例按
denylist:[..., /^\/api\/(.+)/, ...]编写(以稳定的正则字面量/^\/api\/(.+)/为锚点插入)。 若混淆后结构不同,按实际调整sub_filter的搜索串。cloudreve 每次大版本升级后建议复检 Layer 1/2。
排障:若浏览器被 cloudreve SW 缓存过旧内容,可在 DevTools → Application → Storage → “Clear site data” 清一次再访问。
形态 B:独立部署
server 独立跑在 :5731,前端与 API 同 host(http://host:5731/extensions/cr4sync 与
http://host:5731/api/v4/cr4sync),相对 API base 天然可用,无需 nginx。自带账号密码 + 2FA + 扫码
登录,server 代理 cloudreve 鉴权,登录后 token 写入浏览器 localStorage。详见 Auth 接口。
构建带前端的二进制(embed-web)
前端 dist 通过 embed-web feature 打进二进制。默认关闭:CI 的 cargo build --features server
无需构建前端。生产出带 UI 的二进制需两步(先前端后后端):
cd web && npm run build # 产出 web/dist
cd ../cr4sync
cargo build --release --features embed-web
./target/release/cr4sync serve # 浏览器开 /extensions/cr4sync
web/dist被 gitignore,故 embed 必须显式走embed-web;未构建前端就带该 feature 编译会因找不到../web/dist失败。仅需 API(不带 UI)时用--features server即可。
构建带文档的二进制(embed-doc)
mdbook 构建出的 doc/book/ 也可打进二进制,挂在 /admin/doc/book 供管理员浏览。默认关闭。
cd cr4sync/doc && mdbook build # 产出 doc/book
cd ..
cargo build --release --features embed-doc
# 浏览器开 /api/v4/cr4sync/admin/doc/book/ (前端 admin 侧栏「API 文档」入口)
可与 embed-web 叠加:--features embed-web,embed-doc。
访问控制
- 首页 / CLI / config 等文档:公开(无需登录)
installation.html/server/api-*.html/server/db-schema.html:仅 当前部署的server_url命中白名单后缀时开放,否则 403- 其余
server/*.html(部署 / 鉴权 / 日志等):公开
doc/book/被 gitignore,故 embed 必须显式走embed-doc;未mdbook build就带该 feature 编译会因找不到doc/book失败。
权限模型
server 模式下,普通用户默认无迁移权限,需通过 API 申请。审批模式由配置文件 [migrate].approve 控制:
manual(缺省):用户提交申请 → 管理员在 admin 面板审批通过 → 用户获得权限。适合多租户环境,由管理员控制哪些用户可以迁移。auto:用户提交申请即生效,无需审批。适合信任环境或个人使用。
管理员可通过 POST /admin/users/{user_id}/ban 封禁用户,封禁后该用户无法创建新的 job 和 storage,但已有 job 会跑完。解封通过 POST /admin/users/{user_id}/unban。
Jobs 并发池
server 通过 [migrate].max_concurrent_jobs(缺省 10,上限 512)控制全局并发 job 上限。超出上限的 job 在后台排队(status = "queued"),等待前面的 job 完成后自动获得 permit 进入运行态。
调大 max_concurrent_jobs 需注意:
- 每个 job 并发上传约 4MB × 并发数 的内存开销, 预取和cr后端local/remote存储策略开启多分片上传时另算; 单个job理论内存所需内存峰值为
chunck_size * 并发数 * 2(预取)如果cr后端存储策略开启了多分片上传,内存模型理论为chunck_size * 并发数 * 3(如果是3分片并发上传) - 各 driver API QPS 限制(每个 job 有自身 rate_limit)
- Cloudreve 端接收带宽
鉴权
请求头格式
X-CR4-Authorization: Bearer <cloudreve_access_token>
优先用专属头 X-CR4-Authorization——避免与反向代理 / WAF 等基础设施冲突,也便于区分 “给 cr4sync 的 token” 与 “给 cloudreve 的 token”。
缺失时回退标准 Authorization 头:cr4sync 作为 cloudreve 扩展同源部署时,前端复用 cloudreve 登录态直接带标准 Authorization 更自然;标准客户端(curl 等)也可用标准头访问。两者都在时以 X-CR4-Authorization 为准。
取 token 顺序: X-CR4-Authorization → (缺失) Authorization → (都缺) 401
若
X-CR4-Authorization存在但格式非法(非Bearer <token>),直接判 401,不静默回退到Authorization——专属头一旦显式出现就以它为准。
鉴权流程
┌─────────────────────────────────────────┐
│ middleware/auth.rs │
│ 1. 取 X-CR4-Authorization (缺回退 Authorization) │
│ 2. 解析 Bearer 前缀 │
│ 3. base64 解 JWT payload (不验签) │
│ - 取 sub (user_id) │
│ - 校验 exp 未过期 │
│ 4. AuthValidator.validate(token) │
│ - moka cache.get(token) │
│ - hit → 返 AuthResult │
│ - miss → GET cr4/user/info/{sub} │
│ header: Authorization=... │
│ → 200 + body.code=0 → │
│ is_admin = │
│ admin_groups contains │
│ data.group.name │
│ db.upsert_user(...) │
│ cache TTL = min(60s, │
│ exp - now - 30s) │
│ 5. 注入 AuthContext 到 extension │
└─────────────────────────────────────────┘
AuthContext
注入到 axum request extension,handler 用 Extension<AuthContext> 取:
#![allow(unused)]
fn main() {
pub struct AuthContext {
pub user_id: String, // Cloudreve sub
pub is_admin: bool, // group.name 命中 admin 组名集合
pub token: String, // 原始 access_token (现场上传 Cloudreve 时用)
pub group_name: String, // 用户组名
pub email: Option<String>,
pub nickname: Option<String>,
}
}
缓存
AuthValidator 内置 moka::future::Cache<String, AuthResult>:
| 维度 | 值 |
|---|---|
| key | 原始 access_token |
| TTL | min(60s, jwt.exp - now - 30s) |
| 容量 | 默认 10000 entry |
也即:token revoke 后 cr4sync 端最多 60s 后感知,admin 角色变更同理。
admin 自动识别
cr4sync 不维护自己的 admin 列表。每次 /user/info 响应都看 data.group.name,并与 admin 组名集合比对:
{
"code": 0,
"data": {
"id": "sub-xxx",
"email": "a@example.com",
"nickname": "Alice",
"group": {
"name": "管理员"
}
}
}
默认 admin 组名集合包含 "Admin" 和 "管理员",任一命中即 is_admin = true。如果 [serve.cr4].admin_group 配了值,则只认该配置值。
require_admin middleware
/admin/* 子树路由额外挂 require_admin middleware:从 extension 取 AuthContext,is_admin == false ⇒ ApiError::Forbidden(HTTP 403 + envelope.code=1000)。
白名单
/api/v4/cr4sync/health不需要鉴权。embed-web构建下/extensions/cr4sync(内嵌前端 SPA 静态资产)不走鉴权——前端加载后再用 token 调 API。详见 部署。
鉴权失败回执
| 现象 | HTTP | envelope |
|---|---|---|
| 头缺失 (X-CR4-Authorization / Authorization 都无) / 非 Bearer | 401 | { code: 1000, msg: "...", data: null, request_id } |
| JWT base64 解失败 | 401 | { code: 1000, msg: "invalid token format", ... } |
| JWT exp 已过 | 401 | { code: 1000, msg: "token expired", ... } |
| Cloudreve /user/info 返非 0 code | 401 | { code: <cloudreve.code>, msg: <cloudreve.msg>, ... } 透传 |
| Cloudreve /user/info 网络失败 | 500 | { code: 3000, msg: "upstream unavailable", ... } |
非 admin 访问 /admin/* | 403 | { code: 1000, msg: "admin required", ... } |
与 Cloudreve token 流转
仅鉴权时使用调用方的 access_token 校验身份;后续上传 Cloudreve 也强制使用调用方 token,绝不读站长 token.json。详见 Migrate 接口 的“长任务 token 续期“段。
Auth(自带登录)
独立部署(形态 B)时,用户没有 cloudreve 登录态,cr4sync 提供自有登录端点,代理 cloudreve 鉴权。
server 不存储密码/token/会话:登录只是转发到 cloudreve,TokenSet 返回给前端,前端写入 localStorage。
所有 /api/v4/cr4sync/auth/* 不加鉴权中间件(用户还没 token)。
POST /api/v4/cr4sync/auth/login
账密登录。code=0 返回 token;code=203 需要 2FA。
请求 body
{
"email": "you@example.com",
"password": "your-password",
"captcha": "1234",
"ticket": "captcha-ticket"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
email | string | ✓ | Cloudreve 账号邮箱 |
password | string | ✓ | 密码 |
captcha | string? | ✗ | 验证码:图形验证码填用户输入;Turnstile/reCAPTCHA 填 widget 返回的 token |
ticket | string? | ✗ | 图形验证码的 ticket(从 /auth/captcha 响应取);Turnstile/reCAPTCHA 时与 captcha 同值 |
响应 200
直接成功:
{
"code": 0,
"msg": "ok",
"data": {
"token": {
"access_token": "...",
"refresh_token": "...",
"access_expires": "2026-07-03T...",
"refresh_expires": "2026-08-..."
}
},
"request_id": "-"
}
需要 2FA:
{
"code": 0,
"msg": "ok",
"data": {
"session_id": "2fa-session-id"
},
"request_id": "-"
}
POST /api/v4/cr4sync/auth/login/2fa
提交 OTP 验证码完成登录。
请求 body
{
"otp": "123456",
"session_id": "从上一步获取"
}
响应 200
与 /auth/login 成功响应相同结构,data.token 已填充。
GET /api/v4/cr4sync/auth/captcha/status
查询 cloudreve 验证码类型与配置。是否开启验证码由 cloudreve GET /site/config/login 的 login_captcha 字段决定 (true=开启); 验证码类型与 site keys 从 GET /site/config/basic 拿。login_captcha=false 时 enabled=false,前端不加载验证码 widget。
响应 200
{
"code": 0,
"msg": "ok",
"data": {
"enabled": true,
"captcha_type": "normal|turnstile|recaptcha|cap|null",
"captcha_ReCaptchaKey": "site-key-or-null",
"turnstile_site_id": "site-id-or-null",
"captcha_cap_instance_url": null,
"captcha_cap_site_key": null
},
"request_id": "-"
}
| 字段 | 说明 |
|---|---|
enabled | 是否启用了登录验证码 |
captcha_type | 验证码类型:normal(图形)/turnstile/recaptcha/cap/null |
captcha_ReCaptchaKey | reCAPTCHA site key(仅 recaptcha 类型有值) |
turnstile_site_id | Turnstile site key(仅 turnstile 类型有值) |
captcha_cap_* | Cap 验证码配置(仅 cap 类型,当前前端未支持) |
GET /api/v4/cr4sync/auth/captcha
图形验证码(captcha_type=normal 时用)。返回 JSON,前端拿 image 渲染图片、ticket 在登录时提交。
响应 200
{
"code": 0,
"msg": "ok",
"data": {
"image": "data:image/png;base64,iVBOR...",
"ticket": "captcha-ticket-string"
},
"request_id": "-"
}
未启用图形验证码时返回 404。
POST /api/v4/cr4sync/auth/token/refresh
用 refresh_token 换新 token 对(长任务 token 续期)。
请求 body
{
"refresh_token": "..."
}
响应 200
与 /auth/login 成功响应相同结构。
扫码登录 (QR)
依赖 cloudreve 安装了 qr-login-relay 插件, 插件来自 @fsjdb342 三步式:
POST /api/v4/cr4sync/auth/qr/create— 创建扫码会话,返回二维码载荷GET /api/v4/cr4sync/auth/qr/status/:id— 1500ms 轮询状态GET /api/v4/cr4sync/auth/qr/result/:id— 取解密结果,返回 token
POST /auth/qr/create
响应:
{
"code": 0,
"msg": "ok",
"data": {
"session_id": "relay-xxx",
"qr_payload": "{...}",
"cr4_session_id": "01J5...",
"expires_at": 1717000600
},
"request_id": "-"
}
session_id:relay 内部 IDqr_payload:前端用它渲染二维码cr4_session_id:前端用它查/auth/qr/status/{id}和/auth/qr/result/{id}- 若 relay 未部署(不健康),返回 500
GET /auth/qr/status/:id
{
"code": 0,
"msg": "ok",
"data": {
"status": "pending|scanned|authorized|expired"
},
"request_id": "..."
}
GET /auth/qr/result/:id
{
"code": 0,
"msg": "ok",
"data": {
"token": {
"access_token": "...",
"refresh_token": "...",
"access_expires": "...",
"refresh_expires": "..."
}
},
"request_id": "..."
}
一次扫码会话 TTL 120s,过期需重新 POST /auth/qr/create。
响应 envelope
所有响应统一格式:
{
"code": 0,
"msg": "ok",
"data": { ... } | null,
"request_id": "01J5XYZ..."
}
字段
| 字段 | 类型 | 说明 |
|---|---|---|
code | i32 | 0 表示成功;非 0 是错误码,详见 错误码。 |
msg | string | 人类可读的提示信息。code=0 时恒为 "ok"。 |
data | any | null | 业务数据。成功时是端点定义的具体类型,错误时为 null。 |
request_id | string | ULID,单调时间排序 + 随机。每个请求唯一,同时也是响应头 X-Request-Id。 |
HTTP status code 与 envelope code 的关系
cr4sync 区分传输层失败与业务层错误:
| 类别 | HTTP | envelope.code | 何时 |
|---|---|---|---|
| 成功 | 200 | 0 | 正常响应 |
| 业务错误 | 200 | 非 0 | 参数校验失败 / 唯一约束冲突 / 上游 API 失败 |
| 鉴权失败 | 401 | 透传 / 1000 | 头缺失 / Cloudreve 拒绝 |
| 权限不足 | 403 | 1000 | 非 admin 访问 /admin/* |
| 服务内部 | 500 | 3000 / 3001 | panic / IO 失败 |
业务错误一律 HTTP 200,靠 envelope.code 区分。客户端按 envelope.code 而不是 HTTP status 判错。
成功响应示例
{
"code": 0,
"msg": "ok",
"data": {
"version": "0.1.0",
"user_id": "sub-alice",
"is_admin": false,
"email": "alice@example.com",
"nickname": "Alice",
"group_name": "User",
"capabilities": ["sources", "storage", "migrate", "browse"]
},
"request_id": "01J5XYZABCDEFGHIJK"
}
错误响应示例
业务参数校验失败(HTTP 200):
{
"code": 1000,
"msg": "name must contain only ASCII letters / digits / _ / -",
"data": null,
"request_id": "01J5..."
}
鉴权失败(HTTP 401,透传 Cloudreve code):
{
"code": 40301,
"msg": "token expired",
"data": null,
"request_id": "01J5..."
}
非 admin 访问 admin 端点(HTTP 403):
{
"code": 1000,
"msg": "admin required",
"data": null,
"request_id": "01J5..."
}
request_id
- 在
middleware/request_id.rs注入,所有路径都有(含/health白名单)。 - 同时作为响应头
X-Request-Id回写。 - 写入
audit_log表与日志 tracing span,可用于跨层追踪。
客户端实现要点
- 永远先读 envelope.code,不会根据 HTTP 200 / 4xx / 5xx 二分判错。
- request_id 写日志:把每次请求的 request_id 记到客户端日志,排障联调时给 server 端找审计/access log。
- null vs 缺省:
data在错误时恒为null;成功时若是空对象/数组,也是{}/[],不会是null。 - 未来兼容:cr4sync 不保证 envelope 增字段时客户端能容忍——需宽松解析(额外字段忽略,缺字段 default)。
错误码
envelope.code 编码
| 区间 | 含义 |
|---|---|
0 | 成功 |
1xxx | 客户端错误(参数 / 资源不存在 / 冲突) |
2xxx | 上游错误(Cloudreve / 百度 4xx-5xx) |
3xxx | 服务内部错误 |
| Cloudreve 透传 | 鉴权失败时直接透传 Cloudreve /user/info 的 code |
完整映射表
| HTTP | envelope.code | ApiError 变体 | 含义 |
|---|---|---|---|
| 200 | 0 | — | 成功 |
| 200 | 1000 | BadRequest(msg) | 参数校验失败 |
| 200 | 1004 | NotFound(msg) | 资源不存在 |
| 200 | 1009 | Conflict(msg) | 唯一约束冲突(重名) |
| 200 | 2000 | Cli(CliError::Remote(...)) | 上游远程错误(Cloudreve / 百度返非 0) |
| 200 | 2004 | Cli(CliError::NotFound(...)) | 上游资源不存在(4xx 但归为“业务“) |
| 200 | 3002 | Cli(CliError::Config(...)) | 配置错误(缺字段 / 非法值) |
| 200 | 3007 | Cli(CliError::Consistency(...)) | 迁移一致性校验失败 |
| 401 | 1000 | Unauthorized(msg) | 头缺失 / 格式错误 / JWT 过期 |
| 401 | 透传 | UpstreamAuth { code, msg } | Cloudreve 端 token 无效 |
| 401 | 2001 | Cli(CliError::Auth(...)) | 百度凭证过期 / 不存在 |
| 403 | 1000 | Forbidden(msg) | 非 admin 访问 /admin/* |
| 500 | 3000 | Internal(msg) | 服务内部 panic / 未分类错误 |
| 500 | 3001 | Cli(CliError::LocalIo(...)) | 本地 IO 失败(写 db / log) |
鉴权失败时 HTTP 401 + envelope.code 透传上游:客户端可以直接拿这个 code 跟 Cloudreve API 文档对照。其余业务错误一律 HTTP 200 + 业务 code 区分。
常见处理建议
| code | 客户端应做 |
|---|---|
0 | 处理 data |
1000 | 提示用户参数错误(看 msg) |
1004 | 提示资源不存在;可能本地缓存过时,刷新列表 |
1009 | 提示重名/冲突,让用户改名重试 |
2000 / 2001 / 2004 | 上游故障,提示“暂时不可用“,按 msg 决定是否重试 |
3000 / 3001 / 3002 | server 端故障,无重试价值;上报 request_id 给管理员 |
3007 | 一致性校验失败,提示用户检查源 / 目标差异;可调 migrate retry 重试 |
| HTTP 401 | 让用户重新登录 Cloudreve 拿新 token |
| HTTP 403 | 非 admin 路径不要展示 admin 功能 |
last_err 字段
migrate_task 表有个 last_err 字段,记录最近一次失败的错误信息(人类可读字符串)。任务失败状态(failed / verify_failed)下通过 GET /migrate/jobs/{job_id}/tasks 或 GET /admin/users/{user_id}/migrate 可以看到。
日志系统
cr4sync server 把日志输出按 target 分流,配合 tracing-subscriber 的多 layer 架构,让运维和调试都能拿到自己需要的信息。
输出位置
| Layer | 文件 / 输出 | 收集范围 |
|---|---|---|
| stderr | 终端 | 所有 target(按 [log] 配置过滤) |
sync_core.log | 文件 | 同 stderr |
server.log | 文件 | 仅 target = cr4sync 命名空间 |
server.log 用 Targets filter 收集所有 cr4sync 命名空间的日志,排除 sync-core 流水。
按天切割
server.log 与 sync_core.log 均按天切割(tracing-appender rolling daily),当天文件名带日期后缀:
server.log.YYYY-MM-DD、sync_core.log.YYYY-MM-DD。排查时按日期 grep 当天文件即可,避免单文件无限增长。
分层约定
cr4sync server 新写日志点遵守以下约定。该约定也写在 src/logging.rs 模块顶 doc 内:
| 级别 | 应包含 |
|---|---|
INFO | 一行结果性事件。只带最关键字段(如 authenticated user_id=... group_name=... is_admin=...,migrate.job.created job_id=...,migrate.job.completed completed=... failed=...) |
DEBUG | 参数、URL、中间步骤、缓存命中 / 未命中、JSON / text 响应体 |
TRACE | 结构化内部状态、循环细节、JSON 响应原文 |
TRACE 不打 raw 数据传输 body(chunk bytes / range download bytes)——这类位置只在 DEBUG 打 url + offset + size,避免日志撑爆磁盘。
看 cloudreve 远端响应
排查鉴权 / admin 识别问题最常用的姿势是开 trace:
./cr4sync serve --log-level trace
随后任何调到 cloudreve /user/info 的请求都会在 server.log 和 stderr 留下完整的链条:
debug cr4sync::server cloudreve /user/info request url=... sub=...
trace cr4sync::server cloudreve /user/info response status=200 body={"code":0,"data":{...}}
info cr4sync::server authenticated user_id=... group_name=... is_admin=...
body 字符串会被截到约 4 KiB 防爆。
字段不重复的设计
多 fmt layer 共享 FormattedFields 缓冲;如果对同一 span 字段调用 Span::current().record(...),每个 layer 都会 append 一次 → 字段在日志里重复 N 次(N = layer 数)。
cr4sync 目前是:
- 请求 span 在
request_idmiddleware(src/server/middleware/request_id.rs)创建时只带request_id,禁止后续record()回填。 - 用户身份通过
authmiddleware(src/server/middleware/auth.rs)塞进响应 extensions(LogIdentity { user_id, is_admin }),由外层access_logmiddleware(src/server/middleware/access_log.rs)在出口读取并作为事件字段输出。
净效果:access 行每个字段恰好出现一次。