# 本地备份同步七牛 独立 Node.js 脚本项目,使用官方 qiniu SDK 7.15.2。入口命令在 doodle-store 根目录执行。只复制本地文件到已存在的七牛空间,不创建空间、不删除/覆盖远端对象、不修改数据库或业务 URL。 ## 配置 已生成本机 `packages/asset-sync/.env`,源目录配置为: ```dotenv SOURCE_DIR=/Users/joey-xd/sites/doodle/doodle-assets/cloud_backup QINIU_AK= QINIU_SK= QINIU_BUCKET= QINIU_REGION= QINIU_KEY_PREFIX=doodle/ QINIU_PUBLIC_DOMAIN= SYNC_CONCURRENCY=3 SYNC_RETRIES=2 SYNC_MAX_FILE_BYTES=67108864 ``` 填写本地 `.env` 即可,不要将 AK/SK 写到小程序、Git、命令参数或聊天中。该文件被 Git 忽略,并以 0600 权限保存。新机器可从 `.env.example` 复制;相对 SOURCE_DIR 以配置文件目录为基准,运行目录不影响源路径。进程环境变量优先于文件配置。 | 配置 | 说明 | | ------------------- | -------------------------------------------------------------------------------------------------------------------------- | | SOURCE_DIR | 本地备份根目录,工具只读 | | QINIU_AK / QINIU_SK | 七牛服务端 AccessKey / SecretKey,仅正式同步要求 | | QINIU_BUCKET | 所有资源共用的公开空间:图片、头像、字体、JSON 等 | | QINIU_REGION | 可留空自动查询;固定区域时使用七牛 region ID,如 z0、z1、z2、na0、as0 | | QINIU_KEY_PREFIX | 对象前缀;默认 doodle/,空串为原相对路径 | | QINIU_PUBLIC_DOMAIN | 可选 HTTPS origin,例如 https://cdn.example.com;不含 /doodle 等路径,仅用于生成报告中的公共 URL,不证明域名已经绑定该空间 | | SYNC_CONCURRENCY | 并发文件数,1~10,默认 3 | | SYNC_RETRIES | 瞬时错误额外重试次数,0~5,默认 2 | | SYNC_MAX_FILE_BYTES | 每个文件读取内存的大小限制,默认 64 MiB | 空间必须已存在且有权查询和上传。执行前读取空间设置,核对目标空间实际 private=0,工具不会自动调整 ACL。头像与其他资源使用同一个 QINIU_BUCKET,无需配置私有空间。 ## 使用 ```bash # 查看用法 pnpm assets:sync --help # 默认预演全部文件,不调用七牛接口,也不要求 AK/SK pnpm assets:sync # 只预演题型预览图 pnpm assets:sync --only assets/previews # 填写 .env 后,先同步 51 张预览 pnpm assets:sync --apply --only assets/previews # 可选择多个目录 pnpm assets:sync --apply --only assets/fonts --only assets/materials # 全部同步,含头像,均使用 QINIU_BUCKET 公开空间 pnpm assets:sync --apply # 自定义配置文件/报告;报告文件不能已存在,也不能位于源备份内 pnpm assets:sync --env /绝对路径/.env.sync --report /绝对路径/report.json ``` `--only` 按目录前缀或精确文件路径筛选,不是 glob;`--dry-run` 可显式声明预演,与 `--apply` 互斥。通过默认预演确认清单后,再添加 --apply 上传。文件缺失、损坏 JSON、超限文件、软链接及无法识别的预览/头像图片会停止预检。 ## 对象路径和同步行为 默认映射: ```text assets/previews/math/number-find.jpg → 公共 bucket : doodle/assets/previews/math/number-find.jpg user-avatars/<文件名>.png → 同一公共 bucket : doodle/user-avatars/<文件名>.png ``` 头像保留 `user-avatars/` 相对路径,与预览等资源共用公开空间。配置 QINIU_PUBLIC_DOMAIN 后,报告也会生成头像的公开地址。工具不根据文件名推断用户身份,用户归属仍需在后续绑定时核实。 1. 扫描源目录,忽略隐藏文件,记录实际 MIME、字节数、SHA-256 与七牛 ETag;不修改源文件。 2. 正式执行时重新读取并核对本地摘要,使用该次读取的 Buffer 上传,避免核对后源文件被修改。 3. 远端不存在时,用指定 bucket:key 的 insertOnly=1 凭证上传,仅允许新增,启用 SDK CRC 校验和 HTTPS。 4. 远端同 key 且 ETag/大小一致:跳过并标记已核对。不同内容:报告 conflict,保留原远端文件;需要新版本时选择新的前缀/key。 5. 上传完成再次 stat,对比远端 ETag/大小;不是仅凭“请求成功”就记完成。 6. 瞬时网络错误、429、5xx 有限重试,鉴权错误和冲突不盲目重试。上传已成功但响应丢失时,重试遇到 614 后回查摘要恢复。 7. 任一文件失败或冲突时命令退出码为 1,其余文件结果仍写入报告。中断后重新运行同一命令,会重新检查远端并跳过已完成对象;未提供分片级断点续传。 校验方式是七牛内容 ETag 和大小,不等同于从 CDN 下载全部文件后比较 SHA-256。SHA-256 是本地备份摘要,报告明确记录 verifiedBy=qiniu-etag-and-size。不会自动刷新 CDN 或替换同名对象。 ## 报告与后续接入 默认报告写 `packages/asset-sync/reports/<时间>-<随机ID>.json`,目录被 Git 和格式化工具忽略,文件权限 0600;含源相对路径、目标 bucket/key、MIME、大小、摘要、状态和有限错误码。不包含 AK、SK、上传凭证、签名链接或 SDK 原始异常。中断时报告可能未写完,以重新运行后的远端校验为准。 主要状态:planned(预演)、uploaded(上传并核对)、skipped(已有同内容)、conflict(同名异内容)、failed、not_attempted(全批配置/权限预检未通过)。常见全批错误:CREDENTIALS_REQUIRED、BUCKET_REQUIRED、BUCKET_VISIBILITY_MISMATCH。上传用户需有 bucketInfo、stat、upload 所需权限。 这次只做文件同步。同步成功后仍需按报告映射更新新后端 assets(保持现有 ID 并回填 bucket/key/MIME/摘要、status=ready)、字体配置、用户头像绑定及首页 JSON 内的 cloud 引用。content/page-config.json 当前会原样复制,不会自动转换嵌套链接;原云上传和定时任务也不会自动切换到七牛。 ## 验证与当前预演 ```bash pnpm assets:test pnpm check ``` 测试使用临时本地文件和模拟七牛接口,覆盖配置、ETag 边界、头像同空间映射、预演无网络、首次上传、重复跳过、冲突保护、重试、响应丢失恢复、源文件变化、校验失败、凭据脱敏及 CLI 报告权限。没有使用真实 AK/SK 或执行真实上传。 本地备份预演:62 个文件,共 3,568,778 字节;其中题型预览 51、头像 7、字体 1、素材 1、首页配置 1、demo 1。该数量为本地预演结果,不代表已上传。 实现依据:[七牛官方 Node.js SDK](https://developer.qiniu.com/kodo/sdk/nodejs)、[七牛上传策略](https://developer.qiniu.com/kodo/manual/put-policy)。 ## 回填服务端资产 上传报告确认成功后,根目录执行 `pnpm assets:map --report /绝对路径/报告.json` 预演,再添加 `--apply` 写入本机 doodle_dev。该独立回填步骤会重新核对七牛对象与备份摘要,事务更新已有题型预览资产,保留资源 ID 并记录审计;不处理头像归属、首页 JSON 嵌套链接或其他资源绑定。配置必须与 server 的域名、前缀及空间一致,详见根 README。