mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-22 14:41:39 +00:00
Squashed commit of the following:
commit 5eb515f666bfa1ff06c2e96bdc351a4b7456580e
Author: Andrew Senetar <arsenetar@gmail.com>
Date: Sun Mar 27 22:19:39 2022 -0500
Add fallback to md5 if xxhash not available
Mainly here for the case when distributions have not packaged python3-xxhash.
commit 51b18d4c84
Author: Andrew Senetar <arsenetar@gmail.com>
Date: Sat Mar 19 15:25:46 2022 -0500
Switch file hashing to xxhash instead of md5
- Improves performance significantly in some cases
- Add xxhash to requirements.txt and sort requirements
- Rename md5 based members to digest
- Update all tests to use new member names and hashing methods
- Update hash db code to upgrade schema
NOTE: May consider supporting multiple hashing algorithms in the future.
This commit is contained in:
@@ -86,9 +86,9 @@ class NamedObject:
|
||||
folder = "basepath"
|
||||
self._folder = Path(folder)
|
||||
self.size = size
|
||||
self.md5partial = name
|
||||
self.md5 = name
|
||||
self.md5samples = name
|
||||
self.digest_partial = name
|
||||
self.digest = name
|
||||
self.digest_samples = name
|
||||
if with_words:
|
||||
self.words = getwords(name)
|
||||
self.is_ref = False
|
||||
|
||||
@@ -530,7 +530,7 @@ class TestCaseGetMatches:
|
||||
|
||||
|
||||
class TestCaseGetMatchesByContents:
|
||||
def test_big_file_partial_hashes(self):
|
||||
def test_big_file_partial_hashing(self):
|
||||
smallsize = 1
|
||||
bigsize = 100 * 1024 * 1024 # 100MB
|
||||
f = [
|
||||
@@ -539,17 +539,17 @@ class TestCaseGetMatchesByContents:
|
||||
no("smallfoo", size=smallsize),
|
||||
no("smallbar", size=smallsize),
|
||||
]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||
f[2].md5 = f[2].md5partial = "bleh"
|
||||
f[3].md5 = f[3].md5partial = "bleh"
|
||||
f[0].digest = f[0].digest_partial = f[0].digest_samples = "foobar"
|
||||
f[1].digest = f[1].digest_partial = f[1].digest_samples = "foobar"
|
||||
f[2].digest = f[2].digest_partial = "bleh"
|
||||
f[3].digest = f[3].digest_partial = "bleh"
|
||||
r = getmatches_by_contents(f, bigsize=bigsize)
|
||||
eq_(len(r), 2)
|
||||
# User disabled optimization for big files, compute hashes as usual
|
||||
# User disabled optimization for big files, compute digests as usual
|
||||
r = getmatches_by_contents(f, bigsize=0)
|
||||
eq_(len(r), 2)
|
||||
# Other file is now slightly different, md5partial is still the same
|
||||
f[1].md5 = f[1].md5samples = "foobardiff"
|
||||
# Other file is now slightly different, digest_partial is still the same
|
||||
f[1].digest = f[1].digest_samples = "foobardiff"
|
||||
r = getmatches_by_contents(f, bigsize=bigsize)
|
||||
# Successfully filter it out
|
||||
eq_(len(r), 1)
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import hashlib
|
||||
try:
|
||||
import xxhash
|
||||
|
||||
hasher = xxhash.xxh128
|
||||
except ImportError:
|
||||
import hashlib
|
||||
|
||||
hasher = hashlib.md5
|
||||
|
||||
from os import urandom
|
||||
|
||||
from hscommon.path import Path
|
||||
@@ -52,54 +60,54 @@ def test_size_aggregates_subfiles(tmpdir):
|
||||
eq_(b.size, 12)
|
||||
|
||||
|
||||
def test_md5_aggregate_subfiles_sorted(tmpdir):
|
||||
# dir.allfiles can return child in any order. Thus, bundle.md5 must aggregate
|
||||
# all files' md5 it contains, but it must make sure that it does so in the
|
||||
def test_digest_aggregate_subfiles_sorted(tmpdir):
|
||||
# dir.allfiles can return child in any order. Thus, bundle.digest must aggregate
|
||||
# all files' digests it contains, but it must make sure that it does so in the
|
||||
# same order everytime.
|
||||
p = create_fake_fs_with_random_data(Path(str(tmpdir)))
|
||||
b = fs.Folder(p)
|
||||
md51 = fs.File(p["dir1"]["file1.test"]).md5
|
||||
md52 = fs.File(p["dir2"]["file2.test"]).md5
|
||||
md53 = fs.File(p["dir3"]["file3.test"]).md5
|
||||
md54 = fs.File(p["file1.test"]).md5
|
||||
md55 = fs.File(p["file2.test"]).md5
|
||||
md56 = fs.File(p["file3.test"]).md5
|
||||
# The expected md5 is the md5 of md5s for folders and the direct md5 for files
|
||||
folder_md51 = hashlib.md5(md51).digest()
|
||||
folder_md52 = hashlib.md5(md52).digest()
|
||||
folder_md53 = hashlib.md5(md53).digest()
|
||||
md5 = hashlib.md5(folder_md51 + folder_md52 + folder_md53 + md54 + md55 + md56)
|
||||
eq_(b.md5, md5.digest())
|
||||
digest1 = fs.File(p["dir1"]["file1.test"]).digest
|
||||
digest2 = fs.File(p["dir2"]["file2.test"]).digest
|
||||
digest3 = fs.File(p["dir3"]["file3.test"]).digest
|
||||
digest4 = fs.File(p["file1.test"]).digest
|
||||
digest5 = fs.File(p["file2.test"]).digest
|
||||
digest6 = fs.File(p["file3.test"]).digest
|
||||
# The expected digest is the hash of digests for folders and the direct digest for files
|
||||
folder_digest1 = hasher(digest1).digest()
|
||||
folder_digest2 = hasher(digest2).digest()
|
||||
folder_digest3 = hasher(digest3).digest()
|
||||
digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest()
|
||||
eq_(b.digest, digest)
|
||||
|
||||
|
||||
def test_partial_md5_aggregate_subfile_sorted(tmpdir):
|
||||
def test_partial_digest_aggregate_subfile_sorted(tmpdir):
|
||||
p = create_fake_fs_with_random_data(Path(str(tmpdir)))
|
||||
b = fs.Folder(p)
|
||||
md51 = fs.File(p["dir1"]["file1.test"]).md5partial
|
||||
md52 = fs.File(p["dir2"]["file2.test"]).md5partial
|
||||
md53 = fs.File(p["dir3"]["file3.test"]).md5partial
|
||||
md54 = fs.File(p["file1.test"]).md5partial
|
||||
md55 = fs.File(p["file2.test"]).md5partial
|
||||
md56 = fs.File(p["file3.test"]).md5partial
|
||||
# The expected md5 is the md5 of md5s for folders and the direct md5 for files
|
||||
folder_md51 = hashlib.md5(md51).digest()
|
||||
folder_md52 = hashlib.md5(md52).digest()
|
||||
folder_md53 = hashlib.md5(md53).digest()
|
||||
md5 = hashlib.md5(folder_md51 + folder_md52 + folder_md53 + md54 + md55 + md56)
|
||||
eq_(b.md5partial, md5.digest())
|
||||
digest1 = fs.File(p["dir1"]["file1.test"]).digest_partial
|
||||
digest2 = fs.File(p["dir2"]["file2.test"]).digest_partial
|
||||
digest3 = fs.File(p["dir3"]["file3.test"]).digest_partial
|
||||
digest4 = fs.File(p["file1.test"]).digest_partial
|
||||
digest5 = fs.File(p["file2.test"]).digest_partial
|
||||
digest6 = fs.File(p["file3.test"]).digest_partial
|
||||
# The expected digest is the hash of digests for folders and the direct digest for files
|
||||
folder_digest1 = hasher(digest1).digest()
|
||||
folder_digest2 = hasher(digest2).digest()
|
||||
folder_digest3 = hasher(digest3).digest()
|
||||
digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest()
|
||||
eq_(b.digest_partial, digest)
|
||||
|
||||
md51 = fs.File(p["dir1"]["file1.test"]).md5samples
|
||||
md52 = fs.File(p["dir2"]["file2.test"]).md5samples
|
||||
md53 = fs.File(p["dir3"]["file3.test"]).md5samples
|
||||
md54 = fs.File(p["file1.test"]).md5samples
|
||||
md55 = fs.File(p["file2.test"]).md5samples
|
||||
md56 = fs.File(p["file3.test"]).md5samples
|
||||
# The expected md5 is the md5 of md5s for folders and the direct md5 for files
|
||||
folder_md51 = hashlib.md5(md51).digest()
|
||||
folder_md52 = hashlib.md5(md52).digest()
|
||||
folder_md53 = hashlib.md5(md53).digest()
|
||||
md5 = hashlib.md5(folder_md51 + folder_md52 + folder_md53 + md54 + md55 + md56)
|
||||
eq_(b.md5samples, md5.digest())
|
||||
digest1 = fs.File(p["dir1"]["file1.test"]).digest_samples
|
||||
digest2 = fs.File(p["dir2"]["file2.test"]).digest_samples
|
||||
digest3 = fs.File(p["dir3"]["file3.test"]).digest_samples
|
||||
digest4 = fs.File(p["file1.test"]).digest_samples
|
||||
digest5 = fs.File(p["file2.test"]).digest_samples
|
||||
digest6 = fs.File(p["file3.test"]).digest_samples
|
||||
# The expected digest is the digest of digests for folders and the direct digest for files
|
||||
folder_digest1 = hasher(digest1).digest()
|
||||
folder_digest2 = hasher(digest2).digest()
|
||||
folder_digest3 = hasher(digest3).digest()
|
||||
digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest()
|
||||
eq_(b.digest_samples, digest)
|
||||
|
||||
|
||||
def test_has_file_attrs(tmpdir):
|
||||
|
||||
@@ -123,19 +123,19 @@ def test_content_scan(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
f = [no("foo"), no("bar"), no("bleh")]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||
f[2].md5 = f[2].md5partial = f[1].md5samples = "bleh"
|
||||
f[0].digest = f[0].digest_partial = f[0].digest_samples = "foobar"
|
||||
f[1].digest = f[1].digest_partial = f[1].digest_samples = "foobar"
|
||||
f[2].digest = f[2].digest_partial = f[1].digest_samples = "bleh"
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 1)
|
||||
eq_(len(r[0]), 2)
|
||||
eq_(s.discarded_file_count, 0) # don't count the different md5 as discarded!
|
||||
eq_(s.discarded_file_count, 0) # don't count the different digest as discarded!
|
||||
|
||||
|
||||
def test_content_scan_compare_sizes_first(fake_fileexists):
|
||||
class MyFile(no):
|
||||
@property
|
||||
def md5(self):
|
||||
def digest(self):
|
||||
raise AssertionError()
|
||||
|
||||
s = Scanner()
|
||||
@@ -161,14 +161,14 @@ def test_ignore_file_size(fake_fileexists):
|
||||
no("largeignore1", large_size + 1),
|
||||
no("largeignore2", large_size + 1),
|
||||
]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "smallignore"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "smallignore"
|
||||
f[2].md5 = f[2].md5partial = f[2].md5samples = "small"
|
||||
f[3].md5 = f[3].md5partial = f[3].md5samples = "small"
|
||||
f[4].md5 = f[4].md5partial = f[4].md5samples = "large"
|
||||
f[5].md5 = f[5].md5partial = f[5].md5samples = "large"
|
||||
f[6].md5 = f[6].md5partial = f[6].md5samples = "largeignore"
|
||||
f[7].md5 = f[7].md5partial = f[7].md5samples = "largeignore"
|
||||
f[0].digest = f[0].digest_partial = f[0].digest_samples = "smallignore"
|
||||
f[1].digest = f[1].digest_partial = f[1].digest_samples = "smallignore"
|
||||
f[2].digest = f[2].digest_partial = f[2].digest_samples = "small"
|
||||
f[3].digest = f[3].digest_partial = f[3].digest_samples = "small"
|
||||
f[4].digest = f[4].digest_partial = f[4].digest_samples = "large"
|
||||
f[5].digest = f[5].digest_partial = f[5].digest_samples = "large"
|
||||
f[6].digest = f[6].digest_partial = f[6].digest_samples = "largeignore"
|
||||
f[7].digest = f[7].digest_partial = f[7].digest_samples = "largeignore"
|
||||
|
||||
r = s.get_dupe_groups(f)
|
||||
# No ignores
|
||||
@@ -197,21 +197,21 @@ def test_big_file_partial_hashes(fake_fileexists):
|
||||
s.big_file_size_threshold = bigsize
|
||||
|
||||
f = [no("bigfoo", bigsize), no("bigbar", bigsize), no("smallfoo", smallsize), no("smallbar", smallsize)]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||
f[2].md5 = f[2].md5partial = "bleh"
|
||||
f[3].md5 = f[3].md5partial = "bleh"
|
||||
f[0].digest = f[0].digest_partial = f[0].digest_samples = "foobar"
|
||||
f[1].digest = f[1].digest_partial = f[1].digest_samples = "foobar"
|
||||
f[2].digest = f[2].digest_partial = "bleh"
|
||||
f[3].digest = f[3].digest_partial = "bleh"
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 2)
|
||||
|
||||
# md5partial is still the same, but the file is actually different
|
||||
f[1].md5 = f[1].md5samples = "difffoobar"
|
||||
# here we compare the full md5s, as the user disabled the optimization
|
||||
# digest_partial is still the same, but the file is actually different
|
||||
f[1].digest = f[1].digest_samples = "difffoobar"
|
||||
# here we compare the full digests, as the user disabled the optimization
|
||||
s.big_file_size_threshold = 0
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 1)
|
||||
|
||||
# here we should compare the md5samples, and see they are different
|
||||
# here we should compare the digest_samples, and see they are different
|
||||
s.big_file_size_threshold = bigsize
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 1)
|
||||
@@ -221,9 +221,9 @@ def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
f = [no("foo"), no("bar"), no("bleh")]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||
f[2].md5 = f[2].md5partial = f[2].md5samples = "bleh"
|
||||
f[0].digest = f[0].digest_partial = f[0].digest_samples = "foobar"
|
||||
f[1].digest = f[1].digest_partial = f[1].digest_samples = "foobar"
|
||||
f[2].digest = f[2].digest_partial = f[2].digest_samples = "bleh"
|
||||
s.min_match_percentage = 101
|
||||
r = s.get_dupe_groups(f)
|
||||
eq_(len(r), 1)
|
||||
@@ -234,12 +234,16 @@ def test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):
|
||||
eq_(len(r[0]), 2)
|
||||
|
||||
|
||||
def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists):
|
||||
def test_content_scan_doesnt_put_digest_in_words_at_the_end(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.CONTENTS
|
||||
f = [no("foo"), no("bar")]
|
||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||
f[0].digest = f[0].digest_partial = f[
|
||||
0
|
||||
].digest_samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||
f[1].digest = f[1].digest_partial = f[
|
||||
1
|
||||
].digest_samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||
r = s.get_dupe_groups(f)
|
||||
# FIXME looks like we are missing something here?
|
||||
r[0]
|
||||
@@ -587,21 +591,21 @@ def test_folder_scan_exclude_subfolder_matches(fake_fileexists):
|
||||
s = Scanner()
|
||||
s.scan_type = ScanType.FOLDERS
|
||||
topf1 = no("top folder 1", size=42)
|
||||
topf1.md5 = topf1.md5partial = topf1.md5samples = b"some_md5_1"
|
||||
topf1.digest = topf1.digest_partial = topf1.digest_samples = b"some_digest__1"
|
||||
topf1.path = Path("/topf1")
|
||||
topf2 = no("top folder 2", size=42)
|
||||
topf2.md5 = topf2.md5partial = topf2.md5samples = b"some_md5_1"
|
||||
topf2.digest = topf2.digest_partial = topf2.digest_samples = b"some_digest__1"
|
||||
topf2.path = Path("/topf2")
|
||||
subf1 = no("sub folder 1", size=41)
|
||||
subf1.md5 = subf1.md5partial = subf1.md5samples = b"some_md5_2"
|
||||
subf1.digest = subf1.digest_partial = subf1.digest_samples = b"some_digest__2"
|
||||
subf1.path = Path("/topf1/sub")
|
||||
subf2 = no("sub folder 2", size=41)
|
||||
subf2.md5 = subf2.md5partial = subf2.md5samples = b"some_md5_2"
|
||||
subf2.digest = subf2.digest_partial = subf2.digest_samples = b"some_digest__2"
|
||||
subf2.path = Path("/topf2/sub")
|
||||
eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2])), 1) # only top folders
|
||||
# however, if another folder matches a subfolder, keep in in the matches
|
||||
otherf = no("other folder", size=41)
|
||||
otherf.md5 = otherf.md5partial = otherf.md5samples = b"some_md5_2"
|
||||
otherf.digest = otherf.digest_partial = otherf.digest_samples = b"some_digest__2"
|
||||
otherf.path = Path("/otherfolder")
|
||||
eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2, otherf])), 2)
|
||||
|
||||
@@ -624,9 +628,9 @@ def test_dont_count_ref_files_as_discarded(fake_fileexists):
|
||||
o1 = no("foo", path="p1")
|
||||
o2 = no("foo", path="p2")
|
||||
o3 = no("foo", path="p3")
|
||||
o1.md5 = o1.md5partial = o1.md5samples = "foobar"
|
||||
o2.md5 = o2.md5partial = o2.md5samples = "foobar"
|
||||
o3.md5 = o3.md5partial = o3.md5samples = "foobar"
|
||||
o1.digest = o1.digest_partial = o1.digest_samples = "foobar"
|
||||
o2.digest = o2.digest_partial = o2.digest_samples = "foobar"
|
||||
o3.digest = o3.digest_partial = o3.digest_samples = "foobar"
|
||||
o1.is_ref = True
|
||||
o2.is_ref = True
|
||||
eq_(len(s.get_dupe_groups([o1, o2, o3])), 1)
|
||||
|
||||
Reference in New Issue
Block a user