关于Linux:如何使用python用硬链接替换重复的文件?

How to replace duplicate files with hard links using python?

我是个摄影师,做很多备份。多年来,我发现自己有很多硬盘。现在我买了一个NAS,并使用rsync在一个3tbraid1上复制了我的所有图片。根据我的脚本,这些文件中大约有1TB是重复的。这是因为在删除笔记本电脑上的文件之前做了多次备份,而且非常混乱。我确实在旧硬盘上备份了所有这些文件,但如果我的脚本把事情搞砸了,那将是一件痛苦的事。你能看看我的重复查找脚本,告诉我你认为我能运行它吗?我在一个测试文件夹上试过了,看起来还可以,但我不想把NAS搞得一团糟。

脚本在三个文件中有三个步骤。在第一部分中,我找到了所有的图像和元数据文件,并将它们放在一个搁置数据库(datenbank)中,其大小作为键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import os
import shelve

datenbank = shelve.open(os.path.join(os.path.dirname(__file__),"shelve_step1"), flag='c', protocol=None, writeback=False)

#path_to_search = os.path.join(os.path.dirname(__file__),"test")
path_to_search ="/volume1/backup_2tb_wd/"
file_exts = ["xmp","jpg","JPG","XMP","cr2","CR2","PNG","png","tiff","TIFF"]
walker = os.walk(path_to_search)

counter = 0

for dirpath, dirnames, filenames in walker:
  if filenames:
    for filename in filenames:
      counter += 1
      print str(counter)
      for file_ext in file_exts:
        if file_ext in filename:
          filepath = os.path.join(dirpath, filename)
          filesize = str(os.path.getsize(filepath))
          if not filesize in datenbank:
            datenbank[filesize] = []
          tmp = datenbank[filesize]
          if filepath not in tmp:
            tmp.append(filepath)
            datenbank[filesize] = tmp

datenbank.sync()
print"done"
datenbank.close()

第二部分。现在,我删除列表中只有一个文件的所有文件大小,并创建另一个搁置数据库,其中MD5哈希为键,文件列表为值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import os
import shelve
import hashlib

datenbank = shelve.open(os.path.join(os.path.dirname(__file__),"shelve_step1"), flag='c', protocol=None, writeback=False)

datenbank_step2 = shelve.open(os.path.join(os.path.dirname(__file__),"shelve_step2"), flag='c', protocol=None, writeback=False)

counter = 0
space = 0

def md5Checksum(filePath):
    with open(filePath, 'rb') as fh:
        m = hashlib.md5()
        while True:
            data = fh.read(8192)
            if not data:
                break
            m.update(data)
        return m.hexdigest()


for filesize in datenbank:
  filepaths = datenbank[filesize]
  filepath_count = len(filepaths)
  if filepath_count > 1:
    counter += filepath_count -1
    space += (filepath_count -1) * int(filesize)
    for filepath in filepaths:
      print counter
      checksum = md5Checksum(filepath)
      if checksum not in datenbank_step2:
        datenbank_step2[checksum] = []
      temp = datenbank_step2[checksum]
      if filepath not in temp:
        temp.append(filepath)
        datenbank_step2[checksum] = temp

print counter
print str(space)

datenbank_step2.sync()
datenbank_step2.close()
print"done"

最后是最危险的部分。对于evrey md5键,我检索文件列表并执行额外的sha1。如果匹配,我将删除该列表中除第一个文件外的所有文件,并创建一个硬链接来替换删除的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import os
import shelve
import hashlib

datenbank = shelve.open(os.path.join(os.path.dirname(__file__),"shelve_step2"), flag='c', protocol=None, writeback=False)

def sha1Checksum(filePath):
    with open(filePath, 'rb') as fh:
        m = hashlib.sha1()
        while True:
            data = fh.read(8192)
            if not data:
                break
            m.update(data)
        return m.hexdigest()

for hashvalue in datenbank:
  switch = True
  for path in datenbank[hashvalue]:
    if switch:
      original = path
      original_checksum = sha1Checksum(path)
      switch = False
    else:
      if sha1Checksum(path) == original_checksum:
        os.unlink(path)
        os.link(original, path)
        print"delete:", path
print"done"

你怎么认为?非常感谢你。

*如果这在某种程度上很重要:它是一个概要713+并且有一个ext3或ext4文件系统。


这个看起来不错,在清理了一点之后(为了使它能与Python3.4一起工作),我在我的NAS上运行了这个。虽然我有备份之间未修改的文件的硬链接,但移动的文件将被复制。这为我恢复了丢失的磁盘空间。

一个小问题是已经硬链接的文件被删除并重新链接。无论如何,这不会影响最终结果。

我稍微修改了第三个文件("3.py"):

1
2
3
4
5
6
if sha1Checksum(path) == original_checksum:
     tmp_filename = path +".deleteme"
     os.rename(path, tmp_filename)
     os.link(original, path)
     os.unlink(tmp_filename)
     print("Deleted {}".format(path))

这可以确保在电源故障或其他类似错误的情况下,不会丢失任何文件,尽管后面会留下一个尾随的"deleteme"。恢复脚本应该非常简单。


为什么不逐字节比较文件而不是第二个校验和?十亿分之一的两个校验和可能会意外匹配,但直接比较不应该失败。它不应该更慢,甚至可能更快。如果有两个以上的文件,并且您必须相互读取原始文件,那么可能会慢一些。如果你真的想要,你可以通过同时比较所有文件的块来绕过这个问题。

编辑:

我不认为它需要更多的代码,只是不同而已。类似这样的循环体:

1
2
3
data1 = fh1.read(8192)
data2 = fh2.read(8192)
if data1 != data2: return False


注意:如果您不是与python结婚的,那么有一些现成的工具可以为您完成繁重的工作:

https://unix.stackexchange.com/questions/3037/is-there-an-easy-way-to-replace-duplicate-files-with-hardlinks


如何创建硬链接?

在Linux中

1
sudo ln sourcefile linkfile

有时这会失败(对我来说,有时会失败)。另外,python脚本需要以sudo模式运行。

所以我使用符号链接:

1
ln -s sourcefile linkfile

我可以用os.path.islink检查它们

您可以在python中调用如下命令:

1
os.system("ln -s sourcefile linkfile")

或者像这样使用子流程:

1
2
import subprocess
subprocess.call(["ln","-s", sourcefile, linkfile], shell = True)

从命令行和硬链接与软链接中查看执行情况

当它工作的时候,你能把你的整个代码贴出来吗?我也想用它。