收起左侧

群晖硬盘btrfs挂载全指导

2
回复
31
查看
[ 复制链接 ]

0

主题

3

回帖

0

牛值

fnOS系统内测组

把群晖(Synology)硬盘整块搬进飞牛 fnOS:保留数据 + 原生存储空间 + 迁移用户(全命令实操,含一键脚本)

目标:把一块从群晖拆下来的硬盘直接接到飞牛 fnOS 上 让它成为飞牛「原生存储空间」,并把群晖上的用户和家目录也一并迁过来。
适用:群晖 Basic(单盘)卷,即「分区 → mdadm → LVM → btrfs」这种典型布局。
本文所有命令均以 root 在飞牛 SSH 中执行,并给出预期输出;每个高危步骤都有明确警告。仅在btrfs文件系统上做过测试,其他文件系统可参照。


0. 风险总览

【总警告】 下面的操作会改动分区元数据、LVM 卷名、飞牛数据库和系统账号文件。
一步错就可能让飞牛认不出盘、甚至丢数据。请务必:

  1. 先备份群晖盘上真正重要的数据(接入本身不格式化,但任何底层操作都有风险)。
  2. 全程先只读挂载确认数据在,再做写操作。
  3. 改数据库 / 改账号前,先备份对应文件(本文每步都给了备份命令)。
  4. 看不懂某一步在干嘛,就先停下,别 Enter

整个流程分十步:

步骤 做什么 危险级别
1 识别硬盘结构 只读,安全
2 组装 mdadm + 激活 LVM
3 加载 crc32c(否则 btrfs 挂不上)
4 只读试挂,确认数据在 安全
5 改 LVM 名 + 设默认子卷,符合飞牛约定
6 写飞牛 postgres,注册为存储空间
7 挂载群晖系统分区(只读)
8 迁移群晖用户 + 密码
9 家目录 bind 挂载
10 开机持久化(模块 + systemd)

1. 背景知识:两边的存储结构长什么样

群晖 Basic 卷(本文实例盘 /dev/sdd)通常是这样一层套一层的(这是系统安装在这块硬盘上的示例):

/dev/sdd
**─ sdd1 → mdadm 阵列(md127) → ext4 群晖"系统分区"(装 DSM、放 /etc/passwd 等)
**─ sdd2 → mdadm 阵列 → swap 交换分区
**─ sdd3 → mdadm 阵列(md125) → LVM(vg1/volume_1) → btrfs 你的"数据"都在这

也就是说,真正的数据在 btrfs 里,而 btrfs 又装在 LVM 逻辑卷上,LVM 又建在 mdadm 阵列上。要读到数据,必须把这三层一层层"点亮"。

飞牛 fnOS 的存储空间约定则是:

  • LVM 卷组名叫 trim_<uuid>,逻辑卷名叫 0,即设备路径 /dev/mapper/trim_<uuid>-0;
  • 每个存储空间在飞牛的 PostgreSQL(trim 库)mount里有一行记录,飞牛靠它在 UI 显示、开机自动挂到 /volN

所以"接入"的本质 = 把群晖那套 LVM 改名成飞牛的命名,再往飞牛数据库里补一行


2. 准备工作

  1. 关机,把群晖硬盘装到飞牛主机上,开机。
  2. SSH 进飞牛,切 root:
sudo -i
  1. 建一个统一的备份目录,后面每步都会用:
mkdir -p /root/_stor_adopt_backup

3. 第一步:识别硬盘结构(只读,安全)

先看清楚盘接上没、结构对不对。这一步全是只读命令,随便跑。

lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT,MODEL
blkid
cat /proc/mdstat
pvs; vgs; lvs

预期输出(节选,你的盘符/UUID 会不同):

NAME              SIZE FSTYPE            LABEL     MOUNTPOINT
sdd                 4T
**─sdd1              8G linux_raid_member
** **─md127           8G ext4              
**─sdd2              2G linux_raid_member
** **─md126           2G swap
**─sdd3           3.99T linux_raid_member
  **─md125       3.99T LVM2_member
    **─vg1-volume_1 3.99T btrfs

# /proc/mdstat 里能看到 md125 / md126 / md127 三个阵列
# lvs 里能看到:  volume_1   vg1   -wi-a-----  3.99t

看到 vg1/volume_1btrfssdd1 对应的 md 是 ext4,就说明这是标准群晖 Basic 盘。记下:

  • 数据卷:vg1/volume_1(btrfs)
  • 系统分区:sdd1 对应的 md(本例 md127,ext4)

注意 如果 pvs/vgs/lvs 什么都没显示,多半是阵列/LVM 还没激活,继续第二步。


4. 第二步:组装 mdadm + 激活 LVM(低风险)

把三层结构点亮:

mdadm --assemble --scan
vgchange -ay

预期输出:

# mdadm 会打印类似:mdadm: /dev/md125 has been started with 1 drive.
# vgchange 打印:  1 logical volume(s) in volume group "vg1" now active

如果某个 md 显示成 auto-read-only,先让它可写(否则后面写不了):

mdadm --readwrite /dev/md125

此时 /dev/mapper/vg1-volume_1 应该已经存在:

ls -l /dev/mapper/vg1-volume_1

5. 第三步:加载 crc32c,否则 btrfs 挂不上(低风险,但很关键)

飞牛内核默认可能没加载 btrfs 校验要用的 crc32c这一步不做,下一步挂载几乎必然失败,而且报错很误导人。

先直接尝试挂载(下一步会做),如果看到这种报错:

mount: /vol3: mount(2) system call failed: No such file or directory.
# 或 dmesg 里:  BTRFS error: ... crc32c ...

别以为是盘坏了,其实是缺 crc32c 模块。加载它:

modprobe crc32c
grep -i crc32c /proc/crypto | head

预期输出:

name         : crc32c
driver       : crc32c-intel        # 或 crc32c-generic

说明 有的系统 modprobe crc32c_generic 会报 Module ... not found,直接用 modprobe crc32c 即可,内核会自己挑一个实现(intel/generic/cryptoapi)。
开机持久化留到第十步统一做。


6. 第四步:只读试挂,先确认数据真的在(安全,强烈建议)

在做任何改名/写库之前,先只读挂一下,亲眼看到数据,心里才有底。

mkdir -p /mnt/_check
mount -o ro /dev/mapper/vg1-volume_1 /mnt/_check
ls -la /mnt/_check
btrfs subvolume list /mnt/_check

预期输出:

# 顶层能看到群晖的子卷目录,例如:
drwxrwxrwx  @syno
# subvolume list 里能看到:
ID 256 gen 12345 top level 5 path @syno

群晖 Basic 卷的实际数据在 @syno 这个 btrfs 子卷里(共享文件夹 photo/homes/... 都在它下面)。记下这个 ID 256,第五步要用。

看完卸载:

umount /mnt/_check

注意 只读挂载不会改动任何东西。如果这里就看不到数据 / 挂不上,停下来排查,不要继续往下写操作。


7. 第五步:改成飞牛约定 —— LVM 改名 + 默认子卷(高危)

【危险】 这一步会 永久改掉 LVM 卷组名和逻辑卷名,并修改 btrfs 默认子卷。
改名后群晖那套命名就没了(反正盘已经不在群晖上,无所谓),但打错名字可能让盘挂不上
执行前先备份 LVM 元数据:

vgcfgbackup -f /root/_stor_adopt_backup/vg1.bak vg1

7.1 生成飞牛风格的新名字并改名

# 先卸载(如果之前挂着)
umount /vol3 2>/dev/null

# 生成 trim_<uuid> 形式的卷组名
NEWUUID=$(uuidgen | tr 'A-Z-' 'a-z_')
VG="trim_${NEWUUID}"
echo "$VG" | tee /root/_stor_adopt_backup/newvg.txt

# 卷组改名 vg1 -> trim_xxx,逻辑卷改名 volume_1 -> 0
vgrename vg1 "$VG"
lvrename "$VG" volume_1 0
vgchange -ay "$VG"

DM="/dev/mapper/${VG}-0"
ls -l "$DM"

预期输出:

trim_b36db00e_a55c_48ca_95d6_5a127b1ae5b2
  Volume group "vg1" successfully renamed to "trim_b36db00e_..."
  Renamed "volume_1" to "0" in volume group "trim_b36db00e_..."
  1 logical volume(s) ... now active
lrwxrwxrwx 1 root root 7 ... /dev/mapper/trim_b36db00e_...-0 -> ../dm-5

7.2 把默认子卷设成 @syno(让 /volN 直接是共享根)

不做这一步的话,/vol3 打开会多一层 @syno 目录。把默认子卷指向它,数据就直接落在 /vol3 根下。

mkdir -p /mnt/_adopt
mount "$DM" /mnt/_adopt
SYNOID=$(btrfs subvolume list /mnt/_adopt | awk '/path @syno$/{print $2}')
echo "@syno subvol id = $SYNOID"       # 预期 256

btrfs subvolume set-default "$SYNOID" /mnt/_adopt
btrfs subvolume get-default /mnt/_adopt  # 确认已指向 @syno
umount /mnt/_adopt

7.3 用飞牛风格选项挂到 /vol3

先确定一个空闲的 vol 号(飞牛已有 vol1/vol2 就用 vol3,以此类推):

mkdir -p /vol3
mount -o rw,noatime,trimacl,space_cache=v2 "$DM" /vol3 \
  || mount -o rw,noatime,space_cache=v2 "$DM" /vol3   # 老内核不支持 trimacl 时退化

mount | grep ' /vol3 '
ls -la /vol3 | head          # 预期直接看到 photo/ homes/ 等共享
touch /vol3/.wtest && echo "写OK" && rm -f /vol3/.wtest

预期输出:

/dev/mapper/trim_..._0 on /vol3 type btrfs (rw,noatime,...)
drwxrwxrwx ... homes
drwxrwxrwx ... photo
写OK

看到 写OK 就说明读写正常了。


8. 第六步:注册为飞牛「原生存储空间」(高危,改数据库)

到这里 /vol3 已经能用,但只是临时挂载——重启就没了,飞牛 UI 里也看不到。要让它成为原生存储空间,得往飞牛的 PostgreSQL trim 库的 mount 表补一行。

【危险】 直接改飞牛数据库。写错表可能导致飞牛存储模块异常。
务必先备份 mount 表:

( cd /tmp && sudo -u postgres pg_dump -d trim -t mount --data-only --column-inserts ) \
  > /root/_stor_adopt_backup/mount_before.sql

先看一下现有的存储空间,确认你要用的 id(本例用 3)没被占:

( cd /tmp && sudo -u postgres psql -d trim -P pager=off -c \
  "SELECT id,uuid,pool_type FROM mount ORDER BY id;" )

小坑说明sudo -u postgres 时,如果当前目录是 /root,会刷一堆
could not change directory to "/root": Permission denied——无害,是 postgres 用户进不了 /root 目录而已。
上面用 ( cd /tmp && ... ) 就能避免这个噪音。

插入新记录(id=3 对应 /vol3;uuid 就是 trim_<uuid>-0;pool_type=0 表示 btrfs 单盘):

VG=$(cat /root/_stor_adopt_backup/newvg.txt)
MUUID="${VG}-0"

( cd /tmp && sudo -u postgres psql -d trim -P pager=off -c \
"INSERT INTO mount (id,uuid,comment,sync_speed_min,sync_speed_max,stripe_cache_size,warn_frsize,grow,state,pool_type)
 VALUES (3,'${MUUID}','',-1,-1,4096,10,'',0,0) ON CONFLICT (uuid) DO NOTHING;" )

# 确认写进去了
( cd /tmp && sudo -u postgres psql -d trim -P pager=off -c \
  "SELECT id,uuid,pool_type FROM mount ORDER BY id;" )

预期输出:

INSERT 0 1
 id |                    uuid                     | pool_type
----+---------------------------------------------+-----------
  1 | trim_....-0                                 |         0
  2 | trim_....-0                                 |         0
  3 | trim_b36db00e_..._0                         |         0

看到 id=3 那行,说明飞牛已经认它是原生存储空间了(刷新 UI 即可见)。


9. 第七步:挂载群晖系统分区 sdd1(只读,低风险)

群晖的 /etc/passwd/etc/shadow系统分区(sdd1,ext4)里。要迁用户就得先把它挂出来。只读挂,绝对安全。

mkdir -p /vol3/system_mount

# 用 ext4 文件系统 UUID 挂,不依赖会变的 md 编号
SDD1UUID=$(blkid -o value -s UUID /dev/md127)   # 换成你第一步记下的系统分区 md
echo "sdd1 ext4 UUID = $SDD1UUID"

mount -o ro UUID="$SDD1UUID" /vol3/system_mount
ls /vol3/system_mount | head

预期输出:

sdd1 ext4 UUID = 0105bea5-970e-459f-a48d-eb82819a33b5
# system_mount 下能看到群晖系统目录:
bin  etc  lib  root  usr  var  ...

10. 第八步:迁移群晖用户 + 密码(高危,改账号文件)

群晖普通用户的主组 GID 通常是 100(users)。我们把这些用户(排除 admin/guest)迁成飞牛系统用户,并把密码一起带过来。

【危险】 这一步会修改 /etc/passwd/etc/shadow/etc/group
先备份:

ts=$(date +%Y%m%d_%H%M%S)
cp -a /etc/passwd /root/_stor_adopt_backup/passwd.$ts
cp -a /etc/shadow /root/_stor_adopt_backup/shadow.$ts
cp -a /etc/group  /root/_stor_adopt_backup/group.$ts

看一眼要迁哪些人(只读,不动):

awk -F: '$4==100 && $1!="admin" && $1!="guest"{printf "  %-16s uid=%s\n",$1,$3}' \
  /vol3/system_mount/etc/passwd

预期输出:

feidan           uid=1026
  liuxiaojue       uid=1028

确认无误后执行迁移。下面这段会逐个处理,并且:

  • UID 已被占用 → 跳过并记录(绝不覆盖已有账号);
  • 用户名重名 → 跳过并记录;
  • 否则创建系统用户(home=/home/<用户>shell=/usr/sbin/nologin),再从群晖 shadow 迁密码。

注意 一定用 /usr/sbin/nologin(不是 /sbin/nologin)——飞牛/Debian 系的路径是前者,写错会导致 shell 无效。

SM=/vol3/system_mount
while IFS=: read -r u p uid gid gecos home shell; do
  [ "$gid" = 100 ] || continue
  [ "$u" = admin ] || [ "$u" = guest ] && continue

  # UID 冲突?跳过
  if awk -F: -v id="$uid" '$3==id{f=1}END{exit !f}' /etc/passwd; then
    echo "[跳过] $u uid=$uid 与已有用户冲突"; continue; fi
  # 重名?跳过
  if awk -F: -v n="$u" '$1==n{f=1}END{exit !f}' /etc/passwd; then
    echo "[跳过] $u 用户名已存在"; continue; fi

  # 建用户
  useradd -u "$uid" -g 100 -M -d "/home/$u" -s /usr/sbin/nologin "$u" \
    && echo "[新增] $u uid=$uid"
  mkdir -p "/home/$u"; chown "$uid":100 "/home/$u"; chmod 755 "/home/$u"

  # 迁密码(直接搬 shadow 里的哈希)
  hash=$(awk -F: -v n="$u" '$1==n{print $2}' "$SM/etc/shadow")
  case "$hash" in
    \$*) printf '%s:%s\n' "$u" "$hash" | chpasswd -e && echo "  密码已迁移" ;;
    *)   echo "  [注意] $u 源无有效密码哈希,跳过密码" ;;
  esac
done < "$SM/etc/passwd"

预期输出:

[新增] feidan uid=1026
  密码已迁移
[新增] liuxiaojue uid=1028
  密码已迁移

校验:

getent passwd feidan liuxiaojue

说明:飞牛用户 vs 系统用户
上面建的是 Linux 系统用户(能用来做 SMB/权限)。如果还想让它出现在飞牛 UI 的用户列表里,需要把主组设成飞牛的 Users 组:

FN_GID=$(getent group Users | cut -d: -f3)   # 通常 1001
usermod -g "$FN_GID" feidan; usermod -aG 100 feidan     # 主组 Users,附加组 users(100)

11. 第九步:家目录 bind 挂载到 /volN/(中风险)

飞牛按 /<存储空间>/<用户uid> 找用户的个人空间,而群晖的家目录在 /vol3/homes/<用户名>。用 bind 挂载把两者对上(别用软链接——飞牛 UI 不认软链接,当初踩过这个坑)。

# feidan uid=1026, liuxiaojue uid=1028(换成你的)
for p in 1026:feidan 1028:liuxiaojue; do
  id=${p%%:*}; u=${p##*:}
  [ -L /vol3/$id ] && rm -f /vol3/$id        # 清掉可能存在的旧软链
  mkdir -p /vol3/$id
  [ -d /vol3/homes/$u ] || { echo "[注意] 源不存在 /vol3/homes/$u,跳过"; continue; }
  mountpoint -q /vol3/$id || mount --bind /vol3/homes/$u /vol3/$id
done

mount | grep -E '/vol3/(1026|1028)'
ls -la /vol3/1026 | head    # 应能看到 feidan 的家目录内容

预期输出:

/dev/mapper/trim_..._0 on /vol3/1026 type btrfs (...)
/dev/mapper/trim_..._0 on /vol3/1028 type btrfs (...)
# /vol3/1026 里是 feidan 的实际文件

为什么不用软链接:飞牛的文件管理/权限模块只把 bind 挂载当成"真实目录",软链接会显示成一个坏掉的链接文件,属主也不对。

收尾:收紧家目录权限(子项设为 drwx------)

绑定完成后,把每个用户家目录内部收紧为仅本人可访问:用户主文件夹本身保持不变(飞牛需要按它现有的属主/权限来识别),但其下所有子文件夹和子项目统一设为 drwx------(即 700,其他用户完全看不到)。

# 对每个用户的家目录:主目录不动,子项(含所有子文件夹/文件)递归设为 700
for u in feidan liuxiaojue; do
  src=/vol3/homes/$u
  [ -d "$src" ] || { echo "[注意] $src 不存在,跳过"; continue; }
  find "$src" -mindepth 1 -exec chmod 700 {} +    # -mindepth 1 => 排除主目录本身
  echo "[ok] $u 家目录子项已设为 drwx------(主目录 $src 保持不变)"
done

校验(主目录权限不变,里面的子文件夹变成 drwx------):

ls -ld /vol3/homes/feidan          # 主目录:维持原样
ls -la /vol3/homes/feidan | head   # 子项:drwx------

预期输出:

drwxrwxrwx 1 feidan Users ... /vol3/homes/feidan        # 主目录未变
drwx------ 1 feidan Users ... Photo
drwx------ 1 feidan Users ... Drive
...

说明 -mindepth 1 保证 find 只处理主目录里面的东西,不碰主目录本身。
drwx------ 是目录的 700 权限;子项目里若有文件,同样会被设成 700(rwx------),即仅属主可读写执行,其他用户无任何权限。


12. 第十步:开机持久化(中风险)

前面的挂载和绑定重启后都会消失(存储空间本身由飞牛 mount 表管,会自动挂;但 crc32c、system_mount、用户 bind 需要我们自己保证)。

12.1 crc32c 开机自动加载

echo crc32c > /etc/modules-load.d/crc32c.conf
cat /etc/modules-load.d/crc32c.conf

12.2 系统分区(system_mount)开机自动挂

cat > /etc/systemd/system/vol3-system_mount.service <<'EOF'
[Unit]
Description=Mount Synology system partition at /vol3/system_mount
After=multi-user.target
ConditionPathExists=/dev/disk/by-uuid/0105bea5-970e-459f-a48d-eb82819a33b5

[Service]
Type=oneshot
RemainAfterExit=yes
# 等飞牛把 /vol3 挂好(最多 ~120s)
ExecStartPre=/bin/sh -c 'for i in $(seq 1 60); do mountpoint -q /vol3 && exit 0; sleep 2; done; exit 1'
ExecStartPre=/bin/mkdir -p /vol3/system_mount
ExecStart=/bin/sh -c 'mountpoint -q /vol3/system_mount || mount -o ro UUID=0105bea5-970e-459f-a48d-eb82819a33b5 /vol3/system_mount'
ExecStop=/bin/umount /vol3/system_mount

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable vol3-system_mount.service

12.3 用户家目录 bind 开机自动恢复

cat > /etc/systemd/system/vol3-user-binds.service <<'EOF'
[Unit]
Description=Bind-mount migrated Synology user homes to /vol3/{uid}
After=multi-user.target vol3-system_mount.service
ConditionPathExists=/vol3/homes

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/sh -c 'for i in $(seq 1 60); do mountpoint -q /vol3 && exit 0; sleep 2; done; exit 1'
ExecStart=/bin/sh -c 'for p in 1026:feidan 1028:liuxiaojue; do id=${p%%:*}; u=${p##*:}; mkdir -p /vol3/$id; mountpoint -q /vol3/$id || mount --bind /vol3/homes/$u /vol3/$id; done'
ExecStop=/bin/sh -c 'umount /vol3/1026 2>/dev/null; umount /vol3/1028 2>/dev/null; true'

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable vol3-user-binds.service

预期输出:两个 systemctl enable 都会打印
Created symlink /etc/systemd/system/multi-user.target.wants/....service → ....


13. 验证

SSH 进来检查:

mount | grep -E ' /vol3'                # /vol3 + system_mount + 两个 bind 都在
lsmod | grep crc32c                     # crc32c 已加载
systemctl status vol3-system_mount vol3-user-binds --no-pager
ls /vol3                                # 共享文件夹都在

全部正常,飞牛 UI 里也能看到这块存储空间和迁移过来的用户,就大功告成了。

【重要】 强烈建议重启一次,验证开机后一切自动到位。这是检验持久化配置是否正确的唯一办法。

reboot

14. 回滚方案(万一出问题)

想撤销 怎么做
数据库那行 sudo -u postgres psql -d trim -c "DELETE FROM mount WHERE id=3;" 或恢复 mount_before.sql 注意id=3可能不一样!
账号文件 /root/_stor_adopt_backup/passwd.* shadow.* group.* 覆盖回去
LVM 卷名 vgrename trim_xxx vg1; lvrename vg1 0 volume_1(反向改回) 都根据自己的数字来写
systemd 服务 systemctl disable --now vol3-system_mount vol3-user-binds 后删对应 .service

注意 数据本身(btrfs 里的文件)在整个过程中没被改写,回滚只影响"接入配置",不影响数据。


结语:整件事的关键点

  1. 数据在最里层的 btrfs,外面套着 LVM 和 mdadm,得一层层点亮才能读到。
  2. crc32c 是最容易踩的坑——不加载,btrfs 挂载会报一个跟真相毫无关系的错。
  3. 接入 = 改 LVM 命名 + 补一行数据库,飞牛并没有魔法,它只是照着 mount 表和 trim_<uuid> 约定去挂盘。
  4. 先只读、后写入;每步先备份——这是整个流程里唯一不能省的纪律。
  5. 用户目录用 bind 挂载别用软链接。

按这个顺序走,一块群晖老盘就能在飞牛上"原地复活",数据、账号、家目录一个不少。


收藏
送赞
分享

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

0

主题

3

回帖

0

牛值

fnOS系统内测组

6 小时前 楼主 显示全部楼层

一键脚本 https://github.com/WaterBucket0819/fnos_adopt_synology_disk/

image.png

成功结果

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

0

主题

3

回帖

0

牛值

fnOS系统内测组

5 小时前 楼主 显示全部楼层
  1. #!/usr/bin/env bash
  2. ###############################################################################
  3. # del_eadir.sh  批量删除群晖遗留的 @eaDir 缓存文件夹
  4. #
  5. # @eaDir 是群晖(Synology)自动生成的缩略图/索引目录,迁移到飞牛后无用,可安全删除。
  6. #
  7. # 用法:
  8. #   bash del_eadir.sh [目标目录]          # 默认当前目录;先预览统计,再确认删除
  9. #   bash del_eadir.sh /vol3               # 指定目录
  10. #   DRYRUN=1 bash del_eadir.sh /vol3      # 只预览、只统计,绝不删除
  11. #   YES=1    bash del_eadir.sh /vol3      # 跳过确认(无人值守)
  12. ###############################################################################
  13. set -uo pipefail

  14. TARGET="${1:-.}"
  15. DRYRUN="${DRYRUN:-0}"
  16. YES="${YES:-0}"

  17. [ -d "$TARGET" ] || { echo "[错误] 目标目录不存在: $TARGET"; exit 1; }
  18. TARGET="$(cd "$TARGET" && pwd)"   # 归一为绝对路径
  19. echo "[*] 目标目录: $TARGET"

  20. echo "[*] 扫描 @eaDir(可能较慢,取决于文件数量)..."
  21. # -prune: 匹配到 @eaDir 后不再深入其内部,更快且避免删除后重复进入
  22. mapfile -t DIRS < <(find "$TARGET" -type d -name '@eaDir' -prune 2>/dev/null)

  23. CNT="${#DIRS[@]}"
  24. if [ "$CNT" -eq 0 ]; then
  25.   echo "[ok] 没有找到任何 @eaDir,无需处理。"
  26.   exit 0
  27. fi

  28. echo "[*] 共找到 $CNT 个 @eaDir 目录。占用空间统计中..."
  29. SIZE="$(du -shc "${DIRS[@]}" 2>/dev/null | tail -1 | awk '{print $1}')"
  30. echo "[*] 这些 @eaDir 合计约占用: ${SIZE:-未知}"
  31. echo "----- 前 20 个(供核对)-----"
  32. printf '  %s\n' "${DIRS[@]:0:20}"
  33. [ "$CNT" -gt 20 ] && echo "  ... 其余 $((CNT-20)) 个未列出"
  34. echo "----------------------------"

  35. if [ "$DRYRUN" = 1 ]; then
  36.   echo "[演练] DRYRUN=1,仅预览,未删除任何内容。"
  37.   exit 0
  38. fi

  39. if [ "$YES" != 1 ]; then
  40.   printf '确认删除以上全部 %s 个 @eaDir 目录?此操作不可恢复 [y/N]: ' "$CNT"
  41.   read -r ans </dev/tty || ans=""
  42.   case "$ans" in [Yy]*) ;; *) echo "已取消,未删除。"; exit 0;; esac
  43. fi

  44. echo "[*] 开始删除..."
  45. DEL=0; FAIL=0
  46. for d in "${DIRS[@]}"; do
  47.   if rm -rf "$d" 2>/dev/null; then DEL=$((DEL+1)); else FAIL=$((FAIL+1)); echo "  [失败] $d"; fi
  48. done

  49. echo ""
  50. echo "================= 完成 ================="
  51. echo "已删除 : $DEL 个"
  52. echo "失败   : $FAIL 个"
  53. echo "复核残留(应为 0): $(find "$TARGET" -type d -name '@eaDir' -prune 2>/dev/null | wc -l)"
  54. echo "======================================="
复制代码


付一个@eadir的清理脚本
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则