Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

cr4sync

cr4sync 是一个独立的命令行二进制 + 可选的多用户 HTTP API 服务,围绕 Cloudreve v4 实例提供两类核心能力:

  1. 本地 ↔ Cloudreve 同步——一次性同步 / 增量持续监听 / 双向同步,统一走 sync-coreSyncEngine
  2. 第三方网盘 → Cloudreve 单向迁移——目前支持百度网盘,真流式管线 + 断点续传 + 端到端一致性校验。

启用 server cargo feature 还可以以 HTTP API 形式把上述迁移能力暴露给多个 Cloudreve 用户,附带管理员审计能力。

两种使用形态

形态适用场景凭证落盘位置数据库
CLI站长本机单人使用./credential/token.json + ./credential/sources/baidu.json./credential/migrate.db
Server主程序插件,向多用户暴露 HTTP APIserver.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 类型恒为字符串(Cloudreve sub 字段直透)。

安装与编译

前置

  • 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.dbstorage 表,每用户独立。CLI 仍以 [[storage]] 段为准。

字段参考

下表枚举 cr4sync.toml 所有已知字段的类型、可选性、默认值与校验规则。

[serve]

字段类型必填默认说明
addressstring"127.0.0.1"cr4sync serve 监听地址。CLI 用户可全省。必须是合法 IPv4/IPv6 字面值。
portu165731监听端口。

[serve.cr4]

CLI 与 server 都强依赖的 Cloudreve 后端配置。

字段类型必填默认说明
server_urlstringCloudreve 完整 URL,必须带 http://https:// 协议头。
server_portu16按协议推断(http=80 / https=443)自定义端口时填写。
server_api_pathstring"/api/v4"API 前缀。
admin_groupstring内置识别 "Admin" / "管理员"server 模式 admin 组名覆盖。自定义 Cloudreve admin 组名时填写;填写后只认该值。

旧版本兼容:若 [serve] 段直接出现 server_url / server_port / server_api_path(旧的扁平结构),loader 会拒绝并提示迁移到嵌套的 [serve.cr4] 段。

[credential]

字段类型必填默认说明
mailstring明文账号。仅作为 cr4sync login 不带 --mail 时的回退源。
passwdstring明文密码。同上。

强烈建议留空:把账密留在配置文件里有泄露风险(备份 / 版本控制)。改用 cr4sync login --mail x --passwd y 一次性登录,凭证只落盘到 ./credential/token.json(已 .gitignore)。

[db]

字段类型必填默认说明
typestring"sqlite3"数据库类型,目前恒定 sqlite3,预留扩展。
pathpath"./data"sync-core 数据目录;sqlite 文件落到 <path>/sync_core/datas/.sync_db.sqlite3

[conf]

字段类型必填默认说明
retryu323全局重试次数。仅 up / dl 子命令使用;sync 由 sync-core 自己管。

[sync]

cr4sync sync / SyncEngine 编排参数。

字段类型必填默认校验 / 说明
srcpath本地源目录,不存在则 cr4sync 启动失败。
targetstring远端目标路径,/ 自动映射为 cloudreve://my
workerusizeCPU 核数sync-core WorkerPool 容量。
parallelismusize3最大并发传输数,范围 1..=512
sync_modestring"single"single / incremental / append / two_way 之一。
sync_conflictstring"skip"skip(真 no-op) / overwrite(本地覆盖远端) 之一。中文别名 跳过 / 覆盖 也接受。

模式映射:

sync_modesync-core SyncMode行为
singleUploadOnly仅上传,跑完即退
incrementalUploadOnly仅上传,初始 + 持续监听
appendUploadOnly跳过初始,仅持续监听
two_wayFull双向(上传 + 下载 + 冲突策略)

[log]

字段类型必填默认说明
modestring"cr4sync"cr4sync(仅本程序 + sync_core)/ full(所有依赖按 level 输出,调试网络问题用)。
levelstring"info"trace/debug/info/warn/error。运行期可热更新。
log_pathpath"./logs/sync_core.log"主日志文件。
worker_log_terminalboolfalsesync / up -r / dl -r 是否在终端打 worker 日志(缺省 false,避免大量小文件刷屏)。
server_log_pathpath"./logs/server.log"cr4sync serve 使用;HTTP 访问 + 审计的额外 layer 输出。

[migrate]

迁移管线调优。CLI 与 server 均生效。

字段类型必填默认说明
prefetchstring"auto"预取策略: auto(chunk_size ≤ 100MB 自动预取) / always(强制) / never(串行)
retryu323单 chunk 上传/下载失败重试次数, 上限 10. 指数退避 2s→4s→8s→16s→32s→60s(cap). 仅重试可恢复错误 (网络/服务端瞬时故障 + 锁冲突); 鉴权失败 / 一致性错 / 配置错不重试.
conflictstring"auto"cloudreve 锁冲突 (错误码 40073) 处置: auto(强制解锁+重试→失败则跳过) / skip(直接跳过) / force_unlock(强制解锁+重试→失败则中止整个 job)
max_concurrent_jobsu3210server 模式全局并发 job 上限,范围 1..=512。超出上限的 job 在后台排队等待(status = "queued")。调大需注意带宽/内存。
approvestring"manual"迁移权限申请审批模式:"auto"(申请即生效) / "manual"(管理员审批)。仅 server 模式生效。
pikpak_range_optimizationf640.5PikPak 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_idstring内置 Alist 公开应用OAuth client_id,独享配额时填。
client_secretstring同上OAuth client_secret。
redirect_uristring同上OAuth redirect_uri。
refresh_tokenstring一次性导入用。强烈建议留空走 cr4sync login --baidu 交互式粘贴。
is_proxyboolfalse是否启用代理(代理在 source_credential 级,driver 凭证级)
proxystring?null代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填)

server 模式百度凭证写哪里:CLI 落盘到 ./credential/sources/baidu.json;server 落到 server.dbsource_credential 表(每用户一行)。两者互不干扰。

[sources.onedrive]

仅 server 迁移端点用(CLI 模式 OneDrive 需通过 API 完成 OAuth,不走配置文件)。

字段类型必填默认说明
client_idstringAzure 应用 client_id。server 模式下用户在 login-session 中传入,此处为全局默认。
client_secretstringAzure 应用 client_secret。同上。
redirect_uristringOAuth 回调地址。同上。
national_cloudstring"global"OneDrive 云类型:"global"(国际版)或 "china"(21Vianet 世纪互联版)。
is_proxyboolfalse是否启用代理(代理在 source_credential 级,driver 凭证级)
proxystring?null代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填)

OneDrive 无内置默认应用凭证,用户必须自备 Azure 应用。server 模式下 session 创建时传入的凭证优先级高于配置文件。

[sources.aliyun]

仅 server 迁移端点用。

字段类型必填默认说明
client_idstring阿里云盘开放平台 app_id。server 模式下用户在 login-session 中传入,此处为全局默认。显式填 client_id 时走直连模式(POST openapi.alipan.com/oauth/refresh_token)。
client_secretstring开放平台 app_secret。同上。
redirect_uristringOAuth 回调地址。同上。
oauth_token_urlstring"https://api.alistgo.com/alist/ali_open/token"中转 refresh 端点(alist 官方中转)。中转模式用 GET ?refresh_ui=...&server_use=true&driver_txt=alicloud_qr 换 token。显式填 client_id 时走直连,此字段忽略。
refresh_tokenstring阿里云盘 refresh_token。中转模式(无 client_id)时直接填入,免自备开放平台应用。
drive_typestring"resource"仅 aliyun driver 使用。"resource"(资源盘,网页端默认)/ "backup"(备份盘)。选择访问资源盘还是备份盘。
is_proxyboolfalse是否启用代理(代理在 source_credential 级,driver 凭证级)
proxystring?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_idstringS3 access key ID
secret_access_keystringS3 secret access key
endpointstring自动推断自定义端点。AWS S3 可省(SDK 自动选);MinIO 等必填。
regionstring区域(如 us-east-1
bucketstring存储桶名
force_path_styleboolfalse强制 path-style URL(MinIO 需 true
is_proxyboolfalse是否启用代理(代理在 source_credential 级,driver 凭证级)
proxystring?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_config JSON 列。

[sources.oss]

阿里云 OSS,内部复用 S3Client,endpoint 默认从 region 自动拼接。

字段类型必填默认说明
access_key_idstring阿里云 RAM access key ID
secret_access_keystring阿里云 RAM secret access key
regionstring区域(如 cn-hangzhou)。误填 oss-cn-hangzhou 会被规范化去掉 oss- 前缀。endpoint 自动拼为 https://oss-{region}.aliyuncs.com
bucketstringOSS bucket 名
endpointstring自动拼接覆盖模板 endpoint(自建 OSS 兼容服务时用)
is_proxyboolfalse是否启用代理(代理在 source_credential 级,driver 凭证级)
proxystring?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_idstring腾讯云 CAM access key ID(SecretId)
secret_access_keystring腾讯云 CAM secret access key(SecretKey)
regionstring区域(如 ap-guangzhou)。误填 cos-ap-guangzhou 会被规范化去掉 cos- 前缀。endpoint 自动拼为 https://cos.{region}.myqcloud.com
bucketstringCOS bucket 名,必须含 APPID(如 my-bucket-1250000000
endpointstring自动拼接覆盖模板 endpoint
force_path_stylebooltrueCOS 缺省 true(path-style 兼容)
is_proxyboolfalse是否启用代理(代理在 source_credential 级,driver 凭证级)
proxystring?null代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填)

COS bucket 名格式为 <bucketname>-<APPID>,APPID 在腾讯云控制台可查。region 规范化同 OSS:填 cos-ap-guangzhou 会存为 ap-guangzhou

[sources.pikpak]

PikPak driver,账密直接登录(非 OAuth),需代理访问。

字段类型必填默认说明
emailstringPikPak 账号邮箱
passwordstringPikPak 密码
refresh_tokenstring?已持有的 refresh_token(可选,提供则跳过账密登录直接刷新)
device_idstring?MD5(email+password)设备 ID,用户可覆盖
captcha_tokenstring?验证码 token(可选,缺省由服务端自动获取)
is_proxyboolfalse是否启用代理(代理在 source_credential 级,driver 凭证级)
proxystring?null代理名(引用 [[proxy]] 的 name,is_proxy=true 时必填)

PikPak 国内访问不友好,建议配置代理。PikPakAuth + PikPakClient 都接受 proxy 参数,用 build_proxied_http_client 注入。

[sources.webdav]

WebDAV 通用网盘兜底 driver. 支持所有 WebDAV 协议服务 (Nextcloud / Alist / 坚果云等). 匿名访问和认证访问都支持.

字段类型必填默认说明
endpointstring-WebDAV 服务终结点, 如 http://192.168.1.100:8080/dav/
usernamestring-用户名, 匿名访问时不填
passwordstring-密码, 匿名访问时不填
auth_schemestring自动探测basic / digest / anonymous; 缺省首次请求时自动探测
is_proxyboolfalse代理配置 (source_credential 级)
proxystring-引用 [[proxy]].name

server 模式: auth_scheme 由 credential 端点自动探测并存库; CLI 模式可手动指定, 不填则探测.

[sources.cloudreve]

Cloudreve 源 driver (源端 cloudreve 实例作为迁移源). 复用 sync-core ApiClient + AuthClient (登录/fs/download). download 带 Referer + User-Agent (防盗链).

字段类型必填默认说明
server_urlstring-cloudreve 服务地址, 如 https://demo.cloudreve.org (不带 /api/v4)
usernamestring-邮箱 (CLI 登录用 cr4sync login --cloudreve); serve 模式 webui 登录
passwordstring-密码 (CLI 登录用); serve 模式不持久化密码 (仅 token)
refererstringserver_urldownload / login 的 Referer; 缺省 = server_url
user_agentstring浏览器 UAUser-Agent; 缺省 pikpak web UA; 前端可填 navigator.userAgent
is_proxyboolfalse代理配置 (source_credential 级)
proxystring-引用 [[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 里。

字段类型必填默认校验 / 说明
namestring实例名,全局唯一,仅 ASCII 字母数字 / _ / -
driverstring"baidu" / "onedrive" / "aliyun" / "pikpak" / "webdav" / "cloudreve" / "s3" / "oss" / "cos"
root_pathstring"/"源端起点路径。
target_pathstring目标 cloudreve URI(如 cloudreve://my/x)。
parallelismu321单实例并发,上限 8
rate_limitu323源端 API QPS 上限,范围 1..=20
extra_configstringdriver 特定 JSON 配置。aliyun 存 `{“drive_type”:“resource”
is_proxyboolfalse已废弃,请在 [sources.xxx] 配置代理;保留字段兼容旧配置
proxystring?null已废弃,请在 [sources.xxx] 配置代理;保留字段兼容旧配置

[[proxy]](数组段,可多个)

通用代理配置,为第三方网盘 driver 访问提供代理出站能力。代理在 source_credential 级(driver 登录时绑定),通过 [sources.xxx]is_proxy / proxy 字段引用。

字段类型必填默认校验 / 说明
namestring实例名,用户内唯一,仅 ASCII 字母数字 / _ / -
schemestring"http" / "https" / "socks5" / "socks5h"
hoststring代理主机地址(IP 或域名)。
portu16代理端口,范围 1..=65535
usernamestring代理认证用户名(可选)。
passwordstring代理认证密码(可选)。

配置校验汇总

config/loader.rs 在解析时强制以下规则:

  1. [serve.cr4].server_url 必填且必须以 http://https:// 开头。
  2. 旧格式扁平 [serve].server_url 等字段拒绝并提示迁移。
  3. [serve].address 必须是合法 IP 字面值。
  4. [sync].src 必须存在(fs check)。
  5. [sync].worker >= 1
  6. [sync].parallelism in 1..=512
  7. [sync].sync_mode{single, incremental, append, two_way} 内。
  8. [sync].sync_conflict{skip, overwrite, 跳过, 覆盖} 内。
  9. [log].mode{cr4sync, full} 内。

任何一条不满足,启动即报错,不会写回默认值

[license]

License 文件路径配置. license 本身是签名后的二进制文件 (.lic), 由 @ReAxis 签发.


字段类型必填默认说明
pathstring./credential/license.liclicense 文件默认/指定路径

凭证目录

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.jsoncr4sync login (CLI)Cloudreve access + refresh token删了重 login 即可
credential/sources/baidu.jsoncr4sync login --baidu (CLI)百度 OAuth refresh + access删了重 login –baidu
credential/migrate.dbcr4sync mgr 自动建 (CLI)migrate 任务状态机删了下次跑会从零扫,已 completed 任务不会重传
credential/server.dbcr4sync 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用途
loginCloudreve 账密 / 扫码登录;百度 OAuth
sync按配置文件同步本地 ↔ Cloudreve
upupload上传文件 / 目录到 Cloudreve
dldownload从 Cloudreve 下载文件 / 目录
deldelete删除 Cloudreve 文件 / 目录
ls列目录(支持 cloudreve:// / baidu:// / <storage>://
mgrmigrate第三方网盘 → 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
--qrflag扫码模式开关。
--device-name <NAME>string手机端“授权登录“页显示的设备名。缺省 cr4sync-<hostname>
--baiduflag百度 OAuth 模式。
--aliyunflag阿里云盘中转模式。
--pikpakflagPikPak 登录。
--cloudreveflag源端 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 健康检查,未部署时拒绝并提示。

流程:

  1. 生成 X25519 keypair
  2. POST /qr-login-relay/api/session/createqr_payload
  3. 终端用 Unicode 半块字符渲染二维码
  4. 1500ms 轮询 /statusauthorized 后拿 /result
  5. X25519 共享密钥(直接当 AES-256 key,无 HKDF) + AES-256-GCM 解密
  6. 校验 payload.cloudreve 与本机 [serve.cr4].server_url 同站
  7. credential/token.json

二维码默认有效期 120 秒,超时需重新运行。Ctrl-C 随时中止。

③ 百度 OAuth

cr4sync login --baidu

流程(oob 授权码方式):

  1. 终端打印 oob 授权 URL(redirect_uri=oob
  2. 用户在浏览器打开授权 → 授权页回显授权码 code
  3. 粘贴 code 回终端
  4. cr4sync 直连百度 oauth/2.0/token?grant_type=authorization_code 兑换 access_token + refresh_token
  5. 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

流程(中转模式,免注册):

  1. 终端提示访问 https://alistgo.com/zh/tool/aliyundrive/request.html 扫码获取 refresh_token
  2. 用户粘贴 refresh_token
  3. cr4sync 走 alist 中转端点兑换 access_token
  4. 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 提示用户:
    1. 浏览器登录 PikPak (user.mypikpak.net)
    2. F12 -> Application -> Local Storage 找 captcha_token + refresh_token
    3. 填入 [sources.pikpak]refresh_token + captcha_token
    4. 重新执行 cr4sync login --pikpak (走优先级 1)
  • refresh_token + captcha_token 永远成对出现, 优先级高于 email + password
  • device_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临时覆盖同步模式,不写回配置文件。可选值见下表。

模式映射

--modesync-core SyncMode行为
singleUploadOnly仅上传。run_initial_sync 跑完即退出。
incrementalUploadOnly仅上传。run_initial_sync 跑完后进入 run_continuous,Ctrl-C 停止。
appendUploadOnly仅持续监听,跳过 initial。
two_wayFull双向。run_initial_sync 跑完即退出(上传本地新增 + 下载远端新增 + 冲突按 sync_conflict 解)。

配置热更新

incremental / append 模式下运行期支持热更新 cr4sync.toml

  • 软更新(引擎内重读):sync_conflict, parallelism, 日志 level / mode 即时生效。
  • 硬重建(停旧引擎重建):src / target / worker / parallelism 这几个被 sync-core WorkerPool 构造时快照、运行期不重读的字段。cr4sync 外层 loop 检测到变更后 engine.stop() → 丢旧 Arc → 用新 config SyncEngine::new

进度展示

indicatif::MultiProgress 渲染:

  • 文件级粒度(completed/total + running item 名称)
  • 多 worker 并发情况下底部常驻 N 条 spinner(N = 并发数,上限 512)
  • log 与 bar 协同:MultiProgress::suspend 让位写日志再重画 bar

已知限制

  1. UploadOnly 持续阶段冲突一律覆盖:sync-core 在 UploadOnly 的 continuous 阶段把冲突强制映射为 KeepLocal。sync_conflict = "skip" 在持续阶段实际仍上传覆盖。two_way 模式下 skip / overwrite 才完整生效。
  2. 远端 hash 不可得:sync-core 内容比对依赖 mtime + size,“同大小且 mtime 未变的篡改“理论上可漏判。
  3. 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, --recursiveflagfalse递归处理目录。
-f, --forceflagfalse覆盖已存在文件。等价于 --conflict overwrite
--conflict <MODE>enumskipskip(跳过)/ 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 expiredtoken.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, --longflagfalse长格式:<size> <mtime> <name>
-H, --human-readableflagfalse人性化大小(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百度网盘根 / 路径 /xcredential/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, --recursiveflagfalse递归删除目录。目录必须带 -r,否则报错。
-y, --yesflagfalse跳过 y/N 确认提示,脚本场景用。

行为

  1. 单文件:POST /file/delete 直接删。
  2. 目录(带 -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, --yesflagfalse跳过预览/确认提示。
--force-rewriteflagfalse把本次 scan 命中的任务(含 completed)踩回 pending 重传。只动本次涉及行
--force-resetflagfalse清空整个 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::Authrun_concurrent 立即 JoinSet::shutdown,提示 cr4sync login --baidu

已知限制

  1. driver:接受 "baidu" / "onedrive" / "aliyun" / "pikpak" / "webdav" / "cloudreve" / "s3" / "oss" / "cos"
  2. 校验粒度:cloudreve v4 不返 md5,中段被篡改理论可漏判。
  3. dlink 8h 时效:百度单文件 chunk 循环超 8h 由 get_dlink 自然续期,调用方无感。
  4. 校验失败需手动:标记 verify_failed 的任务下次跑会清 session 重头传一次;再失败需 --force-rewrite 或排查源/目标差异。

Server 总览

cr4sync serve 是把第三方网盘 → Cloudreve 迁移能力以 HTTP API 形式暴露给多个 Cloudreve 用户的服务模式。仅在编译时启用 server cargo feature 才包含。

设计原则

  1. CLI 模式完全不变:CLI 仍走 ./credential/token.json + ./credential/sources/baidu.json + ./credential/migrate.db + toml [[storage]]
  2. Server 全状态收敛到 ./credential/server.db:所有 user / 凭证 / storage / migrate / audit 都在这一个 sqlite 文件,与 CLI 完全隔离,备份只需一个文件。
  3. cloudreve 身份强隔离(P0 不变量):server 路径上传 cloudreve 必须使用调用方 JWT,严禁读取站长 credential/token.json。静态 grep 守护测试兜底。
  4. 行级隔离:所有 server.db Repo 方法签名首参数必带 user_id,admin 跨用户走专门 *_admin 方法。
  5. admin 自动识别:Cloudreve /user/info 响应的 data.group.name 默认命中 "Admin" / "管理员" 任一即为 admin;可用 [serve.cr4].admin_group 覆盖,moka 缓存 60s。
  6. JWT 不本地验签:cr4sync 不持有 Cloudreve secret,每次 cache miss 走远端 /user/info/{sub} 校验。
  7. 第三方 refresh_token 加密落 server.db:AES-256-GCM 加密, 密钥编译进二进制混淆存储, 不入 config / DB / 日志。站长拿到 DB 只看到密文。
  8. cloudreve refresh_token 仅内存态:随 POST /migrate body 传入,活在 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 速览

MethodPath说明鉴权
GET/health健康检查
GET/meta(version / user_id / is_admin / banned)
POST/auth/login账密登录 (独立部署)
POST/auth/login/2fa2FA 验证
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-sessionOneDrive 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/credentialS3/OSS/COS 直接凭证 upsert(非 OAuth,driver ∈ s3/oss/cos)
POST/source/pikpak/credentialPikPak 账密登录(非 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/infocloudreve 源用户信息 + 容量
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/sseSSE 进度推送
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}(通过 MigrateProgressSink trait 抽象),CLI 实现 IndicatifSink,server 实现 BroadcastSink
  • 共享 MigrateTaskStore trait,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_guardcargo 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].address127.0.0.1临时覆盖监听地址。必须是合法 IPv4/IPv6。
--port <PORT>u16[serve].port5731临时覆盖监听端口。

必须配置段 (最小启动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 缺失时启动失败。其他全部可省。

启动副作用

  1. 打开 / 创建 ./credential/server.db(缺省路径)。首次启动自动执行 migrations(建表 + 索引)。
  2. 构造 AppState:含 ServerDb / AuthValidator / JobManager / reqwest::Client
  3. 组装 router/health 白名单 + 其余路径强制鉴权 + /admin/*require_admin 中间件。
  4. bind + servetokio::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(前端 Vite base 与 Router basename 已对齐该前缀)。
  • 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.jslocation / 里的 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/cr4synchttp://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
TTLmin(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 取 AuthContextis_admin == falseApiError::Forbidden(HTTP 403 + envelope.code=1000)。

白名单

  • /api/v4/cr4sync/health 不需要鉴权。
  • embed-web 构建下 /extensions/cr4sync(内嵌前端 SPA 静态资产)不走鉴权——前端加载后再用 token 调 API。详见 部署

鉴权失败回执

现象HTTPenvelope
头缺失 (X-CR4-Authorization / Authorization 都无) / 非 Bearer401{ 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 code401{ 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"
}
字段类型必填说明
emailstringCloudreve 账号邮箱
passwordstring密码
captchastring?验证码:图形验证码填用户输入;Turnstile/reCAPTCHA 填 widget 返回的 token
ticketstring?图形验证码的 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/loginlogin_captcha 字段决定 (true=开启); 验证码类型与 site keys 从 GET /site/config/basic 拿。login_captcha=falseenabled=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_ReCaptchaKeyreCAPTCHA site key(仅 recaptcha 类型有值)
turnstile_site_idTurnstile 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 三步式:

  1. POST /api/v4/cr4sync/auth/qr/create — 创建扫码会话,返回二维码载荷
  2. GET /api/v4/cr4sync/auth/qr/status/:id — 1500ms 轮询状态
  3. 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 内部 ID
  • qr_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..."
}

字段

字段类型说明
codei320 表示成功;非 0 是错误码,详见 错误码
msgstring人类可读的提示信息。code=0 时恒为 "ok"
dataany | null业务数据。成功时是端点定义的具体类型,错误时为 null
request_idstringULID,单调时间排序 + 随机。每个请求唯一,同时也是响应头 X-Request-Id

HTTP status code 与 envelope code 的关系

cr4sync 区分传输层失败业务层错误

类别HTTPenvelope.code何时
成功2000正常响应
业务错误200非 0参数校验失败 / 唯一约束冲突 / 上游 API 失败
鉴权失败401透传 / 1000头缺失 / Cloudreve 拒绝
权限不足4031000非 admin 访问 /admin/*
服务内部5003000 / 3001panic / 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,可用于跨层追踪。

客户端实现要点

  1. 永远先读 envelope.code,不会根据 HTTP 200 / 4xx / 5xx 二分判错。
  2. request_id 写日志:把每次请求的 request_id 记到客户端日志,排障联调时给 server 端找审计/access log。
  3. null vs 缺省data 在错误时恒为 null;成功时若是空对象/数组,也是 {} / [],不会是 null
  4. 未来兼容:cr4sync 不保证 envelope 增字段时客户端能容忍——需宽松解析(额外字段忽略,缺字段 default)。

错误码

envelope.code 编码

区间含义
0成功
1xxx客户端错误(参数 / 资源不存在 / 冲突)
2xxx上游错误(Cloudreve / 百度 4xx-5xx)
3xxx服务内部错误
Cloudreve 透传鉴权失败时直接透传 Cloudreve /user/infocode

完整映射表

HTTPenvelope.codeApiError 变体含义
2000成功
2001000BadRequest(msg)参数校验失败
2001004NotFound(msg)资源不存在
2001009Conflict(msg)唯一约束冲突(重名)
2002000Cli(CliError::Remote(...))上游远程错误(Cloudreve / 百度返非 0)
2002004Cli(CliError::NotFound(...))上游资源不存在(4xx 但归为“业务“)
2003002Cli(CliError::Config(...))配置错误(缺字段 / 非法值)
2003007Cli(CliError::Consistency(...))迁移一致性校验失败
4011000Unauthorized(msg)头缺失 / 格式错误 / JWT 过期
401透传UpstreamAuth { code, msg }Cloudreve 端 token 无效
4012001Cli(CliError::Auth(...))百度凭证过期 / 不存在
4031000Forbidden(msg)非 admin 访问 /admin/*
5003000Internal(msg)服务内部 panic / 未分类错误
5003001Cli(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 / 3002server 端故障,无重试价值;上报 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}/tasksGET /admin/users/{user_id}/migrate 可以看到。

日志系统

cr4sync server 把日志输出按 target 分流,配合 tracing-subscriber 的多 layer 架构,让运维和调试都能拿到自己需要的信息。

输出位置

Layer文件 / 输出收集范围
stderr终端所有 target(按 [log] 配置过滤)
sync_core.log文件同 stderr
server.log文件target = cr4sync 命名空间

server.logTargets filter 收集所有 cr4sync 命名空间的日志,排除 sync-core 流水。

按天切割

server.logsync_core.log 均按天切割(tracing-appender rolling daily),当天文件名带日期后缀: server.log.YYYY-MM-DDsync_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 目前是:

  • 请求 spanrequest_id middleware(src/server/middleware/request_id.rs)创建时只带 request_id禁止后续 record() 回填。
  • 用户身份通过 auth middleware(src/server/middleware/auth.rs)塞进响应 extensions(LogIdentity { user_id, is_admin }),由外层 access_log middleware(src/server/middleware/access_log.rs)在出口读取并作为事件字段输出。

净效果:access 行每个字段恰好出现一次。