PyHardLinkBackup 1.7.2__tar.gz → 1.8.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.7.2 → pyhardlinkbackup-1.8.0}/PKG-INFO +30 -17
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/__init__.py +1 -1
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/backup.py +37 -9
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_app/phlb.py +8 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/compare_backup.py +44 -4
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/rebuild_databases.py +12 -2
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/test_backup.py +258 -26
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/test_compare_backup.py +2 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/filesystem.py +76 -8
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/rich_utils.py +8 -25
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/tests/test_file_hash_database.py +12 -2
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/tests/test_file_size_database.py +15 -2
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/tests/test_filesystem.py +44 -1
- pyhardlinkbackup-1.8.0/PyHardLinkBackup/utilities/tyro_cli_shared_args.py +29 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/README.md +29 -16
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/docs/README.md +11 -1
- pyhardlinkbackup-1.7.2/PyHardLinkBackup/utilities/tyro_cli_shared_args.py +0 -12
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.editorconfig +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.github/workflows/tests.yml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.gitignore +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.idea/.gitignore +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.pre-commit-config.yaml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.pre-commit-hooks.yaml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.run/Template Python tests.run.xml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.run/Unittests - __all__.run.xml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.run/cli.py --help.run.xml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.run/dev-cli update.run.xml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.run/only DocTests.run.xml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.run/only DocWrite.run.xml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/.venv-app/lib/python3.12/site-packages/cli_base/tests/shell_complete_snapshots/.gitignore +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/__main__.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_app/__init__.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_dev/__init__.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_dev/benchmark.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_dev/code_style.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_dev/packaging.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_dev/shell_completion.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_dev/testing.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/cli_dev/update_readme_history.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/constants.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/logging_setup.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/__init__.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/test_doc_write.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/test_doctests.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/test_project_setup.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/test_readme.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/test_readme_history.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/tests/test_rebuild_database.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/__init__.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/file_hash_database.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/file_size_database.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/humanize.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/sha256sums.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/tee.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/tests/__init__.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/PyHardLinkBackup/utilities/tests/unittest_utilities.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/cli.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/dev-cli.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/dist/.gitignore +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/docs/about-docs.md +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/noxfile.py +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.0}/pyproject.toml +0 -0
- {pyhardlinkbackup-1.7.2 → pyhardlinkbackup-1.8.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.8.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
|
|
@@ -35,6 +35,8 @@ Some aspects:
|
|
|
35
35
|
Limitations:
|
|
36
36
|
|
|
37
37
|
- Requires a filesystem that supports hardlinks (e.g., btrfs, zfs, ext4, APFS, NTFS with limitations).
|
|
38
|
+
- Empty directories are not backed up.
|
|
39
|
+
|
|
38
40
|
|
|
39
41
|
## installation
|
|
40
42
|
|
|
@@ -64,19 +66,23 @@ usage: phlb backup [-h] [BACKUP OPTIONS]
|
|
|
64
66
|
|
|
65
67
|
Backup the source directory to the destination directory using hard links for deduplication.
|
|
66
68
|
|
|
67
|
-
╭─ positional arguments
|
|
68
|
-
│ source
|
|
69
|
-
│ destination
|
|
70
|
-
|
|
71
|
-
╭─ options
|
|
72
|
-
│ -h, --help
|
|
73
|
-
│ --
|
|
74
|
-
│
|
|
75
|
-
│ --
|
|
76
|
-
│
|
|
77
|
-
│ --
|
|
78
|
-
│
|
|
79
|
-
|
|
69
|
+
╭─ positional arguments ───────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
70
|
+
│ source Source directory to back up. (required) │
|
|
71
|
+
│ destination Destination directory for the backup. (required) │
|
|
72
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
73
|
+
╭─ options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
|
74
|
+
│ -h, --help show this help message and exit │
|
|
75
|
+
│ --name {None}|STR Optional name for the backup (used to create a subdirectory in the backup destination). If not │
|
|
76
|
+
│ provided, the name of the source directory is used. (default: None) │
|
|
77
|
+
│ --one-file-system, --no-one-file-system │
|
|
78
|
+
│ Do not cross filesystem boundaries. (default: True) │
|
|
79
|
+
│ --excludes [STR [STR ...]] │
|
|
80
|
+
│ List of directories to exclude from backup. (default: __pycache__ .cache .temp .tmp .tox .nox) │
|
|
81
|
+
│ --verbosity {debug,info,warning,error} │
|
|
82
|
+
│ Log level for console logging. (default: warning) │
|
|
83
|
+
│ --log-file-level {debug,info,warning,error} │
|
|
84
|
+
│ Log level for the log file (default: info) │
|
|
85
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
80
86
|
```
|
|
81
87
|
[comment]: <> (✂✂✂ auto generated backup help end ✂✂✂)
|
|
82
88
|
|
|
@@ -283,10 +289,20 @@ Overview of main changes:
|
|
|
283
289
|
|
|
284
290
|
[comment]: <> (✂✂✂ auto generated history start ✂✂✂)
|
|
285
291
|
|
|
292
|
+
* [v1.8.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.3...v1.8.0)
|
|
293
|
+
* 2026-01-22 - Add optional "--name" to enforce a name for the backup sub directory
|
|
294
|
+
* 2026-01-22 - Do not cross filesystem boundaries as default
|
|
295
|
+
* 2026-01-22 - Display progress also for large unique file copy
|
|
296
|
+
* 2026-01-22 - Optimize progress bars for smaller screens
|
|
297
|
+
* [v1.7.3](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.2...v1.7.3)
|
|
298
|
+
* 2026-01-21 - Handle directory symlinks correct
|
|
286
299
|
* [v1.7.2](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.1...v1.7.2)
|
|
287
300
|
* 2026-01-21 - Display "Remaining time" to files and sizes, too.
|
|
288
301
|
* [v1.7.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.7.0...v1.7.1)
|
|
289
302
|
* 2026-01-19 - Update requirements to fix problems under Windows
|
|
303
|
+
|
|
304
|
+
<details><summary>Expand older history entries ...</summary>
|
|
305
|
+
|
|
290
306
|
* [v1.7.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.6.0...v1.7.0)
|
|
291
307
|
* 2026-01-19 - Speedup and enhance unittest
|
|
292
308
|
* 2026-01-17 - Remove unfinished copied files on errors
|
|
@@ -300,9 +316,6 @@ Overview of main changes:
|
|
|
300
316
|
* 2026-01-17 - Fix flaky test, because of terminal size
|
|
301
317
|
* 2026-01-17 - Bugfix: Don't hash new large files twice
|
|
302
318
|
* 2026-01-17 - Use compare also in backup tests
|
|
303
|
-
|
|
304
|
-
<details><summary>Expand older history entries ...</summary>
|
|
305
|
-
|
|
306
319
|
* [v1.5.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.4.1...v1.5.0)
|
|
307
320
|
* 2026-01-17 - NEW: Compare command to verify source tree with last backup
|
|
308
321
|
* [v1.4.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.4.0...v1.4.1)
|
|
@@ -16,11 +16,13 @@ from PyHardLinkBackup.utilities.file_size_database import FileSizeDatabase
|
|
|
16
16
|
from PyHardLinkBackup.utilities.filesystem import (
|
|
17
17
|
RemoveFileOnError,
|
|
18
18
|
copy_and_hash,
|
|
19
|
+
copy_with_progress,
|
|
19
20
|
hash_file,
|
|
20
21
|
humanized_fs_scan,
|
|
21
22
|
iter_scandir_files,
|
|
22
23
|
read_and_hash_file,
|
|
23
24
|
supports_hardlinks,
|
|
25
|
+
verbose_path_stat,
|
|
24
26
|
)
|
|
25
27
|
from PyHardLinkBackup.utilities.humanize import PrintTimingContextManager, human_filesize
|
|
26
28
|
from PyHardLinkBackup.utilities.rich_utils import DisplayFileTreeProgress
|
|
@@ -52,6 +54,16 @@ class BackupResult:
|
|
|
52
54
|
error_count: int = 0
|
|
53
55
|
|
|
54
56
|
|
|
57
|
+
def copy_symlink(src_path: Path, dst_path: Path) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Copy file and directory symlinks.
|
|
60
|
+
"""
|
|
61
|
+
target_is_directory = src_path.is_dir()
|
|
62
|
+
logger.debug('Copy symlink: %s to %s (is directory: %r)', src_path, dst_path, target_is_directory)
|
|
63
|
+
target = os.readlink(src_path)
|
|
64
|
+
dst_path.symlink_to(target, target_is_directory=target_is_directory)
|
|
65
|
+
|
|
66
|
+
|
|
55
67
|
def backup_one_file(
|
|
56
68
|
*,
|
|
57
69
|
src_root: Path,
|
|
@@ -74,8 +86,7 @@ def backup_one_file(
|
|
|
74
86
|
size = entry.stat().st_size
|
|
75
87
|
except FileNotFoundError as err:
|
|
76
88
|
logger.warning(f'Broken symlink {src_path}: {err.__class__.__name__}: {err}')
|
|
77
|
-
|
|
78
|
-
dst_path.symlink_to(target)
|
|
89
|
+
copy_symlink(src_path, dst_path)
|
|
79
90
|
backup_result.symlink_files += 1
|
|
80
91
|
return
|
|
81
92
|
|
|
@@ -88,9 +99,7 @@ def backup_one_file(
|
|
|
88
99
|
return
|
|
89
100
|
|
|
90
101
|
if entry.is_symlink():
|
|
91
|
-
|
|
92
|
-
target = os.readlink(src_path)
|
|
93
|
-
dst_path.symlink_to(target)
|
|
102
|
+
copy_symlink(src_path, dst_path)
|
|
94
103
|
backup_result.symlink_files += 1
|
|
95
104
|
return
|
|
96
105
|
|
|
@@ -141,7 +150,7 @@ def backup_one_file(
|
|
|
141
150
|
backup_result.hardlinked_size += size
|
|
142
151
|
else:
|
|
143
152
|
logger.info('Copy unique file: %s to %s', src_path, dst_path)
|
|
144
|
-
|
|
153
|
+
copy_with_progress(src_path, dst_path, progress=progress, total_size=size)
|
|
145
154
|
hash_db[file_hash] = dst_path
|
|
146
155
|
backup_result.copied_files += 1
|
|
147
156
|
backup_result.copied_size += size
|
|
@@ -163,6 +172,8 @@ def backup_tree(
|
|
|
163
172
|
*,
|
|
164
173
|
src_root: Path,
|
|
165
174
|
backup_root: Path,
|
|
175
|
+
backup_name: str | None,
|
|
176
|
+
one_file_system: bool,
|
|
166
177
|
excludes: tuple[str, ...],
|
|
167
178
|
log_manager: LoggingManager,
|
|
168
179
|
) -> BackupResult:
|
|
@@ -172,12 +183,17 @@ def backup_tree(
|
|
|
172
183
|
print(f'Please check source directory: "{src_root}"\n')
|
|
173
184
|
sys.exit(1)
|
|
174
185
|
|
|
186
|
+
src_stat = verbose_path_stat(src_root)
|
|
187
|
+
src_device_id = src_stat.st_dev
|
|
188
|
+
|
|
175
189
|
backup_root = backup_root.resolve()
|
|
176
190
|
if not backup_root.is_dir():
|
|
177
191
|
print('Error: Backup directory does not exist!')
|
|
178
192
|
print(f'Please create "{backup_root}" directory first and start again!\n')
|
|
179
193
|
sys.exit(1)
|
|
180
194
|
|
|
195
|
+
verbose_path_stat(backup_root)
|
|
196
|
+
|
|
181
197
|
if not os.access(backup_root, os.W_OK):
|
|
182
198
|
print('Error: No write access to backup directory!')
|
|
183
199
|
print(f'Please check permissions for backup directory: "{backup_root}"\n')
|
|
@@ -191,13 +207,20 @@ def backup_tree(
|
|
|
191
207
|
# Step 1: Scan source directory:
|
|
192
208
|
excludes: set = set(excludes)
|
|
193
209
|
with PrintTimingContextManager('Filesystem scan completed in'):
|
|
194
|
-
src_file_count, src_total_size = humanized_fs_scan(
|
|
210
|
+
src_file_count, src_total_size = humanized_fs_scan(
|
|
211
|
+
path=src_root,
|
|
212
|
+
one_file_system=one_file_system,
|
|
213
|
+
src_device_id=src_device_id,
|
|
214
|
+
excludes=excludes,
|
|
215
|
+
)
|
|
195
216
|
|
|
196
217
|
phlb_conf_dir = backup_root / '.phlb'
|
|
197
218
|
phlb_conf_dir.mkdir(parents=False, exist_ok=True)
|
|
198
219
|
|
|
199
220
|
timestamp = datetime.datetime.now().strftime('%Y-%m-%d-%H%M%S')
|
|
200
|
-
|
|
221
|
+
if not backup_name:
|
|
222
|
+
backup_name = src_root.name
|
|
223
|
+
backup_main_dir = backup_root / backup_name
|
|
201
224
|
backup_dir = backup_main_dir / timestamp
|
|
202
225
|
backup_dir.mkdir(parents=True, exist_ok=False)
|
|
203
226
|
|
|
@@ -220,7 +243,12 @@ def backup_tree(
|
|
|
220
243
|
backup_result = BackupResult(backup_dir=backup_dir, log_file=log_file)
|
|
221
244
|
|
|
222
245
|
next_update = 0
|
|
223
|
-
for entry in iter_scandir_files(
|
|
246
|
+
for entry in iter_scandir_files(
|
|
247
|
+
path=src_root,
|
|
248
|
+
one_file_system=one_file_system,
|
|
249
|
+
src_device_id=src_device_id,
|
|
250
|
+
excludes=excludes,
|
|
251
|
+
):
|
|
224
252
|
try:
|
|
225
253
|
backup_one_file(
|
|
226
254
|
src_root=src_root,
|
|
@@ -17,7 +17,9 @@ from PyHardLinkBackup.logging_setup import (
|
|
|
17
17
|
)
|
|
18
18
|
from PyHardLinkBackup.utilities.tyro_cli_shared_args import (
|
|
19
19
|
DEFAULT_EXCLUDE_DIRECTORIES,
|
|
20
|
+
TyroBackupNameArgType,
|
|
20
21
|
TyroExcludeDirectoriesArgType,
|
|
22
|
+
TyroOneFileSystemArgType,
|
|
21
23
|
)
|
|
22
24
|
|
|
23
25
|
|
|
@@ -41,6 +43,8 @@ def backup(
|
|
|
41
43
|
),
|
|
42
44
|
],
|
|
43
45
|
/,
|
|
46
|
+
name: TyroBackupNameArgType = None,
|
|
47
|
+
one_file_system: TyroOneFileSystemArgType = True,
|
|
44
48
|
excludes: TyroExcludeDirectoriesArgType = DEFAULT_EXCLUDE_DIRECTORIES,
|
|
45
49
|
verbosity: TyroConsoleLogLevelArgType = DEFAULT_CONSOLE_LOG_LEVEL,
|
|
46
50
|
log_file_level: TyroLogFileLevelArgType = DEFAULT_LOG_FILE_LEVEL,
|
|
@@ -55,6 +59,8 @@ def backup(
|
|
|
55
59
|
backup_tree(
|
|
56
60
|
src_root=src,
|
|
57
61
|
backup_root=dst,
|
|
62
|
+
backup_name=name,
|
|
63
|
+
one_file_system=one_file_system,
|
|
58
64
|
excludes=excludes,
|
|
59
65
|
log_manager=log_manager,
|
|
60
66
|
)
|
|
@@ -77,6 +83,7 @@ def compare(
|
|
|
77
83
|
),
|
|
78
84
|
],
|
|
79
85
|
/,
|
|
86
|
+
one_file_system: TyroOneFileSystemArgType = True,
|
|
80
87
|
excludes: TyroExcludeDirectoriesArgType = DEFAULT_EXCLUDE_DIRECTORIES,
|
|
81
88
|
verbosity: TyroConsoleLogLevelArgType = DEFAULT_CONSOLE_LOG_LEVEL,
|
|
82
89
|
log_file_level: TyroLogFileLevelArgType = DEFAULT_LOG_FILE_LEVEL,
|
|
@@ -91,6 +98,7 @@ def compare(
|
|
|
91
98
|
compare_backup.compare_tree(
|
|
92
99
|
src_root=src,
|
|
93
100
|
backup_root=dst,
|
|
101
|
+
one_file_system=one_file_system,
|
|
94
102
|
excludes=excludes,
|
|
95
103
|
log_manager=log_manager,
|
|
96
104
|
)
|
|
@@ -15,6 +15,7 @@ from PyHardLinkBackup.utilities.filesystem import (
|
|
|
15
15
|
hash_file,
|
|
16
16
|
humanized_fs_scan,
|
|
17
17
|
iter_scandir_files,
|
|
18
|
+
verbose_path_stat,
|
|
18
19
|
)
|
|
19
20
|
from PyHardLinkBackup.utilities.humanize import PrintTimingContextManager, human_filesize
|
|
20
21
|
from PyHardLinkBackup.utilities.rich_utils import DisplayFileTreeProgress
|
|
@@ -55,13 +56,21 @@ def compare_one_file(
|
|
|
55
56
|
compare_result: CompareResult,
|
|
56
57
|
progress: DisplayFileTreeProgress,
|
|
57
58
|
) -> None:
|
|
58
|
-
|
|
59
|
+
if entry.is_file():
|
|
60
|
+
# For the progress bars:
|
|
61
|
+
compare_result.total_file_count += 1
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
src_size = entry.stat().st_size
|
|
65
|
+
except FileNotFoundError as err:
|
|
66
|
+
logger.warning(f'Broken symlink {entry.path}: {err.__class__.__name__}: {err}')
|
|
67
|
+
return
|
|
59
68
|
|
|
60
69
|
# For the progress bars:
|
|
61
|
-
compare_result.total_file_count += 1
|
|
62
70
|
compare_result.total_size += src_size
|
|
63
71
|
|
|
64
72
|
src_path = Path(entry.path)
|
|
73
|
+
|
|
65
74
|
dst_path = compare_dir / src_path.relative_to(src_root)
|
|
66
75
|
|
|
67
76
|
if not dst_path.exists():
|
|
@@ -69,6 +78,24 @@ def compare_one_file(
|
|
|
69
78
|
compare_result.src_file_new_count += 1
|
|
70
79
|
return
|
|
71
80
|
|
|
81
|
+
if src_path.is_dir():
|
|
82
|
+
if not src_path.is_symlink():
|
|
83
|
+
raise RuntimeError(f'Internal error - Directory found: {src_path=}')
|
|
84
|
+
|
|
85
|
+
# compare directory symlink targets:
|
|
86
|
+
src_target = src_path.readlink()
|
|
87
|
+
dst_target = dst_path.readlink()
|
|
88
|
+
if src_target != dst_target:
|
|
89
|
+
logger.warning(
|
|
90
|
+
'Source directory symlink %s target %s differs from compare symlink %s target %s',
|
|
91
|
+
src_path,
|
|
92
|
+
src_target,
|
|
93
|
+
dst_path,
|
|
94
|
+
dst_target,
|
|
95
|
+
)
|
|
96
|
+
compare_result.error_count += 1
|
|
97
|
+
return
|
|
98
|
+
|
|
72
99
|
dst_size = dst_path.stat().st_size
|
|
73
100
|
if src_size != dst_size:
|
|
74
101
|
logger.warning(
|
|
@@ -123,6 +150,7 @@ def compare_tree(
|
|
|
123
150
|
*,
|
|
124
151
|
src_root: Path,
|
|
125
152
|
backup_root: Path,
|
|
153
|
+
one_file_system: bool,
|
|
126
154
|
excludes: tuple[str, ...],
|
|
127
155
|
log_manager: LoggingManager,
|
|
128
156
|
) -> CompareResult:
|
|
@@ -155,9 +183,16 @@ def compare_tree(
|
|
|
155
183
|
log_file = compare_main_dir / f'{now_timestamp}-compare.log'
|
|
156
184
|
log_manager.start_file_logging(log_file)
|
|
157
185
|
|
|
186
|
+
src_device_id = verbose_path_stat(src_root).st_dev
|
|
187
|
+
|
|
158
188
|
excludes: set = set(excludes)
|
|
159
189
|
with PrintTimingContextManager('Filesystem scan completed in'):
|
|
160
|
-
src_file_count, src_total_size = humanized_fs_scan(
|
|
190
|
+
src_file_count, src_total_size = humanized_fs_scan(
|
|
191
|
+
path=src_root,
|
|
192
|
+
one_file_system=one_file_system,
|
|
193
|
+
src_device_id=src_device_id,
|
|
194
|
+
excludes=excludes,
|
|
195
|
+
)
|
|
161
196
|
|
|
162
197
|
with DisplayFileTreeProgress(
|
|
163
198
|
description=f'Compare {src_root}...',
|
|
@@ -171,7 +206,12 @@ def compare_tree(
|
|
|
171
206
|
compare_result = CompareResult(last_timestamp=last_timestamp, compare_dir=compare_dir, log_file=log_file)
|
|
172
207
|
|
|
173
208
|
next_update = 0
|
|
174
|
-
for entry in iter_scandir_files(
|
|
209
|
+
for entry in iter_scandir_files(
|
|
210
|
+
path=src_root,
|
|
211
|
+
one_file_system=one_file_system,
|
|
212
|
+
src_device_id=src_device_id,
|
|
213
|
+
excludes=excludes,
|
|
214
|
+
):
|
|
175
215
|
try:
|
|
176
216
|
compare_one_file(
|
|
177
217
|
src_root=src_root,
|
|
@@ -113,7 +113,12 @@ def rebuild(
|
|
|
113
113
|
log_manager.start_file_logging(log_file=backup_root / f'{timestamp}-rebuild.log')
|
|
114
114
|
|
|
115
115
|
with PrintTimingContextManager('Filesystem scan completed in'):
|
|
116
|
-
file_count, total_size = humanized_fs_scan(
|
|
116
|
+
file_count, total_size = humanized_fs_scan(
|
|
117
|
+
path=backup_root,
|
|
118
|
+
one_file_system=False,
|
|
119
|
+
src_device_id=None,
|
|
120
|
+
excludes={'.phlb'},
|
|
121
|
+
)
|
|
117
122
|
|
|
118
123
|
# We should ignore all files in the root backup directory itself
|
|
119
124
|
# e.g.: Our *-summary.txt and *.log files
|
|
@@ -134,7 +139,12 @@ def rebuild(
|
|
|
134
139
|
rebuild_result = RebuildResult()
|
|
135
140
|
|
|
136
141
|
next_update = 0
|
|
137
|
-
for entry in iter_scandir_files(
|
|
142
|
+
for entry in iter_scandir_files(
|
|
143
|
+
path=backup_root,
|
|
144
|
+
one_file_system=False,
|
|
145
|
+
src_device_id=None,
|
|
146
|
+
excludes={'.phlb'},
|
|
147
|
+
):
|
|
138
148
|
try:
|
|
139
149
|
rebuild_one_file(
|
|
140
150
|
backup_root=backup_root,
|