filebackup 0.5.3__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 +75 -0
- FileBackup/CommandLine/EntryPoint.py +53 -0
- FileBackup/CommandLine/MirrorEntryPoint.py +182 -0
- FileBackup/CommandLine/OffsiteEntryPoint.py +326 -0
- FileBackup/CommandLine/__init__.py +0 -0
- FileBackup/DataStore/FastGlacierDataStore.py +81 -0
- FileBackup/DataStore/FileSystemDataStore.py +218 -0
- FileBackup/DataStore/Interfaces/BulkStorageDataStore.py +36 -0
- FileBackup/DataStore/Interfaces/DataStore.py +37 -0
- FileBackup/DataStore/Interfaces/FileBasedDataStore.py +157 -0
- FileBackup/DataStore/Interfaces/__init__.py +0 -0
- FileBackup/DataStore/S3BrowserDataStore.py +81 -0
- FileBackup/DataStore/SFTPDataStore.py +355 -0
- FileBackup/DataStore/__init__.py +0 -0
- FileBackup/Impl/Common.py +704 -0
- FileBackup/Impl/__init__.py +0 -0
- FileBackup/Mirror.py +551 -0
- FileBackup/Offsite.py +1588 -0
- FileBackup/Snapshot.py +696 -0
- FileBackup/__init__.py +6 -0
- FileBackup/py.typed +0 -0
- filebackup-0.5.3.dist-info/METADATA +99 -0
- filebackup-0.5.3.dist-info/RECORD +25 -0
- filebackup-0.5.3.dist-info/WHEEL +4 -0
- filebackup-0.5.3.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# |
|
|
3
|
+
# | CommandLineArguments.py
|
|
4
|
+
# |
|
|
5
|
+
# | David Brownell <db@DavidBrownell.com>
|
|
6
|
+
# | 2024-06-12 13:20:05
|
|
7
|
+
# |
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
# |
|
|
10
|
+
# | Copyright David Brownell 2024
|
|
11
|
+
# | Distributed under the MIT License.
|
|
12
|
+
# |
|
|
13
|
+
# ----------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from typing import Pattern
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ----------------------------------------------------------------------
|
|
23
|
+
def ToRegex(
|
|
24
|
+
values: list[str],
|
|
25
|
+
) -> list[Pattern]:
|
|
26
|
+
expressions: list[Pattern] = []
|
|
27
|
+
|
|
28
|
+
for value in values:
|
|
29
|
+
try:
|
|
30
|
+
expressions.append(re.compile("^{}$".format(value)))
|
|
31
|
+
except re.error as ex:
|
|
32
|
+
raise typer.BadParameter("The regular expression '{}' is not valid ({}).".format(value, ex))
|
|
33
|
+
|
|
34
|
+
return expressions
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ----------------------------------------------------------------------
|
|
38
|
+
input_filename_or_dirs_argument = typer.Argument(
|
|
39
|
+
..., exists=True, resolve_path=True, help="Input filename or directory."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
ssd_option = typer.Option(
|
|
43
|
+
"--ssd",
|
|
44
|
+
help="Processes tasks in parallel to leverage the capabilities of solid-state-drives.",
|
|
45
|
+
)
|
|
46
|
+
ssd_option_default = False
|
|
47
|
+
|
|
48
|
+
quiet_option = typer.Option("--quiet", help="Reduce the amount of information displayed.")
|
|
49
|
+
quiet_option_default = False
|
|
50
|
+
|
|
51
|
+
force_option = typer.Option(
|
|
52
|
+
"--force",
|
|
53
|
+
help="Ignore previous backup information and overwrite all data in the destination data store.",
|
|
54
|
+
)
|
|
55
|
+
force_option_default = False
|
|
56
|
+
|
|
57
|
+
verbose_option = typer.Option("--verbose", help="Write verbose information to the terminal.")
|
|
58
|
+
verbose_option_default = False
|
|
59
|
+
|
|
60
|
+
debug_option = typer.Option("--debug", help="Write debug information to the terminal.")
|
|
61
|
+
debug_option_default = False
|
|
62
|
+
|
|
63
|
+
file_include_option = typer.Option(
|
|
64
|
+
"--file-include",
|
|
65
|
+
callback=ToRegex,
|
|
66
|
+
help="Regular expression (based on a posix path) used to include files and/or directories when preserving content.",
|
|
67
|
+
)
|
|
68
|
+
file_include_option_default: list[str] = []
|
|
69
|
+
|
|
70
|
+
file_exclude_option = typer.Option(
|
|
71
|
+
"--file-exclude",
|
|
72
|
+
callback=ToRegex,
|
|
73
|
+
help="Regular expression (based on a posix path) used to exclude files and/or directories when preserving content.",
|
|
74
|
+
)
|
|
75
|
+
file_exclude_option_default: list[str] = []
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# |
|
|
3
|
+
# | Copyright (c) 2024 David Brownell
|
|
4
|
+
# | Distributed under the MIT License.
|
|
5
|
+
# |
|
|
6
|
+
# ----------------------------------------------------------------------
|
|
7
|
+
"""Tools to backup and restore files and directories."""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from typer.core import TyperGroup # type: ignore [import-untyped]
|
|
14
|
+
|
|
15
|
+
from FileBackup import __version__
|
|
16
|
+
from FileBackup.CommandLine import MirrorEntryPoint
|
|
17
|
+
from FileBackup.CommandLine import OffsiteEntryPoint
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ----------------------------------------------------------------------
|
|
21
|
+
class NaturalOrderGrouper(TyperGroup):
|
|
22
|
+
# pylint: disable=missing-class-docstring
|
|
23
|
+
# ----------------------------------------------------------------------
|
|
24
|
+
def list_commands(self, *args, **kwargs): # pylint: disable=unused-argument
|
|
25
|
+
return self.commands.keys() # pragma: no cover
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ----------------------------------------------------------------------
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
cls=NaturalOrderGrouper,
|
|
31
|
+
help=__doc__,
|
|
32
|
+
no_args_is_help=True,
|
|
33
|
+
pretty_exceptions_show_locals=False,
|
|
34
|
+
pretty_exceptions_enable=False,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
app.add_typer(MirrorEntryPoint.app, name="mirror", help=MirrorEntryPoint.__doc__)
|
|
39
|
+
app.add_typer(OffsiteEntryPoint.app, name="offsite", help=OffsiteEntryPoint.__doc__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("version", no_args_is_help=False)
|
|
43
|
+
def Version():
|
|
44
|
+
"""Displays the current version and exits."""
|
|
45
|
+
|
|
46
|
+
sys.stdout.write(f"FileBackup v{__version__}\n")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ----------------------------------------------------------------------
|
|
50
|
+
# ----------------------------------------------------------------------
|
|
51
|
+
# ----------------------------------------------------------------------
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
app() # pragma: no cover
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# |
|
|
3
|
+
# | MirrorEntryPoint.py
|
|
4
|
+
# |
|
|
5
|
+
# | David Brownell <db@DavidBrownell.com>
|
|
6
|
+
# | 2024-06-12 13:16:52
|
|
7
|
+
# |
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
# |
|
|
10
|
+
# | Copyright David Brownell 2024
|
|
11
|
+
# | Distributed under the MIT License.
|
|
12
|
+
# |
|
|
13
|
+
# ----------------------------------------------------------------------
|
|
14
|
+
"""\
|
|
15
|
+
Mirrors backup content: files created locally will be added to the backup data store; files deleted
|
|
16
|
+
locally will be removed from the backup data store; files modified locally will be modified at the
|
|
17
|
+
backup data store.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import datetime
|
|
21
|
+
import textwrap
|
|
22
|
+
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Annotated, cast, Pattern
|
|
25
|
+
|
|
26
|
+
import typer
|
|
27
|
+
|
|
28
|
+
from dbrownell_Common.Streams.DoneManager import DoneManager, Flags as DoneManagerFlags # type: ignore [import-untyped]
|
|
29
|
+
from typer.core import TyperGroup # type: ignore [import-untyped]
|
|
30
|
+
|
|
31
|
+
from FileBackup.CommandLine import CommandLineArguments
|
|
32
|
+
from FileBackup.Impl import Common
|
|
33
|
+
from FileBackup import Mirror
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ----------------------------------------------------------------------
|
|
37
|
+
class NaturalOrderGrouper(TyperGroup):
|
|
38
|
+
# pylint: disable=missing-class-docstring
|
|
39
|
+
# ----------------------------------------------------------------------
|
|
40
|
+
def list_commands(self, *args, **kwargs): # pylint: disable=unused-argument
|
|
41
|
+
return self.commands.keys() # pragma: no cover
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ----------------------------------------------------------------------
|
|
45
|
+
app = typer.Typer(
|
|
46
|
+
cls=NaturalOrderGrouper,
|
|
47
|
+
help=__doc__,
|
|
48
|
+
no_args_is_help=True,
|
|
49
|
+
pretty_exceptions_show_locals=False,
|
|
50
|
+
pretty_exceptions_enable=False,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ----------------------------------------------------------------------
|
|
55
|
+
_destination_argument = typer.Argument(
|
|
56
|
+
...,
|
|
57
|
+
help="Destination data store used when mirroring local content; see the comments below for information on the different data store destination formats.",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ----------------------------------------------------------------------
|
|
62
|
+
@app.command(
|
|
63
|
+
"execute",
|
|
64
|
+
epilog=Common.GetDestinationHelp(),
|
|
65
|
+
no_args_is_help=True,
|
|
66
|
+
)
|
|
67
|
+
def Execute(
|
|
68
|
+
destination: Annotated[str, _destination_argument],
|
|
69
|
+
input_filename_or_dirs: Annotated[
|
|
70
|
+
list[Path],
|
|
71
|
+
CommandLineArguments.input_filename_or_dirs_argument,
|
|
72
|
+
],
|
|
73
|
+
ssd: Annotated[bool, CommandLineArguments.ssd_option] = CommandLineArguments.ssd_option_default,
|
|
74
|
+
force: Annotated[bool, CommandLineArguments.force_option] = CommandLineArguments.force_option_default,
|
|
75
|
+
verbose: Annotated[
|
|
76
|
+
bool, CommandLineArguments.verbose_option
|
|
77
|
+
] = CommandLineArguments.verbose_option_default,
|
|
78
|
+
quiet: Annotated[bool, CommandLineArguments.quiet_option] = CommandLineArguments.quiet_option_default,
|
|
79
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
80
|
+
file_include_params: Annotated[
|
|
81
|
+
list[str],
|
|
82
|
+
CommandLineArguments.file_include_option,
|
|
83
|
+
] = CommandLineArguments.file_include_option_default,
|
|
84
|
+
file_exclude_params: Annotated[
|
|
85
|
+
list[str],
|
|
86
|
+
CommandLineArguments.file_exclude_option,
|
|
87
|
+
] = CommandLineArguments.file_exclude_option_default,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Mirrors content to a backup data store."""
|
|
90
|
+
|
|
91
|
+
file_includes = cast(list[Pattern], file_include_params)
|
|
92
|
+
file_excludes = cast(list[Pattern], file_exclude_params)
|
|
93
|
+
|
|
94
|
+
del file_include_params
|
|
95
|
+
del file_exclude_params
|
|
96
|
+
|
|
97
|
+
with DoneManager.CreateCommandLine(
|
|
98
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
99
|
+
) as dm:
|
|
100
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
101
|
+
|
|
102
|
+
Mirror.Backup(
|
|
103
|
+
dm,
|
|
104
|
+
destination,
|
|
105
|
+
input_filename_or_dirs,
|
|
106
|
+
ssd=ssd,
|
|
107
|
+
force=force,
|
|
108
|
+
quiet=quiet,
|
|
109
|
+
file_includes=file_includes,
|
|
110
|
+
file_excludes=file_excludes,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ----------------------------------------------------------------------
|
|
115
|
+
@app.command(
|
|
116
|
+
"validate",
|
|
117
|
+
no_args_is_help=True,
|
|
118
|
+
epilog=textwrap.dedent(
|
|
119
|
+
"""\
|
|
120
|
+
{}
|
|
121
|
+
Validation Types
|
|
122
|
+
================
|
|
123
|
+
standard: Validates that files and directories at the destination exist and file sizes match the expected values.
|
|
124
|
+
complete: Validates that files and directories at the destination exist and file hashes match the expected values.
|
|
125
|
+
""",
|
|
126
|
+
)
|
|
127
|
+
.replace("\n", "\n\n")
|
|
128
|
+
.format(Common.GetDestinationHelp()),
|
|
129
|
+
)
|
|
130
|
+
def Validate(
|
|
131
|
+
destination: Annotated[str, _destination_argument],
|
|
132
|
+
validate_type: Annotated[
|
|
133
|
+
Mirror.ValidateType,
|
|
134
|
+
typer.Argument(
|
|
135
|
+
case_sensitive=False,
|
|
136
|
+
help="Specifies the type of validation to use; the the comments below for information on the different validation types.",
|
|
137
|
+
),
|
|
138
|
+
] = Mirror.ValidateType.standard,
|
|
139
|
+
ssd: Annotated[bool, CommandLineArguments.ssd_option] = CommandLineArguments.ssd_option_default,
|
|
140
|
+
verbose: Annotated[
|
|
141
|
+
bool, CommandLineArguments.verbose_option
|
|
142
|
+
] = CommandLineArguments.verbose_option_default,
|
|
143
|
+
quiet: Annotated[bool, CommandLineArguments.quiet_option] = CommandLineArguments.quiet_option_default,
|
|
144
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Validates previously mirrored content in the backup data store."""
|
|
147
|
+
|
|
148
|
+
with DoneManager.CreateCommandLine(
|
|
149
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
150
|
+
) as dm:
|
|
151
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
152
|
+
|
|
153
|
+
Mirror.Validate(
|
|
154
|
+
dm,
|
|
155
|
+
destination,
|
|
156
|
+
validate_type,
|
|
157
|
+
ssd=ssd,
|
|
158
|
+
quiet=quiet,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ----------------------------------------------------------------------
|
|
163
|
+
@app.command(
|
|
164
|
+
"cleanup",
|
|
165
|
+
epilog=Common.GetDestinationHelp(),
|
|
166
|
+
no_args_is_help=True,
|
|
167
|
+
)
|
|
168
|
+
def Cleanup(
|
|
169
|
+
destination: Annotated[str, _destination_argument],
|
|
170
|
+
verbose: Annotated[
|
|
171
|
+
bool, CommandLineArguments.verbose_option
|
|
172
|
+
] = CommandLineArguments.verbose_option_default,
|
|
173
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Cleans a backup data store after a mirror execution that was interrupted or failed."""
|
|
176
|
+
|
|
177
|
+
with DoneManager.CreateCommandLine(
|
|
178
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
179
|
+
) as dm:
|
|
180
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
181
|
+
|
|
182
|
+
Mirror.Cleanup(dm, destination)
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# |
|
|
3
|
+
# | OffsiteEntryPoint.py
|
|
4
|
+
# |
|
|
5
|
+
# | David Brownell <db@DavidBrownell.com>
|
|
6
|
+
# | 2024-07-04 12:51:52
|
|
7
|
+
# |
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
# |
|
|
10
|
+
# | Copyright David Brownell 2024
|
|
11
|
+
# | Distributed under the MIT License.
|
|
12
|
+
# |
|
|
13
|
+
# ----------------------------------------------------------------------
|
|
14
|
+
"""\
|
|
15
|
+
Copies content to an offsite location: a snapshot is saved after the initial backup and
|
|
16
|
+
deltas are applied to that snapshot for subsequent backups.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import datetime
|
|
20
|
+
import shutil
|
|
21
|
+
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Annotated, cast, Iterator, Optional, Pattern
|
|
25
|
+
|
|
26
|
+
import typer
|
|
27
|
+
|
|
28
|
+
from dbrownell_Common import PathEx # type: ignore[import-untyped]
|
|
29
|
+
from dbrownell_Common.Streams.DoneManager import DoneManager, Flags as DoneManagerFlags # type: ignore[import-untyped]
|
|
30
|
+
from dbrownell_Common import TyperEx
|
|
31
|
+
from typer.core import TyperGroup
|
|
32
|
+
|
|
33
|
+
from FileBackup.CommandLine import CommandLineArguments
|
|
34
|
+
from FileBackup.Impl import Common
|
|
35
|
+
from FileBackup import Offsite
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ----------------------------------------------------------------------
|
|
39
|
+
class NaturalOrderGrouper(TyperGroup):
|
|
40
|
+
# pylint: disable=missing-class-docstring
|
|
41
|
+
# ----------------------------------------------------------------------
|
|
42
|
+
def list_commands(self, *args, **kwargs): # pylint: disable=unused-argument
|
|
43
|
+
return self.commands.keys() # pragma: no cover
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ----------------------------------------------------------------------
|
|
47
|
+
app = typer.Typer(
|
|
48
|
+
cls=NaturalOrderGrouper,
|
|
49
|
+
help=__doc__,
|
|
50
|
+
no_args_is_help=True,
|
|
51
|
+
pretty_exceptions_show_locals=False,
|
|
52
|
+
pretty_exceptions_enable=False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ----------------------------------------------------------------------
|
|
57
|
+
_backup_name_argument = typer.Argument(
|
|
58
|
+
...,
|
|
59
|
+
help="Unique name of the backup; this value allows for multiple distinct backups on the same machine.",
|
|
60
|
+
)
|
|
61
|
+
_destination_argument = typer.Argument(
|
|
62
|
+
...,
|
|
63
|
+
help="Destination data store used to backup content; This value can be 'None' if the backup content should be created locally but manually distributed to the data store (this can be helpful when initially creating backups that are hundreds of GB in size). See the comments below for information on the different data store destination formats.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ----------------------------------------------------------------------
|
|
68
|
+
@app.command(
|
|
69
|
+
"execute",
|
|
70
|
+
epilog=Common.GetDestinationHelp(),
|
|
71
|
+
no_args_is_help=True,
|
|
72
|
+
)
|
|
73
|
+
def Execute(
|
|
74
|
+
backup_name: Annotated[str, _backup_name_argument],
|
|
75
|
+
destination: Annotated[str, _destination_argument],
|
|
76
|
+
input_filename_or_dirs: Annotated[
|
|
77
|
+
list[Path],
|
|
78
|
+
CommandLineArguments.input_filename_or_dirs_argument,
|
|
79
|
+
],
|
|
80
|
+
encryption_password: Annotated[
|
|
81
|
+
Optional[str],
|
|
82
|
+
typer.Option(
|
|
83
|
+
"--encryption-password",
|
|
84
|
+
help="Encrypt the contents for backup prior to transferring them to the destination data store.",
|
|
85
|
+
),
|
|
86
|
+
] = None,
|
|
87
|
+
compress: Annotated[
|
|
88
|
+
bool,
|
|
89
|
+
typer.Option(
|
|
90
|
+
"--compress",
|
|
91
|
+
help="Compress the contents to backup prior to transferring them to the destination data store.",
|
|
92
|
+
),
|
|
93
|
+
] = False,
|
|
94
|
+
ssd: Annotated[bool, CommandLineArguments.ssd_option] = CommandLineArguments.ssd_option_default,
|
|
95
|
+
force: Annotated[bool, CommandLineArguments.force_option] = CommandLineArguments.force_option_default,
|
|
96
|
+
verbose: Annotated[
|
|
97
|
+
bool, CommandLineArguments.verbose_option
|
|
98
|
+
] = CommandLineArguments.verbose_option_default,
|
|
99
|
+
quiet: Annotated[bool, CommandLineArguments.quiet_option] = CommandLineArguments.quiet_option_default,
|
|
100
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
101
|
+
working_dir: Annotated[
|
|
102
|
+
Optional[Path],
|
|
103
|
+
typer.Option(
|
|
104
|
+
"--working-dir",
|
|
105
|
+
file_okay=False,
|
|
106
|
+
resolve_path=True,
|
|
107
|
+
help="Local directory used to stage files prior to transferring them to the destination data store.",
|
|
108
|
+
),
|
|
109
|
+
] = None,
|
|
110
|
+
archive_volume_size: Annotated[
|
|
111
|
+
int,
|
|
112
|
+
typer.Option(
|
|
113
|
+
"--archive-volume-size",
|
|
114
|
+
min=1024,
|
|
115
|
+
help="Compressed/encrypted data will be converted to volumes of this size for easier transmission to the data store; value expressed in terms of bytes.",
|
|
116
|
+
),
|
|
117
|
+
] = Offsite.DEFAULT_ARCHIVE_VOLUME_SIZE,
|
|
118
|
+
ignore_pending_snapshot: Annotated[
|
|
119
|
+
bool,
|
|
120
|
+
typer.Option("--ignore-pending-snapshot", help="Disable the pending warning snapshot and continue."),
|
|
121
|
+
] = False,
|
|
122
|
+
file_include_params: Annotated[
|
|
123
|
+
list[str],
|
|
124
|
+
CommandLineArguments.file_include_option,
|
|
125
|
+
] = CommandLineArguments.file_include_option_default,
|
|
126
|
+
file_exclude_params: Annotated[
|
|
127
|
+
list[str],
|
|
128
|
+
CommandLineArguments.file_exclude_option,
|
|
129
|
+
] = CommandLineArguments.file_exclude_option_default,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Prepares local changes for offsite backup."""
|
|
132
|
+
|
|
133
|
+
file_includes = cast(list[Pattern], file_include_params)
|
|
134
|
+
file_excludes = cast(list[Pattern], file_exclude_params)
|
|
135
|
+
|
|
136
|
+
del file_include_params
|
|
137
|
+
del file_exclude_params
|
|
138
|
+
|
|
139
|
+
with DoneManager.CreateCommandLine(
|
|
140
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
141
|
+
) as dm:
|
|
142
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
143
|
+
|
|
144
|
+
destination_value = None if destination.lower() == "none" else destination
|
|
145
|
+
|
|
146
|
+
with _ResolveWorkingDir(
|
|
147
|
+
dm,
|
|
148
|
+
working_dir,
|
|
149
|
+
always_preserve=destination_value is None,
|
|
150
|
+
) as resolved_working_dir:
|
|
151
|
+
Offsite.Backup(
|
|
152
|
+
dm,
|
|
153
|
+
backup_name,
|
|
154
|
+
destination_value,
|
|
155
|
+
input_filename_or_dirs,
|
|
156
|
+
encryption_password,
|
|
157
|
+
resolved_working_dir,
|
|
158
|
+
compress=compress,
|
|
159
|
+
ssd=ssd,
|
|
160
|
+
force=force,
|
|
161
|
+
quiet=quiet,
|
|
162
|
+
file_includes=file_includes,
|
|
163
|
+
file_excludes=file_excludes,
|
|
164
|
+
archive_volume_size=archive_volume_size,
|
|
165
|
+
ignore_pending_snapshot=ignore_pending_snapshot,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ----------------------------------------------------------------------
|
|
170
|
+
@app.command("commit", no_args_is_help=True)
|
|
171
|
+
def Commit(
|
|
172
|
+
backup_name: Annotated[str, _backup_name_argument],
|
|
173
|
+
verbose: Annotated[
|
|
174
|
+
bool, CommandLineArguments.verbose_option
|
|
175
|
+
] = CommandLineArguments.verbose_option_default,
|
|
176
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Commits a pending snapshot after the changes have been transferred to an offsite data store."""
|
|
179
|
+
|
|
180
|
+
with DoneManager.CreateCommandLine(
|
|
181
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
182
|
+
) as dm:
|
|
183
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
184
|
+
|
|
185
|
+
Offsite.Commit(dm, backup_name)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ----------------------------------------------------------------------
|
|
189
|
+
@app.command(
|
|
190
|
+
"restore",
|
|
191
|
+
epilog=Common.GetDestinationHelp(),
|
|
192
|
+
no_args_is_help=True,
|
|
193
|
+
)
|
|
194
|
+
def Restore( # pylint: disable=dangerous-default-value
|
|
195
|
+
backup_name: Annotated[str, _backup_name_argument],
|
|
196
|
+
backup_source: Annotated[
|
|
197
|
+
str, typer.Argument(help="Data store location containing content that has been backed up.")
|
|
198
|
+
],
|
|
199
|
+
encryption_password: Annotated[
|
|
200
|
+
Optional[str],
|
|
201
|
+
typer.Option(
|
|
202
|
+
"--encryption-password",
|
|
203
|
+
help="Password used when creating the backups.",
|
|
204
|
+
),
|
|
205
|
+
] = None,
|
|
206
|
+
dir_substitution_key_value_args: Annotated[
|
|
207
|
+
list[str],
|
|
208
|
+
TyperEx.TyperDictOption(
|
|
209
|
+
{},
|
|
210
|
+
"--dir-substitution",
|
|
211
|
+
allow_any__=True,
|
|
212
|
+
help='A key-value-pair consisting of a string to replace and its replacement value within a posix string; this can be used when restoring to a location that is different from the location used to create the backup. Example: \'--dir-substitution "C\\:/=C\\:/Restore/" will cause files backed-up as "C:/Foo/Bar.txt" to be restored as "C:/Restore/Foo/Bar.txt". This value can be provided multiple times on the command line when supporting multiple substitutions.',
|
|
213
|
+
),
|
|
214
|
+
] = [],
|
|
215
|
+
dry_run: Annotated[
|
|
216
|
+
bool,
|
|
217
|
+
typer.Option(
|
|
218
|
+
"--dry-run",
|
|
219
|
+
help="Show the changes that would be made during the restoration process, but do not modify the local file system.",
|
|
220
|
+
),
|
|
221
|
+
] = False,
|
|
222
|
+
overwrite: Annotated[
|
|
223
|
+
bool,
|
|
224
|
+
typer.Option(
|
|
225
|
+
"--overwrite",
|
|
226
|
+
help="By default, the restoration process will not overwrite existing files on the local file system; this flag indicates that files should be overwritten as they are restored.",
|
|
227
|
+
),
|
|
228
|
+
] = False,
|
|
229
|
+
ssd: Annotated[bool, CommandLineArguments.ssd_option] = CommandLineArguments.ssd_option_default,
|
|
230
|
+
verbose: Annotated[
|
|
231
|
+
bool, CommandLineArguments.verbose_option
|
|
232
|
+
] = CommandLineArguments.verbose_option_default,
|
|
233
|
+
quiet: Annotated[bool, CommandLineArguments.quiet_option] = CommandLineArguments.quiet_option_default,
|
|
234
|
+
debug: Annotated[bool, CommandLineArguments.debug_option] = CommandLineArguments.debug_option_default,
|
|
235
|
+
working_dir: Annotated[
|
|
236
|
+
Optional[Path],
|
|
237
|
+
typer.Option(
|
|
238
|
+
"--working-dir",
|
|
239
|
+
file_okay=False,
|
|
240
|
+
resolve_path=True,
|
|
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
|
+
),
|
|
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,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Restores content from an offsite data store."""
|
|
252
|
+
|
|
253
|
+
with DoneManager.CreateCommandLine(
|
|
254
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
255
|
+
) as dm:
|
|
256
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
257
|
+
|
|
258
|
+
dir_substitutions = TyperEx.PostprocessDictArgument(dir_substitution_key_value_args)
|
|
259
|
+
|
|
260
|
+
with _ResolveWorkingDir(dm, working_dir) as resolved_working_dir:
|
|
261
|
+
Offsite.Restore(
|
|
262
|
+
dm,
|
|
263
|
+
backup_name,
|
|
264
|
+
backup_source,
|
|
265
|
+
encryption_password,
|
|
266
|
+
resolved_working_dir,
|
|
267
|
+
dir_substitutions,
|
|
268
|
+
ssd=ssd,
|
|
269
|
+
quiet=quiet,
|
|
270
|
+
dry_run=dry_run,
|
|
271
|
+
overwrite=overwrite,
|
|
272
|
+
continue_on_errors=continue_on_errors,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ----------------------------------------------------------------------
|
|
277
|
+
# ----------------------------------------------------------------------
|
|
278
|
+
# ----------------------------------------------------------------------
|
|
279
|
+
@contextmanager
|
|
280
|
+
def _ResolveWorkingDir(
|
|
281
|
+
dm: DoneManager,
|
|
282
|
+
working_dir: Path | None,
|
|
283
|
+
*,
|
|
284
|
+
always_preserve: bool = False,
|
|
285
|
+
) -> Iterator[Path]:
|
|
286
|
+
if working_dir is None:
|
|
287
|
+
delete_dir = not always_preserve
|
|
288
|
+
working_dir = PathEx.CreateTempDirectory()
|
|
289
|
+
else:
|
|
290
|
+
delete_dir = False
|
|
291
|
+
|
|
292
|
+
was_successful = True
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
assert working_dir is not None
|
|
296
|
+
yield working_dir
|
|
297
|
+
|
|
298
|
+
except:
|
|
299
|
+
was_successful = False
|
|
300
|
+
raise
|
|
301
|
+
|
|
302
|
+
finally:
|
|
303
|
+
assert working_dir is not None
|
|
304
|
+
|
|
305
|
+
if delete_dir:
|
|
306
|
+
was_successful = was_successful and dm.result == 0
|
|
307
|
+
|
|
308
|
+
if was_successful:
|
|
309
|
+
shutil.rmtree(working_dir)
|
|
310
|
+
else:
|
|
311
|
+
if dm.result <= 0:
|
|
312
|
+
# dm.result can be 0 if an exception was raised
|
|
313
|
+
type_desc = "errors"
|
|
314
|
+
elif dm.result > 0:
|
|
315
|
+
type_desc = "warnings"
|
|
316
|
+
else:
|
|
317
|
+
assert False, dm.result # pragma: no cover
|
|
318
|
+
|
|
319
|
+
dm.WriteInfo(f"The temporary directory '{working_dir}' was preserved due to {type_desc}.\n")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ----------------------------------------------------------------------
|
|
323
|
+
# ----------------------------------------------------------------------
|
|
324
|
+
# ----------------------------------------------------------------------
|
|
325
|
+
if __name__ == "__main__":
|
|
326
|
+
app()
|
|
File without changes
|