FileBackup 0.3.6__py3-none-any.whl → 0.4.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/CommandLineArguments.py +1 -3
- FileBackup/CommandLine/MirrorEntryPoint.py +6 -18
- FileBackup/CommandLine/OffsiteEntryPoint.py +8 -24
- FileBackup/DataStore/FastGlacierDataStore.py +7 -5
- FileBackup/DataStore/S3BrowserDataStore.py +81 -0
- FileBackup/Impl/Common.py +54 -23
- FileBackup/Mirror.py +7 -21
- FileBackup/Offsite.py +26 -53
- FileBackup/Snapshot.py +17 -32
- FileBackup/__init__.py +1 -1
- FileBackup/py.typed +0 -0
- filebackup-0.4.1.dist-info/METADATA +98 -0
- filebackup-0.4.1.dist-info/RECORD +26 -0
- {FileBackup-0.3.6.dist-info → filebackup-0.4.1.dist-info}/WHEEL +1 -2
- {FileBackup-0.3.6.dist-info → filebackup-0.4.1.dist-info}/entry_points.txt +1 -0
- FileBackup-0.3.6.dist-info/LICENSE.txt → filebackup-0.4.1.dist-info/licenses/LICENSE +1 -1
- FileBackup-0.3.6.dist-info/METADATA +0 -73
- FileBackup-0.3.6.dist-info/RECORD +0 -25
- FileBackup-0.3.6.dist-info/top_level.txt +0 -1
|
@@ -29,9 +29,7 @@ def ToRegex(
|
|
|
29
29
|
try:
|
|
30
30
|
expressions.append(re.compile("^{}$".format(value)))
|
|
31
31
|
except re.error as ex:
|
|
32
|
-
raise typer.BadParameter(
|
|
33
|
-
"The regular expression '{}' is not valid ({}).".format(value, ex)
|
|
34
|
-
)
|
|
32
|
+
raise typer.BadParameter("The regular expression '{}' is not valid ({}).".format(value, ex))
|
|
35
33
|
|
|
36
34
|
return expressions
|
|
37
35
|
|
|
@@ -71,18 +71,12 @@ def Execute(
|
|
|
71
71
|
CommandLineArguments.input_filename_or_dirs_argument,
|
|
72
72
|
],
|
|
73
73
|
ssd: Annotated[bool, CommandLineArguments.ssd_option] = CommandLineArguments.ssd_option_default,
|
|
74
|
-
force: Annotated[
|
|
75
|
-
bool, CommandLineArguments.force_option
|
|
76
|
-
] = CommandLineArguments.force_option_default,
|
|
74
|
+
force: Annotated[bool, CommandLineArguments.force_option] = CommandLineArguments.force_option_default,
|
|
77
75
|
verbose: Annotated[
|
|
78
76
|
bool, CommandLineArguments.verbose_option
|
|
79
77
|
] = CommandLineArguments.verbose_option_default,
|
|
80
|
-
quiet: Annotated[
|
|
81
|
-
|
|
82
|
-
] = CommandLineArguments.quiet_option_default,
|
|
83
|
-
debug: Annotated[
|
|
84
|
-
bool, CommandLineArguments.debug_option
|
|
85
|
-
] = CommandLineArguments.debug_option_default,
|
|
78
|
+
quiet: Annotated[bool, CommandLineArguments.quiet_option] = CommandLineArguments.quiet_option_default,
|
|
79
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
86
80
|
file_include_params: Annotated[
|
|
87
81
|
list[str],
|
|
88
82
|
CommandLineArguments.file_include_option,
|
|
@@ -146,12 +140,8 @@ def Validate(
|
|
|
146
140
|
verbose: Annotated[
|
|
147
141
|
bool, CommandLineArguments.verbose_option
|
|
148
142
|
] = CommandLineArguments.verbose_option_default,
|
|
149
|
-
quiet: Annotated[
|
|
150
|
-
|
|
151
|
-
] = CommandLineArguments.quiet_option_default,
|
|
152
|
-
debug: Annotated[
|
|
153
|
-
bool, CommandLineArguments.debug_option
|
|
154
|
-
] = CommandLineArguments.debug_option_default,
|
|
143
|
+
quiet: Annotated[bool, CommandLineArguments.quiet_option] = CommandLineArguments.quiet_option_default,
|
|
144
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
155
145
|
) -> None:
|
|
156
146
|
"""Validates previously mirrored content in the backup data store."""
|
|
157
147
|
|
|
@@ -180,9 +170,7 @@ def Cleanup(
|
|
|
180
170
|
verbose: Annotated[
|
|
181
171
|
bool, CommandLineArguments.verbose_option
|
|
182
172
|
] = CommandLineArguments.verbose_option_default,
|
|
183
|
-
debug: Annotated[
|
|
184
|
-
bool, CommandLineArguments.debug_option
|
|
185
|
-
] = CommandLineArguments.debug_option_default,
|
|
173
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
186
174
|
) -> None:
|
|
187
175
|
"""Cleans a backup data store after a mirror execution that was interrupted or failed."""
|
|
188
176
|
|
|
@@ -92,18 +92,12 @@ def Execute(
|
|
|
92
92
|
),
|
|
93
93
|
] = False,
|
|
94
94
|
ssd: Annotated[bool, CommandLineArguments.ssd_option] = CommandLineArguments.ssd_option_default,
|
|
95
|
-
force: Annotated[
|
|
96
|
-
bool, CommandLineArguments.force_option
|
|
97
|
-
] = CommandLineArguments.force_option_default,
|
|
95
|
+
force: Annotated[bool, CommandLineArguments.force_option] = CommandLineArguments.force_option_default,
|
|
98
96
|
verbose: Annotated[
|
|
99
97
|
bool, CommandLineArguments.verbose_option
|
|
100
98
|
] = CommandLineArguments.verbose_option_default,
|
|
101
|
-
quiet: Annotated[
|
|
102
|
-
|
|
103
|
-
] = CommandLineArguments.quiet_option_default,
|
|
104
|
-
debug: Annotated[
|
|
105
|
-
bool, CommandLineArguments.debug_option
|
|
106
|
-
] = CommandLineArguments.debug_option_default,
|
|
99
|
+
quiet: Annotated[bool, CommandLineArguments.quiet_option] = CommandLineArguments.quiet_option_default,
|
|
100
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
107
101
|
working_dir: Annotated[
|
|
108
102
|
Optional[Path],
|
|
109
103
|
typer.Option(
|
|
@@ -123,9 +117,7 @@ def Execute(
|
|
|
123
117
|
] = Offsite.DEFAULT_ARCHIVE_VOLUME_SIZE,
|
|
124
118
|
ignore_pending_snapshot: Annotated[
|
|
125
119
|
bool,
|
|
126
|
-
typer.Option(
|
|
127
|
-
"--ignore-pending-snapshot", help="Disable the pending warning snapshot and continue."
|
|
128
|
-
),
|
|
120
|
+
typer.Option("--ignore-pending-snapshot", help="Disable the pending warning snapshot and continue."),
|
|
129
121
|
] = False,
|
|
130
122
|
file_include_params: Annotated[
|
|
131
123
|
list[str],
|
|
@@ -181,9 +173,7 @@ def Commit(
|
|
|
181
173
|
verbose: Annotated[
|
|
182
174
|
bool, CommandLineArguments.verbose_option
|
|
183
175
|
] = CommandLineArguments.verbose_option_default,
|
|
184
|
-
debug: Annotated[
|
|
185
|
-
bool, CommandLineArguments.debug_option
|
|
186
|
-
] = CommandLineArguments.debug_option_default,
|
|
176
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
187
177
|
) -> None:
|
|
188
178
|
"""Commits a pending snapshot after the changes have been transferred to an offsite data store."""
|
|
189
179
|
|
|
@@ -240,12 +230,8 @@ def Restore( # pylint: disable=dangerous-default-value
|
|
|
240
230
|
verbose: Annotated[
|
|
241
231
|
bool, CommandLineArguments.verbose_option
|
|
242
232
|
] = CommandLineArguments.verbose_option_default,
|
|
243
|
-
quiet: Annotated[
|
|
244
|
-
|
|
245
|
-
] = CommandLineArguments.quiet_option_default,
|
|
246
|
-
debug: Annotated[
|
|
247
|
-
bool, CommandLineArguments.debug_option
|
|
248
|
-
] = CommandLineArguments.debug_option_default,
|
|
233
|
+
quiet: Annotated[bool, CommandLineArguments.quiet_option] = CommandLineArguments.quiet_option_default,
|
|
234
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
249
235
|
working_dir: Annotated[
|
|
250
236
|
Optional[Path],
|
|
251
237
|
typer.Option(
|
|
@@ -323,9 +309,7 @@ def _ResolveWorkingDir(
|
|
|
323
309
|
else:
|
|
324
310
|
assert False, dm.result # pragma: no cover
|
|
325
311
|
|
|
326
|
-
dm.WriteInfo(
|
|
327
|
-
f"The temporary directory '{working_dir}' was preserved due to {type_desc}.\n"
|
|
328
|
-
)
|
|
312
|
+
dm.WriteInfo(f"The temporary directory '{working_dir}' was preserved due to {type_desc}.\n")
|
|
329
313
|
|
|
330
314
|
|
|
331
315
|
# ----------------------------------------------------------------------
|
|
@@ -70,10 +70,12 @@ class FastGlacierDataStore(BulkStorageDataStore):
|
|
|
70
70
|
)
|
|
71
71
|
return
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
command_line = f'glacier-con upload "{self.account_name}" "{local_path / "*"}" "{self.aws_region}" "{self._glacier_dir.as_posix()}"'
|
|
73
|
+
self._validated_command_line = True
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
with dm.Nested("Uploading to Fast Glacier...") as upload_dm:
|
|
76
|
+
command_line = f'glacier-con upload "{self.account_name}" "{local_path / "*"}" "{self.aws_region}" "{self._glacier_dir.as_posix()}"'
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
upload_dm.WriteVerbose(f"Command Line: {command_line}\n\n")
|
|
79
|
+
|
|
80
|
+
with upload_dm.YieldStream() as stream:
|
|
81
|
+
upload_dm.result = SubprocessEx.Stream(command_line, stream)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# |
|
|
3
|
+
# | S3BrowserDataStore.py
|
|
4
|
+
# |
|
|
5
|
+
# | David Brownell <db@DavidBrownell.com>
|
|
6
|
+
# | 2025-02-23 14:08:12
|
|
7
|
+
# |
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
# |
|
|
10
|
+
# | Copyright David Brownell 2025
|
|
11
|
+
# | Distributed under the MIT License.
|
|
12
|
+
# |
|
|
13
|
+
# ----------------------------------------------------------------------
|
|
14
|
+
"""Contains the S3BrowserDataStore object"""
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from dbrownell_Common.Streams.DoneManager import DoneManager # type: ignore[import-untyped]
|
|
20
|
+
from dbrownell_Common import SubprocessEx # type: ignore[import-untyped]
|
|
21
|
+
from dbrownell_Common.Types import override # type: ignore[import-untyped]
|
|
22
|
+
|
|
23
|
+
from FileBackup.DataStore.Interfaces.BulkStorageDataStore import BulkStorageDataStore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ----------------------------------------------------------------------
|
|
27
|
+
class S3BrowserDataStore(BulkStorageDataStore):
|
|
28
|
+
"""Data store that uses the S3 Browser application (https://s3browser.com/)"""
|
|
29
|
+
|
|
30
|
+
# ----------------------------------------------------------------------
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
account_name: str,
|
|
34
|
+
bucket_name: str,
|
|
35
|
+
s3_dir: Optional[Path],
|
|
36
|
+
) -> None:
|
|
37
|
+
super(S3BrowserDataStore, self).__init__()
|
|
38
|
+
|
|
39
|
+
self.account_name = account_name
|
|
40
|
+
self.bucket_name = bucket_name
|
|
41
|
+
|
|
42
|
+
self._s3_dir = self.bucket_name / (s3_dir or Path())
|
|
43
|
+
|
|
44
|
+
self._validated_command_line = False
|
|
45
|
+
|
|
46
|
+
# ----------------------------------------------------------------------
|
|
47
|
+
@override
|
|
48
|
+
def ExecuteInParallel(self) -> bool:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
# ----------------------------------------------------------------------
|
|
52
|
+
@override
|
|
53
|
+
def Upload(
|
|
54
|
+
self,
|
|
55
|
+
dm: DoneManager,
|
|
56
|
+
local_path: Path,
|
|
57
|
+
) -> None:
|
|
58
|
+
if self._validated_command_line is False:
|
|
59
|
+
with dm.Nested(
|
|
60
|
+
"Validating S3 Browser on the command line...",
|
|
61
|
+
suffix="\n",
|
|
62
|
+
) as check_dm:
|
|
63
|
+
result = SubprocessEx.Run("s3browser-cli license show")
|
|
64
|
+
|
|
65
|
+
check_dm.WriteVerbose(result.output)
|
|
66
|
+
|
|
67
|
+
if result.returncode != 0:
|
|
68
|
+
check_dm.WriteError(
|
|
69
|
+
"S3 Browser is not available; please make sure it exists in the path and run the script again.\n"
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
self._validated_command_line = True
|
|
74
|
+
|
|
75
|
+
with dm.Nested("Uploading via S3 Browser...") as upload_dm:
|
|
76
|
+
command_line = f's3browser-cli file upload "{self.account_name}" "{local_path / "*"}" "{self._s3_dir.as_posix()}"'
|
|
77
|
+
|
|
78
|
+
upload_dm.WriteVerbose(f"Command Line: {command_line}\n\n")
|
|
79
|
+
|
|
80
|
+
with upload_dm.YieldStream() as stream:
|
|
81
|
+
upload_dm.result = SubprocessEx.Stream(command_line, stream)
|
FileBackup/Impl/Common.py
CHANGED
|
@@ -22,7 +22,16 @@ from contextlib import contextmanager
|
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
23
|
from enum import auto, Enum
|
|
24
24
|
from pathlib import Path
|
|
25
|
-
from typing import
|
|
25
|
+
from typing import (
|
|
26
|
+
Any,
|
|
27
|
+
Callable,
|
|
28
|
+
cast,
|
|
29
|
+
Iterable,
|
|
30
|
+
Iterator,
|
|
31
|
+
Optional,
|
|
32
|
+
Pattern,
|
|
33
|
+
TYPE_CHECKING,
|
|
34
|
+
)
|
|
26
35
|
from urllib import parse as urlparse
|
|
27
36
|
|
|
28
37
|
from dbrownell_Common import ExecuteTasks # type: ignore[import-untyped]
|
|
@@ -35,6 +44,7 @@ from FileBackup.DataStore.FastGlacierDataStore import FastGlacierDataStore
|
|
|
35
44
|
from FileBackup.DataStore.FileSystemDataStore import FileSystemDataStore
|
|
36
45
|
from FileBackup.DataStore.Interfaces.DataStore import DataStore, ItemType
|
|
37
46
|
from FileBackup.DataStore.Interfaces.FileBasedDataStore import FileBasedDataStore
|
|
47
|
+
from FileBackup.DataStore.S3BrowserDataStore import S3BrowserDataStore
|
|
38
48
|
from FileBackup.DataStore.SFTPDataStore import SFTPDataStore, SSH_PORT
|
|
39
49
|
|
|
40
50
|
if TYPE_CHECKING:
|
|
@@ -79,6 +89,22 @@ FAST_GLACIER_TEMPLATE_REGEX = re.compile(
|
|
|
79
89
|
[sep] )\/(?#
|
|
80
90
|
Posix Working Dir )(?P<working_dir>.+)(?#
|
|
81
91
|
Working Dir End ))?(?#
|
|
92
|
+
End )$(?#
|
|
93
|
+
)""",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
S3_BROWSER_TEMPLATE_REGEX = re.compile(
|
|
98
|
+
r"""(?#
|
|
99
|
+
Start )^(?#
|
|
100
|
+
Prefix )s3_browser:\/\/(?#
|
|
101
|
+
Account Name )(?P<account_name>[^@]+)(?#
|
|
102
|
+
[sep] )@(?#
|
|
103
|
+
Bucket Name )(?P<bucket_name>[^\/]+)(?#
|
|
104
|
+
Working Dir Begin )(?:(?#
|
|
105
|
+
Posix Working Dir )(?P<working_dir>.+)(?#
|
|
106
|
+
Working Dir End ))?(?#
|
|
107
|
+
End )$(?#
|
|
82
108
|
)""",
|
|
83
109
|
)
|
|
84
110
|
|
|
@@ -129,11 +155,7 @@ class DiffResult:
|
|
|
129
155
|
# ----------------------------------------------------------------------
|
|
130
156
|
def __post_init__(self):
|
|
131
157
|
assert (
|
|
132
|
-
(
|
|
133
|
-
self.operation == DiffOperation.add
|
|
134
|
-
and self.this_hash is not None
|
|
135
|
-
and self.other_hash is None
|
|
136
|
-
)
|
|
158
|
+
(self.operation == DiffOperation.add and self.this_hash is not None and self.other_hash is None)
|
|
137
159
|
or (
|
|
138
160
|
self.operation == DiffOperation.modify
|
|
139
161
|
and self.this_hash is not None
|
|
@@ -288,6 +310,17 @@ def GetDestinationHelp() -> str:
|
|
|
288
310
|
fast_glacier://MyFastGlacierAccount@us-west-2
|
|
289
311
|
fast_glacier://MyFastGlacierAccount@us-west-2/Glacier/Dir
|
|
290
312
|
|
|
313
|
+
S3 Browser
|
|
314
|
+
----------
|
|
315
|
+
Write content using the S3 Browser application (https://s3browser.com/).
|
|
316
|
+
|
|
317
|
+
Format:
|
|
318
|
+
s3_browser://<s3_browser_account_name>@<bucket_name>[/<working_dir>]
|
|
319
|
+
|
|
320
|
+
Examples:
|
|
321
|
+
s3_browser://MyS3BrowserAccount@MyBucket
|
|
322
|
+
s3_browser://MyS3BrowserAccount@MyBucket/A/Working/Dir
|
|
323
|
+
|
|
291
324
|
""",
|
|
292
325
|
).replace("\n", "\n\n")
|
|
293
326
|
|
|
@@ -337,6 +370,16 @@ def YieldDataStore(
|
|
|
337
370
|
)
|
|
338
371
|
return
|
|
339
372
|
|
|
373
|
+
# S3 Browser
|
|
374
|
+
s3_browser_match = S3_BROWSER_TEMPLATE_REGEX.match(destination)
|
|
375
|
+
if s3_browser_match:
|
|
376
|
+
yield S3BrowserDataStore(
|
|
377
|
+
s3_browser_match.group("account_name"),
|
|
378
|
+
s3_browser_match.group("bucket_name"),
|
|
379
|
+
Path(s3_browser_match.group("working_dir") or ""),
|
|
380
|
+
)
|
|
381
|
+
return
|
|
382
|
+
|
|
340
383
|
# Create a FileSystemDataStore instance
|
|
341
384
|
is_local_filesystem_override_value_for_testing: Optional[bool] = None
|
|
342
385
|
|
|
@@ -381,14 +424,10 @@ def CreateFilterFunc(
|
|
|
381
424
|
) -> bool:
|
|
382
425
|
filename_str = filename.as_posix()
|
|
383
426
|
|
|
384
|
-
if file_excludes is not None and any(
|
|
385
|
-
exclude.search(filename_str) for exclude in file_excludes
|
|
386
|
-
):
|
|
427
|
+
if file_excludes is not None and any(exclude.search(filename_str) for exclude in file_excludes):
|
|
387
428
|
return False
|
|
388
429
|
|
|
389
|
-
if file_includes is not None and not any(
|
|
390
|
-
include.search(filename_str) for include in file_includes
|
|
391
|
-
):
|
|
430
|
+
if file_includes is not None and not any(include.search(filename_str) for include in file_includes):
|
|
392
431
|
return False
|
|
393
432
|
|
|
394
433
|
return True
|
|
@@ -450,11 +489,7 @@ def CalculateDiffs(
|
|
|
450
489
|
stream.write(
|
|
451
490
|
" {}) [{}] {}\n".format(
|
|
452
491
|
diff_index + 1,
|
|
453
|
-
(
|
|
454
|
-
"FILE"
|
|
455
|
-
if diff.path.is_file()
|
|
456
|
-
else "DIR " if diff.path.is_dir() else "????"
|
|
457
|
-
),
|
|
492
|
+
("FILE" if diff.path.is_file() else "DIR " if diff.path.is_dir() else "????"),
|
|
458
493
|
(
|
|
459
494
|
diff.path
|
|
460
495
|
if dm.capabilities.is_headless
|
|
@@ -519,9 +554,7 @@ def WriteFile(
|
|
|
519
554
|
dest_filename: Path,
|
|
520
555
|
status_func: Callable[[int], None],
|
|
521
556
|
) -> None:
|
|
522
|
-
temp_dest_filename =
|
|
523
|
-
dest_filename.parent / f"{dest_filename.stem}.__temp__{dest_filename.suffix}"
|
|
524
|
-
)
|
|
557
|
+
temp_dest_filename = dest_filename.parent / f"{dest_filename.stem}.__temp__{dest_filename.suffix}"
|
|
525
558
|
|
|
526
559
|
with source_filename.open("rb") as source:
|
|
527
560
|
data_store.MakeDirs(temp_dest_filename.parent)
|
|
@@ -620,9 +653,7 @@ def CopyLocalContent(
|
|
|
620
653
|
destination_data_store,
|
|
621
654
|
diff.path,
|
|
622
655
|
dest_filename,
|
|
623
|
-
lambda bytes_transferred: cast(
|
|
624
|
-
None, status.OnProgress(bytes_transferred, None)
|
|
625
|
-
),
|
|
656
|
+
lambda bytes_transferred: cast(None, status.OnProgress(bytes_transferred, None)),
|
|
626
657
|
)
|
|
627
658
|
else:
|
|
628
659
|
assert False, diff.path # pragma: no cover
|
FileBackup/Mirror.py
CHANGED
|
@@ -118,9 +118,7 @@ def Backup(
|
|
|
118
118
|
)
|
|
119
119
|
else:
|
|
120
120
|
with dm.Nested("\nReading the destination snapshot...") as destination_dm:
|
|
121
|
-
destination_snapshot = Snapshot.LoadPersisted(
|
|
122
|
-
destination_dm, destination_data_store
|
|
123
|
-
)
|
|
121
|
+
destination_snapshot = Snapshot.LoadPersisted(destination_dm, destination_data_store)
|
|
124
122
|
|
|
125
123
|
if destination_dm.result != 0:
|
|
126
124
|
return
|
|
@@ -151,9 +149,7 @@ def Backup(
|
|
|
151
149
|
# Persist all content
|
|
152
150
|
with dm.Nested("\nPersisting content...") as persist_dm:
|
|
153
151
|
# Transfer the snapshot
|
|
154
|
-
pending_snapshot_filename = Path(
|
|
155
|
-
Snapshot.PERSISTED_FILE_NAME + Common.PENDING_COMMIT_EXTENSION
|
|
156
|
-
)
|
|
152
|
+
pending_snapshot_filename = Path(Snapshot.PERSISTED_FILE_NAME + Common.PENDING_COMMIT_EXTENSION)
|
|
157
153
|
|
|
158
154
|
temp_directory = PathEx.CreateTempDirectory()
|
|
159
155
|
|
|
@@ -268,9 +264,7 @@ def Backup(
|
|
|
268
264
|
],
|
|
269
265
|
Remove,
|
|
270
266
|
quiet=quiet,
|
|
271
|
-
max_num_threads=(
|
|
272
|
-
None if destination_data_store.ExecuteInParallel() else 1
|
|
273
|
-
),
|
|
267
|
+
max_num_threads=(None if destination_data_store.ExecuteInParallel() else 1),
|
|
274
268
|
refresh_per_second=Common.EXECUTE_TASKS_REFRESH_PER_SECOND,
|
|
275
269
|
),
|
|
276
270
|
)
|
|
@@ -314,9 +308,7 @@ def Backup(
|
|
|
314
308
|
lambda fullpath, item_type: (
|
|
315
309
|
cast(FileBasedDataStore, destination_data_store).RemoveDir(fullpath)
|
|
316
310
|
if item_type == Common.ItemType.Dir
|
|
317
|
-
else cast(FileBasedDataStore, destination_data_store).RemoveFile(
|
|
318
|
-
fullpath
|
|
319
|
-
)
|
|
311
|
+
else cast(FileBasedDataStore, destination_data_store).RemoveFile(fullpath)
|
|
320
312
|
),
|
|
321
313
|
),
|
|
322
314
|
]:
|
|
@@ -349,9 +341,7 @@ def Backup(
|
|
|
349
341
|
],
|
|
350
342
|
Commit,
|
|
351
343
|
quiet=quiet,
|
|
352
|
-
max_num_threads=(
|
|
353
|
-
None if destination_data_store.ExecuteInParallel() else 1
|
|
354
|
-
),
|
|
344
|
+
max_num_threads=(None if destination_data_store.ExecuteInParallel() else 1),
|
|
355
345
|
refresh_per_second=Common.EXECUTE_TASKS_REFRESH_PER_SECOND,
|
|
356
346
|
)
|
|
357
347
|
|
|
@@ -431,14 +421,10 @@ def Validate(
|
|
|
431
421
|
# the backup with the values of the current snapshot are based on what is on the file
|
|
432
422
|
# system. Convert the data in the mirror snapshot so it matches the values in the
|
|
433
423
|
# current snapshot before we compare the contents of each.
|
|
434
|
-
new_root = Snapshot.Node(
|
|
435
|
-
None, None, Common.DirHashPlaceholder(explicitly_added=False), None
|
|
436
|
-
)
|
|
424
|
+
new_root = Snapshot.Node(None, None, Common.DirHashPlaceholder(explicitly_added=False), None)
|
|
437
425
|
|
|
438
426
|
for node in mirrored_snapshot.node.Enum():
|
|
439
|
-
destination_path = destination_data_store.SnapshotFilenameToDestinationName(
|
|
440
|
-
node.fullpath
|
|
441
|
-
)
|
|
427
|
+
destination_path = destination_data_store.SnapshotFilenameToDestinationName(node.fullpath)
|
|
442
428
|
|
|
443
429
|
if node.is_dir:
|
|
444
430
|
if not node.children:
|
FileBackup/Offsite.py
CHANGED
|
@@ -82,8 +82,7 @@ class SnapshotFilenames:
|
|
|
82
82
|
return cls(
|
|
83
83
|
backup_name,
|
|
84
84
|
snapshot_filename,
|
|
85
|
-
snapshot_filename.parent
|
|
86
|
-
/ f"{snapshot_filename.stem}.__pending__{snapshot_filename.suffix}",
|
|
85
|
+
snapshot_filename.parent / f"{snapshot_filename.stem}.__pending__{snapshot_filename.suffix}",
|
|
87
86
|
)
|
|
88
87
|
|
|
89
88
|
|
|
@@ -331,9 +330,7 @@ def Backup(
|
|
|
331
330
|
file_content_data_store,
|
|
332
331
|
diff.path,
|
|
333
332
|
dest_filename,
|
|
334
|
-
lambda bytes_written: cast(
|
|
335
|
-
None, status.OnProgress(bytes_written, None)
|
|
336
|
-
),
|
|
333
|
+
lambda bytes_written: cast(None, status.OnProgress(bytes_written, None)),
|
|
337
334
|
)
|
|
338
335
|
|
|
339
336
|
return dest_filename
|
|
@@ -351,15 +348,10 @@ def Backup(
|
|
|
351
348
|
ExecuteTasks.TransformTasksEx(
|
|
352
349
|
preserve_dm,
|
|
353
350
|
"Processing",
|
|
354
|
-
[
|
|
355
|
-
ExecuteTasks.TaskData(str(diff.path), diff)
|
|
356
|
-
for diff in diffs_to_process
|
|
357
|
-
],
|
|
351
|
+
[ExecuteTasks.TaskData(str(diff.path), diff) for diff in diffs_to_process],
|
|
358
352
|
PrepareTask,
|
|
359
353
|
quiet=quiet,
|
|
360
|
-
max_num_threads=(
|
|
361
|
-
None if file_content_data_store.ExecuteInParallel() else 1
|
|
362
|
-
),
|
|
354
|
+
max_num_threads=(None if file_content_data_store.ExecuteInParallel() else 1),
|
|
363
355
|
refresh_per_second=Common.EXECUTE_TASKS_REFRESH_PER_SECOND,
|
|
364
356
|
)
|
|
365
357
|
|
|
@@ -436,12 +428,12 @@ def Backup(
|
|
|
436
428
|
) as validate_dm:
|
|
437
429
|
assert zip_binary is not None
|
|
438
430
|
|
|
439
|
-
command_line =
|
|
440
|
-
|
|
441
|
-
validate_dm.WriteVerbose(
|
|
442
|
-
f"Command Line: {_ScrubZipCommandLine(command_line)}\n\n"
|
|
431
|
+
command_line = (
|
|
432
|
+
f'{zip_binary} t "{file_content_root / ARCHIVE_FILENAME}.001"{encryption_arg}'
|
|
443
433
|
)
|
|
444
434
|
|
|
435
|
+
validate_dm.WriteVerbose(f"Command Line: {_ScrubZipCommandLine(command_line)}\n\n")
|
|
436
|
+
|
|
445
437
|
with validate_dm.YieldStream() as stream:
|
|
446
438
|
validate_dm.result = SubprocessEx.Stream(command_line, stream)
|
|
447
439
|
|
|
@@ -639,9 +631,7 @@ def Restore(
|
|
|
639
631
|
for directory in directories:
|
|
640
632
|
match = dir_regex.match(directory)
|
|
641
633
|
if not match:
|
|
642
|
-
preprocess_dm.WriteError(
|
|
643
|
-
f"'{directory}' is not a recognized directory name.\n"
|
|
644
|
-
)
|
|
634
|
+
preprocess_dm.WriteError(f"'{directory}' is not a recognized directory name.\n")
|
|
645
635
|
return
|
|
646
636
|
|
|
647
637
|
offsite_directories.setdefault(directory, []).append(
|
|
@@ -774,17 +764,13 @@ def Restore(
|
|
|
774
764
|
dest: Path,
|
|
775
765
|
) -> None:
|
|
776
766
|
dest /= source.name
|
|
777
|
-
os.symlink(
|
|
778
|
-
source, dest, target_is_directory=source.is_dir()
|
|
779
|
-
)
|
|
767
|
+
os.symlink(source, dest, target_is_directory=source.is_dir())
|
|
780
768
|
|
|
781
769
|
# ----------------------------------------------------------------------
|
|
782
770
|
|
|
783
771
|
func = CreateSymLink
|
|
784
772
|
|
|
785
|
-
temp_dest_dir = destination_dir.parent / (
|
|
786
|
-
destination_dir.name + "__temp__"
|
|
787
|
-
)
|
|
773
|
+
temp_dest_dir = destination_dir.parent / (destination_dir.name + "__temp__")
|
|
788
774
|
|
|
789
775
|
shutil.rmtree(temp_dest_dir, ignore_errors=True)
|
|
790
776
|
temp_dest_dir.mkdir(parents=True)
|
|
@@ -813,27 +799,22 @@ def Restore(
|
|
|
813
799
|
|
|
814
800
|
# ----------------------------------------------------------------------
|
|
815
801
|
|
|
816
|
-
directory_working_dirs: list[Path | None | Exception] = (
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
PrepareTask,
|
|
825
|
-
quiet=quiet,
|
|
826
|
-
max_num_threads=None if ssd and data_store.ExecuteInParallel() else 1,
|
|
827
|
-
refresh_per_second=Common.EXECUTE_TASKS_REFRESH_PER_SECOND,
|
|
828
|
-
)
|
|
802
|
+
directory_working_dirs: list[Path | None | Exception] = ExecuteTasks.TransformTasksEx(
|
|
803
|
+
preprocess_dm,
|
|
804
|
+
"Processing",
|
|
805
|
+
[ExecuteTasks.TaskData(str(directory), directory) for directory in directories],
|
|
806
|
+
PrepareTask,
|
|
807
|
+
quiet=quiet,
|
|
808
|
+
max_num_threads=None if ssd and data_store.ExecuteInParallel() else 1,
|
|
809
|
+
refresh_per_second=Common.EXECUTE_TASKS_REFRESH_PER_SECOND,
|
|
829
810
|
)
|
|
830
811
|
|
|
831
812
|
if preprocess_dm.result != 0:
|
|
832
813
|
return
|
|
833
814
|
|
|
834
|
-
assert all(
|
|
835
|
-
|
|
836
|
-
)
|
|
815
|
+
assert all(isinstance(working_dir, Path) for working_dir in directory_working_dirs), (
|
|
816
|
+
directory_working_dirs
|
|
817
|
+
)
|
|
837
818
|
|
|
838
819
|
with preprocess_dm.Nested("Staging working content...") as stage_dm:
|
|
839
820
|
# ----------------------------------------------------------------------
|
|
@@ -864,9 +845,7 @@ def Restore(
|
|
|
864
845
|
|
|
865
846
|
with stage_dm.Nested(
|
|
866
847
|
f"Processing '{directory}' ({index + 1} of {len(directories)})...",
|
|
867
|
-
lambda: "{} added".format(
|
|
868
|
-
inflect.no("instruction", len(these_instructions))
|
|
869
|
-
),
|
|
848
|
+
lambda: "{} added".format(inflect.no("instruction", len(these_instructions))),
|
|
870
849
|
):
|
|
871
850
|
# link the content
|
|
872
851
|
for root_str, _, filenames in os.walk(
|
|
@@ -1077,9 +1056,7 @@ def Restore(
|
|
|
1077
1056
|
Common.DiffOperation.remove: ("Removing", OnRemoveInstruction),
|
|
1078
1057
|
}
|
|
1079
1058
|
|
|
1080
|
-
for directory_index, (directory, these_instructions) in enumerate(
|
|
1081
|
-
instructions.items()
|
|
1082
|
-
):
|
|
1059
|
+
for directory_index, (directory, these_instructions) in enumerate(instructions.items()):
|
|
1083
1060
|
with all_instructions_dm.Nested(
|
|
1084
1061
|
f"Processing '{directory}' ({directory_index + 1} of {len(instructions)})...",
|
|
1085
1062
|
suffix="\n",
|
|
@@ -1117,9 +1094,7 @@ def Restore(
|
|
|
1117
1094
|
|
|
1118
1095
|
if not dry_run:
|
|
1119
1096
|
for instruction_index, instruction in enumerate(these_instructions):
|
|
1120
|
-
prefix, on_instruction_func = operation_map[
|
|
1121
|
-
instruction.operation
|
|
1122
|
-
]
|
|
1097
|
+
prefix, on_instruction_func = operation_map[instruction.operation]
|
|
1123
1098
|
|
|
1124
1099
|
with instructions_dm.Nested(
|
|
1125
1100
|
f"{prefix} the {'file' if instruction.file_content_path is not None else 'directory'} '{instruction.local_filename}' ({instruction_index + 1} of {len(these_instructions)})...",
|
|
@@ -1495,9 +1470,7 @@ def _VerifyRestoredFiles(
|
|
|
1495
1470
|
for root_str, _, filenames in os.walk(contents_dir):
|
|
1496
1471
|
root = Path(root_str)
|
|
1497
1472
|
|
|
1498
|
-
all_filenames += [
|
|
1499
|
-
root / filename for filename in filenames if filename != INDEX_HASH_FILENAME
|
|
1500
|
-
]
|
|
1473
|
+
all_filenames += [root / filename for filename in filenames if filename != INDEX_HASH_FILENAME]
|
|
1501
1474
|
|
|
1502
1475
|
data_store = FileSystemDataStore()
|
|
1503
1476
|
|
FileBackup/Snapshot.py
CHANGED
|
@@ -94,9 +94,7 @@ class Snapshot:
|
|
|
94
94
|
)
|
|
95
95
|
|
|
96
96
|
assert (isinstance(self.hash_value, DirHashPlaceholder) and self.file_size is None) or (
|
|
97
|
-
isinstance(self.hash_value, str)
|
|
98
|
-
and self.file_size is not None
|
|
99
|
-
and self.file_size >= 0
|
|
97
|
+
isinstance(self.hash_value, str) and self.file_size is not None and self.file_size >= 0
|
|
100
98
|
), (self.hash_value, self.file_size)
|
|
101
99
|
|
|
102
100
|
assert not self.children or self.is_dir
|
|
@@ -179,9 +177,7 @@ class Snapshot:
|
|
|
179
177
|
result = cls(name, parent, hash_value, file_size)
|
|
180
178
|
|
|
181
179
|
if result.is_dir:
|
|
182
|
-
result.children = {
|
|
183
|
-
k: cls.FromJson(k, result, v) for k, v in value["children"].items()
|
|
184
|
-
}
|
|
180
|
+
result.children = {k: cls.FromJson(k, result, v) for k, v in value["children"].items()}
|
|
185
181
|
|
|
186
182
|
return result
|
|
187
183
|
|
|
@@ -343,9 +339,7 @@ class Snapshot:
|
|
|
343
339
|
new_node = node.children.get(part, None)
|
|
344
340
|
|
|
345
341
|
if new_node is None:
|
|
346
|
-
new_node = self.__class__(
|
|
347
|
-
part, node, DirHashPlaceholder(explicitly_added=False), None
|
|
348
|
-
)
|
|
342
|
+
new_node = self.__class__(part, node, DirHashPlaceholder(explicitly_added=False), None)
|
|
349
343
|
node.children[part] = new_node
|
|
350
344
|
|
|
351
345
|
node = new_node
|
|
@@ -427,9 +421,7 @@ class Snapshot:
|
|
|
427
421
|
def OnNestedExit():
|
|
428
422
|
# We need to do some extra work because inflect doesn't work when we need a word between
|
|
429
423
|
# the number and plural where the singular form of the plural ends in 'y'.
|
|
430
|
-
num_empty_dirs = sum(
|
|
431
|
-
len(input_info.empty_dirs) for input_info in all_input_infos.values()
|
|
432
|
-
)
|
|
424
|
+
num_empty_dirs = sum(len(input_info.empty_dirs) for input_info in all_input_infos.values())
|
|
433
425
|
|
|
434
426
|
return "{} found, {} empty {} found".format(
|
|
435
427
|
inflect.no(
|
|
@@ -460,9 +452,7 @@ class Snapshot:
|
|
|
460
452
|
filenames.append(input_item)
|
|
461
453
|
|
|
462
454
|
elif input_item_type == ItemType.Dir:
|
|
463
|
-
for this_root, these_directories, these_filenames in data_store.Walk(
|
|
464
|
-
input_item
|
|
465
|
-
):
|
|
455
|
+
for this_root, these_directories, these_filenames in data_store.Walk(input_item):
|
|
466
456
|
if not these_directories and not these_filenames:
|
|
467
457
|
empty_dirs.append(this_root)
|
|
468
458
|
continue
|
|
@@ -471,9 +461,7 @@ class Snapshot:
|
|
|
471
461
|
fullpath = this_root / this_filename
|
|
472
462
|
|
|
473
463
|
if data_store.GetItemType(fullpath) != ItemType.File:
|
|
474
|
-
status.OnInfo(
|
|
475
|
-
f"The file '{fullpath}' is not a supported item type."
|
|
476
|
-
)
|
|
464
|
+
status.OnInfo(f"The file '{fullpath}' is not a supported item type.")
|
|
477
465
|
continue
|
|
478
466
|
|
|
479
467
|
if not filter_filename_func(fullpath):
|
|
@@ -489,9 +477,7 @@ class Snapshot:
|
|
|
489
477
|
else:
|
|
490
478
|
# By default, FileSystemDataStore and SFTPDataStore will not get here, as
|
|
491
479
|
# the will not traverse directory symlinks.
|
|
492
|
-
raise Exception(
|
|
493
|
-
f"'{input_item}' is not a supported item type."
|
|
494
|
-
) # pragma: no cover
|
|
480
|
+
raise Exception(f"'{input_item}' is not a supported item type.") # pragma: no cover
|
|
495
481
|
|
|
496
482
|
short_desc = "{} found".format(inflect.no("file", len(filenames)))
|
|
497
483
|
|
|
@@ -529,8 +515,7 @@ class Snapshot:
|
|
|
529
515
|
return cls(Snapshot.Node(None, None, DirHashPlaceholder(explicitly_added=False), None))
|
|
530
516
|
|
|
531
517
|
with dm.Nested(
|
|
532
|
-
"\n"
|
|
533
|
-
+ ("Calculating hashes..." if calculate_hashes else "Retrieving file information..."),
|
|
518
|
+
"\n" + ("Calculating hashes..." if calculate_hashes else "Retrieving file information..."),
|
|
534
519
|
) as hashes_dm:
|
|
535
520
|
# ----------------------------------------------------------------------
|
|
536
521
|
def PrepareTask(
|
|
@@ -651,9 +636,7 @@ class Snapshot:
|
|
|
651
636
|
*Progress.get_default_columns(),
|
|
652
637
|
TimeElapsedColumn(),
|
|
653
638
|
"{task.fields[status]}",
|
|
654
|
-
console=Capabilities.Get(stdout_context.stream).CreateRichConsole(
|
|
655
|
-
stdout_context.stream
|
|
656
|
-
),
|
|
639
|
+
console=Capabilities.Get(stdout_context.stream).CreateRichConsole(stdout_context.stream),
|
|
657
640
|
transient=True,
|
|
658
641
|
) as progress_bar:
|
|
659
642
|
total_progress_id = progress_bar.add_task(
|
|
@@ -674,9 +657,7 @@ class Snapshot:
|
|
|
674
657
|
progress_bar.update(total_progress_id, advance=len(chunk))
|
|
675
658
|
|
|
676
659
|
try:
|
|
677
|
-
return Snapshot(
|
|
678
|
-
Snapshot.Node.FromJson(None, None, json.loads(content.decode("UTF-8")))
|
|
679
|
-
)
|
|
660
|
+
return Snapshot(Snapshot.Node.FromJson(None, None, json.loads(content.decode("UTF-8"))))
|
|
680
661
|
except KeyError as ex:
|
|
681
662
|
raise Exception(f"The content at '{snapshot_filename}' is not valid.") from ex
|
|
682
663
|
|
|
@@ -704,8 +685,12 @@ class Snapshot:
|
|
|
704
685
|
"""Enumerates the differences between two Snapshots"""
|
|
705
686
|
|
|
706
687
|
if compare_hashes:
|
|
707
|
-
|
|
688
|
+
|
|
689
|
+
def CompareFunc(a, b) -> bool:
|
|
690
|
+
return a.hash_value == b.hash_value
|
|
708
691
|
else:
|
|
709
|
-
compare_func = lambda a, b: a.file_size == b.file_size
|
|
710
692
|
|
|
711
|
-
|
|
693
|
+
def CompareFunc(a, b) -> bool:
|
|
694
|
+
return a.file_size == b.file_size
|
|
695
|
+
|
|
696
|
+
yield from self.node.CreateDiffs(other.node, CompareFunc)[0]
|
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.4.1"
|
FileBackup/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: FileBackup
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Tool for creating and restoring file system backups.
|
|
5
|
+
Project-URL: Homepage, https://github.com/davidbrownell/FileBackup
|
|
6
|
+
Project-URL: Documentation, https://github.com/davidbrownell/FileBackup
|
|
7
|
+
Project-URL: Repository, https://github.com/davidbrownell/FileBackup
|
|
8
|
+
Author-email: David Brownell <github@DavidBrownell.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: backup,mirror,offsite,restore
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Natural Language :: English
|
|
19
|
+
Classifier: Operating System :: MacOS
|
|
20
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
21
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
22
|
+
Classifier: Programming Language :: Python
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
28
|
+
Classifier: Topic :: System :: Archiving :: Mirroring
|
|
29
|
+
Classifier: Topic :: System :: Systems Administration
|
|
30
|
+
Classifier: Topic :: Utilities
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Requires-Dist: dbrownell-common>=0.14.6
|
|
33
|
+
Requires-Dist: paramiko>=3.5.1
|
|
34
|
+
Requires-Dist: typer>=0.15.3
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
**Project:**
|
|
38
|
+
[](https://github.com/davidbrownell/FileBackup/blob/master/LICENSE)
|
|
39
|
+
|
|
40
|
+
**Package:**
|
|
41
|
+
[](https://pypi.org/project/FileBackup/)
|
|
42
|
+
[](https://pypi.org/project/FileBackup/)
|
|
43
|
+
[](https://pypistats.org/packages/FileBackup)
|
|
44
|
+
|
|
45
|
+
**Development:**
|
|
46
|
+
[](https://github.com/astral-sh/uv)
|
|
47
|
+
[](https://github.com/davidbrownell/FileBackup/actions/workflows/CICD.yml)
|
|
48
|
+
[](https://github.com/davidbrownell/FileBackup/actions)
|
|
49
|
+
[](https://github.com/davidbrownell/FileBackup/commits/main/)
|
|
50
|
+
|
|
51
|
+
<!-- Content above this delimiter will be copied to the generated README.md file. DO NOT REMOVE THIS COMMENT, as it will cause regeneration to fail. -->
|
|
52
|
+
|
|
53
|
+
## Contents
|
|
54
|
+
- [Overview](#overview)
|
|
55
|
+
- [Installation](#installation)
|
|
56
|
+
- [Development](#development)
|
|
57
|
+
- [Additional Information](#additional-information)
|
|
58
|
+
- [License](#license)
|
|
59
|
+
|
|
60
|
+
## Overview
|
|
61
|
+
TODO: Complete this section
|
|
62
|
+
|
|
63
|
+
### How to use FileBackup
|
|
64
|
+
TODO: Complete this section
|
|
65
|
+
|
|
66
|
+
<!-- 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. -->
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
| Installation Method | Command |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| Via [uv](https://github.com/astral-sh/uv) | `uv add FileBackup` |
|
|
73
|
+
| Via [pip](https://pip.pypa.io/en/stable/) | `pip install FileBackup` |
|
|
74
|
+
|
|
75
|
+
### Verifying Signed Artifacts
|
|
76
|
+
Artifacts are signed and validated using [py-minisign](https://github.com/x13a/py-minisign) and the public key in the file `./minisign_key.pub`.
|
|
77
|
+
|
|
78
|
+
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:
|
|
79
|
+
|
|
80
|
+
`uv run --with py-minisign python -c "import minisign; minisign.PublicKey.from_file('minisign_key.pub').verify_file('<filename>')"`
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
## Additional Information
|
|
86
|
+
Additional information can be found at these locations.
|
|
87
|
+
|
|
88
|
+
| Title | Document | Description |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| Code of Conduct | [CODE_OF_CONDUCT.md](https://github.com/davidbrownell/FileBackup/blob/main/CODE_OF_CONDUCT.md) | Information about the norms, rules, and responsibilities we adhere to when participating in this open source community. |
|
|
91
|
+
| Contributing | [CONTRIBUTING.md](https://github.com/davidbrownell/FileBackup/blob/main/CONTRIBUTING.md) | Information about contributing to this project. |
|
|
92
|
+
| Development | [DEVELOPMENT.md](https://github.com/davidbrownell/FileBackup/blob/main/DEVELOPMENT.md) | Information about development activities involved in making changes to this project. |
|
|
93
|
+
| Governance | [GOVERNANCE.md](https://github.com/davidbrownell/FileBackup/blob/main/GOVERNANCE.md) | Information about how this project is governed. |
|
|
94
|
+
| Maintainers | [MAINTAINERS.md](https://github.com/davidbrownell/FileBackup/blob/main/MAINTAINERS.md) | Information about individuals who maintain this project. |
|
|
95
|
+
| Security | [SECURITY.md](https://github.com/davidbrownell/FileBackup/blob/main/SECURITY.md) | Information about how to privately report security issues associated with this project. |
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
FileBackup is licensed under the <a href="https://choosealicense.com/licenses/MIT/" target="_blank">MIT</a> license.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
FileBackup/Mirror.py,sha256=GDgTWVNUYn5O7vlH5YVi0LppwQN4R99J61g1UCBwlfI,23544
|
|
2
|
+
FileBackup/Offsite.py,sha256=GAEUvdQ2enAMfIyiCRQb5B23xHizNda2aRDdq76PIfQ,61947
|
|
3
|
+
FileBackup/Snapshot.py,sha256=Dj-XFjAFWkqNZIlMHqTOI1Ygdm1jB3orIwDxMQSrupQ,26970
|
|
4
|
+
FileBackup/__init__.py,sha256=oGJQp7XFkEmtI84E9DqGrdtVoA1X3TK2mwORIAotyfI,597
|
|
5
|
+
FileBackup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
FileBackup/CommandLine/CommandLineArguments.py,sha256=7oLUf9tODd_OjBNzIjQiiyZ9ujy-KbW53Pg0dF0MQG8,2378
|
|
7
|
+
FileBackup/CommandLine/EntryPoint.py,sha256=Fn62wtKV6uiTJ1dQpolNFUyWWeUOKKgetOFSXPgoe-w,1813
|
|
8
|
+
FileBackup/CommandLine/MirrorEntryPoint.py,sha256=WlGIz_AZ7OdE-hVL19lQExUXbEjQ8_4bbIuITWxdWO4,6704
|
|
9
|
+
FileBackup/CommandLine/OffsiteEntryPoint.py,sha256=ZpZBpIk4ZLC27oWjvDriiqqwtwqHLJ2tfMOFdhVyigU,12368
|
|
10
|
+
FileBackup/CommandLine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
FileBackup/DataStore/FastGlacierDataStore.py,sha256=5J4W9FqB92hnHoRoZLo-DfMQ6uyjaOAWWrnuc0pvnJo,3017
|
|
12
|
+
FileBackup/DataStore/FileSystemDataStore.py,sha256=XUvIQe2Yl-W6OPfvsJlPzDPsGclDOkKlozO0GJ-05bo,6791
|
|
13
|
+
FileBackup/DataStore/S3BrowserDataStore.py,sha256=3NHl-advSbyXuL1kDu1wswbcAZbhmAGDu8yzlOuY5Us,2954
|
|
14
|
+
FileBackup/DataStore/SFTPDataStore.py,sha256=TKz-hwwHg4avxKHDFRRz7FlKx3d40wT15ug1gCpu6VU,11374
|
|
15
|
+
FileBackup/DataStore/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
FileBackup/DataStore/Interfaces/BulkStorageDataStore.py,sha256=qZo92nPDP_aVTxlohsc1ySnU9Skgxgo7Mj_tT0KxFZ8,1310
|
|
17
|
+
FileBackup/DataStore/Interfaces/DataStore.py,sha256=FpQmxoqBS5indJYRxibkSXHTgk7U6HFyViI0BDqCcVw,1169
|
|
18
|
+
FileBackup/DataStore/Interfaces/FileBasedDataStore.py,sha256=eMA_cQ-CED83z5uMDQZ18UFzBRvaXOwWz67kVu-2ftM,5307
|
|
19
|
+
FileBackup/DataStore/Interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
FileBackup/Impl/Common.py,sha256=2WFMx5BdCnZmo-cuLYZlIx4Nr_ZBKmJWfMaFVUpsqKo,24935
|
|
21
|
+
FileBackup/Impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
filebackup-0.4.1.dist-info/METADATA,sha256=RIPSLlTV_DHWAyA4w23ILziHhk022iKX50Xgy-DL04s,5802
|
|
23
|
+
filebackup-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
24
|
+
filebackup-0.4.1.dist-info/entry_points.txt,sha256=2SUVO3jHsi06MYfUEdqxaVwVvTQVjxzIWlHexRKWRgQ,121
|
|
25
|
+
filebackup-0.4.1.dist-info/licenses/LICENSE,sha256=2dPVskJuHRLwSF9fQiAfln1RVWya2DsKNPvgknRLTYw,1074
|
|
26
|
+
filebackup-0.4.1.dist-info/RECORD,,
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: FileBackup
|
|
3
|
-
Version: 0.3.6
|
|
4
|
-
Summary: Tool for creating and restoring file system backups.
|
|
5
|
-
Author-email: David Brownell <github@DavidBrownell.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/davidbrownell/FileBackup
|
|
8
|
-
Project-URL: Documentation, https://github.com/davidbrownell/FileBackup
|
|
9
|
-
Project-URL: Repository, https://github.com/davidbrownell/FileBackup
|
|
10
|
-
Keywords: backup,mirror,offsite,restore
|
|
11
|
-
Classifier: Development Status :: 4 - Beta
|
|
12
|
-
Classifier: Environment :: Console
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
-
Classifier: Intended Audience :: System Administrators
|
|
16
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
-
Classifier: Natural Language :: English
|
|
18
|
-
Classifier: Operating System :: MacOS
|
|
19
|
-
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
-
Classifier: Programming Language :: Python
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
-
Classifier: Topic :: System :: Archiving :: Backup
|
|
26
|
-
Classifier: Topic :: System :: Archiving :: Mirroring
|
|
27
|
-
Classifier: Topic :: System :: Systems Administration
|
|
28
|
-
Classifier: Topic :: Utilities
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
30
|
-
License-File: LICENSE.txt
|
|
31
|
-
Requires-Dist: dbrownell-Common
|
|
32
|
-
Requires-Dist: paramiko ~=3.4
|
|
33
|
-
Requires-Dist: typer ~=0.9
|
|
34
|
-
Provides-Extra: dev
|
|
35
|
-
Requires-Dist: dbrownell-DevTools ; extra == 'dev'
|
|
36
|
-
Provides-Extra: package
|
|
37
|
-
Requires-Dist: build ~=1.2 ; extra == 'package'
|
|
38
|
-
Requires-Dist: cx-Freeze ~=6.15 ; extra == 'package'
|
|
39
|
-
Requires-Dist: twine ~=5.1 ; extra == 'package'
|
|
40
|
-
|
|
41
|
-
# FileBackup
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Tool for creating and restoring file system backups.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
## Overview
|
|
50
|
-
TODO: Complete this section
|
|
51
|
-
|
|
52
|
-
### How to use FileBackup
|
|
53
|
-
TODO: Complete this section
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
## Additional Information
|
|
58
|
-
Additional information can be found at these locations.
|
|
59
|
-
|
|
60
|
-
<!-- [BEGIN] Additional Information -->
|
|
61
|
-
| Title | Document | Description |
|
|
62
|
-
| --- | --- | --- |
|
|
63
|
-
| Code of Conduct | [CODE_OF_CONDUCT.md](https://github.com/davidbrownell/FileBackup/blob/main/CODE_OF_CONDUCT.md) | Information about the the norms, rules, and responsibilities we adhere to when participating in this open source community. |
|
|
64
|
-
| Contributing | [CONTRIBUTING.md](https://github.com/davidbrownell/FileBackup/blob/main/CONTRIBUTING.md) | Information about contributing code changes to this project. |
|
|
65
|
-
| Development | [DEVELOPMENT.md](https://github.com/davidbrownell/FileBackup/blob/main/DEVELOPMENT.md) | Information about development activities involved in making changes to this project. |
|
|
66
|
-
| Governance | [GOVERNANCE.md](https://github.com/davidbrownell/FileBackup/blob/main/GOVERNANCE.md) | Information about how this project is governed. |
|
|
67
|
-
| Maintainers | [MAINTAINERS.md](https://github.com/davidbrownell/FileBackup/blob/main/MAINTAINERS.md) | Information about individuals who maintain this project. |
|
|
68
|
-
| Security | [SECURITY.md](https://github.com/davidbrownell/FileBackup/blob/main/SECURITY.md) | Information about how to privately report security issues associated with this project. |
|
|
69
|
-
<!-- [END] Additional Information -->
|
|
70
|
-
|
|
71
|
-
## License
|
|
72
|
-
|
|
73
|
-
FileBackup is licensed under the <a href="https://choosealicense.com/licenses/mit/" target="_blank">MIT</a> license.
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
FileBackup/Mirror.py,sha256=FA9vfvPHOJzmN__Tx09ndWMeMTSWcED52BuVfeOBuKc,23904
|
|
2
|
-
FileBackup/Offsite.py,sha256=2kK5ToJIHXIAPChtREcztp1e1L95ZJF6adIk-8d5V8g,62796
|
|
3
|
-
FileBackup/Snapshot.py,sha256=r6l35Tl75WIQALtzN81r62XNkzJiTddzuihmlSb56Aw,27336
|
|
4
|
-
FileBackup/__init__.py,sha256=lhHWzRFcuwSYms0q5XOm17GhlPgSqT4ESBJzRZP-5uo,597
|
|
5
|
-
FileBackup/CommandLine/CommandLineArguments.py,sha256=79K4jVxtZ8xk1TfSJT8KIdkqZFjCEAspoIKP21GUXxE,2410
|
|
6
|
-
FileBackup/CommandLine/EntryPoint.py,sha256=Fn62wtKV6uiTJ1dQpolNFUyWWeUOKKgetOFSXPgoe-w,1813
|
|
7
|
-
FileBackup/CommandLine/MirrorEntryPoint.py,sha256=ZQ3mz0AgBwwn7n9uOuxN-lEQvm22u8SQbpWASkvRsAA,6800
|
|
8
|
-
FileBackup/CommandLine/OffsiteEntryPoint.py,sha256=enbgeD_nhdR2T6UufySt33f4P2FnOVzsHiEktyVtLlo,12528
|
|
9
|
-
FileBackup/CommandLine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
FileBackup/DataStore/FastGlacierDataStore.py,sha256=YxmYY0IVZ6HTFE_2zMTz4g09Z9jJcXM9sMu_Yp86DAg,2982
|
|
11
|
-
FileBackup/DataStore/FileSystemDataStore.py,sha256=XUvIQe2Yl-W6OPfvsJlPzDPsGclDOkKlozO0GJ-05bo,6791
|
|
12
|
-
FileBackup/DataStore/SFTPDataStore.py,sha256=TKz-hwwHg4avxKHDFRRz7FlKx3d40wT15ug1gCpu6VU,11374
|
|
13
|
-
FileBackup/DataStore/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
FileBackup/DataStore/Interfaces/BulkStorageDataStore.py,sha256=qZo92nPDP_aVTxlohsc1ySnU9Skgxgo7Mj_tT0KxFZ8,1310
|
|
15
|
-
FileBackup/DataStore/Interfaces/DataStore.py,sha256=FpQmxoqBS5indJYRxibkSXHTgk7U6HFyViI0BDqCcVw,1169
|
|
16
|
-
FileBackup/DataStore/Interfaces/FileBasedDataStore.py,sha256=eMA_cQ-CED83z5uMDQZ18UFzBRvaXOwWz67kVu-2ftM,5307
|
|
17
|
-
FileBackup/DataStore/Interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
FileBackup/Impl/Common.py,sha256=8XV0ARuLLci8AGjvW2lfJyXbA2FnyRpUHel4AI-7ReM,23712
|
|
19
|
-
FileBackup/Impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
FileBackup-0.3.6.dist-info/LICENSE.txt,sha256=6ycKI8SWG920Saey4PGi5YUVLX0C_8hfqJr7Q0sqGI8,1071
|
|
21
|
-
FileBackup-0.3.6.dist-info/METADATA,sha256=VAmADMs2CGKydEooi87xZBiYv3EybFPp653_Lb7Xog0,3300
|
|
22
|
-
FileBackup-0.3.6.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
23
|
-
FileBackup-0.3.6.dist-info/entry_points.txt,sha256=OlHrvdhWJQGIp4vCUJAqOWxW7y0ZH59qQ-2ImWFUcPI,69
|
|
24
|
-
FileBackup-0.3.6.dist-info/top_level.txt,sha256=mXd2b6GEOpQCnmzgYEb6c_rfwlgP3RgmMuVDkQasxKw,11
|
|
25
|
-
FileBackup-0.3.6.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
FileBackup
|