返回星港

不复制 133GB:用最小 ACL 让 Cloudreve 原地接管 SMB 里的旧文件

OBSERVER又菜又爱玩的小朱 LAUNCH JUMPS0 次跃迁
不复制 133GB:用最小 ACL 让 Cloudreve 原地接管 SMB 里的旧文件 · 封面

起因:133GB 的老文件,不想再复制一份

网盘搭起来之后,第一件想做的事是让 SMB 共享里那批老文件也能在网页上看到。这批数据在 /media/ztl/LinuxDate/files/我的文件,共 101 个文件、41 个子目录,合计 143217817324 字节,大约 133.4GiB,其中不少是单个几 GB 的大文件。

最直接的思路是复制一份进网盘的上传目录,但 133GB 意味着接近翻倍占用,而且以后两边内容会各自演化,反而更难维护。所以换了个思路:让 Cloudreve 原地登记这些文件,不搬动、不复制,靠权限控制保证它只能读。

目标与前提

项目情况
软件Cloudreve 社区版 4.19.1 ARM64
源目录/media/ztl/LinuxDate/files/我的文件
目标管理员用户 1、存储策略 2「外置机械硬盘」、网盘根目录 /
数据规模101 个文件、41 个子目录、143217817324 字节(约 133.4GiB)
外接盘ext4,固定 UUID 8b9cbec5-c4d7-466f-acea-d1c9531ccada

明确不做的事:不复制、不移动、不重命名源文件,不改动 SMB 配置,不改原属主,不改 systemd 可写范围,也不改存储策略模板。提取媒体信息是关闭的,避免自动生成大规模缩略图或扫描媒体内容。

第一步:设计权限,只开一条能走进去的缝

难点不在导入本身,而在权限。Cloudreve 以专用 nologin 用户 cloudreve 运行,而这个用户对 /media/ztl/LinuxDate/files 本来没有任何访问权。如果粗暴地 chmod -R 或递归 chown,等于把整盘数据权限都改了,风险太大。

最终采用的是“只加命名 ACL、只读、不递归”的方案:

对象cloudreve 用户的权限
/media/ztl/LinuxDate/files只有 --x,可穿越但不能列举其他兄弟目录
源树目录r-x
普通文件r--

另外:无写权限,没有新增 default ACL,原属主 ztl 和 owner 权限保持不变,没有 777,也没有递归 chown。systemd 的 ProtectSystem=strict 同样保持不动,所以源树在服务进程的挂载命名空间里依然是只读的。

这里有个容易误判的地方值得单独说:只读不等于网页上的删除按钮会变灰。数据库里的逻辑记录仍然可以被删除,只是源文件实体回收可能失败。所以不要拿网盘的“清空”之类的操作当成“只取消导入”。

权限设置脚本完整内容

权限是脚本化的,归档里的 restore.sh 就是实际落地用的那一份:默认只预演,必须显式加 --apply 才落盘;归档校验、外盘 UUID 核对、符号链接与特殊文件检查、其他命名 ACL 的存在性检查全都前置,任何一项不过就拒绝修改。完整内容如下。

#!/usr/bin/env bash
set -euo pipefail
# 默认只预演;仅给既有源文件增加Cloudreve只读ACL,不复制、不导入、不改属主。
base=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
source_dir='/media/ztl/LinuxDate/files/我的文件'
parent='/media/ztl/LinuxDate/files'
mount='/media/ztl/LinuxDate'
expected_uuid='8b9cbec5-c4d7-466f-acea-d1c9531ccada'
mode=${1:-}
[[ -z "$mode" || "$mode" == --apply ]] || { echo '用法:restore.sh [--apply]' >&2; exit 2; }
if [[ -f "$base/MANIFEST.sha256" ]]; then (cd "$base" && sha256sum -c MANIFEST.sha256 >/dev/null); fi
[[ $(findmnt -rn -M "$mount" -o UUID) == "$expected_uuid" ]] || { echo '外盘UUID不匹配,拒绝操作。' >&2; exit 1; }
[[ -d "$source_dir" && ! -L "$source_dir" && ! -L "$parent" ]] || { echo '目标缺失或是符号链接,拒绝操作。' >&2; exit 1; }
# 不追踪符号链接;若源树存在链接或特殊文件,先人工评估。
if [[ $(find "$source_dir" -xdev ! -type d ! -type f -printf x | wc -c) != 0 ]]; then echo '源目录包含链接或特殊文件,请先审阅。' >&2; exit 1; fi
printf '目标:%s\n父目录:%s\n' "$source_dir" "$parent"
echo '给父目录只穿越权限,源目录及子目录r-x,普通文件r--;无默认ACL,不授权写入。'
echo '不修改Cloudreve数据库、存储策略、systemd写入范围、SMB配置或文件属主。'
[[ "$mode" == --apply ]] || { echo '预演完成,未改权限。'; exit 0; }
[[ $(id -u) -eq 0 ]] || { echo '应用需要root。' >&2; exit 1; }
getent passwd cloudreve >/dev/null
# 非本脚本产生的命名ACL必须先审阅,避免扩大原有ACL掩码影响其他授权。
if { getfacl -R -p "$source_dir"; getfacl -p "$parent"; } | awk '/^(default:)?(user|group):[^:]+:/ && $0 !~ /^user:cloudreve:r(-x|--)$/ && $0 !~ /^user:cloudreve:--x$/ {bad=1} END{exit !bad}'; then
    echo '发现其他命名ACL,需要人工检查掩码,拒绝自动修改。' >&2; exit 1
fi
stamp=$(date +%Y%m%d-%H%M%S)
install -d -m 0700 /root/AI/cloudreve/private
getfacl -p "$parent" > "/root/AI/cloudreve/private/parent-acl.$stamp.txt"
getfacl -R -p "$source_dir" > "/root/AI/cloudreve/private/source-acl.$stamp.txt"
setfacl -m u:cloudreve:--x "$parent"
find "$source_dir" -xdev -type d -exec setfacl -m u:cloudreve:r-x -- {} +
find "$source_dir" -xdev -type f -exec setfacl -m u:cloudreve:r-- -- {} +
echo '只读ACL已应用;没有更改owner权限或加入可写systemd范围。'

扫描与校验命令

脚本里真正动权限和做检查的就那么几条,单独抄出来便于手工核对(这里把脚本变量 $parent、$source_dir 展开成了实际路径):

# 改动前后各存一份 ACL 快照,方便回滚
getfacl -p /media/ztl/LinuxDate/files > parent-acl-before.txt
getfacl -R -p /media/ztl/LinuxDate/files/我的文件 > source-acl-before.txt

# 检查源树里有没有符号链接或特殊文件,有则先人工评估
find /media/ztl/LinuxDate/files/我的文件 -xdev ! -type d ! -type f -printf x | wc -c

# 父目录只给穿越权限,源树目录 r-x、普通文件 r--,全都不授权写入
setfacl -m u:cloudreve:--x /media/ztl/LinuxDate/files
find /media/ztl/LinuxDate/files/我的文件 -xdev -type d -exec setfacl -m u:cloudreve:r-x -- {} +
find /media/ztl/LinuxDate/files/我的文件 -xdev -type f -exec setfacl -m u:cloudreve:r-- -- {} +

校验侧看三件事:getfacl -p 确认父目录只有 user:cloudreve:--x、源树是 r-x/r-- 且没有 default 项;getent passwd cloudreve 确认专用用户存在;再以该用户身份实际打开一个文件确认能读、尝试写入确认被拒。

第二步:为什么前两次导入扫描到 0 个文件

在修好权限之前,曾经建过两个导入任务,结果都是扫描到 0 个文件——但任务状态显示 completed。这类“成功但什么也没干”的结果最容易骗人。

原因就是上面说的权限:专用用户既无法穿越父目录,也无法读取源目录,扫描自然什么都看不到。修好权限之后新建了任务 6,没有去重跑那两条历史任务。

第三步:怎么触发导入

当时手上只有设备的 SSH 和数据库凭据,并没有索取用户的 Web 登录密码。所以没有走网页端点击,而是用了一个更底层但仍然走官方流程的办法:参照空导入任务 5 的已读参数,新建一条 queued 状态的任务 6,短暂停止本体后把任务行插入,再启动服务,由 Cloudreve 官方的队列恢复机制执行原生导入。

关键点在于:直接写入的只有任务行,文件记录、实体记录、容量记录全部由 Cloudreve 原生流程完成,没有手工造数据。导入前做了一份 600 权限的完整逻辑数据库备份。

没有保存“自动重放”的 SQL 脚本,这是刻意的:防止重装系统后误重复导入,或者因为账号 ID、策略 ID 变化而写进错误的地方。

归档里对这一段的原文记录

把归档 README 里的对应说明原样抄录在这里,可以对照着理解边界。注意这里没有可复制的导入任务 JSON、创建任务的接口调用或可直接重放的 SQL——当时刻意没有留档,理由见上:

因为只有设备SSH和数据库凭据,没有索取用户Web登录密码:对空导入任务5的已读参数建新queued任务6,短暂停止本体后插入,重启由官方队列恢复机制执行原生导入。直接写入的仅任务行,文件/实体/容量记录均由Cloudreve原生流程完成。导入前做600权限完整逻辑数据库备份。没有保存自动重放SQL脚本,防止重装后误重复导入或账号/策略ID变化。

要复刻这一步,前置条件是:源树权限已经修好、管理员用户 1 与存储策略 2 已存在、且没有任何进行中的任务(否则扫描仍可能是 0 个文件)。正常路径是在有 Web 登录态时由页面触发导入任务;直接写任务表属于没有登录态时的兜底手段,动手前必须先做一份完整的数据库备份,并且不要把它当成可反复执行的流程。

导入结果

检查项结果
任务状态任务 6 completed
导入数量101 个文件、41 个子目录(源树含根共 42 个目录,网盘含用户根共 42 个)
数据量143217817324 字节,约 133.4GiB
路径核对逐个源路径与大小和数据库实体一致
源文件路径、大小、mtime 前后快照一致,未改动
实际读取在 systemd 命名空间下成功打开全部 101 个文件,各读取最多 1 字节
服务状态SMB 与 Cloudreve active,HTTP 200,全部线程 CPU 0–3

Blob 到底是什么

做这件事的时候常被问到“Blob 是不是就是存文件的目录”,值得单独解释一下:

所以原地导入只是把旧路径登记进数据库,不会因为策略模板与旧路径不同就把文件搬走。元数据本身不额外复制 143GB,但这些文件会正常计入管理员的已用容量。

注意事项与踩坑

  • 通过 SMB 新增的文件不会自动导入。 网盘里只有导入那一刻登记的这批。
  • 改、移、删实体都可能让记录失效。 源文件被修改、移动或删除后,Cloudreve 的路径记录和元数据可能对不上。
  • 源树后续新增的文件未必继承读取 ACL。 需要时按需重新检查并导入,不存在自动递归跟踪。
  • 新上传路径模板不是源目录。 该策略的上传路径是 /media/ztl/LinuxDate/files/cloudreve/{uid}/{path},与源目录无关。本次没有开放新上传路径的写权限,要通过这条策略上传新文件得另外配置。
  • 归档与业务是分开的。 归档目录 /root/AI/backup/cloudreve-import-readonly/ 只有部署和权限的还原能力,包含 README、restore.sh、files 镜像和 MANIFEST.sha256,history 里保留了修改前的 ACL 记录;不包含数据库、实际 conf.ini、口令、用户文件或任何业务数据。恢复目录 ACL 也不等于恢复网盘记录。

回滚与还原

如果确认此后再没有其他 ACL 变更,可以用备份快照整体还原本次之前的权限:

setfacl --restore=history/parent-acl-before.txt
setfacl --restore=history/source-acl-before.txt

这只回滚权限,不回滚数据库。如果之后已经有别的权限改动,不要恢复整份旧 ACL 去覆盖,应该人工审阅,只移除本次新增的 cloudreve 命名项。

如果只想撤销某一项授权,在确认没有其他流程依赖后可以单点删除:

setfacl -x u:cloudreve <路径>

重装后的还原顺序(依赖 bash、coreutils、util-linux、acl 以及已有的 cloudreve 系统用户):

  • 先恢复 Cloudreve 部署、专用用户、固定挂载、用户数据库和源文件——数据库与源文件的一致性备份必须另存,源文件夹不能缺失;
  • sha256sum -c MANIFEST.sha256 校验归档;
  • ./restore.sh 默认 dry-run,不改权限,先确认目标与 UUID;
  • sudo ./restore.sh --apply 只补 ACL,不会自动导入、不重启服务、不改 systemd、不提交 SQL;
  • 如果已存在其他命名 ACL,脚本会拒绝盲目调整 mask,需先人工评估;遇到符号链接或特殊文件也拒绝自动授权;
  • 最后验证 cloudreve 用户能遍历和读文件,确认源树在 systemd 命名空间仍是只读,再在网页上检查文件和下载。

另外提醒一句:导入前的数据库一致性备份在 /root/AI/cloudreve/private/before-import.*.sql,权限 600,含用户安全数据,禁止进入普通归档或 Git。恢复整库会撤销备份之后的其他改动,需要暂停服务并另行评估;也不要把“从 UI 删除导入文件”当成无风险的数据库回滚手段。

这次做了什么、没做什么

已验证的部分:101 个文件的路径与大小匹配,42 个源目录可遍历,无文件写权限,原元数据一致,实际服务读取成功,SMB active,HTTP 200,全部线程 CPU 0–3。

没有做的部分也说清楚:没有完整下载 134GB,没有浏览器登录回归,没有整机重启,没有热拔盘,也没有实测功耗。