FileBackup 0.4.4__py3-none-any.whl → 0.5.1__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.
- FileBackup/CommandLine/OffsiteEntryPoint.py +7 -0
- FileBackup/Offsite.py +191 -150
- FileBackup/__init__.py +1 -1
- {filebackup-0.4.4.dist-info → filebackup-0.5.1.dist-info}/METADATA +2 -2
- {filebackup-0.4.4.dist-info → filebackup-0.5.1.dist-info}/RECORD +8 -8
- {filebackup-0.4.4.dist-info → filebackup-0.5.1.dist-info}/WHEEL +0 -0
- {filebackup-0.4.4.dist-info → filebackup-0.5.1.dist-info}/entry_points.txt +0 -0
- {filebackup-0.4.4.dist-info → filebackup-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
37
|
-
from dbrownell_Common import ExecuteTasks
|
|
38
|
-
from dbrownell_Common.InflectEx import inflect
|
|
39
|
-
from dbrownell_Common import PathEx
|
|
40
|
-
from dbrownell_Common.Streams.DoneManager import DoneManager
|
|
41
|
-
from dbrownell_Common import SubprocessEx
|
|
42
|
-
from dbrownell_Common import TextwrapEx
|
|
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
|
-
) ->
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
745
|
-
lambda
|
|
741
|
+
this_working_dir / "transferred",
|
|
742
|
+
lambda bytes_transferred: cast(
|
|
746
743
|
None,
|
|
747
744
|
status.OnProgress(
|
|
748
|
-
ProcessDirectoryState.
|
|
749
|
-
|
|
745
|
+
ProcessDirectoryState.Transferring.value + 1,
|
|
746
|
+
bytes_transferred,
|
|
750
747
|
),
|
|
751
748
|
),
|
|
752
|
-
) as (
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
755
|
+
encryption_password,
|
|
757
756
|
lambda message: cast(
|
|
758
757
|
None,
|
|
759
758
|
status.OnProgress(
|
|
760
|
-
ProcessDirectoryState.
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
797
|
+
func = CreateSymLink
|
|
784
798
|
|
|
785
|
-
|
|
799
|
+
temp_dest_dir = final_dir.parent / (final_dir.name + "__temp__")
|
|
786
800
|
|
|
787
|
-
|
|
788
|
-
|
|
801
|
+
shutil.rmtree(temp_dest_dir, ignore_errors=True)
|
|
802
|
+
temp_dest_dir.mkdir(parents=True)
|
|
789
803
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
804
|
+
items = [
|
|
805
|
+
item
|
|
806
|
+
for item in decompressed_dir.iterdir()
|
|
807
|
+
if item.name != INDEX_HASH_FILENAME
|
|
808
|
+
]
|
|
795
809
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
816
|
+
if item.exists():
|
|
817
|
+
func(item, temp_dest_dir)
|
|
803
818
|
|
|
804
|
-
|
|
819
|
+
shutil.move(temp_dest_dir, final_dir)
|
|
805
820
|
|
|
806
|
-
|
|
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[
|
|
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
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1039
|
+
local_filename.parent.mkdir(parents=True, exist_ok=True)
|
|
1040
|
+
shutil.move(temp_filename, local_filename)
|
|
1016
1041
|
|
|
1017
|
-
|
|
1042
|
+
# ----------------------------------------------------------------------
|
|
1018
1043
|
|
|
1019
|
-
|
|
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
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
-
|
|
1445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1481
|
+
result = SubprocessEx.Run(
|
|
1482
|
+
f'{_GetZipBinary()} x "{archive_filename}" "-p{password}"',
|
|
1483
|
+
cwd=decompressed_dir,
|
|
1484
|
+
)
|
|
1454
1485
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
textwrap.dedent(
|
|
1486
|
+
if result.returncode != 0:
|
|
1487
|
+
message = textwrap.dedent(
|
|
1458
1488
|
"""\
|
|
1459
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1526
|
+
with dm.Nested("Verifying files...") as verify_dm:
|
|
1527
|
+
# Ensure that the content is valid
|
|
1528
|
+
all_filenames: list[Path] = []
|
|
1491
1529
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1530
|
+
for root_str, _, filenames in os.walk(contents_dir):
|
|
1531
|
+
root = Path(root_str)
|
|
1494
1532
|
|
|
1495
|
-
|
|
1533
|
+
all_filenames += [root / filename for filename in filenames if filename != INDEX_HASH_FILENAME]
|
|
1496
1534
|
|
|
1497
|
-
|
|
1535
|
+
data_store = FileSystemDataStore()
|
|
1498
1536
|
|
|
1499
|
-
|
|
1537
|
+
errors: list[str] = []
|
|
1500
1538
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
-
|
|
1545
|
+
file_size = filename.stat().st_size or 1
|
|
1508
1546
|
|
|
1509
|
-
|
|
1547
|
+
status_template = f"Validating file {filename_index + 1} of {len(all_filenames)} [{PathEx.GetSizeDisplay(file_size)}] {{:.02f}}%..."
|
|
1510
1548
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
# Note that this value will be overwritten by calls to `python ../../Build.py update_version` based
|
|
10
10
|
# on changes observed in the git repository. The default value below will be used until the value
|
|
11
11
|
# here is explicitly updated by the Continuous Integration system as part of a commit.
|
|
12
|
-
__version__ = "0.
|
|
12
|
+
__version__ = "0.5.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FileBackup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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.
|
|
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
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
FileBackup/Mirror.py,sha256=GDgTWVNUYn5O7vlH5YVi0LppwQN4R99J61g1UCBwlfI,23544
|
|
2
|
-
FileBackup/Offsite.py,sha256=
|
|
2
|
+
FileBackup/Offsite.py,sha256=_QOywhHX8rmSJJ0TC3dKF4m-cFGx-l9Splng351crqw,65496
|
|
3
3
|
FileBackup/Snapshot.py,sha256=Dj-XFjAFWkqNZIlMHqTOI1Ygdm1jB3orIwDxMQSrupQ,26970
|
|
4
|
-
FileBackup/__init__.py,sha256=
|
|
4
|
+
FileBackup/__init__.py,sha256=mbDIiinNsmldCAGbrTOXoPVPYmGGiRz94d8JOF3bEyc,597
|
|
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=
|
|
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.
|
|
23
|
-
filebackup-0.
|
|
24
|
-
filebackup-0.
|
|
25
|
-
filebackup-0.
|
|
26
|
-
filebackup-0.
|
|
22
|
+
filebackup-0.5.1.dist-info/METADATA,sha256=Rk1-xC4FGaXbba_ORWFeVHAvd86amimeq0yDtBf9Ft8,5762
|
|
23
|
+
filebackup-0.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
24
|
+
filebackup-0.5.1.dist-info/entry_points.txt,sha256=2SUVO3jHsi06MYfUEdqxaVwVvTQVjxzIWlHexRKWRgQ,121
|
|
25
|
+
filebackup-0.5.1.dist-info/licenses/LICENSE,sha256=2dPVskJuHRLwSF9fQiAfln1RVWya2DsKNPvgknRLTYw,1074
|
|
26
|
+
filebackup-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|