PyHardLinkBackup 1.8.3__tar.gz → 1.9.0__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.3 → pyhardlinkbackup-1.9.0}/.pre-commit-config.yaml +1 -1
- pyhardlinkbackup-1.9.0/.venv-app/lib/python3.14/site-packages/cli_base/tests/shell_complete_snapshots/.gitignore +1 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PKG-INFO +12 -5
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/__init__.py +1 -1
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/backup.py +3 -2
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_app/__init__.py +3 -2
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_backup.py +25 -19
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_project_setup.py +1 -1
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_readme.py +0 -6
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/file_hash_database.py +3 -10
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/test_file_hash_database.py +27 -7
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/README.md +10 -3
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/docs/README.md +9 -4
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/pyproject.toml +6 -3
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/uv.lock +262 -252
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.editorconfig +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.github/workflows/tests.yml +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.gitignore +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.idea/.gitignore +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.pre-commit-hooks.yaml +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.run/Template Python tests.run.xml +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.run/Unittests - __all__.run.xml +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.run/cli.py --help.run.xml +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.run/dev-cli update.run.xml +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.run/only DocTests.run.xml +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.run/only DocWrite.run.xml +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/.venv-app/.gitignore +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/LICENSE +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/__main__.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_app/phlb.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/__init__.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/__main__.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/benchmark.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/code_style.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/debugging.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/packaging.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/testing.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/cli_dev/update_readme_history.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/compare_backup.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/constants.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/logging_setup.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/rebuild_databases.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/__init__.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_compare_backup.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_doc_write.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_doctests.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_readme_history.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/tests/test_rebuild_database.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/__init__.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/file_size_database.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/filesystem.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/humanize.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/rich_utils.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/sha256sums.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tee.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/__init__.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/test_file_size_database.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/test_filesystem.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tests/unittest_utilities.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/PyHardLinkBackup/utilities/tyro_cli_shared_args.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/cli.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/dev-cli.py +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/dist/.gitignore +0 -0
- {pyhardlinkbackup-1.8.3 → pyhardlinkbackup-1.9.0}/noxfile.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!.*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyHardLinkBackup
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
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
|
|
@@ -9,7 +9,7 @@ License: GPL-3.0-or-later
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
11
|
Requires-Dist: bx-py-utils
|
|
12
|
-
Requires-Dist: cli-base-utilities
|
|
12
|
+
Requires-Dist: cli-base-utilities
|
|
13
13
|
Requires-Dist: rich
|
|
14
14
|
Requires-Dist: tyro
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
@@ -298,6 +298,13 @@ Overview of main changes:
|
|
|
298
298
|
|
|
299
299
|
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)
|
|
300
300
|
|
|
301
|
+
* [v1.9.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.4...v1.9.0)
|
|
302
|
+
* 2026-04-14 - Update existing links in has database
|
|
303
|
+
* 2026-04-14 - Update requirements
|
|
304
|
+
* [v1.8.4](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.3...v1.8.4)
|
|
305
|
+
* 2026-04-09 - Update requirements
|
|
306
|
+
* 2026-04-09 - Apply project updates
|
|
307
|
+
* 2026-04-08 - Bump cryptography from 46.0.6 to 46.0.7
|
|
301
308
|
* [v1.8.3](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.2...v1.8.3)
|
|
302
309
|
* 2026-04-01 - Update requirements
|
|
303
310
|
* 2026-03-30 - Bump pygments from 2.19.2 to 2.20.0
|
|
@@ -307,6 +314,9 @@ Overview of main changes:
|
|
|
307
314
|
* 2026-03-28 - Update requirements
|
|
308
315
|
* 2026-03-28 - apply manageprojects updates
|
|
309
316
|
* 2026-03-25 - fix some code styles
|
|
317
|
+
|
|
318
|
+
<details><summary>Expand older history entries ...</summary>
|
|
319
|
+
|
|
310
320
|
* [v1.8.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.0...v1.8.1)
|
|
311
321
|
* 2026-01-24 - Update packaging commands related to new direct "uv" usage
|
|
312
322
|
* 2026-01-24 - Bugfix "rebuild" command
|
|
@@ -321,9 +331,6 @@ Overview of main changes:
|
|
|
321
331
|
* 2026-01-22 - Do not cross filesystem boundaries as default
|
|
322
332
|
* 2026-01-22 - Display progress also for large unique file copy
|
|
323
333
|
* 2026-01-22 - Optimize progress bars for smaller screens
|
|
324
|
-
|
|
325
|
-
<details><summary>Expand older history entries ...</summary>
|
|
326
|
-
|
|
327
334
|
* [v1.7.3](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.2...v1.7.3)
|
|
328
335
|
* 2026-01-21 - Handle directory symlinks correct
|
|
329
336
|
* [v1.7.2](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.1...v1.7.2)
|
|
@@ -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.3 → pyhardlinkbackup-1.9.0}/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.3 → pyhardlinkbackup-1.9.0}/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,13 @@ Overview of main changes:
|
|
|
282
282
|
|
|
283
283
|
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)
|
|
284
284
|
|
|
285
|
+
* [v1.9.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.4...v1.9.0)
|
|
286
|
+
* 2026-04-14 - Update existing links in has database
|
|
287
|
+
* 2026-04-14 - Update requirements
|
|
288
|
+
* [v1.8.4](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.3...v1.8.4)
|
|
289
|
+
* 2026-04-09 - Update requirements
|
|
290
|
+
* 2026-04-09 - Apply project updates
|
|
291
|
+
* 2026-04-08 - Bump cryptography from 46.0.6 to 46.0.7
|
|
285
292
|
* [v1.8.3](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.2...v1.8.3)
|
|
286
293
|
* 2026-04-01 - Update requirements
|
|
287
294
|
* 2026-03-30 - Bump pygments from 2.19.2 to 2.20.0
|
|
@@ -291,6 +298,9 @@ Overview of main changes:
|
|
|
291
298
|
* 2026-03-28 - Update requirements
|
|
292
299
|
* 2026-03-28 - apply manageprojects updates
|
|
293
300
|
* 2026-03-25 - fix some code styles
|
|
301
|
+
|
|
302
|
+
<details><summary>Expand older history entries ...</summary>
|
|
303
|
+
|
|
294
304
|
* [v1.8.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.8.0...v1.8.1)
|
|
295
305
|
* 2026-01-24 - Update packaging commands related to new direct "uv" usage
|
|
296
306
|
* 2026-01-24 - Bugfix "rebuild" command
|
|
@@ -305,9 +315,6 @@ Overview of main changes:
|
|
|
305
315
|
* 2026-01-22 - Do not cross filesystem boundaries as default
|
|
306
316
|
* 2026-01-22 - Display progress also for large unique file copy
|
|
307
317
|
* 2026-01-22 - Optimize progress bars for smaller screens
|
|
308
|
-
|
|
309
|
-
<details><summary>Expand older history entries ...</summary>
|
|
310
|
-
|
|
311
318
|
* [v1.7.3](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.2...v1.7.3)
|
|
312
319
|
* 2026-01-21 - Handle directory symlinks correct
|
|
313
320
|
* [v1.7.2](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.1...v1.7.2)
|
|
@@ -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
|
|
|
@@ -9,7 +9,7 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"cli-base-utilities
|
|
12
|
+
"cli-base-utilities", # 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
|
|
@@ -40,10 +40,12 @@ dev = [
|
|
|
40
40
|
# Don't install bleed-edge versions of dependencies, to avoid undetected issues and supply chain attacks.
|
|
41
41
|
exclude-newer = "1 week"
|
|
42
42
|
|
|
43
|
+
[tool.uv.exclude-newer-package]
|
|
43
44
|
# Exclude own packages from the "exclude-newer" rule and
|
|
44
45
|
# add external packages temporarily to fix known issues or current CVEs
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
uv = "2026-04-13T12:00:00Z"
|
|
47
|
+
cli-base-utilities = "2026-04-13T12:00:00Z"
|
|
48
|
+
cryptography = "2026-04-08T12:00:00Z"
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
[tool.cli_base.pip_audit]
|
|
@@ -150,6 +152,7 @@ cookiecutter_directory = "uv-python"
|
|
|
150
152
|
applied_migrations = [
|
|
151
153
|
"ae7c1ba", # 2025-12-16T19:28:11+01:00
|
|
152
154
|
"69ee933", # 2026-03-28T10:01:57+01:00
|
|
155
|
+
"3cbc419", # 2026-04-09T17:37:01+02:00
|
|
153
156
|
]
|
|
154
157
|
|
|
155
158
|
[manageprojects.cookiecutter_context.cookiecutter]
|