FileBackup 0.1.1__py3-none-any.whl → 0.2.0__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 +77 -0
- FileBackup/{EntryPoint.py → CommandLine/EntryPoint.py} +6 -49
- FileBackup/CommandLine/MirrorEntryPoint.py +194 -0
- FileBackup/CommandLine/__init__.py +0 -0
- FileBackup/DataStore/FastGlacierDataStore.py +79 -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/SFTPDataStore.py +355 -0
- FileBackup/DataStore/__init__.py +0 -0
- FileBackup/Impl/Common.py +674 -0
- FileBackup/Impl/__init__.py +0 -0
- FileBackup/Mirror.py +565 -0
- FileBackup/Snapshot.py +711 -0
- FileBackup/__init__.py +1 -3
- {FileBackup-0.1.1.dist-info → FileBackup-0.2.0.dist-info}/METADATA +3 -1
- FileBackup-0.2.0.dist-info/RECORD +23 -0
- FileBackup-0.2.0.dist-info/entry_points.txt +2 -0
- FileBackup/Math.py +0 -27
- FileBackup-0.1.1.dist-info/RECORD +0 -9
- FileBackup-0.1.1.dist-info/entry_points.txt +0 -2
- {FileBackup-0.1.1.dist-info → FileBackup-0.2.0.dist-info}/LICENSE.txt +0 -0
- {FileBackup-0.1.1.dist-info → FileBackup-0.2.0.dist-info}/WHEEL +0 -0
- {FileBackup-0.1.1.dist-info → FileBackup-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
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(
|
|
33
|
+
"The regular expression '{}' is not valid ({}).".format(value, ex)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return expressions
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ----------------------------------------------------------------------
|
|
40
|
+
input_filename_or_dirs_argument = typer.Argument(
|
|
41
|
+
..., exists=True, resolve_path=True, help="Input filename or directory."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
ssd_option = typer.Option(
|
|
45
|
+
"--ssd",
|
|
46
|
+
help="Processes tasks in parallel to leverage the capabilities of solid-state-drives.",
|
|
47
|
+
)
|
|
48
|
+
ssd_option_default = False
|
|
49
|
+
|
|
50
|
+
quiet_option = typer.Option("--quiet", help="Reduce the amount of information displayed.")
|
|
51
|
+
quiet_option_default = False
|
|
52
|
+
|
|
53
|
+
force_option = typer.Option(
|
|
54
|
+
"--force",
|
|
55
|
+
help="Ignore previous backup information and overwrite all data in the destination data store.",
|
|
56
|
+
)
|
|
57
|
+
force_option_default = False
|
|
58
|
+
|
|
59
|
+
verbose_option = typer.Option("--verbose", help="Write verbose information to the terminal.")
|
|
60
|
+
verbose_option_default = False
|
|
61
|
+
|
|
62
|
+
debug_option = typer.Option("--debug", help="Write debug information to the terminal.")
|
|
63
|
+
debug_option_default = False
|
|
64
|
+
|
|
65
|
+
file_include_option = typer.Option(
|
|
66
|
+
"--file-include",
|
|
67
|
+
callback=ToRegex,
|
|
68
|
+
help="Regular expression (based on a posix path) used to include files and/or directories when preserving content.",
|
|
69
|
+
)
|
|
70
|
+
file_include_option_default: list[str] = []
|
|
71
|
+
|
|
72
|
+
file_exclude_option = typer.Option(
|
|
73
|
+
"--file-exclude",
|
|
74
|
+
callback=ToRegex,
|
|
75
|
+
help="Regular expression (based on a posix path) used to exclude files and/or directories when preserving content.",
|
|
76
|
+
)
|
|
77
|
+
file_exclude_option_default: list[str] = []
|
|
@@ -12,7 +12,8 @@ import typer
|
|
|
12
12
|
|
|
13
13
|
from typer.core import TyperGroup # type: ignore [import-untyped]
|
|
14
14
|
|
|
15
|
-
from FileBackup import
|
|
15
|
+
from FileBackup import __version__
|
|
16
|
+
from FileBackup.CommandLine import MirrorEntryPoint
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
# ----------------------------------------------------------------------
|
|
@@ -33,56 +34,12 @@ app = typer.Typer(
|
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
@app.command("Add")
|
|
38
|
-
def Add(
|
|
39
|
-
x: int,
|
|
40
|
-
y: int,
|
|
41
|
-
) -> None:
|
|
42
|
-
"""Adds 2 values."""
|
|
43
|
-
|
|
44
|
-
sys.stdout.write(str(Math.Add(x, y)))
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# ----------------------------------------------------------------------
|
|
48
|
-
@app.command("Sub")
|
|
49
|
-
def Sub(
|
|
50
|
-
x: int,
|
|
51
|
-
y: int,
|
|
52
|
-
) -> None:
|
|
53
|
-
"""Subtracts 2 values."""
|
|
54
|
-
|
|
55
|
-
sys.stdout.write(str(Math.Sub(x, y)))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# ----------------------------------------------------------------------
|
|
59
|
-
@app.command("Mult")
|
|
60
|
-
def Mult(
|
|
61
|
-
x: int,
|
|
62
|
-
y: int,
|
|
63
|
-
) -> None:
|
|
64
|
-
"""Multiplies 2 values."""
|
|
65
|
-
|
|
66
|
-
sys.stdout.write(str(Math.Mult(x, y)))
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# ----------------------------------------------------------------------
|
|
70
|
-
@app.command("Div")
|
|
71
|
-
def Div(
|
|
72
|
-
x: int,
|
|
73
|
-
y: int,
|
|
74
|
-
) -> None:
|
|
75
|
-
"""Divides 1 value by another."""
|
|
76
|
-
|
|
77
|
-
sys.stdout.write(str(Math.Div(x, y)))
|
|
37
|
+
app.add_typer(MirrorEntryPoint.app, name="mirror", help=MirrorEntryPoint.__doc__)
|
|
78
38
|
|
|
79
39
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"""Prints the version of the package."""
|
|
84
|
-
|
|
85
|
-
sys.stdout.write(__version__)
|
|
40
|
+
@app.command("version", no_args_is_help=False)
|
|
41
|
+
def Version():
|
|
42
|
+
sys.stdout.write(f"FileBackup v{__version__}\n")
|
|
86
43
|
|
|
87
44
|
|
|
88
45
|
# ----------------------------------------------------------------------
|
|
@@ -0,0 +1,194 @@
|
|
|
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()
|
|
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[
|
|
75
|
+
bool, CommandLineArguments.force_option
|
|
76
|
+
] = CommandLineArguments.force_option_default,
|
|
77
|
+
verbose: Annotated[
|
|
78
|
+
bool, CommandLineArguments.verbose_option
|
|
79
|
+
] = CommandLineArguments.verbose_option_default,
|
|
80
|
+
quiet: Annotated[
|
|
81
|
+
bool, CommandLineArguments.quiet_option
|
|
82
|
+
] = CommandLineArguments.quiet_option_default,
|
|
83
|
+
debug: Annotated[
|
|
84
|
+
bool, CommandLineArguments.debug_option
|
|
85
|
+
] = CommandLineArguments.debug_option_default,
|
|
86
|
+
file_include_params: Annotated[
|
|
87
|
+
list[str],
|
|
88
|
+
CommandLineArguments.file_include_option,
|
|
89
|
+
] = CommandLineArguments.file_include_option_default,
|
|
90
|
+
file_exclude_params: Annotated[
|
|
91
|
+
list[str],
|
|
92
|
+
CommandLineArguments.file_exclude_option,
|
|
93
|
+
] = CommandLineArguments.file_exclude_option_default,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Mirrors content to a backup data store."""
|
|
96
|
+
|
|
97
|
+
file_includes = cast(list[Pattern], file_include_params)
|
|
98
|
+
file_excludes = cast(list[Pattern], file_exclude_params)
|
|
99
|
+
|
|
100
|
+
del file_include_params
|
|
101
|
+
del file_exclude_params
|
|
102
|
+
|
|
103
|
+
with DoneManager.CreateCommandLine(
|
|
104
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
105
|
+
) as dm:
|
|
106
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
107
|
+
|
|
108
|
+
Mirror.Backup(
|
|
109
|
+
dm,
|
|
110
|
+
destination,
|
|
111
|
+
input_filename_or_dirs,
|
|
112
|
+
ssd=ssd,
|
|
113
|
+
force=force,
|
|
114
|
+
quiet=quiet,
|
|
115
|
+
file_includes=file_includes,
|
|
116
|
+
file_excludes=file_excludes,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ----------------------------------------------------------------------
|
|
121
|
+
@app.command(
|
|
122
|
+
"validate",
|
|
123
|
+
no_args_is_help=True,
|
|
124
|
+
epilog=textwrap.dedent(
|
|
125
|
+
"""\
|
|
126
|
+
{}
|
|
127
|
+
Validation Types
|
|
128
|
+
================
|
|
129
|
+
standard: Validates that files and directories at the destination exist and file sizes match the expected values.
|
|
130
|
+
complete: Validates that files and directories at the destination exist and file hashes match the expected values.
|
|
131
|
+
""",
|
|
132
|
+
)
|
|
133
|
+
.replace("\n", "\n\n")
|
|
134
|
+
.format(Common.GetDestinationHelp()),
|
|
135
|
+
)
|
|
136
|
+
def Validate(
|
|
137
|
+
destination: Annotated[str, _destination_argument],
|
|
138
|
+
validate_type: Annotated[
|
|
139
|
+
Mirror.ValidateType,
|
|
140
|
+
typer.Argument(
|
|
141
|
+
case_sensitive=False,
|
|
142
|
+
help="Specifies the type of validation to use; the the comments below for information on the different validation types.",
|
|
143
|
+
),
|
|
144
|
+
] = Mirror.ValidateType.standard,
|
|
145
|
+
ssd: Annotated[bool, CommandLineArguments.ssd_option] = CommandLineArguments.ssd_option_default,
|
|
146
|
+
verbose: Annotated[
|
|
147
|
+
bool, CommandLineArguments.verbose_option
|
|
148
|
+
] = CommandLineArguments.verbose_option_default,
|
|
149
|
+
quiet: Annotated[
|
|
150
|
+
bool, CommandLineArguments.quiet_option
|
|
151
|
+
] = CommandLineArguments.quiet_option_default,
|
|
152
|
+
debug: Annotated[
|
|
153
|
+
bool, CommandLineArguments.debug_option
|
|
154
|
+
] = CommandLineArguments.debug_option_default,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Validates perviously mirrored content in the backup data store."""
|
|
157
|
+
|
|
158
|
+
with DoneManager.CreateCommandLine(
|
|
159
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
160
|
+
) as dm:
|
|
161
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
162
|
+
|
|
163
|
+
Mirror.Validate(
|
|
164
|
+
dm,
|
|
165
|
+
destination,
|
|
166
|
+
validate_type,
|
|
167
|
+
ssd=ssd,
|
|
168
|
+
quiet=quiet,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ----------------------------------------------------------------------
|
|
173
|
+
@app.command(
|
|
174
|
+
"cleanup",
|
|
175
|
+
epilog=Common.GetDestinationHelp(),
|
|
176
|
+
no_args_is_help=True,
|
|
177
|
+
)
|
|
178
|
+
def Cleanup(
|
|
179
|
+
destination: Annotated[str, _destination_argument],
|
|
180
|
+
verbose: Annotated[
|
|
181
|
+
bool, CommandLineArguments.verbose_option
|
|
182
|
+
] = CommandLineArguments.verbose_option_default,
|
|
183
|
+
debug: Annotated[
|
|
184
|
+
bool, CommandLineArguments.debug_option
|
|
185
|
+
] = CommandLineArguments.debug_option_default,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Cleans a backup data store after a mirror execution that was interrupted or failed."""
|
|
188
|
+
|
|
189
|
+
with DoneManager.CreateCommandLine(
|
|
190
|
+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
191
|
+
) as dm:
|
|
192
|
+
dm.WriteVerbose(str(datetime.datetime.now()) + "\n\n")
|
|
193
|
+
|
|
194
|
+
Mirror.Cleanup(dm, destination)
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# |
|
|
3
|
+
# | FastGlacierDataStore.py
|
|
4
|
+
# |
|
|
5
|
+
# | David Brownell <db@DavidBrownell.com>
|
|
6
|
+
# | 2024-06-10 14:32:10
|
|
7
|
+
# |
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
# |
|
|
10
|
+
# | Copyright David Brownell 2024
|
|
11
|
+
# | Distributed under the MIT License.
|
|
12
|
+
# |
|
|
13
|
+
# ----------------------------------------------------------------------
|
|
14
|
+
"""Contains the FastGlacierDataStore 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 FastGlacierDataStore(BulkStorageDataStore):
|
|
28
|
+
"""Data store that uses the Fast Glacier application (https://fastglacier.com/)"""
|
|
29
|
+
|
|
30
|
+
# ----------------------------------------------------------------------
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
account_name: str,
|
|
34
|
+
aws_region: str,
|
|
35
|
+
glacier_dir: Optional[Path],
|
|
36
|
+
):
|
|
37
|
+
super(FastGlacierDataStore, self).__init__()
|
|
38
|
+
|
|
39
|
+
self.account_name = account_name
|
|
40
|
+
self.aws_region = aws_region
|
|
41
|
+
|
|
42
|
+
self._glacier_dir = glacier_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 Fast Glacier on the command line...",
|
|
61
|
+
suffix="\n",
|
|
62
|
+
) as check_dm:
|
|
63
|
+
result = SubprocessEx.Run("glacier-con --version")
|
|
64
|
+
|
|
65
|
+
check_dm.WriteVerbose(result.output)
|
|
66
|
+
|
|
67
|
+
if result.returncode != 0 and "glacier-con.exe upload" not in result.output:
|
|
68
|
+
check_dm.WriteError(
|
|
69
|
+
"Fast Glacier is not available; please make sure it exists in the path and run the script again.\n"
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
with dm.Nested("Uploading to Fast Glacier...") as upload_dm:
|
|
74
|
+
command_line = f'glacier-con upload "{self.account_name}" "{local_path / "*"}" "{self.aws_region}" "{self._glacier_dir.as_posix()}"'
|
|
75
|
+
|
|
76
|
+
upload_dm.WriteVerbose(f"Command Line: {command_line}\n\n")
|
|
77
|
+
|
|
78
|
+
with upload_dm.YieldStream() as stream:
|
|
79
|
+
upload_dm.result = SubprocessEx.Stream(command_line, stream)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# |
|
|
3
|
+
# | FileSystemDataStore.py
|
|
4
|
+
# |
|
|
5
|
+
# | David Brownell <db@DavidBrownell.com>
|
|
6
|
+
# | 2024-06-10 13:42:04
|
|
7
|
+
# |
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
# |
|
|
10
|
+
# | Copyright David Brownell 2024
|
|
11
|
+
# | Distributed under the MIT License.
|
|
12
|
+
# |
|
|
13
|
+
# ----------------------------------------------------------------------
|
|
14
|
+
"""Contains the FileSystemDataStore object"""
|
|
15
|
+
|
|
16
|
+
import itertools
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
|
|
20
|
+
from contextlib import contextmanager
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Generator, Optional
|
|
23
|
+
|
|
24
|
+
from dbrownell_Common import PathEx # type: ignore[import-untyped]
|
|
25
|
+
from dbrownell_Common.Types import override # type: ignore[import-untyped]
|
|
26
|
+
|
|
27
|
+
from FileBackup.DataStore.Interfaces.FileBasedDataStore import FileBasedDataStore, ItemType
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ----------------------------------------------------------------------
|
|
31
|
+
class FileSystemDataStore(FileBasedDataStore):
|
|
32
|
+
"""DataStore associated with a standard, local file system"""
|
|
33
|
+
|
|
34
|
+
# ----------------------------------------------------------------------
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
root: Path = Path.cwd(),
|
|
38
|
+
*,
|
|
39
|
+
ssd: bool = False,
|
|
40
|
+
is_local_filesystem_override_value_for_testing: Optional[bool] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
assert not root.exists() or root.is_dir(), root
|
|
43
|
+
|
|
44
|
+
super(FileSystemDataStore, self).__init__(
|
|
45
|
+
is_local_filesystem=(
|
|
46
|
+
is_local_filesystem_override_value_for_testing
|
|
47
|
+
if is_local_filesystem_override_value_for_testing is not None
|
|
48
|
+
else root.root == Path.cwd().root
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self._working_dir: Path = root
|
|
53
|
+
self._ssd = ssd
|
|
54
|
+
|
|
55
|
+
# ----------------------------------------------------------------------
|
|
56
|
+
@override
|
|
57
|
+
def ExecuteInParallel(self) -> bool:
|
|
58
|
+
return self._ssd
|
|
59
|
+
|
|
60
|
+
# ----------------------------------------------------------------------
|
|
61
|
+
@override
|
|
62
|
+
def ValidateBackupInputs(
|
|
63
|
+
self,
|
|
64
|
+
input_filename_or_dirs: list[Path],
|
|
65
|
+
) -> None:
|
|
66
|
+
for input_filename_or_dir in input_filename_or_dirs:
|
|
67
|
+
if input_filename_or_dir.is_file():
|
|
68
|
+
input_dir = input_filename_or_dir.parent
|
|
69
|
+
elif input_filename_or_dir.is_dir():
|
|
70
|
+
input_dir = input_filename_or_dir
|
|
71
|
+
else:
|
|
72
|
+
raise Exception(f"'{input_filename_or_dir}' is not a supported item type.")
|
|
73
|
+
|
|
74
|
+
if PathEx.IsDescendant(self._working_dir, input_dir):
|
|
75
|
+
raise Exception(
|
|
76
|
+
f"The directory '{input_filename_or_dir}' overlaps with the destination path '{self._working_dir}'."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ----------------------------------------------------------------------
|
|
80
|
+
@override
|
|
81
|
+
def SnapshotFilenameToDestinationName(
|
|
82
|
+
self,
|
|
83
|
+
path: Path,
|
|
84
|
+
) -> Path:
|
|
85
|
+
if path.parts[0] == "/":
|
|
86
|
+
path = Path(*path.parts[1:])
|
|
87
|
+
elif path.parts[0]:
|
|
88
|
+
# Probably on Windows
|
|
89
|
+
path = Path(path.parts[0].replace(":", "_").rstrip("\\")) / Path(*path.parts[1:])
|
|
90
|
+
|
|
91
|
+
return self.GetWorkingDir() / path
|
|
92
|
+
|
|
93
|
+
# ----------------------------------------------------------------------
|
|
94
|
+
@override
|
|
95
|
+
def GetBytesAvailable(self) -> Optional[int]:
|
|
96
|
+
# Find a directory that exists
|
|
97
|
+
for potential_dir in itertools.chain(
|
|
98
|
+
[self._working_dir],
|
|
99
|
+
self._working_dir.parents,
|
|
100
|
+
[Path.cwd()],
|
|
101
|
+
):
|
|
102
|
+
if potential_dir.is_dir():
|
|
103
|
+
return shutil.disk_usage(potential_dir).free
|
|
104
|
+
|
|
105
|
+
assert False, self._working_dir
|
|
106
|
+
|
|
107
|
+
# ----------------------------------------------------------------------
|
|
108
|
+
@override
|
|
109
|
+
def GetWorkingDir(self) -> Path:
|
|
110
|
+
return self._working_dir
|
|
111
|
+
|
|
112
|
+
# ----------------------------------------------------------------------
|
|
113
|
+
@override
|
|
114
|
+
def SetWorkingDir(
|
|
115
|
+
self,
|
|
116
|
+
path: Path,
|
|
117
|
+
) -> None:
|
|
118
|
+
self._working_dir /= path
|
|
119
|
+
|
|
120
|
+
# ----------------------------------------------------------------------
|
|
121
|
+
@override
|
|
122
|
+
def GetItemType(
|
|
123
|
+
self,
|
|
124
|
+
path: Path,
|
|
125
|
+
) -> Optional[ItemType]:
|
|
126
|
+
path = self._working_dir / path
|
|
127
|
+
|
|
128
|
+
if not path.exists():
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
if path.is_symlink():
|
|
132
|
+
return ItemType.SymLink
|
|
133
|
+
|
|
134
|
+
if path.is_file():
|
|
135
|
+
return ItemType.File
|
|
136
|
+
|
|
137
|
+
if path.is_dir():
|
|
138
|
+
return ItemType.Dir
|
|
139
|
+
|
|
140
|
+
raise Exception(f"'{path}' is not a known type.")
|
|
141
|
+
|
|
142
|
+
# ----------------------------------------------------------------------
|
|
143
|
+
@override
|
|
144
|
+
def GetFileSize(
|
|
145
|
+
self,
|
|
146
|
+
path: Path,
|
|
147
|
+
) -> int:
|
|
148
|
+
return (self._working_dir / path).stat().st_size
|
|
149
|
+
|
|
150
|
+
# ----------------------------------------------------------------------
|
|
151
|
+
@override
|
|
152
|
+
def RemoveDir(
|
|
153
|
+
self,
|
|
154
|
+
path: Path,
|
|
155
|
+
) -> None:
|
|
156
|
+
shutil.rmtree(self._working_dir / path)
|
|
157
|
+
|
|
158
|
+
# ----------------------------------------------------------------------
|
|
159
|
+
@override
|
|
160
|
+
def RemoveFile(
|
|
161
|
+
self,
|
|
162
|
+
path: Path,
|
|
163
|
+
) -> None:
|
|
164
|
+
(self._working_dir / path).unlink()
|
|
165
|
+
|
|
166
|
+
# ----------------------------------------------------------------------
|
|
167
|
+
@override
|
|
168
|
+
def MakeDirs(
|
|
169
|
+
self,
|
|
170
|
+
path: Path,
|
|
171
|
+
) -> None:
|
|
172
|
+
(self._working_dir / path).mkdir(parents=True, exist_ok=True)
|
|
173
|
+
|
|
174
|
+
# ----------------------------------------------------------------------
|
|
175
|
+
@override
|
|
176
|
+
@contextmanager
|
|
177
|
+
def Open(
|
|
178
|
+
self,
|
|
179
|
+
filename: Path,
|
|
180
|
+
*args,
|
|
181
|
+
**kwargs,
|
|
182
|
+
):
|
|
183
|
+
with (self._working_dir / filename).open(*args, **kwargs) as f:
|
|
184
|
+
yield f
|
|
185
|
+
|
|
186
|
+
# ----------------------------------------------------------------------
|
|
187
|
+
@override
|
|
188
|
+
def Rename(
|
|
189
|
+
self,
|
|
190
|
+
old_path: Path,
|
|
191
|
+
new_path: Path,
|
|
192
|
+
) -> None:
|
|
193
|
+
old_path = self._working_dir / old_path
|
|
194
|
+
new_path = self._working_dir / new_path
|
|
195
|
+
|
|
196
|
+
if new_path.is_file():
|
|
197
|
+
new_path.unlink()
|
|
198
|
+
elif new_path.is_dir():
|
|
199
|
+
shutil.rmtree(new_path)
|
|
200
|
+
|
|
201
|
+
shutil.move(old_path, new_path)
|
|
202
|
+
|
|
203
|
+
# ----------------------------------------------------------------------
|
|
204
|
+
@override
|
|
205
|
+
def Walk(
|
|
206
|
+
self,
|
|
207
|
+
path: Path = Path(),
|
|
208
|
+
) -> Generator[
|
|
209
|
+
tuple[
|
|
210
|
+
Path, # root
|
|
211
|
+
list[str], # directories
|
|
212
|
+
list[str], # files
|
|
213
|
+
],
|
|
214
|
+
None,
|
|
215
|
+
None,
|
|
216
|
+
]:
|
|
217
|
+
for root, directories, filenames in os.walk(self._working_dir / path):
|
|
218
|
+
yield Path(root), directories, filenames
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------
|
|
2
|
+
# |
|
|
3
|
+
# | BulkStorageDataStore.py
|
|
4
|
+
# |
|
|
5
|
+
# | David Brownell <db@DavidBrownell.com>
|
|
6
|
+
# | 2024-06-10 13:33:45
|
|
7
|
+
# |
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
# |
|
|
10
|
+
# | Copyright David Brownell 2024
|
|
11
|
+
# | Distributed under the MIT License.
|
|
12
|
+
# |
|
|
13
|
+
# ----------------------------------------------------------------------
|
|
14
|
+
"""Contains the BulkStorageDataStore object"""
|
|
15
|
+
|
|
16
|
+
from abc import abstractmethod
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from dbrownell_Common.Streams.DoneManager import DoneManager # type: ignore[import-untyped]
|
|
20
|
+
|
|
21
|
+
from FileBackup.DataStore.Interfaces.DataStore import DataStore # type: ignore[import-untyped]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ----------------------------------------------------------------------
|
|
25
|
+
class BulkStorageDataStore(DataStore):
|
|
26
|
+
"""Abstraction for data stores that can upload content in bulk but not easily retrieve it (such as cloud storage)"""
|
|
27
|
+
|
|
28
|
+
# ----------------------------------------------------------------------
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def Upload(
|
|
31
|
+
self,
|
|
32
|
+
dm: DoneManager,
|
|
33
|
+
local_path: Path,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Uploads all content in the provided path and its children"""
|
|
36
|
+
raise Exception("Abstract method") # pragma: no cover
|