PyHardLinkBackup 1.8.4__tar.gz → 1.9.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.pre-commit-config.yaml +1 -1
- pyhardlinkbackup-1.9.1/.venv-app/lib/python3.14/site-packages/cli_base/tests/shell_complete_snapshots/.gitignore +1 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PKG-INFO +11 -6
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/__init__.py +1 -1
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/backup.py +3 -2
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_app/__init__.py +3 -2
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_backup.py +25 -19
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_project_setup.py +1 -1
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_readme.py +0 -6
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/file_hash_database.py +3 -10
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/tests/test_file_hash_database.py +27 -7
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/README.md +8 -3
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/docs/README.md +9 -4
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/pyproject.toml +4 -3
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/uv.lock +78 -173
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.editorconfig +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.github/workflows/tests.yml +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.gitignore +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.idea/.gitignore +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.pre-commit-hooks.yaml +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.run/Template Python tests.run.xml +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.run/Unittests - __all__.run.xml +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.run/cli.py --help.run.xml +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.run/dev-cli update.run.xml +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.run/only DocTests.run.xml +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.run/only DocWrite.run.xml +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/.venv-app/.gitignore +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/LICENSE +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/__main__.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_app/phlb.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_dev/__init__.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_dev/__main__.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_dev/benchmark.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_dev/code_style.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_dev/debugging.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_dev/packaging.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_dev/testing.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/cli_dev/update_readme_history.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/compare_backup.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/constants.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/logging_setup.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/rebuild_databases.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/__init__.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_compare_backup.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_doc_write.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_doctests.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_readme_history.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_rebuild_database.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/__init__.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/file_size_database.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/filesystem.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/humanize.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/rich_utils.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/sha256sums.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/tee.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/tests/__init__.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/tests/test_file_size_database.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/tests/test_filesystem.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/tests/unittest_utilities.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/tyro_cli_shared_args.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/cli.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/dev-cli.py +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/dist/.gitignore +0 -0
- {pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/noxfile.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!.*
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyHardLinkBackup
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.1
|
|
4
4
|
Summary: HardLink/Deduplication Backups with Python
|
|
5
5
|
Project-URL: Documentation, https://github.com/jedie/PyHardLinkBackup
|
|
6
6
|
Project-URL: Source, https://github.com/jedie/PyHardLinkBackup
|
|
7
7
|
Author-email: Jens Diemer <PyHardLinkBackup@jensdiemer.de>
|
|
8
8
|
License: GPL-3.0-or-later
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
11
|
Requires-Dist: bx-py-utils
|
|
12
|
-
Requires-Dist: cli-base-utilities>=0.
|
|
12
|
+
Requires-Dist: cli-base-utilities>=0.30.0
|
|
13
13
|
Requires-Dist: rich
|
|
14
14
|
Requires-Dist: tyro
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
@@ -298,6 +298,11 @@ Overview of main changes:
|
|
|
298
298
|
|
|
299
299
|
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)
|
|
300
300
|
|
|
301
|
+
* [v1.9.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.9.0...v1.9.1)
|
|
302
|
+
* 2026-04-14 - fix pipx installation and set requires-python to ">=3.13"
|
|
303
|
+
* [v1.9.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.4...v1.9.0)
|
|
304
|
+
* 2026-04-14 - Update existing links in has database
|
|
305
|
+
* 2026-04-14 - Update requirements
|
|
301
306
|
* [v1.8.4](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.3...v1.8.4)
|
|
302
307
|
* 2026-04-09 - Update requirements
|
|
303
308
|
* 2026-04-09 - Apply project updates
|
|
@@ -305,6 +310,9 @@ Overview of main changes:
|
|
|
305
310
|
* [v1.8.3](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.2...v1.8.3)
|
|
306
311
|
* 2026-04-01 - Update requirements
|
|
307
312
|
* 2026-03-30 - Bump pygments from 2.19.2 to 2.20.0
|
|
313
|
+
|
|
314
|
+
<details><summary>Expand older history entries ...</summary>
|
|
315
|
+
|
|
308
316
|
* [v1.8.2](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.1...v1.8.2)
|
|
309
317
|
* 2026-03-28 - Update tests
|
|
310
318
|
* 2026-03-28 - fix code style
|
|
@@ -320,9 +328,6 @@ Overview of main changes:
|
|
|
320
328
|
* 2026-01-22 - rebuid command: skip hashing same files by check the inode uniqueness
|
|
321
329
|
* 2026-01-22 - Add "fs-info" in dev cli
|
|
322
330
|
* 2026-01-22 - rebuild command: fix wrong progress bar
|
|
323
|
-
|
|
324
|
-
<details><summary>Expand older history entries ...</summary>
|
|
325
|
-
|
|
326
331
|
* [v1.8.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.3...v1.8.0)
|
|
327
332
|
* 2026-01-22 - Add optional "--name" to enforce a name for the backup sub directory
|
|
328
333
|
* 2026-01-22 - Do not cross filesystem boundaries as default
|
|
@@ -130,7 +130,6 @@ def backup_one_file(
|
|
|
130
130
|
else:
|
|
131
131
|
logger.info('Store unique file: %s to %s', src_path, dst_path)
|
|
132
132
|
dst_path.write_bytes(file_content)
|
|
133
|
-
hash_db[file_hash] = dst_path
|
|
134
133
|
backup_result.copied_files += 1
|
|
135
134
|
backup_result.copied_size += size
|
|
136
135
|
|
|
@@ -146,10 +145,12 @@ def backup_one_file(
|
|
|
146
145
|
else:
|
|
147
146
|
logger.info('Copy unique file: %s to %s', src_path, dst_path)
|
|
148
147
|
copy_with_progress(src_path, dst_path, progress=progress, total_size=size)
|
|
149
|
-
hash_db[file_hash] = dst_path
|
|
150
148
|
backup_result.copied_files += 1
|
|
151
149
|
backup_result.copied_size += size
|
|
152
150
|
|
|
151
|
+
# Store new file in hash database or update existing entry to latest backuped file:
|
|
152
|
+
hash_db[file_hash] = dst_path
|
|
153
|
+
|
|
153
154
|
# Keep original file metadata (permission bits, time stamps, and flags)
|
|
154
155
|
shutil.copystat(src_path, dst_path)
|
|
155
156
|
else:
|
|
@@ -31,9 +31,10 @@ def version():
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def main(args: Sequence[str] | None = None):
|
|
34
|
-
|
|
34
|
+
project_name = 'phlb' # Enforce program name if pipx used
|
|
35
|
+
print_version(PyHardLinkBackup, project_name=project_name)
|
|
35
36
|
app.cli(
|
|
36
|
-
prog=
|
|
37
|
+
prog=project_name,
|
|
37
38
|
description=constants.CLI_EPILOG,
|
|
38
39
|
use_underscores=False, # use hyphens instead of underscores
|
|
39
40
|
sort_subcommands=True,
|
|
@@ -274,6 +274,7 @@ class BackupTreeTestCase(
|
|
|
274
274
|
'wb backups/source/2026-01-01-123456/min_sized_file1.bin',
|
|
275
275
|
'w backups/.phlb/hash-lookup/bb/c4/bbc4de2ca238d1ec41fb622b75b5cf7d31a6d2ac92405043dd8f8220364fefc8',
|
|
276
276
|
'a backups/source/2026-01-01-123456/SHA256SUMS',
|
|
277
|
+
'w backups/.phlb/hash-lookup/bb/c4/bbc4de2ca238d1ec41fb622b75b5cf7d31a6d2ac92405043dd8f8220364fefc8',
|
|
277
278
|
'a backups/source/2026-01-01-123456/SHA256SUMS',
|
|
278
279
|
'w backups/source/2026-01-01-123456-summary.txt',
|
|
279
280
|
],
|
|
@@ -320,7 +321,7 @@ class BackupTreeTestCase(
|
|
|
320
321
|
assert_hash_db_info(
|
|
321
322
|
backup_root=self.backup_root,
|
|
322
323
|
expected="""
|
|
323
|
-
bb/c4/bbc4de2ca238d1… -> source/2026-01-01-123456/
|
|
324
|
+
bb/c4/bbc4de2ca238d1… -> source/2026-01-01-123456/min_sized_file2.bin
|
|
324
325
|
e3/71/e3711d0eacddeb… -> source/2026-01-01-123456/large_file1.bin
|
|
325
326
|
""",
|
|
326
327
|
)
|
|
@@ -416,15 +417,15 @@ class BackupTreeTestCase(
|
|
|
416
417
|
redirected_out.stdout,
|
|
417
418
|
)
|
|
418
419
|
|
|
419
|
-
# The FileHashDatabase
|
|
420
|
+
# The FileHashDatabase always points to the latest backed-up files:
|
|
420
421
|
with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
|
|
421
422
|
assert_hash_db_info(
|
|
422
423
|
backup_root=self.backup_root,
|
|
423
424
|
expected="""
|
|
424
425
|
23/d2/23d2ce40d26211… -> source/2026-01-02-123456/min_sized_file_newA.bin
|
|
425
426
|
9a/56/9a567077114134… -> source/2026-01-02-123456/min_sized_file_newB.bin
|
|
426
|
-
bb/c4/bbc4de2ca238d1… -> source/2026-01-
|
|
427
|
-
e3/71/e3711d0eacddeb… -> source/2026-01-
|
|
427
|
+
bb/c4/bbc4de2ca238d1… -> source/2026-01-02-123456/min_sized_file2.bin
|
|
428
|
+
e3/71/e3711d0eacddeb… -> source/2026-01-02-123456/large_file2.bin
|
|
428
429
|
""",
|
|
429
430
|
)
|
|
430
431
|
|
|
@@ -463,9 +464,13 @@ class BackupTreeTestCase(
|
|
|
463
464
|
'a backups/source/2026-01-02-123456/SHA256SUMS',
|
|
464
465
|
'wb backups/source/2026-01-02-123456/hardlink2file1',
|
|
465
466
|
'a backups/source/2026-01-02-123456/SHA256SUMS',
|
|
467
|
+
'w backups/.phlb/hash-lookup/e3/71/e3711d0eacddeb105af4ad9b0d63069d759acf32e49712663419e68dc294a94a',
|
|
466
468
|
'a backups/source/2026-01-02-123456/SHA256SUMS',
|
|
469
|
+
'w backups/.phlb/hash-lookup/e3/71/e3711d0eacddeb105af4ad9b0d63069d759acf32e49712663419e68dc294a94a',
|
|
467
470
|
'a backups/source/2026-01-02-123456/SHA256SUMS',
|
|
471
|
+
'w backups/.phlb/hash-lookup/bb/c4/bbc4de2ca238d1ec41fb622b75b5cf7d31a6d2ac92405043dd8f8220364fefc8',
|
|
468
472
|
'a backups/source/2026-01-02-123456/SHA256SUMS',
|
|
473
|
+
'w backups/.phlb/hash-lookup/bb/c4/bbc4de2ca238d1ec41fb622b75b5cf7d31a6d2ac92405043dd8f8220364fefc8',
|
|
469
474
|
'a backups/source/2026-01-02-123456/SHA256SUMS',
|
|
470
475
|
'wb backups/source/2026-01-02-123456/min_sized_file_newA.bin',
|
|
471
476
|
'w backups/.phlb/hash-lookup/23/d2/23d2ce40d26211a9ffe8096fd1f927f2abd094691839d24f88440f7c5168d500',
|
|
@@ -499,8 +504,8 @@ class BackupTreeTestCase(
|
|
|
499
504
|
# Don't create broken hardlinks!
|
|
500
505
|
|
|
501
506
|
"""DocWrite: README.md ## FileHashDatabase - Missing hardlink target file
|
|
502
|
-
|
|
503
|
-
|
|
507
|
+
Deleting files from old backups is safe: the hash DB entry always points to the
|
|
508
|
+
most recently backed-up file, so subsequent backups can still create hardlinks.
|
|
504
509
|
"""
|
|
505
510
|
|
|
506
511
|
# Let's remove one of the files used for hardlinking from the first backup:
|
|
@@ -515,8 +520,8 @@ class BackupTreeTestCase(
|
|
|
515
520
|
self.assertIn('Backup complete', redirected_out.stdout)
|
|
516
521
|
backup_dir = result.backup_dir
|
|
517
522
|
|
|
518
|
-
# Note: min_sized_file1.bin and min_sized_file2.bin
|
|
519
|
-
#
|
|
523
|
+
# Note: min_sized_file1.bin and min_sized_file2.bin accumulate hardlinks
|
|
524
|
+
# because hash_db always points to the latest backup file.
|
|
520
525
|
with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
|
|
521
526
|
assert_fs_tree_overview(
|
|
522
527
|
root=backup_dir,
|
|
@@ -527,8 +532,8 @@ class BackupTreeTestCase(
|
|
|
527
532
|
hardlink2file1 12:00:00 file 1 14 8a11514a
|
|
528
533
|
large_file1.bin 12:00:00 hardlink 5 1001 fb3014ff
|
|
529
534
|
large_file2.bin 12:00:00 hardlink 5 1001 fb3014ff
|
|
530
|
-
min_sized_file1.bin 12:00:00 hardlink
|
|
531
|
-
min_sized_file2.bin 12:00:00 hardlink
|
|
535
|
+
min_sized_file1.bin 12:00:00 hardlink 5 1000 f0d93de4
|
|
536
|
+
min_sized_file2.bin 12:00:00 hardlink 5 1000 f0d93de4
|
|
532
537
|
min_sized_file_newA.bin 12:00:00 hardlink 2 1001 a48f0e33
|
|
533
538
|
min_sized_file_newB.bin 12:00:00 hardlink 2 1000 7d9c564d
|
|
534
539
|
small_file_newA.txt 12:00:00 file 1 10 76d1acf1
|
|
@@ -547,26 +552,26 @@ class BackupTreeTestCase(
|
|
|
547
552
|
backup_count=12,
|
|
548
553
|
backup_size=6091,
|
|
549
554
|
symlink_files=1,
|
|
550
|
-
hardlinked_files=
|
|
551
|
-
hardlinked_size=
|
|
552
|
-
copied_files=
|
|
553
|
-
copied_size=
|
|
555
|
+
hardlinked_files=6,
|
|
556
|
+
hardlinked_size=6003,
|
|
557
|
+
copied_files=5,
|
|
558
|
+
copied_size=74,
|
|
554
559
|
copied_small_files=5,
|
|
555
560
|
copied_small_size=74,
|
|
556
561
|
error_count=0,
|
|
557
562
|
),
|
|
558
563
|
)
|
|
559
564
|
|
|
560
|
-
#
|
|
565
|
+
# All files points now to "2026-01-03" and non of them to the first "2026-01-01" backup:
|
|
561
566
|
self.assertEqual(backup_dir.name, '2026-01-03-123456') # Latest backup dir name
|
|
562
567
|
with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
|
|
563
568
|
assert_hash_db_info(
|
|
564
569
|
backup_root=self.backup_root,
|
|
565
570
|
expected="""
|
|
566
|
-
23/d2/23d2ce40d26211… -> source/2026-01-
|
|
567
|
-
9a/56/9a567077114134… -> source/2026-01-
|
|
568
|
-
bb/c4/bbc4de2ca238d1… -> source/2026-01-03-123456/
|
|
569
|
-
e3/71/e3711d0eacddeb… -> source/2026-01-
|
|
571
|
+
23/d2/23d2ce40d26211… -> source/2026-01-03-123456/min_sized_file_newA.bin
|
|
572
|
+
9a/56/9a567077114134… -> source/2026-01-03-123456/min_sized_file_newB.bin
|
|
573
|
+
bb/c4/bbc4de2ca238d1… -> source/2026-01-03-123456/min_sized_file2.bin
|
|
574
|
+
e3/71/e3711d0eacddeb… -> source/2026-01-03-123456/large_file2.bin
|
|
570
575
|
""",
|
|
571
576
|
)
|
|
572
577
|
|
|
@@ -916,6 +921,7 @@ class BackupTreeTestCase(
|
|
|
916
921
|
[
|
|
917
922
|
'w backups/.phlb_test',
|
|
918
923
|
'a backups/source/2026-02-22-123456-backup.log',
|
|
924
|
+
'w backups/.phlb/hash-lookup/23/d2/23d2ce40d26211a9ffe8096fd1f927f2abd094691839d24f88440f7c5168d500',
|
|
919
925
|
'a backups/source/2026-02-22-123456/SHA256SUMS',
|
|
920
926
|
'wb backups/source/2026-02-22-123456/large_fileB.txt',
|
|
921
927
|
'w backups/.phlb/hash-lookup/2a/92/2a925556d3ec9e4258624a324cd9300a9a3d9c86dac6bbbb63071bdb7787afd2',
|
{pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/tests/test_project_setup.py
RENAMED
|
@@ -22,7 +22,7 @@ class ProjectSetupTestCase(TestCase):
|
|
|
22
22
|
assert_is_file(cli_bin)
|
|
23
23
|
|
|
24
24
|
output = subprocess.check_output([cli_bin, 'version'], text=True)
|
|
25
|
-
self.assertIn(f'
|
|
25
|
+
self.assertIn(f'phlb v{__version__}', output)
|
|
26
26
|
|
|
27
27
|
dev_cli_bin = PACKAGE_ROOT / 'dev-cli.py'
|
|
28
28
|
assert_is_file(dev_cli_bin)
|
|
@@ -47,9 +47,6 @@ class ReadmeTestCase(BaseTestCase):
|
|
|
47
47
|
),
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
# Installed via pipx is called 'phlb', not 'cli.py':
|
|
51
|
-
stdout = stdout.replace('./cli.py', 'phlb')
|
|
52
|
-
|
|
53
50
|
assert_cli_help_in_readme(text_block=stdout, marker='main help')
|
|
54
51
|
|
|
55
52
|
def test_backup_help(self):
|
|
@@ -63,9 +60,6 @@ class ReadmeTestCase(BaseTestCase):
|
|
|
63
60
|
),
|
|
64
61
|
)
|
|
65
62
|
|
|
66
|
-
# Installed via pipx is called 'phlb', not 'cli.py':
|
|
67
|
-
stdout = stdout.replace('./cli.py', 'phlb')
|
|
68
|
-
|
|
69
63
|
assert_cli_help_in_readme(text_block=stdout, marker='backup help')
|
|
70
64
|
|
|
71
65
|
def test_dev_help(self):
|
{pyhardlinkbackup-1.8.4 → pyhardlinkbackup-1.9.1}/PyHardLinkBackup/utilities/file_hash_database.py
RENAMED
|
@@ -5,10 +5,6 @@ from pathlib import Path
|
|
|
5
5
|
logger = logging.getLogger(__name__)
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class HashAlreadyExistsError(ValueError):
|
|
9
|
-
pass
|
|
10
|
-
|
|
11
|
-
|
|
12
8
|
class FileHashDatabase:
|
|
13
9
|
"""DocWrite: README.md ## FileHashDatabase
|
|
14
10
|
A simple "database" to store file content hash <-> relative path mappings.
|
|
@@ -54,12 +50,9 @@ class FileHashDatabase:
|
|
|
54
50
|
return abs_file_path
|
|
55
51
|
|
|
56
52
|
def __setitem__(self, hash: str, abs_file_path: Path):
|
|
53
|
+
"""
|
|
54
|
+
Create or update the hash entry with the given absolute file path.
|
|
55
|
+
"""
|
|
57
56
|
hash_path = self._get_hash_path(hash)
|
|
58
57
|
hash_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
-
|
|
60
|
-
# File should be found before and results in hardlink creation!
|
|
61
|
-
# So deny change of existing hashes:
|
|
62
|
-
if hash_path.exists():
|
|
63
|
-
raise HashAlreadyExistsError(f'Hash {hash} already exists in the database!')
|
|
64
|
-
|
|
65
58
|
hash_path.write_text(str(abs_file_path.relative_to(self.backup_root)))
|
|
@@ -8,7 +8,7 @@ from bx_py_utils.test_utils.assertion import assert_text_equal
|
|
|
8
8
|
from bx_py_utils.test_utils.log_utils import NoLogs
|
|
9
9
|
from cli_base.cli_tools.test_utils.base_testcases import BaseTestCase
|
|
10
10
|
|
|
11
|
-
from PyHardLinkBackup.utilities.file_hash_database import FileHashDatabase
|
|
11
|
+
from PyHardLinkBackup.utilities.file_hash_database import FileHashDatabase
|
|
12
12
|
from PyHardLinkBackup.utilities.filesystem import iter_scandir_files
|
|
13
13
|
|
|
14
14
|
|
|
@@ -127,10 +127,28 @@ class FileHashDatabaseTestCase(BaseTestCase):
|
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
########################################################################################
|
|
130
|
-
#
|
|
130
|
+
# Update existing hash to point to a newer file:
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
"""DocWrite: README.md ## FileHashDatabase
|
|
133
|
+
The entry for each hash is always updated to point to the most recently backed-up file.
|
|
134
|
+
This means you can safely delete old backups: the hash DB will still point to a valid
|
|
135
|
+
file in the most recent backup, so deduplication continues to work correctly.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
file_c_path = backup_root_path / 'rel/path/to/file-C'
|
|
139
|
+
file_c_path.parent.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
file_c_path.touch()
|
|
141
|
+
|
|
142
|
+
hash_db['12abcd345678abcdef'] = file_c_path
|
|
143
|
+
self.assertEqual(hash_db.get('12abcd345678abcdef'), file_c_path)
|
|
144
|
+
with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
|
|
145
|
+
assert_hash_db_info(
|
|
146
|
+
backup_root=hash_db.backup_root,
|
|
147
|
+
expected="""
|
|
148
|
+
12/34/12345678abcdef… -> rel/path/to/file-A
|
|
149
|
+
12/ab/12abcd345678ab… -> rel/path/to/file-C
|
|
150
|
+
""",
|
|
151
|
+
)
|
|
134
152
|
|
|
135
153
|
########################################################################################
|
|
136
154
|
# Don't use stale entries pointing to missing files:
|
|
@@ -139,8 +157,10 @@ class FileHashDatabaseTestCase(BaseTestCase):
|
|
|
139
157
|
file_a_path.unlink()
|
|
140
158
|
|
|
141
159
|
"""DocWrite: README.md ## FileHashDatabase - Missing hardlink target file
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
The `get()` method checks whether the referenced file still exists.
|
|
161
|
+
If not, the stale entry is removed and a warning is logged.
|
|
162
|
+
On the next backup run, the file is then copied fresh instead of hardlinked.
|
|
163
|
+
"""
|
|
144
164
|
with self.assertLogs(level=logging.WARNING) as logs:
|
|
145
165
|
self.assertIs(hash_db.get('12345678abcdef'), None)
|
|
146
166
|
self.assertIn('Hash database entry found, but file does not exist', ''.join(logs.output))
|
|
@@ -148,6 +168,6 @@ class FileHashDatabaseTestCase(BaseTestCase):
|
|
|
148
168
|
assert_hash_db_info(
|
|
149
169
|
backup_root=hash_db.backup_root,
|
|
150
170
|
expected="""
|
|
151
|
-
12/ab/12abcd345678ab… -> rel/path/to/file-
|
|
171
|
+
12/ab/12abcd345678ab… -> rel/path/to/file-C
|
|
152
172
|
""",
|
|
153
173
|
)
|
|
@@ -282,6 +282,11 @@ Overview of main changes:
|
|
|
282
282
|
|
|
283
283
|
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)
|
|
284
284
|
|
|
285
|
+
* [v1.9.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.9.0...v1.9.1)
|
|
286
|
+
* 2026-04-14 - fix pipx installation and set requires-python to ">=3.13"
|
|
287
|
+
* [v1.9.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.4...v1.9.0)
|
|
288
|
+
* 2026-04-14 - Update existing links in has database
|
|
289
|
+
* 2026-04-14 - Update requirements
|
|
285
290
|
* [v1.8.4](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.3...v1.8.4)
|
|
286
291
|
* 2026-04-09 - Update requirements
|
|
287
292
|
* 2026-04-09 - Apply project updates
|
|
@@ -289,6 +294,9 @@ Overview of main changes:
|
|
|
289
294
|
* [v1.8.3](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.2...v1.8.3)
|
|
290
295
|
* 2026-04-01 - Update requirements
|
|
291
296
|
* 2026-03-30 - Bump pygments from 2.19.2 to 2.20.0
|
|
297
|
+
|
|
298
|
+
<details><summary>Expand older history entries ...</summary>
|
|
299
|
+
|
|
292
300
|
* [v1.8.2](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.1...v1.8.2)
|
|
293
301
|
* 2026-03-28 - Update tests
|
|
294
302
|
* 2026-03-28 - fix code style
|
|
@@ -304,9 +312,6 @@ Overview of main changes:
|
|
|
304
312
|
* 2026-01-22 - rebuid command: skip hashing same files by check the inode uniqueness
|
|
305
313
|
* 2026-01-22 - Add "fs-info" in dev cli
|
|
306
314
|
* 2026-01-22 - rebuild command: fix wrong progress bar
|
|
307
|
-
|
|
308
|
-
<details><summary>Expand older history entries ...</summary>
|
|
309
|
-
|
|
310
315
|
* [v1.8.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.3...v1.8.0)
|
|
311
316
|
* 2026-01-22 - Add optional "--name" to enforce a name for the backup sub directory
|
|
312
317
|
* 2026-01-22 - Do not cross filesystem boundaries as default
|
|
@@ -36,13 +36,18 @@ Notes:
|
|
|
36
36
|
* The "relative path" that will be stored is not validated, so it can be any string.
|
|
37
37
|
* We don't "cache" anything in Memory, to avoid high memory consumption for large datasets.
|
|
38
38
|
|
|
39
|
+
The entry for each hash is always updated to point to the most recently backed-up file.
|
|
40
|
+
This means you can safely delete old backups: the hash DB will still point to a valid
|
|
41
|
+
file in the most recent backup, so deduplication continues to work correctly.
|
|
42
|
+
|
|
39
43
|
## FileHashDatabase - Missing hardlink target file
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
Deleting files from old backups is safe: the hash DB entry always points to the
|
|
46
|
+
most recently backed-up file, so subsequent backups can still create hardlinks.
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
The `get()` method checks whether the referenced file still exists.
|
|
49
|
+
If not, the stale entry is removed and a warning is logged.
|
|
50
|
+
On the next backup run, the file is then copied fresh instead of hardlinked.
|
|
46
51
|
|
|
47
52
|
## FileSizeDatabase
|
|
48
53
|
|
|
@@ -7,9 +7,9 @@ readme = "README.md"
|
|
|
7
7
|
authors = [
|
|
8
8
|
{name = 'Jens Diemer', email = 'PyHardLinkBackup@jensdiemer.de'}
|
|
9
9
|
]
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"cli-base-utilities>=0.
|
|
12
|
+
"cli-base-utilities>=0.30.0", # https://github.com/jedie/cli-base-utilities
|
|
13
13
|
"bx_py_utils", # https://github.com/boxine/bx_py_utils
|
|
14
14
|
"tyro", # https://github.com/brentyi/tyro
|
|
15
15
|
"rich", # https://github.com/Textualize/rich
|
|
@@ -43,8 +43,9 @@ exclude-newer = "1 week"
|
|
|
43
43
|
[tool.uv.exclude-newer-package]
|
|
44
44
|
# Exclude own packages from the "exclude-newer" rule and
|
|
45
45
|
# add external packages temporarily to fix known issues or current CVEs
|
|
46
|
+
uv = "2026-04-13T12:00:00Z"
|
|
47
|
+
cli-base-utilities = "2026-04-13T12:00:00Z"
|
|
46
48
|
cryptography = "2026-04-08T12:00:00Z"
|
|
47
|
-
django = "2026-04-08T12:00:00Z"
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
[tool.cli_base.pip_audit]
|