PyHardLinkBackup 1.5.0__tar.gz → 1.6.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.5.0 → pyhardlinkbackup-1.6.0}/PKG-INFO +8 -4
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/__init__.py +1 -1
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/backup.py +1 -1
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/compare_backup.py +2 -1
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_backup.py +81 -3
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_compare_backup.py +66 -7
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/README.md +7 -3
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.editorconfig +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.github/workflows/tests.yml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.gitignore +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.idea/.gitignore +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.pre-commit-config.yaml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.pre-commit-hooks.yaml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.run/Template Python tests.run.xml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.run/Unittests - __all__.run.xml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.run/cli.py --help.run.xml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.run/dev-cli update.run.xml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.run/only DocTests.run.xml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.run/only DocWrite.run.xml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/.venv-app/lib/python3.12/site-packages/cli_base/tests/shell_complete_snapshots/.gitignore +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/__main__.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_app/__init__.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_app/phlb.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/__init__.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/benchmark.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/code_style.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/packaging.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/shell_completion.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/testing.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/update_readme_history.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/constants.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/logging_setup.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/rebuild_databases.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/__init__.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_doc_write.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_doctests.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_project_setup.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_readme.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_readme_history.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_rebuild_database.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/__init__.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/file_hash_database.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/file_size_database.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/filesystem.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/humanize.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/rich_utils.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/sha256sums.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/tee.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/tests/__init__.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/tests/test_file_hash_database.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/tests/test_file_size_database.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/tests/test_filesystem.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/tyro_cli_shared_args.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/cli.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/dev-cli.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/dist/.gitignore +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/docs/README.md +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/docs/about-docs.md +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/noxfile.py +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/pyproject.toml +0 -0
- {pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyHardLinkBackup
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.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
|
|
@@ -232,6 +232,10 @@ Overview of main changes:
|
|
|
232
232
|
|
|
233
233
|
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)
|
|
234
234
|
|
|
235
|
+
* [v1.6.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.5.0...v1.6.0)
|
|
236
|
+
* 2026-01-17 - Fix flaky test, because of terminal size
|
|
237
|
+
* 2026-01-17 - Bugfix: Don't hash new large files twice
|
|
238
|
+
* 2026-01-17 - Use compare also in backup tests
|
|
235
239
|
* [v1.5.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.4.1...v1.5.0)
|
|
236
240
|
* 2026-01-17 - NEW: Compare command to verify source tree with last backup
|
|
237
241
|
* [v1.4.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.4.0...v1.4.1)
|
|
@@ -240,15 +244,15 @@ Overview of main changes:
|
|
|
240
244
|
* 2026-01-16 - Create log file in backup and a summary.txt
|
|
241
245
|
* 2026-01-16 - Run CI tests on macos, too.
|
|
242
246
|
* 2026-01-16 - add dev cli command "scan-benchmark"
|
|
247
|
+
|
|
248
|
+
<details><summary>Expand older history entries ...</summary>
|
|
249
|
+
|
|
243
250
|
* [v1.3.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.2.0...v1.3.0)
|
|
244
251
|
* 2026-01-15 - Verify SHA256SUMS files in "rebuild" command, too.
|
|
245
252
|
* 2026-01-15 - Code cleanup: use more generic names for and in BackupProgress
|
|
246
253
|
* 2026-01-15 - Add tests for rebuild
|
|
247
254
|
* 2026-01-15 - Add command to "rebuld" the size and hash filesystem database
|
|
248
255
|
* 2026-01-15 - Add screenshots in the README
|
|
249
|
-
|
|
250
|
-
<details><summary>Expand older history entries ...</summary>
|
|
251
|
-
|
|
252
256
|
* [v1.2.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.1.0...v1.2.0)
|
|
253
257
|
* 2026-01-15 - Add error handling: Log exception but continue with the backup
|
|
254
258
|
* 2026-01-15 - Check permission and hadlink support on destination path
|
|
@@ -138,7 +138,7 @@ def backup_one_file(
|
|
|
138
138
|
backup_result.hardlinked_size += size
|
|
139
139
|
else:
|
|
140
140
|
logger.info('Copy unique file: %s to %s', src_path, dst_path)
|
|
141
|
-
|
|
141
|
+
shutil.copyfile(src_path, dst_path)
|
|
142
142
|
hash_db[file_hash] = dst_path
|
|
143
143
|
backup_result.copied_files += 1
|
|
144
144
|
backup_result.copied_size += size
|
|
@@ -26,6 +26,7 @@ logger = logging.getLogger(__name__)
|
|
|
26
26
|
|
|
27
27
|
@dataclasses.dataclass
|
|
28
28
|
class CompareResult:
|
|
29
|
+
last_timestamp: str
|
|
29
30
|
compare_dir: Path
|
|
30
31
|
log_file: Path
|
|
31
32
|
#
|
|
@@ -162,7 +163,7 @@ def compare_tree(
|
|
|
162
163
|
size_db = FileSizeDatabase(phlb_conf_dir)
|
|
163
164
|
hash_db = FileHashDatabase(backup_root, phlb_conf_dir)
|
|
164
165
|
|
|
165
|
-
compare_result = CompareResult(compare_dir=compare_dir, log_file=log_file)
|
|
166
|
+
compare_result = CompareResult(last_timestamp=last_timestamp, compare_dir=compare_dir, log_file=log_file)
|
|
166
167
|
|
|
167
168
|
next_update = 0
|
|
168
169
|
for entry in iter_scandir_files(src_root, excludes=excludes):
|
|
@@ -14,13 +14,13 @@ from bx_py_utils.test_utils.assertion import assert_text_equal
|
|
|
14
14
|
from bx_py_utils.test_utils.datetime import parse_dt
|
|
15
15
|
from bx_py_utils.test_utils.log_utils import NoLogs
|
|
16
16
|
from bx_py_utils.test_utils.redirect import RedirectOut
|
|
17
|
-
from cli_base.cli_tools.test_utils.base_testcases import OutputMustCapturedTestCaseMixin
|
|
18
17
|
from freezegun import freeze_time
|
|
19
18
|
from tabulate import tabulate
|
|
20
19
|
|
|
21
20
|
from PyHardLinkBackup.backup import BackupResult, backup_tree
|
|
22
21
|
from PyHardLinkBackup.constants import CHUNK_SIZE
|
|
23
22
|
from PyHardLinkBackup.logging_setup import DEFAULT_CONSOLE_LOG_LEVEL, DEFAULT_LOG_FILE_LEVEL, LoggingManager
|
|
23
|
+
from PyHardLinkBackup.tests.test_compare_backup import assert_compare_backup
|
|
24
24
|
from PyHardLinkBackup.utilities.file_size_database import FileSizeDatabase
|
|
25
25
|
from PyHardLinkBackup.utilities.filesystem import copy_and_hash, iter_scandir_files
|
|
26
26
|
from PyHardLinkBackup.utilities.tests.test_file_hash_database import assert_hash_db_info
|
|
@@ -120,7 +120,7 @@ def assert_fs_tree_overview(root: Path, expected_overview: str):
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
class BackupTreeTestCase(
|
|
123
|
-
OutputMustCapturedTestCaseMixin,
|
|
123
|
+
# TODO: OutputMustCapturedTestCaseMixin,
|
|
124
124
|
unittest.TestCase,
|
|
125
125
|
):
|
|
126
126
|
def test_happy_path(self):
|
|
@@ -254,6 +254,20 @@ class BackupTreeTestCase(
|
|
|
254
254
|
""",
|
|
255
255
|
)
|
|
256
256
|
|
|
257
|
+
#######################################################################################
|
|
258
|
+
# Compare the backup
|
|
259
|
+
|
|
260
|
+
assert_compare_backup(
|
|
261
|
+
test_case=self,
|
|
262
|
+
src_root=src_root,
|
|
263
|
+
backup_root=backup_root,
|
|
264
|
+
excludes=('.cache',),
|
|
265
|
+
excpected_last_timestamp='2026-01-01-123456', # Freezed time, see above
|
|
266
|
+
excpected_total_file_count=7,
|
|
267
|
+
excpected_successful_file_count=7,
|
|
268
|
+
excpected_error_count=0,
|
|
269
|
+
)
|
|
270
|
+
|
|
257
271
|
#######################################################################################
|
|
258
272
|
# Backup again with new added files:
|
|
259
273
|
|
|
@@ -352,6 +366,20 @@ class BackupTreeTestCase(
|
|
|
352
366
|
""",
|
|
353
367
|
)
|
|
354
368
|
|
|
369
|
+
#######################################################################################
|
|
370
|
+
# Compare the backup
|
|
371
|
+
|
|
372
|
+
assert_compare_backup(
|
|
373
|
+
test_case=self,
|
|
374
|
+
src_root=src_root,
|
|
375
|
+
backup_root=backup_root,
|
|
376
|
+
excludes=('.cache',),
|
|
377
|
+
excpected_last_timestamp='2026-01-02-123456', # Freezed time, see above
|
|
378
|
+
excpected_total_file_count=12,
|
|
379
|
+
excpected_successful_file_count=12,
|
|
380
|
+
excpected_error_count=0,
|
|
381
|
+
)
|
|
382
|
+
|
|
355
383
|
#######################################################################################
|
|
356
384
|
# Don't create broken hardlinks!
|
|
357
385
|
|
|
@@ -422,7 +450,7 @@ class BackupTreeTestCase(
|
|
|
422
450
|
copied_size=1074,
|
|
423
451
|
copied_small_files=5,
|
|
424
452
|
copied_small_size=74,
|
|
425
|
-
error_count=0
|
|
453
|
+
error_count=0,
|
|
426
454
|
),
|
|
427
455
|
)
|
|
428
456
|
|
|
@@ -439,6 +467,20 @@ class BackupTreeTestCase(
|
|
|
439
467
|
""",
|
|
440
468
|
)
|
|
441
469
|
|
|
470
|
+
#######################################################################################
|
|
471
|
+
# Compare the backup
|
|
472
|
+
|
|
473
|
+
assert_compare_backup(
|
|
474
|
+
test_case=self,
|
|
475
|
+
src_root=src_root,
|
|
476
|
+
backup_root=backup_root,
|
|
477
|
+
excludes=('.cache',),
|
|
478
|
+
excpected_last_timestamp='2026-01-03-123456', # Freezed time, see above
|
|
479
|
+
excpected_total_file_count=12,
|
|
480
|
+
excpected_successful_file_count=12,
|
|
481
|
+
excpected_error_count=0,
|
|
482
|
+
)
|
|
483
|
+
|
|
442
484
|
def test_symlink(self):
|
|
443
485
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
444
486
|
temp_path = Path(temp_dir).resolve()
|
|
@@ -558,6 +600,24 @@ class BackupTreeTestCase(
|
|
|
558
600
|
with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
|
|
559
601
|
assert_hash_db_info(backup_root=backup_root, expected='')
|
|
560
602
|
|
|
603
|
+
#######################################################################################
|
|
604
|
+
# Compare the backup
|
|
605
|
+
|
|
606
|
+
assert_compare_backup(
|
|
607
|
+
test_case=self,
|
|
608
|
+
src_root=src_root,
|
|
609
|
+
backup_root=backup_root,
|
|
610
|
+
std_out_parts=(
|
|
611
|
+
'Compare completed.',
|
|
612
|
+
'broken_symlink', # <<< the error we expect
|
|
613
|
+
),
|
|
614
|
+
excludes=('.cache',),
|
|
615
|
+
excpected_last_timestamp='2026-01-01-123456', # Freezed time, see above
|
|
616
|
+
excpected_total_file_count=3,
|
|
617
|
+
excpected_successful_file_count=3,
|
|
618
|
+
excpected_error_count=1, # One broken symlink
|
|
619
|
+
)
|
|
620
|
+
|
|
561
621
|
def test_error_handling(self):
|
|
562
622
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
563
623
|
temp_path = Path(temp_dir).resolve()
|
|
@@ -626,3 +686,21 @@ class BackupTreeTestCase(
|
|
|
626
686
|
error_count=1,
|
|
627
687
|
),
|
|
628
688
|
)
|
|
689
|
+
|
|
690
|
+
#######################################################################################
|
|
691
|
+
# Compare the backup
|
|
692
|
+
|
|
693
|
+
assert_compare_backup(
|
|
694
|
+
test_case=self,
|
|
695
|
+
src_root=src_root,
|
|
696
|
+
backup_root=backup_root,
|
|
697
|
+
std_out_parts=(
|
|
698
|
+
'Compare completed.',
|
|
699
|
+
'file2.txt not found', # <<< the error we expect
|
|
700
|
+
),
|
|
701
|
+
excludes=('.cache',),
|
|
702
|
+
excpected_last_timestamp='2026-01-01-123456', # Freezed time, see above
|
|
703
|
+
excpected_total_file_count=3,
|
|
704
|
+
excpected_successful_file_count=2,
|
|
705
|
+
excpected_error_count=0,
|
|
706
|
+
)
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_compare_backup.py
RENAMED
|
@@ -4,7 +4,8 @@ from pathlib import Path
|
|
|
4
4
|
from unittest import TestCase
|
|
5
5
|
|
|
6
6
|
from bx_py_utils.test_utils.redirect import RedirectOut
|
|
7
|
-
from cli_base.cli_tools.test_utils.
|
|
7
|
+
from cli_base.cli_tools.test_utils.assertion import assert_in
|
|
8
|
+
from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich
|
|
8
9
|
|
|
9
10
|
from PyHardLinkBackup.compare_backup import CompareResult, LoggingManager, compare_tree
|
|
10
11
|
from PyHardLinkBackup.logging_setup import DEFAULT_LOG_FILE_LEVEL
|
|
@@ -13,7 +14,44 @@ from PyHardLinkBackup.utilities.file_size_database import FileSizeDatabase
|
|
|
13
14
|
from PyHardLinkBackup.utilities.filesystem import hash_file
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
def assert_compare_backup(
|
|
18
|
+
test_case: TestCase,
|
|
19
|
+
src_root: Path,
|
|
20
|
+
backup_root: Path,
|
|
21
|
+
excpected_last_timestamp: str,
|
|
22
|
+
excpected_total_file_count: int,
|
|
23
|
+
excpected_successful_file_count: int,
|
|
24
|
+
std_out_parts: tuple[str, ...] = ('Compare completed.',),
|
|
25
|
+
excludes: tuple[str, ...] = (),
|
|
26
|
+
excpected_error_count: int = 0,
|
|
27
|
+
) -> None:
|
|
28
|
+
with (
|
|
29
|
+
NoColorEnvRich(
|
|
30
|
+
width=200, # Wide width to avoid line breaks in test output that failed assert_in()
|
|
31
|
+
),
|
|
32
|
+
RedirectOut() as redirected_out,
|
|
33
|
+
):
|
|
34
|
+
result = compare_tree(
|
|
35
|
+
src_root=src_root,
|
|
36
|
+
backup_root=backup_root,
|
|
37
|
+
excludes=excludes,
|
|
38
|
+
log_manager=LoggingManager(
|
|
39
|
+
console_level='info',
|
|
40
|
+
file_level=DEFAULT_LOG_FILE_LEVEL,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
stdout = redirected_out.stdout
|
|
44
|
+
test_case.assertEqual(redirected_out.stderr, '', stdout)
|
|
45
|
+
|
|
46
|
+
assert_in(content=stdout, parts=std_out_parts)
|
|
47
|
+
|
|
48
|
+
test_case.assertEqual(result.last_timestamp, excpected_last_timestamp, stdout)
|
|
49
|
+
test_case.assertEqual(result.total_file_count, excpected_total_file_count, stdout)
|
|
50
|
+
test_case.assertEqual(result.successful_file_count, excpected_successful_file_count, stdout)
|
|
51
|
+
test_case.assertEqual(result.error_count, excpected_error_count, stdout)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CompareBackupTestCase(TestCase):
|
|
17
55
|
def test_happy_path(self):
|
|
18
56
|
with tempfile.TemporaryDirectory() as src_dir, tempfile.TemporaryDirectory() as backup_dir:
|
|
19
57
|
src_root = Path(src_dir).resolve()
|
|
@@ -26,9 +64,14 @@ class CompareBackupTestCase(OutputMustCapturedTestCaseMixin, TestCase):
|
|
|
26
64
|
compare_main_dir = backup_root / src_root.name
|
|
27
65
|
compare_main_dir.mkdir()
|
|
28
66
|
|
|
67
|
+
# Create some "older" compare dirs
|
|
68
|
+
(compare_main_dir / '2025-12-31-235959').mkdir()
|
|
69
|
+
(compare_main_dir / '2026-01-10-235959').mkdir()
|
|
70
|
+
|
|
71
|
+
# Create "last" backup dir:
|
|
29
72
|
timestamp = '2026-01-17-120000'
|
|
30
|
-
|
|
31
|
-
|
|
73
|
+
last_backup_dir = compare_main_dir / timestamp
|
|
74
|
+
last_backup_dir.mkdir()
|
|
32
75
|
|
|
33
76
|
# Create source files
|
|
34
77
|
(src_root / 'small_file.txt').write_text('hello world')
|
|
@@ -40,7 +83,7 @@ class CompareBackupTestCase(OutputMustCapturedTestCaseMixin, TestCase):
|
|
|
40
83
|
total_size = 0
|
|
41
84
|
total_file_count = 0
|
|
42
85
|
for file_path in src_root.iterdir():
|
|
43
|
-
shutil.copy2(file_path,
|
|
86
|
+
shutil.copy2(file_path, last_backup_dir / file_path.name)
|
|
44
87
|
total_size += file_path.stat().st_size
|
|
45
88
|
total_file_count += 1
|
|
46
89
|
self.assertEqual(total_file_count, 3)
|
|
@@ -51,9 +94,11 @@ class CompareBackupTestCase(OutputMustCapturedTestCaseMixin, TestCase):
|
|
|
51
94
|
size_db.add(FileSizeDatabase.MIN_SIZE + 1)
|
|
52
95
|
hash_db = FileHashDatabase(backup_root, phlb_conf_dir)
|
|
53
96
|
src_hash = hash_file(large_file_in_dbs)
|
|
54
|
-
hash_db[src_hash] =
|
|
97
|
+
hash_db[src_hash] = last_backup_dir / 'large_file_in_dbs.txt'
|
|
55
98
|
|
|
99
|
+
#######################################################################################
|
|
56
100
|
# Run compare_tree
|
|
101
|
+
|
|
57
102
|
with RedirectOut() as redirected_out:
|
|
58
103
|
result = compare_tree(
|
|
59
104
|
src_root=src_root,
|
|
@@ -69,7 +114,8 @@ class CompareBackupTestCase(OutputMustCapturedTestCaseMixin, TestCase):
|
|
|
69
114
|
self.assertEqual(
|
|
70
115
|
result,
|
|
71
116
|
CompareResult(
|
|
72
|
-
|
|
117
|
+
last_timestamp='2026-01-17-120000',
|
|
118
|
+
compare_dir=last_backup_dir,
|
|
73
119
|
log_file=result.log_file,
|
|
74
120
|
total_file_count=total_file_count,
|
|
75
121
|
total_size=total_size,
|
|
@@ -84,3 +130,16 @@ class CompareBackupTestCase(OutputMustCapturedTestCaseMixin, TestCase):
|
|
|
84
130
|
),
|
|
85
131
|
redirected_out.stdout,
|
|
86
132
|
)
|
|
133
|
+
|
|
134
|
+
#######################################################################################
|
|
135
|
+
# Check again with our test helper:
|
|
136
|
+
|
|
137
|
+
assert_compare_backup(
|
|
138
|
+
test_case=self,
|
|
139
|
+
src_root=src_root,
|
|
140
|
+
backup_root=backup_root,
|
|
141
|
+
excpected_last_timestamp='2026-01-17-120000',
|
|
142
|
+
excpected_total_file_count=total_file_count,
|
|
143
|
+
excpected_successful_file_count=total_file_count,
|
|
144
|
+
excpected_error_count=0,
|
|
145
|
+
)
|
|
@@ -217,6 +217,10 @@ Overview of main changes:
|
|
|
217
217
|
|
|
218
218
|
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)
|
|
219
219
|
|
|
220
|
+
* [v1.6.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.5.0...v1.6.0)
|
|
221
|
+
* 2026-01-17 - Fix flaky test, because of terminal size
|
|
222
|
+
* 2026-01-17 - Bugfix: Don't hash new large files twice
|
|
223
|
+
* 2026-01-17 - Use compare also in backup tests
|
|
220
224
|
* [v1.5.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.4.1...v1.5.0)
|
|
221
225
|
* 2026-01-17 - NEW: Compare command to verify source tree with last backup
|
|
222
226
|
* [v1.4.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.4.0...v1.4.1)
|
|
@@ -225,15 +229,15 @@ Overview of main changes:
|
|
|
225
229
|
* 2026-01-16 - Create log file in backup and a summary.txt
|
|
226
230
|
* 2026-01-16 - Run CI tests on macos, too.
|
|
227
231
|
* 2026-01-16 - add dev cli command "scan-benchmark"
|
|
232
|
+
|
|
233
|
+
<details><summary>Expand older history entries ...</summary>
|
|
234
|
+
|
|
228
235
|
* [v1.3.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.2.0...v1.3.0)
|
|
229
236
|
* 2026-01-15 - Verify SHA256SUMS files in "rebuild" command, too.
|
|
230
237
|
* 2026-01-15 - Code cleanup: use more generic names for and in BackupProgress
|
|
231
238
|
* 2026-01-15 - Add tests for rebuild
|
|
232
239
|
* 2026-01-15 - Add command to "rebuld" the size and hash filesystem database
|
|
233
240
|
* 2026-01-15 - Add screenshots in the README
|
|
234
|
-
|
|
235
|
-
<details><summary>Expand older history entries ...</summary>
|
|
236
|
-
|
|
237
241
|
* [v1.2.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.1.0...v1.2.0)
|
|
238
242
|
* 2026-01-15 - Add error handling: Log exception but continue with the backup
|
|
239
243
|
* 2026-01-15 - Check permission and hadlink support on destination path
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/shell_completion.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/cli_dev/update_readme_history.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_project_setup.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_readme_history.py
RENAMED
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/tests/test_rebuild_database.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/file_hash_database.py
RENAMED
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/file_size_database.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyhardlinkbackup-1.5.0 → pyhardlinkbackup-1.6.0}/PyHardLinkBackup/utilities/tyro_cli_shared_args.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|