在功能有限的 Linux(NAS 盒)中,如何查找重复文件,然后删除重复文件并将硬链接替换为一个文件?

在功能有限的 Linux(NAS 盒)中,如何查找重复文件,然后删除重复文件并将硬链接替换为一个文件?

我有一个运行某个 Linux 版本的 NAS 盒,我用它来备份所有内容。

基本上可以肯定有些文件是完全相同的重复文件。

既然如此,我想做的是:

  1. 识别重复文件,其中“重复”= 相同的 SHA256 校验和。(相同的 SHA512 也可以接受,但可能需要更长时间。您建议哪种?)
  2. 允许其中一个副本作为“主”副本,删除所有其他副本,并将硬链接替换为剩余副本。这应该会在 NAS 卷上释放大量空间。

请注意,找到的第一个文件是“主”文件的不错选择,所有其他文件都可以删除并硬链接到它。权限和所有权不是问题,因为只有一个用户,而且(不要在这里讨厌我),无论如何,权限都是完全开放的。

还请注意,我想要硬链接,以便如果我删除一个文件(无论出于何种原因),所有其他文件都会保留。

请注意,我可以通过 SSH shell 控制台访问 NAS 盒。

问题:

  1. 是否可以 ?
  2. 我该怎么做 ?
  3. 如果某个文件有“X”个硬链接,而我删除了其他所有人都硬链接到的原始文件,那么剩余的硬链接是否仍保留到真实文件?(我怀疑答案是“是”,即一个文件会保留到所有硬链接都被删除为止。)

更新以添加其他上下文

NAS 盒有两个驱动器,其中一个是外部的,可以移除,并且可以在我的 Ubuntu 笔记本电脑上处理。

另一个是内部的,基本上是不可触碰的。虽然我可以移除它,但它的设置方式非常定制,触碰它会迅速导致灾难。

此外,内部操作系统是运行 BusyBox 的 Linux“网络设备”版本。它似乎实现了标准功能,包括 find、grep、sed、awk 等。

即:(由 返回busybox --help

Currently defined functions:
        adjtimex, ar, arp, arping, ash, awk, basename, cat, chgrp, chmod,
        chown, chroot, clear, cmp, cp, crond, crontab, cut, date, dd, df,
        dhcprelay, diff, dirname, dmesg, dnsdomainname, dos2unix, dpkg,
        dumpleases, echo, egrep, env, expand, expr, false, fgrep, find, free,
        fsck, getopt, getty, grep, halt, head, hostname, id, ifconfig,
        ifenslave, init, ionice, ipcalc, kill, killall, ln, logger, logname,
        logread, losetup, ls, lsof, lspci, lsusb, md5sum, mdev, mkdir, mkfifo,
        mknod, mkswap, mktemp, modprobe, more, mount, mv, nice, nohup,
        nslookup, pidof, ping, ping6, pivot_root, poweroff, printenv, printf,
        ps, pwd, rdate, readlink, reboot, renice, rm, rmdir, route, sed, seq,
        sh, sleep, sort, split, stat, swapoff, swapon, sync, sysctl, syslogd,
        tac, tail, tar, tee, tftp, tftpd, top, touch, tr, traceroute,
        traceroute6, true, tty, udhcpc, udhcpd, umount, uname, uniq, unix2dos,
        uptime, usleep, vconfig, vi, watch, wc, which, xargs, zcip

答案1

1

是否可以?

是的,但不是,sha256sum因为看起来你没有这个工具。你有md5sum


2

我该怎么做?

使用以下脚本。将其保存到文件(例如deduplicate),使其可执行(chmod +x deduplicate)。该脚本需要适当的输入和参数才能正确完成其工作,因此在运行它之前请阅读整个答案。

脚本

#/bin/sh

exec 3>/dev/tty

if echo | head -c 1 >/dev/null 2>/dev/null; then
   fhead () { head -c 8192; }
else
   fhead () { dd bs=1 count=8192 2>/dev/null; }
fi

grep -v '~DEDUPED~$' \
| (
while IFS= read -r pathname; do
   wc -c -- "$pathname"
   printf . >&3
done
) | sort -n \
| (
sequence=
oldsize=
oldpathname=
while IFS= read -r line; do
   size="${line%% *}"
   pathname="${line#* }"
   if [ "$size" = "$oldsize" ]; then
      if [ -z "$sequence" ]; then
         sequence=y
         printf '%s %s\n' "$(<"$oldpathname" fhead | md5sum -b)" "$oldpathname"
         printf '+' >&3
      fi
      printf '%s %s\n' "$(<"$pathname" fhead | md5sum -b)" "$pathname"
      printf '+' >&3
   else
      oldsize="$size"
      oldpathname="$pathname"
      sequence=
      printf '-' >&3
   fi
done
) | sort -k 1,1 \
| (
sequence=
oldsum=
oldpathname=
while IFS= read -r line; do
   sum="${line%% *}"
   pathname="${line#* ?- }"
   if [ "$sum" = "$oldsum" ]; then
      if [ -z "$sequence" ]; then
         sequence=y
         md5sum -b -- "$oldpathname"
         printf '#' >&3
      fi
      md5sum -b -- "$pathname"
      printf '#' >&3
   else
      oldsum="$sum"
      oldpathname="$pathname"
      sequence=
      printf '=' >&3
   fi
done
echo >&3
) | sort -k 1,1 \
| (
oldsum=
oldpathname=
while IFS= read -r line; do
   sum="${line%% *}"
   pathname="${line#* ?}"
   if [ "$sum" = "$oldsum" ]; then
      if [ "$1" = --no-dry-run ]; then
         mv -- "$pathname" "$pathname~DEDUPED~" \
         && ln -f -- "$oldpathname" "$pathname" \
         && printf '%s ----> %s\n' "$pathname" "$oldpathname"
      else
         printf '%s ----> %s\n' "$pathname" "$oldpathname"
      fi
   else
      oldsum="$sum"
      oldpathname="$pathname"
   fi
done )

用法

该脚本旨在从中读取路径名find。注意事项:

  • 因为硬链接仅在单个文件系统内有效,所以建议一次为一个文件系统(或其一部分)运行该脚本。
  • 您应该只为脚本提供常规文件的路径名。
  • 重复数据删除空文件不会给您太多空间(如果有的话),因此您可以将它们排除在外。
  • 由于我不知道您的工具集是否支持使用以空字符结尾的字符串,因此所有内容都设计为使用以换行符结尾的字符串。您有责任不为脚本提供任何包含换行符的路径名。

所有这些意味着您应该运行如下命令:

find /the/mountpoint \
     -xdev \
     -type f \
     -size +0 \
   ! -name "$(printf '*\n*')" \
| ./deduplicate

笔记像这样调用的脚本将仅打印它将执行的操作(并且形式可能不明确)。要使脚本真正影响文件系统(重复数据删除),您需要传递--no-dry-run作为第一个参数(find … | ./deduplicate --no-dry-run)。

程序

该脚本首先使用wc -c。对于每个大小不唯一的文件,它计算md5sum前 8192 个字节(head -c如果支持,则使用回到dd如果不是)。对于每个总和不唯一的文件,它会计算md5sum整个文件的总和。结果相同的文件被视为重复文件:一堆中的第一个文件保持不变,其他文件将被替换为指向它的硬链接(如果--no-dry-run)。请注意,“一堆中的第一个”不一定是“找到的第一个文件”。

这个脚本相当笨,它不检查它得到的一些路径名是否已经指向同一个文件(inode),它甚至不检查路径名是否相同。例如,如果你使用,find /the/mountpoint /the/mountpoint … | ./deduplicate那么脚本将获取每个路径名两次,这将导致大量的工作徒劳无功。

可移植性

head -c不可移植,但如果不受支持,则脚本将dd自动恢复。

md5sum不可移植,但我知道您的版本busybox提供了它。我过去常常md5sum -b明确请求二进制模式;这可能无关紧要。如果您md5sum不理解,-b那么您可以忽略此选项。

误报

最终总和相同的文件将被视为重复文件,最终总和是唯一标准;在此阶段,大小和部分总和甚至都不重要(这些只是为了尽早排除明显独特的文件)。脚本不会运行cmp以确保ln对真正相同的文件采取行动。

出于这个原因,我引入了mv -- "$pathname" "$pathname~DEDUPED~"。此命令负责重命名否则将被硬链接替换的文件,从而“释放”路径名并备份旧内容。稍后,您可以通过调用

find /the/mountpoint -name '*~DEDUPED~'

检查其替换并根据需要恢复。请注意,脚本使用 过滤其输入grep -v '~DEDUPED~$',因此您可以再次运行它,它不会尝试对这些备份文件进行重复数据删除。

添加-exec rm {} + -print如果您愿意,可以到上述命令的末尾批量删除所有备份文件。

所有权和模式

该脚本不关心所有权和模式(权限)。硬链接只是单个文件的不同路径名,仅此而已,因此如果ln成功,则所有权和模式将与内容一样“共享”。所有权和/或权限可能会导致mvln失败。在评论中,您表示这对您来说不是问题。

但其他用户应注意。

进度指示器

脚本将进度指示器打印到/dev/tty。在不同阶段,它由.(“sizes” 阶段)-+(“partial md5sum” 阶段)或=#(“whole md5sum” 阶段)字符组成。这只是为了显示脚本正在进展。

该指标远非完美。其中可能会出现错误消息(如果有)。

摆脱指标的最简单方法是改为exec 3>/dev/tty靠近exec 3>/dev/null脚本的开头。


3

如果某个文件有“X”个硬链接,而我删除了其他所有人都硬链接到的原始文件,那么剩余的硬链接是否仍保留到真实文件?

是的。实际上,您不会删除“原始文件”,而只是取消链接名称。新的硬链接只是真实文件的替代路径名。旧路径名是(并且曾经是!)与新路径名一样硬链接。当它是链接到文件的唯一路径名时,我们通常不会将路径名称为“硬链接”;当有两个或多个路径名时,我们开始这样做,但每个路径名都相同,我们无法区分原始路径名。只有在取消链接所有名称后(并关闭所有句柄)文件系统将删除该文件。另请参阅此答案:硬链接的用例

相关内容