PyHardLinkBackup 1.0.1__tar.gz → 1.1.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.
Files changed (54) hide show
  1. pyhardlinkbackup-1.0.1/README.md → pyhardlinkbackup-1.1.0/PKG-INFO +29 -3
  2. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/__init__.py +1 -1
  3. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/backup.py +1 -1
  4. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/tests/test_backup.py +134 -109
  5. pyhardlinkbackup-1.1.0/PyHardLinkBackup/utilities/tests/base_testcases.py +88 -0
  6. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/tests/test_file_hash_database.py +40 -35
  7. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/tests/test_file_size_database.py +48 -41
  8. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/tests/test_filesystem.py +2 -2
  9. pyhardlinkbackup-1.0.1/PKG-INFO → pyhardlinkbackup-1.1.0/README.md +14 -18
  10. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.editorconfig +0 -0
  11. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.github/workflows/tests.yml +0 -0
  12. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.gitignore +0 -0
  13. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.idea/.gitignore +0 -0
  14. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.pre-commit-config.yaml +0 -0
  15. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.pre-commit-hooks.yaml +0 -0
  16. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.run/Template Python tests.run.xml +0 -0
  17. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.run/Unittests - __all__.run.xml +0 -0
  18. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.run/cli.py --help.run.xml +0 -0
  19. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.run/dev-cli update.run.xml +0 -0
  20. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.run/only DocTests.run.xml +0 -0
  21. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.run/only DocWrite.run.xml +0 -0
  22. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/.venv-app/lib/python3.12/site-packages/cli_base/tests/shell_complete_snapshots/.gitignore +0 -0
  23. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/__main__.py +0 -0
  24. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_app/__init__.py +0 -0
  25. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_app/phlb.py +0 -0
  26. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_dev/__init__.py +0 -0
  27. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_dev/benchmark.py +0 -0
  28. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_dev/code_style.py +0 -0
  29. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_dev/packaging.py +0 -0
  30. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_dev/shell_completion.py +0 -0
  31. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_dev/testing.py +0 -0
  32. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/cli_dev/update_readme_history.py +0 -0
  33. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/constants.py +0 -0
  34. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/tests/__init__.py +0 -0
  35. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/tests/test_doc_write.py +0 -0
  36. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/tests/test_doctests.py +0 -0
  37. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/tests/test_project_setup.py +0 -0
  38. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/tests/test_readme.py +0 -0
  39. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/tests/test_readme_history.py +0 -0
  40. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/__init__.py +0 -0
  41. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/file_hash_database.py +0 -0
  42. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/file_size_database.py +0 -0
  43. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/filesystem.py +0 -0
  44. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/humanize.py +0 -0
  45. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/rich_utils.py +0 -0
  46. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/PyHardLinkBackup/utilities/tests/__init__.py +0 -0
  47. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/cli.py +0 -0
  48. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/dev-cli.py +0 -0
  49. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/dist/.gitignore +0 -0
  50. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/docs/README.md +0 -0
  51. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/docs/about-docs.md +0 -0
  52. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/noxfile.py +0 -0
  53. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/pyproject.toml +0 -0
  54. {pyhardlinkbackup-1.0.1 → pyhardlinkbackup-1.1.0}/uv.lock +0 -0
@@ -1,3 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyHardLinkBackup
3
+ Version: 1.1.0
4
+ Summary: HardLink/Deduplication Backups with Python
5
+ Project-URL: Documentation, https://github.com/jedie/PyHardLinkBackup
6
+ Project-URL: Source, https://github.com/jedie/PyHardLinkBackup
7
+ Author-email: Jens Diemer <PyHardLinkBackup@jensdiemer.de>
8
+ License: GPL-3.0-or-later
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: bx-py-utils
11
+ Requires-Dist: cli-base-utilities
12
+ Requires-Dist: rich
13
+ Requires-Dist: tyro
14
+ Description-Content-Type: text/markdown
15
+
1
16
  # PyHardLinkBackup
2
17
 
3
18
  [![tests](https://github.com/jedie/PyHardLinkBackup/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/jedie/PyHardLinkBackup/actions/workflows/tests.yml)
@@ -185,10 +200,21 @@ usage: ./dev-cli.py [-h] {benchmark-hashes,coverage,install,lint,mypy,nox,pip-au
185
200
 
186
201
  v1 is a complete rewrite of PyHardLinkBackup.
187
202
 
203
+ Overview of main changes:
204
+
205
+ * Remove Django dependency:
206
+ * No SQlite database anymore -> Data for deduplication stored in filesystem only
207
+ * No Django Admin, because we have no database anymore ;)
208
+ * Change hash algorithm from SHA512 to SHA256, because it's faster and still secure enough
209
+ * Don't store `*.sha512` for every file anymore -> We store one `SHA256SUMS` file in every backup directory
210
+
188
211
  ## History
189
212
 
190
213
  [comment]: <> (✂✂✂ auto generated history start ✂✂✂)
191
214
 
215
+ * [v1.1.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.0.1...v1.1.0)
216
+ * 2026-01-14 - Change backup timestamp directory to old schema: '%Y-%m-%d-%H%M%S'
217
+ * 2026-01-14 - Add "Overview of main changes" to README
192
218
  * [v1.0.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.0.0...v1.0.1)
193
219
  * 2026-01-13 - Store SHA256SUMS files in backup directories
194
220
  * [v1.0.0](https://github.com/jedie/PyHardLinkBackup/compare/v0.13.0...v1.0.0)
@@ -219,6 +245,9 @@ v1 is a complete rewrite of PyHardLinkBackup.
219
245
  * 2020-03-17 - dynamic chunk size
220
246
  * 2020-03-17 - ignore *.sha512 by default
221
247
  * 2020-03-17 - Update boot_pyhardlinkbackup.sh
248
+
249
+ <details><summary>Expand older history entries ...</summary>
250
+
222
251
  * [v0.12.3](https://github.com/jedie/PyHardLinkBackup/compare/v0.12.2...v0.12.3)
223
252
  * 2020-03-17 - update README.rst
224
253
  * 2020-03-17 - don't publish if tests fail
@@ -228,9 +257,6 @@ v1 is a complete rewrite of PyHardLinkBackup.
228
257
  * 2020-03-16 - just warn if used directly (needfull for devlopment to call this directly ;)
229
258
  * 2020-03-16 - update requirements
230
259
  * 2020-03-16 - +pytest-randomly
231
-
232
- <details><summary>Expand older history entries ...</summary>
233
-
234
260
  * [v0.12.2](https://github.com/jedie/PyHardLinkBackup/compare/v0.12.1...v0.12.2)
235
261
  * 2020-03-06 - repare v0.12.2 release
236
262
  * 2020-03-06 - enhance log file content
@@ -3,5 +3,5 @@
3
3
  """
4
4
 
5
5
  # See https://packaging.python.org/en/latest/specifications/version-specifiers/
6
- __version__ = '1.0.1'
6
+ __version__ = '1.1.0'
7
7
  __author__ = 'Jens Diemer <PyHardLinkBackup@jensdiemer.de>'
@@ -75,7 +75,7 @@ def backup_tree(*, src_root: Path, backup_root: Path, excludes: set[str]) -> Bac
75
75
  phlb_conf_dir = backup_root / '.phlb'
76
76
  phlb_conf_dir.mkdir(parents=False, exist_ok=True)
77
77
 
78
- backup_dir = backup_root / src_root.name / datetime.now().strftime('%Y%m%d_%H%M%S')
78
+ backup_dir = backup_root / src_root.name / datetime.now().strftime('%Y-%m-%d-%H%M%S')
79
79
  logger.info('Backup %s to %s', src_root, backup_dir)
80
80
  backup_dir.mkdir(parents=True, exist_ok=False)
81
81
 
@@ -6,13 +6,13 @@ import textwrap
6
6
  import zlib
7
7
  from collections.abc import Iterable
8
8
  from pathlib import Path
9
- from unittest import TestCase
10
9
  from unittest.mock import patch
11
10
 
12
11
  from bx_py_utils.path import assert_is_file
13
12
  from bx_py_utils.test_utils.assertion import assert_text_equal
14
13
  from bx_py_utils.test_utils.datetime import parse_dt
15
14
  from bx_py_utils.test_utils.log_utils import NoLogs
15
+ from bx_py_utils.test_utils.redirect import RedirectOut
16
16
  from freezegun import freeze_time
17
17
  from tabulate import tabulate
18
18
 
@@ -20,6 +20,7 @@ from PyHardLinkBackup.backup import BackupResult, backup_tree
20
20
  from PyHardLinkBackup.constants import CHUNK_SIZE
21
21
  from PyHardLinkBackup.utilities.file_size_database import FileSizeDatabase
22
22
  from PyHardLinkBackup.utilities.filesystem import iter_scandir_files
23
+ from PyHardLinkBackup.utilities.tests.base_testcases import BaseTestCase
23
24
  from PyHardLinkBackup.utilities.tests.test_file_hash_database import assert_hash_db_info
24
25
 
25
26
 
@@ -49,8 +50,9 @@ def set_file_times(path: Path, dt: datetime.datetime):
49
50
  if dt.tzinfo is not None:
50
51
  dt = dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
51
52
  fixed_time = dt.timestamp()
52
- for entry in iter_scandir_files(path, excludes=set()):
53
- os.utime(entry.path, (fixed_time, fixed_time))
53
+ with NoLogs(logger_name=''):
54
+ for entry in iter_scandir_files(path, excludes=set()):
55
+ os.utime(entry.path, (fixed_time, fixed_time))
54
56
 
55
57
 
56
58
  def _fs_tree_overview(root: Path) -> str:
@@ -99,8 +101,7 @@ def _fs_tree_overview(root: Path) -> str:
99
101
 
100
102
  def assert_fs_tree_overview(root: Path, expected_overview: str):
101
103
  expected_overview = textwrap.dedent(expected_overview).strip()
102
- with NoLogs(logger_name='PyHardLinkBackup'):
103
- actual_overview = _fs_tree_overview(root)
104
+ actual_overview = _fs_tree_overview(root)
104
105
  assert_text_equal(
105
106
  actual_overview,
106
107
  expected_overview,
@@ -108,7 +109,7 @@ def assert_fs_tree_overview(root: Path, expected_overview: str):
108
109
  )
109
110
 
110
111
 
111
- class BackupTreeTestCase(TestCase):
112
+ class BackupTreeTestCase(BaseTestCase):
112
113
  def test_happy_path(self):
113
114
  with tempfile.TemporaryDirectory() as temp_dir:
114
115
  temp_path = Path(temp_dir)
@@ -156,16 +157,19 @@ class BackupTreeTestCase(TestCase):
156
157
  self.assertLogs(level=logging.INFO),
157
158
  patch('PyHardLinkBackup.backup.iter_scandir_files', SortedIterScandirFiles),
158
159
  freeze_time('2026-01-01T12:34:56Z', auto_tick_seconds=0),
160
+ RedirectOut() as redirected_out,
159
161
  ):
160
162
  result = backup_tree(
161
163
  src_root=src_root,
162
164
  backup_root=backup_root,
163
165
  excludes={'.cache'},
164
166
  )
167
+ self.assertEqual(redirected_out.stderr, '')
168
+ self.assertIn('Backup complete', redirected_out.stdout)
165
169
  backup_dir = result.backup_dir
166
170
  self.assertEqual(
167
171
  str(Path(backup_dir).relative_to(temp_path)),
168
- 'backup/source/20260101_123456',
172
+ 'backup/source/2026-01-01-123456',
169
173
  )
170
174
  self.assertEqual(
171
175
  result,
@@ -184,47 +188,50 @@ class BackupTreeTestCase(TestCase):
184
188
  )
185
189
 
186
190
  # The sources:
187
- assert_fs_tree_overview(
188
- root=src_root,
189
- expected_overview="""
190
- path birthtime type nlink size CRC32
191
- .cache/tempfile.tmp 12:00:00 file 1 38 41d7a2c9
192
- file2.txt 12:00:00 hardlink 2 14 8a11514a
193
- hardlink2file1 12:00:00 hardlink 2 14 8a11514a
194
- large_file.bin 12:00:00 file 1 67108865 9671eaac
195
- min_sized_file1.bin 12:00:00 file 1 1000 f0d93de4
196
- min_sized_file2.bin 12:00:00 file 1 1000 f0d93de4
197
- subdir/file.txt 12:00:00 file 1 22 c0167e63
198
- symlink2file1 12:00:00 symlink 2 14 8a11514a
199
- """,
200
- )
191
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
192
+ assert_fs_tree_overview(
193
+ root=src_root,
194
+ expected_overview="""
195
+ path birthtime type nlink size CRC32
196
+ .cache/tempfile.tmp 12:00:00 file 1 38 41d7a2c9
197
+ file2.txt 12:00:00 hardlink 2 14 8a11514a
198
+ hardlink2file1 12:00:00 hardlink 2 14 8a11514a
199
+ large_file.bin 12:00:00 file 1 67108865 9671eaac
200
+ min_sized_file1.bin 12:00:00 file 1 1000 f0d93de4
201
+ min_sized_file2.bin 12:00:00 file 1 1000 f0d93de4
202
+ subdir/file.txt 12:00:00 file 1 22 c0167e63
203
+ symlink2file1 12:00:00 symlink 2 14 8a11514a
204
+ """,
205
+ )
201
206
  # The backup:
202
207
  # * /.cache/ -> excluded
203
208
  # * min_sized_file1.bin and min_sized_file2.bin -> hardlinked
204
- assert_fs_tree_overview(
205
- root=backup_dir,
206
- expected_overview="""
207
- path birthtime type nlink size CRC32
208
- SHA256SUMS - file 1 410 45c07cf7
209
- file2.txt 12:00:00 file 1 14 8a11514a
210
- hardlink2file1 12:00:00 file 1 14 8a11514a
211
- large_file.bin 12:00:00 file 1 67108865 9671eaac
212
- min_sized_file1.bin 12:00:00 hardlink 2 1000 f0d93de4
213
- min_sized_file2.bin 12:00:00 hardlink 2 1000 f0d93de4
214
- subdir/SHA256SUMS - file 1 75 1af5ecc7
215
- subdir/file.txt 12:00:00 file 1 22 c0167e63
216
- symlink2file1 12:00:00 symlink 2 14 8a11514a
217
- """,
218
- )
209
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
210
+ assert_fs_tree_overview(
211
+ root=backup_dir,
212
+ expected_overview="""
213
+ path birthtime type nlink size CRC32
214
+ SHA256SUMS - file 1 410 45c07cf7
215
+ file2.txt 12:00:00 file 1 14 8a11514a
216
+ hardlink2file1 12:00:00 file 1 14 8a11514a
217
+ large_file.bin 12:00:00 file 1 67108865 9671eaac
218
+ min_sized_file1.bin 12:00:00 hardlink 2 1000 f0d93de4
219
+ min_sized_file2.bin 12:00:00 hardlink 2 1000 f0d93de4
220
+ subdir/SHA256SUMS - file 1 75 1af5ecc7
221
+ subdir/file.txt 12:00:00 file 1 22 c0167e63
222
+ symlink2file1 12:00:00 symlink 2 14 8a11514a
223
+ """,
224
+ )
219
225
 
220
226
  # Let's check our FileHashDatabase:
221
- assert_hash_db_info(
222
- backup_root=backup_root,
223
- expected="""
224
- bb/c4/bbc4de2ca238d1… -> source/20260101_123456/min_sized_file1.bin
225
- e6/37/e6374ac11d9049… -> source/20260101_123456/large_file.bin
226
- """,
227
- )
227
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
228
+ assert_hash_db_info(
229
+ backup_root=backup_root,
230
+ expected="""
231
+ bb/c4/bbc4de2ca238d1… -> source/2026-01-01-123456/min_sized_file1.bin
232
+ e6/37/e6374ac11d9049… -> source/2026-01-01-123456/large_file.bin
233
+ """,
234
+ )
228
235
 
229
236
  #######################################################################################
230
237
  # Just backup again:
@@ -233,16 +240,19 @@ class BackupTreeTestCase(TestCase):
233
240
  self.assertLogs(level=logging.INFO),
234
241
  patch('PyHardLinkBackup.backup.iter_scandir_files', SortedIterScandirFiles),
235
242
  freeze_time('2026-01-02T12:34:56Z', auto_tick_seconds=0),
243
+ RedirectOut() as redirected_out,
236
244
  ):
237
245
  result = backup_tree(
238
246
  src_root=src_root,
239
247
  backup_root=backup_root,
240
248
  excludes={'.cache'},
241
249
  )
250
+ self.assertEqual(redirected_out.stderr, '')
251
+ self.assertIn('Backup complete', redirected_out.stdout)
242
252
  backup_dir = result.backup_dir
243
253
  self.assertEqual(
244
254
  str(Path(backup_dir).relative_to(temp_path)),
245
- 'backup/source/20260102_123456',
255
+ 'backup/source/2026-01-02-123456',
246
256
  )
247
257
  self.assertEqual(
248
258
  result,
@@ -262,30 +272,32 @@ class BackupTreeTestCase(TestCase):
262
272
  # The second backup:
263
273
  # * /.cache/ -> excluded
264
274
  # * min_sized_file1.bin and min_sized_file2.bin -> hardlinked
265
- assert_fs_tree_overview(
266
- root=backup_dir,
267
- expected_overview="""
268
- path birthtime type nlink size CRC32
269
- SHA256SUMS - file 1 410 45c07cf7
270
- file2.txt 12:00:00 file 1 14 8a11514a
271
- hardlink2file1 12:00:00 file 1 14 8a11514a
272
- large_file.bin 12:00:00 hardlink 2 67108865 9671eaac
273
- min_sized_file1.bin 12:00:00 hardlink 4 1000 f0d93de4
274
- min_sized_file2.bin 12:00:00 hardlink 4 1000 f0d93de4
275
- subdir/SHA256SUMS - file 1 75 1af5ecc7
276
- subdir/file.txt 12:00:00 file 1 22 c0167e63
277
- symlink2file1 12:00:00 symlink 2 14 8a11514a
278
- """,
279
- )
275
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
276
+ assert_fs_tree_overview(
277
+ root=backup_dir,
278
+ expected_overview="""
279
+ path birthtime type nlink size CRC32
280
+ SHA256SUMS - file 1 410 45c07cf7
281
+ file2.txt 12:00:00 file 1 14 8a11514a
282
+ hardlink2file1 12:00:00 file 1 14 8a11514a
283
+ large_file.bin 12:00:00 hardlink 2 67108865 9671eaac
284
+ min_sized_file1.bin 12:00:00 hardlink 4 1000 f0d93de4
285
+ min_sized_file2.bin 12:00:00 hardlink 4 1000 f0d93de4
286
+ subdir/SHA256SUMS - file 1 75 1af5ecc7
287
+ subdir/file.txt 12:00:00 file 1 22 c0167e63
288
+ symlink2file1 12:00:00 symlink 2 14 8a11514a
289
+ """,
290
+ )
280
291
 
281
292
  # The FileHashDatabase remains the same:
282
- assert_hash_db_info(
283
- backup_root=backup_root,
284
- expected="""
285
- bb/c4/bbc4de2ca238d1… -> source/20260101_123456/min_sized_file1.bin
286
- e6/37/e6374ac11d9049… -> source/20260101_123456/large_file.bin
287
- """,
288
- )
293
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
294
+ assert_hash_db_info(
295
+ backup_root=backup_root,
296
+ expected="""
297
+ bb/c4/bbc4de2ca238d1… -> source/2026-01-01-123456/min_sized_file1.bin
298
+ e6/37/e6374ac11d9049… -> source/2026-01-01-123456/large_file.bin
299
+ """,
300
+ )
289
301
 
290
302
  #######################################################################################
291
303
  # Don't create broken hardlinks!
@@ -296,7 +308,7 @@ class BackupTreeTestCase(TestCase):
296
308
  """
297
309
 
298
310
  # Let's remove one of the files used for hardlinking from the first backup:
299
- min_sized_file1_bak_path = backup_root / 'source/20260101_123456/min_sized_file1.bin'
311
+ min_sized_file1_bak_path = backup_root / 'source/2026-01-01-123456/min_sized_file1.bin'
300
312
  assert_is_file(min_sized_file1_bak_path)
301
313
  min_sized_file1_bak_path.unlink()
302
314
 
@@ -305,31 +317,35 @@ class BackupTreeTestCase(TestCase):
305
317
  self.assertLogs(level=logging.INFO),
306
318
  patch('PyHardLinkBackup.backup.iter_scandir_files', SortedIterScandirFiles),
307
319
  freeze_time('2026-01-03T12:34:56Z', auto_tick_seconds=0),
320
+ RedirectOut() as redirected_out,
308
321
  ):
309
322
  result = backup_tree(
310
323
  src_root=src_root,
311
324
  backup_root=backup_root,
312
325
  excludes={'.cache'},
313
326
  )
327
+ self.assertEqual(redirected_out.stderr, '')
328
+ self.assertIn('Backup complete', redirected_out.stdout)
314
329
  backup_dir = result.backup_dir
315
330
 
316
331
  # Note: min_sized_file1.bin and min_sized_file2.bin are hardlinked,
317
332
  # but not with the first backup anymore! So it's only nlink=2 now!
318
- assert_fs_tree_overview(
319
- root=backup_dir,
320
- expected_overview="""
321
- path birthtime type nlink size CRC32
322
- SHA256SUMS - file 1 410 45c07cf7
323
- file2.txt 12:00:00 file 1 14 8a11514a
324
- hardlink2file1 12:00:00 file 1 14 8a11514a
325
- large_file.bin 12:00:00 hardlink 3 67108865 9671eaac
326
- min_sized_file1.bin 12:00:00 hardlink 2 1000 f0d93de4
327
- min_sized_file2.bin 12:00:00 hardlink 2 1000 f0d93de4
328
- subdir/SHA256SUMS - file 1 75 1af5ecc7
329
- subdir/file.txt 12:00:00 file 1 22 c0167e63
330
- symlink2file1 12:00:00 symlink 2 14 8a11514a
331
- """,
332
- )
333
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
334
+ assert_fs_tree_overview(
335
+ root=backup_dir,
336
+ expected_overview="""
337
+ path birthtime type nlink size CRC32
338
+ SHA256SUMS - file 1 410 45c07cf7
339
+ file2.txt 12:00:00 file 1 14 8a11514a
340
+ hardlink2file1 12:00:00 file 1 14 8a11514a
341
+ large_file.bin 12:00:00 hardlink 3 67108865 9671eaac
342
+ min_sized_file1.bin 12:00:00 hardlink 2 1000 f0d93de4
343
+ min_sized_file2.bin 12:00:00 hardlink 2 1000 f0d93de4
344
+ subdir/SHA256SUMS - file 1 75 1af5ecc7
345
+ subdir/file.txt 12:00:00 file 1 22 c0167e63
346
+ symlink2file1 12:00:00 symlink 2 14 8a11514a
347
+ """,
348
+ )
333
349
 
334
350
  self.assertEqual(
335
351
  result,
@@ -348,14 +364,15 @@ class BackupTreeTestCase(TestCase):
348
364
  )
349
365
 
350
366
  # Note: min_sized_file1.bin is now from the 2026-01-03 backup!
351
- self.assertEqual(backup_dir.name, '20260103_123456') # Latest backup dir name
352
- assert_hash_db_info(
353
- backup_root=backup_root,
354
- expected="""
355
- bb/c4/bbc4de2ca238d1… -> source/20260103_123456/min_sized_file1.bin
356
- e6/37/e6374ac11d9049… -> source/20260101_123456/large_file.bin
357
- """,
358
- )
367
+ self.assertEqual(backup_dir.name, '2026-01-03-123456') # Latest backup dir name
368
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
369
+ assert_hash_db_info(
370
+ backup_root=backup_root,
371
+ expected="""
372
+ bb/c4/bbc4de2ca238d1… -> source/2026-01-03-123456/min_sized_file1.bin
373
+ e6/37/e6374ac11d9049… -> source/2026-01-01-123456/large_file.bin
374
+ """,
375
+ )
359
376
 
360
377
  def test_symlink(self):
361
378
  with tempfile.TemporaryDirectory() as temp_dir:
@@ -392,30 +409,37 @@ class BackupTreeTestCase(TestCase):
392
409
  #######################################################################################
393
410
  # Create first backup:
394
411
 
395
- with self.assertLogs(level=logging.INFO), freeze_time('2026-01-01T12:34:56Z', auto_tick_seconds=0):
412
+ with (
413
+ self.assertLogs(level=logging.INFO),
414
+ freeze_time('2026-01-01T12:34:56Z', auto_tick_seconds=0),
415
+ RedirectOut() as redirected_out,
416
+ ):
396
417
  result = backup_tree(src_root=src_root, backup_root=backup_root, excludes=set())
418
+ self.assertEqual(redirected_out.stderr, '')
419
+ self.assertIn('Backup complete', redirected_out.stdout)
397
420
  backup_dir1 = result.backup_dir
398
421
  self.assertEqual(
399
422
  str(Path(backup_dir1).relative_to(temp_path)),
400
- 'bak/src/20260101_123456',
423
+ 'bak/src/2026-01-01-123456',
401
424
  )
402
425
 
403
- assert_fs_tree_overview(
404
- root=temp_path, # The complete overview os source + backup and outside file
405
- expected_overview="""
406
- path birthtime type nlink size CRC32
407
- bak/src/20260101_123456/SHA256SUMS - file 1 82 c03fd60e
408
- bak/src/20260101_123456/broken_symlink - symlink - - -
409
- bak/src/20260101_123456/source_file.txt 12:00:00 file 1 31 9309a10c
410
- bak/src/20260101_123456/symlink2outside 12:00:00 symlink 1 36 24b5bf4c
411
- bak/src/20260101_123456/symlink2source 12:00:00 symlink 1 31 9309a10c
412
- outside_file.txt 12:00:00 file 1 36 24b5bf4c
413
- src/broken_symlink - symlink - - -
414
- src/source_file.txt 12:00:00 file 1 31 9309a10c
415
- src/symlink2outside 12:00:00 symlink 1 36 24b5bf4c
416
- src/symlink2source 12:00:00 symlink 1 31 9309a10c
417
- """,
418
- )
426
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
427
+ assert_fs_tree_overview(
428
+ root=temp_path, # The complete overview os source + backup and outside file
429
+ expected_overview="""
430
+ path birthtime type nlink size CRC32
431
+ bak/src/2026-01-01-123456/SHA256SUMS - file 1 82 c03fd60e
432
+ bak/src/2026-01-01-123456/broken_symlink - symlink - - -
433
+ bak/src/2026-01-01-123456/source_file.txt 12:00:00 file 1 31 9309a10c
434
+ bak/src/2026-01-01-123456/symlink2outside 12:00:00 symlink 1 36 24b5bf4c
435
+ bak/src/2026-01-01-123456/symlink2source 12:00:00 symlink 1 31 9309a10c
436
+ outside_file.txt 12:00:00 file 1 36 24b5bf4c
437
+ src/broken_symlink - symlink - - -
438
+ src/source_file.txt 12:00:00 file 1 31 9309a10c
439
+ src/symlink2outside 12:00:00 symlink 1 36 24b5bf4c
440
+ src/symlink2source 12:00:00 symlink 1 31 9309a10c
441
+ """,
442
+ )
419
443
 
420
444
  self.assertEqual(
421
445
  result,
@@ -448,4 +472,5 @@ class BackupTreeTestCase(TestCase):
448
472
 
449
473
  """DocWrite: README.md ## backup implementation - Symlinks
450
474
  Symlinks are not stored in our FileHashDatabase, because they are not considered for hardlinking."""
451
- assert_hash_db_info(backup_root=backup_root, expected='')
475
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
476
+ assert_hash_db_info(backup_root=backup_root, expected='')
@@ -0,0 +1,88 @@
1
+ import contextlib
2
+ import logging
3
+ import unittest
4
+
5
+ from bx_py_utils.test_utils.context_managers import MassContextManager
6
+
7
+
8
+ class RaiseLogOutput(logging.Handler):
9
+ LOGGING_FORMAT = '%(levelname)s:%(name)s:%(message)s'
10
+
11
+ def __init__(self):
12
+ super().__init__()
13
+ self.setFormatter(logging.Formatter(self.LOGGING_FORMAT))
14
+
15
+ def emit(self, record):
16
+ raise AssertionError(
17
+ f'Uncaptured log message during the test:\n'
18
+ '------------------------------------------------------------------------------------\n'
19
+ f'{self.format(record)}\n'
20
+ '------------------------------------------------------------------------------------\n'
21
+ '(Hint: use self.assertLogs() context manager)'
22
+ )
23
+
24
+
25
+ class LoggingMustBeCapturedTestCaseMixin:
26
+ def setUp(self):
27
+ super().setUp()
28
+ self.logger = logging.getLogger()
29
+
30
+ self.old_handlers = self.logger.handlers[:]
31
+ self.old_level = self.logger.level
32
+ self.old_propagate = self.logger.propagate
33
+
34
+ self._log_buffer_handler = RaiseLogOutput()
35
+ self.logger.addHandler(self._log_buffer_handler)
36
+ self.logger.setLevel(logging.DEBUG)
37
+ self.logger.propagate = False
38
+
39
+ def tearDown(self):
40
+ self.logger.handlers = self.old_handlers
41
+ self.logger.propagate = self.old_propagate
42
+ self.logger.setLevel(self.old_level)
43
+ super().tearDown()
44
+
45
+
46
+ class RaiseOutput(MassContextManager):
47
+ def __init__(self, test_case: unittest.TestCase):
48
+ self.test_case = test_case
49
+ self.mocks = (
50
+ contextlib.redirect_stdout(self),
51
+ contextlib.redirect_stderr(self),
52
+ )
53
+
54
+ def write(self, txt):
55
+ self.test_case.fail(
56
+ f'Output was written during the test:\n'
57
+ '------------------------------------------------------------------------------------\n'
58
+ f'{txt!r}\n'
59
+ '------------------------------------------------------------------------------------\n'
60
+ f'(Hint: use RedirectOut context manager)'
61
+ )
62
+
63
+ def flush(self):
64
+ pass
65
+
66
+ def getvalue(self):
67
+ return ''
68
+
69
+
70
+ class OutputMustCapturedTestCaseMixin:
71
+ def setUp(self):
72
+ super().setUp()
73
+ self._cm = RaiseOutput(self)
74
+ self._cm_result = self._cm.__enter__()
75
+
76
+ def tearDown(self):
77
+ self._cm.__exit__(None, None, None)
78
+ super().tearDown()
79
+
80
+
81
+ class BaseTestCase(
82
+ OutputMustCapturedTestCaseMixin,
83
+ LoggingMustBeCapturedTestCaseMixin,
84
+ unittest.TestCase,
85
+ ):
86
+ """
87
+ A base TestCase that ensures that all logging and output is captured during tests.
88
+ """
@@ -2,7 +2,6 @@ import logging
2
2
  import tempfile
3
3
  import textwrap
4
4
  from pathlib import Path
5
- from unittest import TestCase
6
5
 
7
6
  from bx_py_utils.path import assert_is_dir
8
7
  from bx_py_utils.test_utils.assertion import assert_text_equal
@@ -10,6 +9,7 @@ from bx_py_utils.test_utils.log_utils import NoLogs
10
9
 
11
10
  from PyHardLinkBackup.utilities.file_hash_database import FileHashDatabase, HashAlreadyExistsError
12
11
  from PyHardLinkBackup.utilities.filesystem import iter_scandir_files
12
+ from PyHardLinkBackup.utilities.tests.base_testcases import BaseTestCase
13
13
 
14
14
 
15
15
  class TemporaryFileHashDatabase(tempfile.TemporaryDirectory):
@@ -25,6 +25,7 @@ class TemporaryFileHashDatabase(tempfile.TemporaryDirectory):
25
25
 
26
26
 
27
27
  def get_hash_db_filenames(hash_db: FileHashDatabase) -> list[str]:
28
+ # with NoLogs('PyHardLinkBackup.utilities.filesystem'):
28
29
  return sorted(
29
30
  str(Path(entry.path).relative_to(hash_db.base_path))
30
31
  for entry in iter_scandir_files(hash_db.base_path, excludes=set())
@@ -35,19 +36,19 @@ def get_hash_db_info(backup_root: Path) -> str:
35
36
  db_base_path = backup_root / '.phlb' / 'hash-lookup'
36
37
  assert_is_dir(db_base_path)
37
38
 
38
- lines = []
39
- for entry in iter_scandir_files(db_base_path, excludes=set()):
40
- hash_path = Path(entry.path)
41
- rel_path = hash_path.relative_to(db_base_path)
42
- rel_file_path = hash_path.read_text()
43
- lines.append(f'{str(rel_path)[:20]}… -> {rel_file_path}')
39
+ with NoLogs(logger_name='XY'):
40
+ lines = []
41
+ for entry in iter_scandir_files(db_base_path, excludes=set()):
42
+ hash_path = Path(entry.path)
43
+ rel_path = hash_path.relative_to(db_base_path)
44
+ rel_file_path = hash_path.read_text()
45
+ lines.append(f'{str(rel_path)[:20]}… -> {rel_file_path}')
44
46
  return '\n'.join(sorted(lines))
45
47
 
46
48
 
47
49
  def assert_hash_db_info(backup_root: Path, expected: str):
48
50
  expected = textwrap.dedent(expected).strip()
49
- with NoLogs(logger_name='PyHardLinkBackup'):
50
- actual = get_hash_db_info(backup_root)
51
+ actual = get_hash_db_info(backup_root)
51
52
  assert_text_equal(
52
53
  actual,
53
54
  expected,
@@ -55,7 +56,7 @@ def assert_hash_db_info(backup_root: Path, expected: str):
55
56
  )
56
57
 
57
58
 
58
- class FileHashDatabaseTestCase(TestCase):
59
+ class FileHashDatabaseTestCase(BaseTestCase):
59
60
  def test_happy_path(self):
60
61
  with TemporaryFileHashDatabase() as hash_db:
61
62
  self.assertIsInstance(hash_db, FileHashDatabase)
@@ -73,10 +74,11 @@ class FileHashDatabaseTestCase(TestCase):
73
74
  self.assertIs(hash_db.get('12345678abcdef'), None)
74
75
  hash_db['12345678abcdef'] = file_a_path
75
76
  self.assertEqual(hash_db.get('12345678abcdef'), file_a_path)
76
- self.assertEqual(
77
- get_hash_db_filenames(hash_db),
78
- ['12/34/12345678abcdef'],
79
- )
77
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
78
+ self.assertEqual(
79
+ get_hash_db_filenames(hash_db),
80
+ ['12/34/12345678abcdef'],
81
+ )
80
82
 
81
83
  ########################################################################################
82
84
  # Another instance using the same directory:
@@ -94,21 +96,23 @@ class FileHashDatabaseTestCase(TestCase):
94
96
 
95
97
  another_hash_db['12abcd345678abcdef'] = file_b_path
96
98
  self.assertEqual(another_hash_db.get('12abcd345678abcdef'), file_b_path)
97
- self.assertEqual(
98
- get_hash_db_filenames(another_hash_db),
99
- [
100
- '12/34/12345678abcdef',
101
- '12/ab/12abcd345678abcdef',
102
- ],
103
- )
104
-
105
- assert_hash_db_info(
106
- backup_root=hash_db.backup_root,
107
- expected="""
108
- 12/34/12345678abcdef… -> rel/path/to/file-A
109
- 12/ab/12abcd345678ab… -> rel/path/to/file-B
110
- """,
111
- )
99
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
100
+ self.assertEqual(
101
+ get_hash_db_filenames(another_hash_db),
102
+ [
103
+ '12/34/12345678abcdef',
104
+ '12/ab/12abcd345678abcdef',
105
+ ],
106
+ )
107
+
108
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
109
+ assert_hash_db_info(
110
+ backup_root=hash_db.backup_root,
111
+ expected="""
112
+ 12/34/12345678abcdef… -> rel/path/to/file-A
113
+ 12/ab/12abcd345678ab… -> rel/path/to/file-B
114
+ """,
115
+ )
112
116
 
113
117
  ########################################################################################
114
118
  # Deny "overwrite" of existing hash:
@@ -128,9 +132,10 @@ class FileHashDatabaseTestCase(TestCase):
128
132
  with self.assertLogs(level=logging.WARNING) as logs:
129
133
  self.assertIs(hash_db.get('12345678abcdef'), None)
130
134
  self.assertIn('Hash database entry found, but file does not exist', ''.join(logs.output))
131
- assert_hash_db_info(
132
- backup_root=hash_db.backup_root,
133
- expected="""
134
- 12/ab/12abcd345678ab… -> rel/path/to/file-B
135
- """,
136
- )
135
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
136
+ assert_hash_db_info(
137
+ backup_root=hash_db.backup_root,
138
+ expected="""
139
+ 12/ab/12abcd345678ab… -> rel/path/to/file-B
140
+ """,
141
+ )
@@ -1,10 +1,13 @@
1
+ import logging
1
2
  import tempfile
2
3
  from collections.abc import Iterable
3
4
  from pathlib import Path
4
- from unittest import TestCase
5
+
6
+ from bx_py_utils.test_utils.log_utils import NoLogs
5
7
 
6
8
  from PyHardLinkBackup.utilities.file_size_database import FileSizeDatabase
7
9
  from PyHardLinkBackup.utilities.filesystem import iter_scandir_files
10
+ from PyHardLinkBackup.utilities.tests.base_testcases import BaseTestCase
8
11
 
9
12
 
10
13
  class TemporaryFileSizeDatabase(tempfile.TemporaryDirectory):
@@ -27,10 +30,11 @@ def get_size_db_filenames(size_db: FileSizeDatabase) -> Iterable[str]:
27
30
 
28
31
 
29
32
  def get_sizes(size_db: FileSizeDatabase) -> Iterable[int]:
30
- return sorted(int(entry.name) for entry in iter_scandir_files(size_db.base_path, excludes=set()))
33
+ with NoLogs('PyHardLinkBackup.utilities.filesystem'):
34
+ return sorted(int(entry.name) for entry in iter_scandir_files(size_db.base_path, excludes=set()))
31
35
 
32
36
 
33
- class FileSizeDatabaseTestCase(TestCase):
37
+ class FileSizeDatabaseTestCase(BaseTestCase):
34
38
  def test_happy_path(self):
35
39
  with TemporaryFileSizeDatabase() as size_db:
36
40
  self.assertIsInstance(size_db, FileSizeDatabase)
@@ -52,27 +56,29 @@ class FileSizeDatabaseTestCase(TestCase):
52
56
  self.assertIn(1234, size_db)
53
57
  self.assertIn(567890, size_db)
54
58
 
55
- self.assertEqual(get_sizes(size_db), [1234, 567890])
56
- self.assertEqual(
57
- get_size_db_filenames(size_db),
58
- [
59
- '12/34/1234',
60
- '56/78/567890',
61
- ],
62
- )
59
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
60
+ self.assertEqual(get_sizes(size_db), [1234, 567890])
61
+ self.assertEqual(
62
+ get_size_db_filenames(size_db),
63
+ [
64
+ '12/34/1234',
65
+ '56/78/567890',
66
+ ],
67
+ )
63
68
 
64
69
  ########################################################################################
65
70
  # Another instance using the same directory:
66
71
 
67
72
  another_size_db = FileSizeDatabase(phlb_conf_dir=size_db.base_path.parent)
68
- self.assertEqual(get_sizes(another_size_db), [1234, 567890])
69
- self.assertEqual(
70
- get_size_db_filenames(another_size_db),
71
- [
72
- '12/34/1234',
73
- '56/78/567890',
74
- ],
75
- )
73
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
74
+ self.assertEqual(get_sizes(another_size_db), [1234, 567890])
75
+ self.assertEqual(
76
+ get_size_db_filenames(another_size_db),
77
+ [
78
+ '12/34/1234',
79
+ '56/78/567890',
80
+ ],
81
+ )
76
82
 
77
83
  ########################################################################################
78
84
  # "Share" directories:
@@ -107,25 +113,26 @@ class FileSizeDatabaseTestCase(TestCase):
107
113
  ########################################################################################
108
114
  # Check final state:
109
115
 
110
- self.assertEqual(
111
- get_size_db_filenames(size_db),
112
- [
113
- '12/34/1234',
114
- '12/34/123400001111',
115
- '12/34/123400002222',
116
- '12/88/128800003333',
117
- '12/99/129900004444',
118
- '56/78/567890',
119
- ],
120
- )
121
- self.assertEqual(
122
- get_sizes(size_db),
123
- [
124
- 1234,
125
- 567890,
126
- 123400001111,
127
- 123400002222,
128
- 128800003333,
129
- 129900004444,
130
- ],
131
- )
116
+ with self.assertLogs('PyHardLinkBackup', level=logging.DEBUG):
117
+ self.assertEqual(
118
+ get_size_db_filenames(size_db),
119
+ [
120
+ '12/34/1234',
121
+ '12/34/123400001111',
122
+ '12/34/123400002222',
123
+ '12/88/128800003333',
124
+ '12/99/129900004444',
125
+ '56/78/567890',
126
+ ],
127
+ )
128
+ self.assertEqual(
129
+ get_sizes(size_db),
130
+ [
131
+ 1234,
132
+ 567890,
133
+ 123400001111,
134
+ 123400002222,
135
+ 128800003333,
136
+ 129900004444,
137
+ ],
138
+ )
@@ -1,14 +1,14 @@
1
1
  import hashlib
2
2
  import os
3
3
  import tempfile
4
- import unittest
5
4
  from pathlib import Path
6
5
 
7
6
  from PyHardLinkBackup.constants import HASH_ALGO
8
7
  from PyHardLinkBackup.utilities.filesystem import copy_and_hash, hash_file, iter_scandir_files, read_and_hash_file
8
+ from PyHardLinkBackup.utilities.tests.base_testcases import BaseTestCase
9
9
 
10
10
 
11
- class TestHashFile(unittest.TestCase):
11
+ class TestHashFile(BaseTestCase):
12
12
  def test_hash_file(self):
13
13
  self.assertEqual(
14
14
  hashlib.new(HASH_ALGO, b'test content').hexdigest(),
@@ -1,18 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: PyHardLinkBackup
3
- Version: 1.0.1
4
- Summary: HardLink/Deduplication Backups with Python
5
- Project-URL: Documentation, https://github.com/jedie/PyHardLinkBackup
6
- Project-URL: Source, https://github.com/jedie/PyHardLinkBackup
7
- Author-email: Jens Diemer <PyHardLinkBackup@jensdiemer.de>
8
- License: GPL-3.0-or-later
9
- Requires-Python: >=3.12
10
- Requires-Dist: bx-py-utils
11
- Requires-Dist: cli-base-utilities
12
- Requires-Dist: rich
13
- Requires-Dist: tyro
14
- Description-Content-Type: text/markdown
15
-
16
1
  # PyHardLinkBackup
17
2
 
18
3
  [![tests](https://github.com/jedie/PyHardLinkBackup/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/jedie/PyHardLinkBackup/actions/workflows/tests.yml)
@@ -200,10 +185,21 @@ usage: ./dev-cli.py [-h] {benchmark-hashes,coverage,install,lint,mypy,nox,pip-au
200
185
 
201
186
  v1 is a complete rewrite of PyHardLinkBackup.
202
187
 
188
+ Overview of main changes:
189
+
190
+ * Remove Django dependency:
191
+ * No SQlite database anymore -> Data for deduplication stored in filesystem only
192
+ * No Django Admin, because we have no database anymore ;)
193
+ * Change hash algorithm from SHA512 to SHA256, because it's faster and still secure enough
194
+ * Don't store `*.sha512` for every file anymore -> We store one `SHA256SUMS` file in every backup directory
195
+
203
196
  ## History
204
197
 
205
198
  [comment]: <> (✂✂✂ auto generated history start ✂✂✂)
206
199
 
200
+ * [v1.1.0](https://github.com/jedie/PyHardLinkBackup/compare/v1.0.1...v1.1.0)
201
+ * 2026-01-14 - Change backup timestamp directory to old schema: '%Y-%m-%d-%H%M%S'
202
+ * 2026-01-14 - Add "Overview of main changes" to README
207
203
  * [v1.0.1](https://github.com/jedie/PyHardLinkBackup/compare/v1.0.0...v1.0.1)
208
204
  * 2026-01-13 - Store SHA256SUMS files in backup directories
209
205
  * [v1.0.0](https://github.com/jedie/PyHardLinkBackup/compare/v0.13.0...v1.0.0)
@@ -234,6 +230,9 @@ v1 is a complete rewrite of PyHardLinkBackup.
234
230
  * 2020-03-17 - dynamic chunk size
235
231
  * 2020-03-17 - ignore *.sha512 by default
236
232
  * 2020-03-17 - Update boot_pyhardlinkbackup.sh
233
+
234
+ <details><summary>Expand older history entries ...</summary>
235
+
237
236
  * [v0.12.3](https://github.com/jedie/PyHardLinkBackup/compare/v0.12.2...v0.12.3)
238
237
  * 2020-03-17 - update README.rst
239
238
  * 2020-03-17 - don't publish if tests fail
@@ -243,9 +242,6 @@ v1 is a complete rewrite of PyHardLinkBackup.
243
242
  * 2020-03-16 - just warn if used directly (needfull for devlopment to call this directly ;)
244
243
  * 2020-03-16 - update requirements
245
244
  * 2020-03-16 - +pytest-randomly
246
-
247
- <details><summary>Expand older history entries ...</summary>
248
-
249
245
  * [v0.12.2](https://github.com/jedie/PyHardLinkBackup/compare/v0.12.1...v0.12.2)
250
246
  * 2020-03-06 - repare v0.12.2 release
251
247
  * 2020-03-06 - enhance log file content