FileBackup 0.4.4__py3-none-any.whl → 0.5.2__py3-none-any.whl

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.
@@ -241,6 +241,12 @@ def Restore( # pylint: disable=dangerous-default-value
241
241
  help="Working directory to use when decompressing archives; provide this value during a dry run and subsequent execution to only download and extract the backup content once.",
242
242
  ),
243
243
  ] = None,
244
+ continue_on_errors: Annotated[
245
+ bool,
246
+ typer.Option(
247
+ "--continue-on-errors", help="Continue restoring files even if some files cannot be restored."
248
+ ),
249
+ ] = False,
244
250
  ) -> None:
245
251
  """Restores content from an offsite data store."""
246
252
 
@@ -263,6 +269,7 @@ def Restore( # pylint: disable=dangerous-default-value
263
269
  quiet=quiet,
264
270
  dry_run=dry_run,
265
271
  overwrite=overwrite,
272
+ continue_on_errors=continue_on_errors,
266
273
  )
267
274
 
268
275
 
FileBackup/Offsite.py CHANGED
@@ -30,16 +30,17 @@ import uuid
30
30
  from contextlib import contextmanager
31
31
  from dataclasses import dataclass
32
32
  from enum import auto, Enum
33
+ from io import StringIO
33
34
  from pathlib import Path
34
35
  from typing import Any, Callable, cast, Iterator, Pattern
35
36
 
36
- from dbrownell_Common.ContextlibEx import ExitStack # type: ignore[import-untyped]
37
- from dbrownell_Common import ExecuteTasks # type: ignore[import-untyped]
38
- from dbrownell_Common.InflectEx import inflect # type: ignore[import-untyped]
39
- from dbrownell_Common import PathEx # type: ignore[import-untyped]
40
- from dbrownell_Common.Streams.DoneManager import DoneManager # type: ignore[import-untyped]
41
- from dbrownell_Common import SubprocessEx # type: ignore[import-untyped]
42
- from dbrownell_Common import TextwrapEx # type: ignore[import-untyped]
37
+ from dbrownell_Common.ContextlibEx import ExitStack
38
+ from dbrownell_Common import ExecuteTasks
39
+ from dbrownell_Common.InflectEx import inflect
40
+ from dbrownell_Common import PathEx
41
+ from dbrownell_Common.Streams.DoneManager import DoneManager, Flags as DoneManagerFlags
42
+ from dbrownell_Common import SubprocessEx
43
+ from dbrownell_Common import TextwrapEx
43
44
 
44
45
  from FileBackup.DataStore.FileSystemDataStore import FileSystemDataStore
45
46
  from FileBackup.DataStore.Interfaces.BulkStorageDataStore import BulkStorageDataStore
@@ -530,6 +531,7 @@ def Restore(
530
531
  quiet: bool,
531
532
  dry_run: bool,
532
533
  overwrite: bool,
534
+ continue_on_errors: bool = False,
533
535
  ) -> None:
534
536
  with Common.YieldDataStore(
535
537
  dm,
@@ -708,7 +710,7 @@ def Restore(
708
710
  # ----------------------------------------------------------------------
709
711
  def ExecuteTask(
710
712
  status: ExecuteTasks.Status,
711
- ) -> Path:
713
+ ) -> ExecuteTasks.TransformResultComplete:
712
714
  # This function will create the following directory structure:
713
715
  #
714
716
  # <working_dir>
@@ -717,93 +719,109 @@ def Restore(
717
719
  # └── decompressed (temporary)
718
720
  # └── final
719
721
 
720
- assert working_dir is not None
721
- this_working_dir = working_dir / directory
722
- final_dir = this_working_dir / "final"
723
-
724
- if final_dir.is_dir():
725
- # The destination already exists, no need to process it further
726
- return final_dir
727
-
728
- with _YieldTransferredArchive(
729
- data_store, # type: ignore
730
- directory,
731
- this_working_dir / "transferred",
732
- lambda bytes_transferred: cast(
733
- None,
734
- status.OnProgress(
735
- ProcessDirectoryState.Transferring.value + 1,
736
- bytes_transferred,
737
- ),
738
- ),
739
- ) as (transferred_dir, transferred_dir_is_temporary):
740
- with _YieldDecompressedFiles(
741
- transferred_dir,
742
- this_working_dir / "decompressed",
722
+ sink = StringIO()
723
+
724
+ with DoneManager.Create(
725
+ sink,
726
+ "",
727
+ line_prefix="",
728
+ flags=DoneManagerFlags.Create(verbose=dm.is_verbose, debug=dm.is_debug),
729
+ ) as dm_sink:
730
+ assert working_dir is not None
731
+ this_working_dir = working_dir / directory
732
+ final_dir = this_working_dir / "final"
733
+
734
+ if final_dir.is_dir():
735
+ # The destination already exists, no need to process it further
736
+ return ExecuteTasks.TransformResultComplete(final_dir, 0)
737
+
738
+ with _YieldTransferredArchive(
739
+ data_store, # type: ignore
743
740
  directory,
744
- encryption_password,
745
- lambda message: cast(
741
+ this_working_dir / "transferred",
742
+ lambda bytes_transferred: cast(
746
743
  None,
747
744
  status.OnProgress(
748
- ProcessDirectoryState.Extracting.value + 1,
749
- message,
745
+ ProcessDirectoryState.Transferring.value + 1,
746
+ bytes_transferred,
750
747
  ),
751
748
  ),
752
- ) as (decompressed_dir, decompressed_dir_is_temporary):
753
- # Validate the contents
754
- _VerifyFiles(
749
+ ) as (transferred_dir, transferred_dir_is_temporary):
750
+ with _YieldDecompressedFiles(
751
+ dm_sink,
752
+ transferred_dir,
753
+ this_working_dir / "decompressed",
755
754
  directory,
756
- decompressed_dir,
755
+ encryption_password,
757
756
  lambda message: cast(
758
757
  None,
759
758
  status.OnProgress(
760
- ProcessDirectoryState.Verifying.value + 1,
759
+ ProcessDirectoryState.Extracting.value + 1,
761
760
  message,
762
761
  ),
763
762
  ),
764
- )
763
+ continue_on_errors=continue_on_errors,
764
+ ) as (decompressed_dir, decompressed_dir_is_temporary):
765
+ # Validate the contents
766
+ _VerifyFiles(
767
+ dm_sink,
768
+ directory,
769
+ decompressed_dir,
770
+ lambda message: cast(
771
+ None,
772
+ status.OnProgress(
773
+ ProcessDirectoryState.Verifying.value + 1,
774
+ message,
775
+ ),
776
+ ),
777
+ continue_on_errors=continue_on_errors,
778
+ )
765
779
 
766
- # Move/Copy the content. Note that the code assumes a flat
767
- # directory structure and doesn't do anything to account for
768
- # nested dirs. This assumption matches the current archive
769
- # format.
770
- if transferred_dir_is_temporary or decompressed_dir_is_temporary:
771
- func = cast(Callable[[Path, Path], None], shutil.move)
772
- else:
773
- # ----------------------------------------------------------------------
774
- def CreateSymLink(
775
- source: Path,
776
- dest: Path,
777
- ) -> None:
778
- dest /= source.name
779
- os.symlink(source, dest, target_is_directory=source.is_dir())
780
+ # Move/Copy the content. Note that the code assumes a flat
781
+ # directory structure and doesn't do anything to account for
782
+ # nested dirs. This assumption matches the current archive
783
+ # format.
784
+ if transferred_dir_is_temporary or decompressed_dir_is_temporary:
785
+ func = cast(Callable[[Path, Path], None], shutil.move)
786
+ else:
787
+ # ----------------------------------------------------------------------
788
+ def CreateSymLink(
789
+ source: Path,
790
+ dest: Path,
791
+ ) -> None:
792
+ dest /= source.name
793
+ os.symlink(source, dest, target_is_directory=source.is_dir())
780
794
 
781
- # ----------------------------------------------------------------------
795
+ # ----------------------------------------------------------------------
782
796
 
783
- func = CreateSymLink
797
+ func = CreateSymLink
784
798
 
785
- temp_dest_dir = final_dir.parent / (final_dir.name + "__temp__")
799
+ temp_dest_dir = final_dir.parent / (final_dir.name + "__temp__")
786
800
 
787
- shutil.rmtree(temp_dest_dir, ignore_errors=True)
788
- temp_dest_dir.mkdir(parents=True)
801
+ shutil.rmtree(temp_dest_dir, ignore_errors=True)
802
+ temp_dest_dir.mkdir(parents=True)
789
803
 
790
- items = [
791
- item
792
- for item in decompressed_dir.iterdir()
793
- if item.name != INDEX_HASH_FILENAME
794
- ]
804
+ items = [
805
+ item
806
+ for item in decompressed_dir.iterdir()
807
+ if item.name != INDEX_HASH_FILENAME
808
+ ]
795
809
 
796
- for item_index, item in enumerate(items):
797
- status.OnProgress(
798
- ProcessDirectoryState.Moving.value + 1,
799
- f"Moving {item_index + 1} of {len(items)}...",
800
- )
810
+ for item_index, item in enumerate(items):
811
+ status.OnProgress(
812
+ ProcessDirectoryState.Moving.value + 1,
813
+ f"Moving {item_index + 1} of {len(items)}...",
814
+ )
801
815
 
802
- func(item, temp_dest_dir)
816
+ if item.exists():
817
+ func(item, temp_dest_dir)
803
818
 
804
- shutil.move(temp_dest_dir, final_dir)
819
+ shutil.move(temp_dest_dir, final_dir)
805
820
 
806
- return final_dir
821
+ if dm_sink.result != 0:
822
+ status.Log(sink.getvalue())
823
+
824
+ return ExecuteTasks.TransformResultComplete(final_dir, dm_sink.result)
807
825
 
808
826
  # ----------------------------------------------------------------------
809
827
 
@@ -811,7 +829,7 @@ def Restore(
811
829
 
812
830
  # ----------------------------------------------------------------------
813
831
 
814
- directory_working_dirs: list[Path | None | Exception] = ExecuteTasks.TransformTasksEx(
832
+ directory_working_dirs: list[object | None | Exception] = ExecuteTasks.TransformTasksEx(
815
833
  preprocess_dm,
816
834
  "Processing",
817
835
  [ExecuteTasks.TaskData(str(directory), directory) for directory in directories],
@@ -821,7 +839,8 @@ def Restore(
821
839
  refresh_per_second=Common.EXECUTE_TASKS_REFRESH_PER_SECOND,
822
840
  )
823
841
 
824
- if preprocess_dm.result != 0:
842
+ # Allow the process to continue if warnings were encountered
843
+ if preprocess_dm.result < 0:
825
844
  return
826
845
 
827
846
  assert all(isinstance(working_dir, Path) for working_dir in directory_working_dirs), (
@@ -980,6 +999,8 @@ def Restore(
980
999
 
981
1000
  # ----------------------------------------------------------------------
982
1001
  def WriteImpl(
1002
+ dm: DoneManager,
1003
+ action: str,
983
1004
  local_filename: Path,
984
1005
  content_filename: Path | None,
985
1006
  ) -> None:
@@ -998,25 +1019,29 @@ def Restore(
998
1019
  commit_actions.append(CommitDir)
999
1020
  return
1000
1021
 
1022
+ content_filename = content_filename.resolve()
1001
1023
  temp_filename = temp_directory / str(uuid.uuid4())
1002
1024
 
1003
- with content_filename.resolve().open("rb") as source:
1004
- with temp_filename.open("wb") as dest:
1005
- dest.write(source.read())
1025
+ if not content_filename.is_file():
1026
+ dm.WriteError(f"The file could not be {action} as its archive data is missing.\n")
1027
+ else:
1028
+ with content_filename.open("rb") as source:
1029
+ with temp_filename.open("wb") as dest:
1030
+ dest.write(source.read())
1006
1031
 
1007
- # ----------------------------------------------------------------------
1008
- def CommitFile() -> None:
1009
- if local_filename.is_dir():
1010
- shutil.rmtree(local_filename)
1011
- elif local_filename.is_file():
1012
- local_filename.unlink()
1032
+ # ----------------------------------------------------------------------
1033
+ def CommitFile() -> None:
1034
+ if local_filename.is_dir():
1035
+ shutil.rmtree(local_filename)
1036
+ elif local_filename.is_file():
1037
+ local_filename.unlink()
1013
1038
 
1014
- local_filename.parent.mkdir(parents=True, exist_ok=True)
1015
- shutil.move(temp_filename, local_filename)
1039
+ local_filename.parent.mkdir(parents=True, exist_ok=True)
1040
+ shutil.move(temp_filename, local_filename)
1016
1041
 
1017
- # ----------------------------------------------------------------------
1042
+ # ----------------------------------------------------------------------
1018
1043
 
1019
- commit_actions.append(CommitFile)
1044
+ commit_actions.append(CommitFile)
1020
1045
 
1021
1046
  # ----------------------------------------------------------------------
1022
1047
  def OnAddInstruction(
@@ -1029,7 +1054,7 @@ def Restore(
1029
1054
  )
1030
1055
  return
1031
1056
 
1032
- WriteImpl(instruction.local_filename, instruction.file_content_path)
1057
+ WriteImpl(dm, "restored", instruction.local_filename, instruction.file_content_path)
1033
1058
 
1034
1059
  # ----------------------------------------------------------------------
1035
1060
  def OnModifyInstruction(
@@ -1037,7 +1062,7 @@ def Restore(
1037
1062
  instruction: Instruction,
1038
1063
  ) -> None:
1039
1064
  assert instruction.file_content_path is not None
1040
- WriteImpl(instruction.local_filename, instruction.file_content_path)
1065
+ WriteImpl(dm, "modified", instruction.local_filename, instruction.file_content_path)
1041
1066
 
1042
1067
  # ----------------------------------------------------------------------
1043
1068
  def OnRemoveInstruction(
@@ -1114,7 +1139,7 @@ def Restore(
1114
1139
  ) as execute_dm:
1115
1140
  on_instruction_func(execute_dm, instruction)
1116
1141
 
1117
- if execute_dm.result != 0:
1142
+ if execute_dm.result != 0 and not continue_on_errors:
1118
1143
  break
1119
1144
 
1120
1145
  instructions_dm.WriteLine("")
@@ -1382,11 +1407,14 @@ def _YieldTransferredArchive(
1382
1407
  # ----------------------------------------------------------------------
1383
1408
  @contextmanager
1384
1409
  def _YieldDecompressedFiles(
1410
+ dm: DoneManager,
1385
1411
  transferred_dir: Path,
1386
1412
  decompressed_dir: Path,
1387
1413
  directory_name: str,
1388
1414
  encryption_password: str | None,
1389
1415
  status_func: Callable[[str], None],
1416
+ *,
1417
+ continue_on_errors: bool,
1390
1418
  ) -> Iterator[
1391
1419
  tuple[
1392
1420
  Path,
@@ -1411,18 +1439,17 @@ def _YieldDecompressedFiles(
1411
1439
  #
1412
1440
  password = encryption_password or str(uuid.uuid4())
1413
1441
 
1414
- # Validate
1415
- status_func("Validating archive...")
1416
-
1417
1442
  archive_filename = transferred_dir / (ARCHIVE_FILENAME + ".001")
1418
1443
 
1419
1444
  if not archive_filename.is_file():
1420
1445
  raise Exception(f"The archive file '{archive_filename.name}' was not found.")
1421
1446
 
1422
- result = SubprocessEx.Run(f'{_GetZipBinary()} t "{archive_filename}" "-p{password}"')
1423
- if result.returncode != 0:
1424
- raise Exception(
1425
- textwrap.dedent(
1447
+ # Validate
1448
+ status_func("Validating archive...")
1449
+ with dm.Nested("Validating archive...") as validate_dm:
1450
+ result = SubprocessEx.Run(f'{_GetZipBinary()} t "{archive_filename}" "-p{password}"')
1451
+ if result.returncode != 0:
1452
+ message = textwrap.dedent(
1426
1453
  """\
1427
1454
  Archive validation failed for the directory '{}' ({}).
1428
1455
 
@@ -1438,29 +1465,32 @@ def _YieldDecompressedFiles(
1438
1465
  4,
1439
1466
  skip_first_line=True,
1440
1467
  ),
1441
- ),
1442
- )
1468
+ )
1443
1469
 
1444
- # Extract
1445
- status_func("Extracting archive...")
1470
+ if continue_on_errors:
1471
+ validate_dm.WriteWarning(message)
1472
+ else:
1473
+ raise Exception(message)
1446
1474
 
1475
+ # Extract
1447
1476
  with _YieldTempDirectory(decompressed_dir, "extracting the archive") as decompressed_dir:
1448
- decompressed_dir.mkdir(parents=True, exist_ok=True)
1477
+ status_func("Extracting archive...")
1478
+ with dm.Nested("Extracting archive...") as extract_dm:
1479
+ decompressed_dir.mkdir(parents=True, exist_ok=True)
1449
1480
 
1450
- result = SubprocessEx.Run(
1451
- f'{_GetZipBinary()} x "{archive_filename}" "-p{password}"',
1452
- cwd=decompressed_dir,
1453
- )
1481
+ result = SubprocessEx.Run(
1482
+ f'{_GetZipBinary()} x "{archive_filename}" "-p{password}"',
1483
+ cwd=decompressed_dir,
1484
+ )
1454
1485
 
1455
- if result.returncode != 0:
1456
- raise Exception(
1457
- textwrap.dedent(
1486
+ if result.returncode != 0:
1487
+ message = textwrap.dedent(
1458
1488
  """\
1459
- Archive extraction failed for the directory '{}' ({}).
1489
+ Archive extraction failed for the directory '{}' ({}).
1460
1490
 
1461
- {}
1491
+ {}
1462
1492
 
1463
- """,
1493
+ """,
1464
1494
  ).format(
1465
1495
  directory_name,
1466
1496
  result.returncode,
@@ -1469,67 +1499,74 @@ def _YieldDecompressedFiles(
1469
1499
  4,
1470
1500
  skip_first_line=True,
1471
1501
  ),
1472
- ),
1473
- )
1502
+ )
1503
+
1504
+ if continue_on_errors:
1505
+ extract_dm.WriteWarning(message)
1506
+ else:
1507
+ raise Exception(message)
1474
1508
 
1475
1509
  yield decompressed_dir, True
1476
1510
 
1477
1511
 
1478
1512
  # ----------------------------------------------------------------------
1479
1513
  def _VerifyFiles(
1514
+ dm: DoneManager,
1480
1515
  directory_name: str,
1481
1516
  contents_dir: Path,
1482
1517
  status_func: Callable[[str], None],
1518
+ *,
1519
+ continue_on_errors: bool,
1483
1520
  ) -> None:
1484
1521
  # Ensure that the index is present
1485
1522
  for index_filename in [INDEX_FILENAME, INDEX_HASH_FILENAME]:
1486
1523
  if not (contents_dir / index_filename).is_file():
1487
1524
  raise Exception(f"The index file '{index_filename}' does not exist.")
1488
1525
 
1489
- # Ensure that the content is valid
1490
- all_filenames: list[Path] = []
1526
+ with dm.Nested("Verifying files...") as verify_dm:
1527
+ # Ensure that the content is valid
1528
+ all_filenames: list[Path] = []
1491
1529
 
1492
- for root_str, _, filenames in os.walk(contents_dir):
1493
- root = Path(root_str)
1530
+ for root_str, _, filenames in os.walk(contents_dir):
1531
+ root = Path(root_str)
1494
1532
 
1495
- all_filenames += [root / filename for filename in filenames if filename != INDEX_HASH_FILENAME]
1533
+ all_filenames += [root / filename for filename in filenames if filename != INDEX_HASH_FILENAME]
1496
1534
 
1497
- data_store = FileSystemDataStore()
1535
+ data_store = FileSystemDataStore()
1498
1536
 
1499
- errors: list[str] = []
1537
+ errors: list[str] = []
1500
1538
 
1501
- for filename_index, filename in enumerate(all_filenames):
1502
- if filename.name == INDEX_FILENAME:
1503
- expected_hash_value = (contents_dir / INDEX_HASH_FILENAME).read_text().strip()
1504
- else:
1505
- expected_hash_value = filename.name
1539
+ for filename_index, filename in enumerate(all_filenames):
1540
+ if filename.name == INDEX_FILENAME:
1541
+ expected_hash_value = (contents_dir / INDEX_HASH_FILENAME).read_text().strip()
1542
+ else:
1543
+ expected_hash_value = filename.name
1506
1544
 
1507
- file_size = filename.stat().st_size or 1
1545
+ file_size = filename.stat().st_size or 1
1508
1546
 
1509
- status_template = f"Validating file {filename_index + 1} of {len(all_filenames)} [{PathEx.GetSizeDisplay(file_size)}] {{:.02f}}%..."
1547
+ status_template = f"Validating file {filename_index + 1} of {len(all_filenames)} [{PathEx.GetSizeDisplay(file_size)}] {{:.02f}}%..."
1510
1548
 
1511
- actual_hash_value = Common.CalculateHash(
1512
- data_store,
1513
- filename,
1514
- lambda bytes_transferred: status_func(
1515
- status_template.format((bytes_transferred / file_size) * 100)
1516
- ),
1517
- )
1518
-
1519
- if actual_hash_value != expected_hash_value:
1520
- errors.append(
1521
- textwrap.dedent(
1522
- f"""\
1523
- Filename: {filename.relative_to(contents_dir)}
1524
- Expected: {expected_hash_value}
1525
- Actual: {actual_hash_value}
1526
- """,
1549
+ actual_hash_value = Common.CalculateHash(
1550
+ data_store,
1551
+ filename,
1552
+ lambda bytes_transferred: status_func(
1553
+ status_template.format((bytes_transferred / file_size) * 100)
1527
1554
  ),
1528
1555
  )
1529
1556
 
1530
- if errors:
1531
- raise Exception(
1532
- textwrap.dedent(
1557
+ if actual_hash_value != expected_hash_value:
1558
+ errors.append(
1559
+ textwrap.dedent(
1560
+ f"""\
1561
+ Filename: {filename.relative_to(contents_dir)}
1562
+ Expected: {expected_hash_value}
1563
+ Actual: {actual_hash_value}
1564
+ """,
1565
+ ),
1566
+ )
1567
+
1568
+ if errors:
1569
+ message = textwrap.dedent(
1533
1570
  """\
1534
1571
  Corrupt files were encountered in the directory '{}'.
1535
1572
 
@@ -1543,5 +1580,9 @@ def _VerifyFiles(
1543
1580
  4,
1544
1581
  skip_first_line=True,
1545
1582
  ),
1546
- ),
1547
- )
1583
+ )
1584
+
1585
+ if continue_on_errors:
1586
+ verify_dm.WriteWarning(message)
1587
+ else:
1588
+ raise Exception(message)
FileBackup/__init__.py CHANGED
@@ -1,12 +1,8 @@
1
- # ----------------------------------------------------------------------
2
- # |
3
- # | Copyright (c) 2024 David Brownell
4
- # | Distributed under the MIT License.
5
- # |
6
- # ----------------------------------------------------------------------
7
- # pylint: disable=missing-module-docstring,invalid-name
1
+ # noqa: D104
8
2
 
9
- # Note that this value will be overwritten by calls to `python ../../Build.py update_version` based
10
- # on changes observed in the git repository. The default value below will be used until the value
11
- # here is explicitly updated by the Continuous Integration system as part of a commit.
12
- __version__ = "0.4.4"
3
+ # Wheel names will be generated according to this value. Do not manually modify this value; instead
4
+ # update it according to committed changes by running this command from the root of the repository:
5
+ #
6
+ # uv run python -m AutoGitSemVer.scripts.UpdatePythonVersion ./src/FileBackup/__init__.py ./src
7
+ #
8
+ __version__ = "0.5.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FileBackup
3
- Version: 0.4.4
3
+ Version: 0.5.2
4
4
  Summary: Tool for creating and restoring file system backups.
5
5
  Project-URL: Homepage, https://github.com/davidbrownell/FileBackup
6
6
  Project-URL: Documentation, https://github.com/davidbrownell/FileBackup
@@ -28,7 +28,7 @@ Classifier: Topic :: System :: Archiving :: Mirroring
28
28
  Classifier: Topic :: System :: Systems Administration
29
29
  Classifier: Topic :: Utilities
30
30
  Requires-Python: >=3.10
31
- Requires-Dist: dbrownell-common>=0.14.6
31
+ Requires-Dist: dbrownell-common>=0.15.0
32
32
  Requires-Dist: paramiko>=3.5.1
33
33
  Requires-Dist: typer>=0.15.3
34
34
  Description-Content-Type: text/markdown
@@ -59,7 +59,7 @@ Description-Content-Type: text/markdown
59
59
  ## Overview
60
60
  TODO: Complete this section
61
61
 
62
- ### How to use FileBackup
62
+ ### How to use `FileBackup`
63
63
  TODO: Complete this section
64
64
 
65
65
  <!-- Content below this delimiter will be copied to the generated README.md file. DO NOT REMOVE THIS COMMENT, as it will cause regeneration to fail. -->
@@ -72,11 +72,13 @@ TODO: Complete this section
72
72
  | Via [pip](https://pip.pypa.io/en/stable/) | `pip install FileBackup` |
73
73
 
74
74
  ### Verifying Signed Artifacts
75
- Artifacts are signed and validated using [py-minisign](https://github.com/x13a/py-minisign) and the public key in the file `./minisign_key.pub`.
75
+ Artifacts are signed and verified using [py-minisign](https://github.com/x13a/py-minisign) and the public key in the file `./minisign_key.pub`.
76
76
 
77
77
  To verify that an artifact is valid, visit [the latest release](https://github.com/davidbrownell/FileBackup/releases/latest) and download the `.minisign` signature file that corresponds to the artifact, then run the following command, replacing `<filename>` with the name of the artifact to be verified:
78
78
 
79
- `uv run --with py-minisign python -c "import minisign; minisign.PublicKey.from_file('minisign_key.pub').verify_file('<filename>')"`
79
+ ```shell
80
+ uv run --with py-minisign python -c "import minisign; minisign.PublicKey.from_file('minisign_key.pub').verify_file('<filename>'); print('The file has been verified.')"
81
+ ```
80
82
 
81
83
  ## Development
82
84
  Please visit [Contributing](https://github.com/davidbrownell/FileBackup/blob/main/CONTRIBUTING.md) and [Development](https://github.com/davidbrownell/FileBackup/blob/main/DEVELOPMENT.md) for information on contributing to this project.
@@ -94,4 +96,4 @@ Additional information can be found at these locations.
94
96
  | Security | [SECURITY.md](https://github.com/davidbrownell/FileBackup/blob/main/SECURITY.md) | Information about how to privately report security issues associated with this project. |
95
97
 
96
98
  ## License
97
- FileBackup is licensed under the <a href="https://choosealicense.com/licenses/MIT/" target="_blank">MIT</a> license.
99
+ `FileBackup` is licensed under the <a href="https://choosealicense.com/licenses/MIT/" target="_blank">MIT</a> license.
@@ -1,12 +1,12 @@
1
1
  FileBackup/Mirror.py,sha256=GDgTWVNUYn5O7vlH5YVi0LppwQN4R99J61g1UCBwlfI,23544
2
- FileBackup/Offsite.py,sha256=IFi6_bN5LIQdS47WqYphLvtx7d9mV3_zOsfVaWLNzUM,63054
2
+ FileBackup/Offsite.py,sha256=_QOywhHX8rmSJJ0TC3dKF4m-cFGx-l9Splng351crqw,65496
3
3
  FileBackup/Snapshot.py,sha256=Dj-XFjAFWkqNZIlMHqTOI1Ygdm1jB3orIwDxMQSrupQ,26970
4
- FileBackup/__init__.py,sha256=655mOBYNTfSBiePVm-85Y13RwifOu84XpRLXHL_Fx7U,597
4
+ FileBackup/__init__.py,sha256=JPB9HoofnsZ24vqzjS1ervhu0GisXhZa8AXRUR3Hzvg,338
5
5
  FileBackup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  FileBackup/CommandLine/CommandLineArguments.py,sha256=7oLUf9tODd_OjBNzIjQiiyZ9ujy-KbW53Pg0dF0MQG8,2378
7
7
  FileBackup/CommandLine/EntryPoint.py,sha256=Fn62wtKV6uiTJ1dQpolNFUyWWeUOKKgetOFSXPgoe-w,1813
8
8
  FileBackup/CommandLine/MirrorEntryPoint.py,sha256=WlGIz_AZ7OdE-hVL19lQExUXbEjQ8_4bbIuITWxdWO4,6704
9
- FileBackup/CommandLine/OffsiteEntryPoint.py,sha256=ZpZBpIk4ZLC27oWjvDriiqqwtwqHLJ2tfMOFdhVyigU,12368
9
+ FileBackup/CommandLine/OffsiteEntryPoint.py,sha256=OscTJi57_-dYX2NsfzTONqWhxm-NvON-aY80r6-aRwk,12634
10
10
  FileBackup/CommandLine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  FileBackup/DataStore/FastGlacierDataStore.py,sha256=5J4W9FqB92hnHoRoZLo-DfMQ6uyjaOAWWrnuc0pvnJo,3017
12
12
  FileBackup/DataStore/FileSystemDataStore.py,sha256=XUvIQe2Yl-W6OPfvsJlPzDPsGclDOkKlozO0GJ-05bo,6791
@@ -19,8 +19,8 @@ FileBackup/DataStore/Interfaces/FileBasedDataStore.py,sha256=eMA_cQ-CED83z5uMDQZ
19
19
  FileBackup/DataStore/Interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  FileBackup/Impl/Common.py,sha256=2WFMx5BdCnZmo-cuLYZlIx4Nr_ZBKmJWfMaFVUpsqKo,24935
21
21
  FileBackup/Impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- filebackup-0.4.4.dist-info/METADATA,sha256=iynFleOqIop_GgtOoApRlRBPWZ2DwH7S2Y2TNkOeE3U,5762
23
- filebackup-0.4.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
- filebackup-0.4.4.dist-info/entry_points.txt,sha256=2SUVO3jHsi06MYfUEdqxaVwVvTQVjxzIWlHexRKWRgQ,121
25
- filebackup-0.4.4.dist-info/licenses/LICENSE,sha256=2dPVskJuHRLwSF9fQiAfln1RVWya2DsKNPvgknRLTYw,1074
26
- filebackup-0.4.4.dist-info/RECORD,,
22
+ filebackup-0.5.2.dist-info/METADATA,sha256=MMDJrdhkFCA7H8RHVVqGnLTJxCbHaR4JL4qId9_6mkE,5814
23
+ filebackup-0.5.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
+ filebackup-0.5.2.dist-info/entry_points.txt,sha256=2SUVO3jHsi06MYfUEdqxaVwVvTQVjxzIWlHexRKWRgQ,121
25
+ filebackup-0.5.2.dist-info/licenses/LICENSE,sha256=2dPVskJuHRLwSF9fQiAfln1RVWya2DsKNPvgknRLTYw,1074
26
+ filebackup-0.5.2.dist-info/RECORD,,