declarative-data-backup 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/PKG-INFO +7 -2
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/README.md +6 -1
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/pyproject.toml +1 -1
- declarative_data_backup-0.3.1/src/declarative_data_backup/borg/borg_cli.py +164 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/PKG-INFO +7 -2
- declarative_data_backup-0.3.0/src/declarative_data_backup/borg/borg_cli.py +0 -69
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/setup.cfg +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/__init__.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/__main__.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/backup.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/borg/__init__.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/borg/archive_naming.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/borg/borg.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/__init__.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/__init__.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/models.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/utils.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/yaml_parser.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/backup_config/yaml_raw_loader.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/config/logging.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/environment.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup/errors.py +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/SOURCES.txt +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/dependency_links.txt +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/requires.txt +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/src/declarative_data_backup.egg-info/top_level.txt +0 -0
- {declarative_data_backup-0.3.0 → declarative_data_backup-0.3.1}/tests/test_backup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: declarative-data-backup
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Declarative Borg-based data backups
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -149,7 +149,7 @@ Optional variables:
|
|
|
149
149
|
| --- | --- |
|
|
150
150
|
| `BORG_REMOTE_PATH` | Remote Borg executable path, passed as Borg's `--remote-path`. Leave unset to use Borg's default. |
|
|
151
151
|
| `BORG_EXIT_CODES` | Borg exit-code mode: `legacy` or `modern`. |
|
|
152
|
-
| `BACKUP_PROGRESS` | Set to `true` to
|
|
152
|
+
| `BACKUP_PROGRESS` | Set to `true` to log the latest Borg progress every 30 seconds during archive creation. Defaults to `false`. |
|
|
153
153
|
| `BACKUP_VERBOSE` | Set to `true` to show Borg's per-file create and per-archive prune output. Defaults to `false` for concise logs. |
|
|
154
154
|
|
|
155
155
|
Provide credentials through a secret manager or protected files; do not put
|
|
@@ -167,6 +167,11 @@ the run fails and pruning is skipped. Logs include group and source start/end
|
|
|
167
167
|
messages, while Borg's per-file and per-archive output is disabled by default.
|
|
168
168
|
Set `BACKUP_VERBOSE=true` for detailed Borg output or `BACKUP_PROGRESS=true`
|
|
169
169
|
for live progress during archive creation; either can increase log volume.
|
|
170
|
+
Progress is logged as plain status lines, not a terminal progress bar. Each
|
|
171
|
+
line includes the group, source, elapsed time, and latest reported file/byte
|
|
172
|
+
counters or Borg phase. Unchanged counters do not prove forward progress;
|
|
173
|
+
before Borg reports a phase, the status only indicates that the process is
|
|
174
|
+
running. Warnings and statistics are not throttled.
|
|
170
175
|
|
|
171
176
|
## Configuration reference
|
|
172
177
|
|
|
@@ -141,7 +141,7 @@ Optional variables:
|
|
|
141
141
|
| --- | --- |
|
|
142
142
|
| `BORG_REMOTE_PATH` | Remote Borg executable path, passed as Borg's `--remote-path`. Leave unset to use Borg's default. |
|
|
143
143
|
| `BORG_EXIT_CODES` | Borg exit-code mode: `legacy` or `modern`. |
|
|
144
|
-
| `BACKUP_PROGRESS` | Set to `true` to
|
|
144
|
+
| `BACKUP_PROGRESS` | Set to `true` to log the latest Borg progress every 30 seconds during archive creation. Defaults to `false`. |
|
|
145
145
|
| `BACKUP_VERBOSE` | Set to `true` to show Borg's per-file create and per-archive prune output. Defaults to `false` for concise logs. |
|
|
146
146
|
|
|
147
147
|
Provide credentials through a secret manager or protected files; do not put
|
|
@@ -159,6 +159,11 @@ the run fails and pruning is skipped. Logs include group and source start/end
|
|
|
159
159
|
messages, while Borg's per-file and per-archive output is disabled by default.
|
|
160
160
|
Set `BACKUP_VERBOSE=true` for detailed Borg output or `BACKUP_PROGRESS=true`
|
|
161
161
|
for live progress during archive creation; either can increase log volume.
|
|
162
|
+
Progress is logged as plain status lines, not a terminal progress bar. Each
|
|
163
|
+
line includes the group, source, elapsed time, and latest reported file/byte
|
|
164
|
+
counters or Borg phase. Unchanged counters do not prove forward progress;
|
|
165
|
+
before Borg reports a phase, the status only indicates that the process is
|
|
166
|
+
running. Warnings and statistics are not throttled.
|
|
162
167
|
|
|
163
168
|
## Configuration reference
|
|
164
169
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import selectors
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from ..environment import Environment
|
|
11
|
+
from ..errors import BorgWarningError, fail
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
PROGRESS_INTERVAL_SECONDS = 30
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_with_progress(
|
|
18
|
+
command: list[str], environment: dict[str, str], context: str
|
|
19
|
+
) -> int:
|
|
20
|
+
started_at = time.monotonic()
|
|
21
|
+
next_report = started_at + PROGRESS_INTERVAL_SECONDS
|
|
22
|
+
latest_progress = "Waiting for Borg progress (process running)."
|
|
23
|
+
|
|
24
|
+
def handle_line(line: bytes) -> None:
|
|
25
|
+
nonlocal latest_progress
|
|
26
|
+
text = line.decode("utf-8", errors="replace")
|
|
27
|
+
try:
|
|
28
|
+
event = json.loads(text)
|
|
29
|
+
except json.JSONDecodeError:
|
|
30
|
+
print(text, file=sys.stderr, flush=True)
|
|
31
|
+
return
|
|
32
|
+
if not isinstance(event, dict):
|
|
33
|
+
print(text, file=sys.stderr, flush=True)
|
|
34
|
+
return
|
|
35
|
+
kind = event.get("type")
|
|
36
|
+
if kind in ("archive_progress", "progress_message", "progress_percent"):
|
|
37
|
+
if event.get("finished"):
|
|
38
|
+
return
|
|
39
|
+
if kind == "archive_progress":
|
|
40
|
+
latest_progress = (
|
|
41
|
+
f"files={event.get('nfiles')} "
|
|
42
|
+
f"original_bytes={event.get('original_size')} "
|
|
43
|
+
f"compressed_bytes={event.get('compressed_size')} "
|
|
44
|
+
f"deduplicated_bytes={event.get('deduplicated_size')}"
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
latest_progress = str(event.get("message", text))
|
|
48
|
+
return
|
|
49
|
+
if kind == "log_message":
|
|
50
|
+
print(event.get("message", text), file=sys.stderr, flush=True)
|
|
51
|
+
else:
|
|
52
|
+
print(text, file=sys.stderr, flush=True)
|
|
53
|
+
|
|
54
|
+
with subprocess.Popen(
|
|
55
|
+
command,
|
|
56
|
+
env=environment,
|
|
57
|
+
stderr=subprocess.PIPE,
|
|
58
|
+
) as process:
|
|
59
|
+
assert process.stderr is not None
|
|
60
|
+
try:
|
|
61
|
+
with selectors.DefaultSelector() as selector:
|
|
62
|
+
selector.register(process.stderr, selectors.EVENT_READ)
|
|
63
|
+
pending = b""
|
|
64
|
+
while selector.get_map():
|
|
65
|
+
for key, _ in selector.select(
|
|
66
|
+
max(0, next_report - time.monotonic())
|
|
67
|
+
):
|
|
68
|
+
chunk = os.read(key.fd, 65536)
|
|
69
|
+
if not chunk:
|
|
70
|
+
selector.unregister(key.fileobj)
|
|
71
|
+
if pending:
|
|
72
|
+
handle_line(pending)
|
|
73
|
+
break
|
|
74
|
+
pending += chunk
|
|
75
|
+
while b"\n" in pending:
|
|
76
|
+
line, pending = pending.split(b"\n", 1)
|
|
77
|
+
handle_line(line)
|
|
78
|
+
now = time.monotonic()
|
|
79
|
+
if now >= next_report and selector.get_map():
|
|
80
|
+
logger.info(
|
|
81
|
+
"Borg progress for %s (elapsed_seconds=%.0f; "
|
|
82
|
+
"latest reported: %s).",
|
|
83
|
+
context,
|
|
84
|
+
now - started_at,
|
|
85
|
+
latest_progress,
|
|
86
|
+
)
|
|
87
|
+
next_report = now + PROGRESS_INTERVAL_SECONDS
|
|
88
|
+
return process.wait()
|
|
89
|
+
except BaseException:
|
|
90
|
+
process.terminate()
|
|
91
|
+
try:
|
|
92
|
+
process.wait(timeout=10)
|
|
93
|
+
except subprocess.TimeoutExpired:
|
|
94
|
+
process.kill()
|
|
95
|
+
process.wait()
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def ensure_borg_binary() -> None:
|
|
100
|
+
if shutil.which("borg") is None:
|
|
101
|
+
fail("Required executable 'borg' was not found on PATH.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def borg_environment(settings: Environment) -> dict[str, str]:
|
|
105
|
+
environment = os.environ.copy()
|
|
106
|
+
environment.update(
|
|
107
|
+
{
|
|
108
|
+
"BORG_REPO": settings.borg_repo,
|
|
109
|
+
"BORG_BASE_DIR": str(settings.borg_base_dir),
|
|
110
|
+
"BORG_KEY_FILE": str(settings.key_file),
|
|
111
|
+
"BORG_RSH": settings.borg_rsh,
|
|
112
|
+
"BORG_PASSCOMMAND": settings.borg_passcommand,
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
if settings.borg_exit_codes is not None:
|
|
116
|
+
environment["BORG_EXIT_CODES"] = settings.borg_exit_codes
|
|
117
|
+
return environment
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run_borg(
|
|
121
|
+
args: list[str],
|
|
122
|
+
settings: Environment,
|
|
123
|
+
operation: str,
|
|
124
|
+
context: str,
|
|
125
|
+
) -> None:
|
|
126
|
+
logger.info("Running Borg %s for %s.", operation, context)
|
|
127
|
+
command = ["borg"]
|
|
128
|
+
remote_path = settings.borg_remote_path
|
|
129
|
+
if remote_path is not None:
|
|
130
|
+
if not remote_path.strip() or "\0" in remote_path:
|
|
131
|
+
fail("BORG_REMOTE_PATH must be a nonempty Borg executable path.")
|
|
132
|
+
command.extend(("--remote-path", remote_path))
|
|
133
|
+
command.extend(args)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
if settings.backup_progress and operation == "create":
|
|
137
|
+
command.insert(1, "--log-json")
|
|
138
|
+
returncode = run_with_progress(
|
|
139
|
+
command,
|
|
140
|
+
borg_environment(settings),
|
|
141
|
+
context,
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
returncode = subprocess.run(
|
|
145
|
+
command,
|
|
146
|
+
env=borg_environment(settings),
|
|
147
|
+
check=False,
|
|
148
|
+
).returncode
|
|
149
|
+
except FileNotFoundError:
|
|
150
|
+
fail("Required executable 'borg' could not be started.")
|
|
151
|
+
except OSError as exc:
|
|
152
|
+
fail(
|
|
153
|
+
f"Unable to start Borg {operation} for {context}: {exc.strerror or 'I/O error'}."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if returncode == 0:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if returncode == 1:
|
|
160
|
+
message = f"Borg {operation} reported warnings for {context} (exit code 1)."
|
|
161
|
+
logger.warning("%s", message)
|
|
162
|
+
raise BorgWarningError(message)
|
|
163
|
+
|
|
164
|
+
fail(f"Borg {operation} failed for {context} (exit code {returncode}).")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: declarative-data-backup
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Declarative Borg-based data backups
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -149,7 +149,7 @@ Optional variables:
|
|
|
149
149
|
| --- | --- |
|
|
150
150
|
| `BORG_REMOTE_PATH` | Remote Borg executable path, passed as Borg's `--remote-path`. Leave unset to use Borg's default. |
|
|
151
151
|
| `BORG_EXIT_CODES` | Borg exit-code mode: `legacy` or `modern`. |
|
|
152
|
-
| `BACKUP_PROGRESS` | Set to `true` to
|
|
152
|
+
| `BACKUP_PROGRESS` | Set to `true` to log the latest Borg progress every 30 seconds during archive creation. Defaults to `false`. |
|
|
153
153
|
| `BACKUP_VERBOSE` | Set to `true` to show Borg's per-file create and per-archive prune output. Defaults to `false` for concise logs. |
|
|
154
154
|
|
|
155
155
|
Provide credentials through a secret manager or protected files; do not put
|
|
@@ -167,6 +167,11 @@ the run fails and pruning is skipped. Logs include group and source start/end
|
|
|
167
167
|
messages, while Borg's per-file and per-archive output is disabled by default.
|
|
168
168
|
Set `BACKUP_VERBOSE=true` for detailed Borg output or `BACKUP_PROGRESS=true`
|
|
169
169
|
for live progress during archive creation; either can increase log volume.
|
|
170
|
+
Progress is logged as plain status lines, not a terminal progress bar. Each
|
|
171
|
+
line includes the group, source, elapsed time, and latest reported file/byte
|
|
172
|
+
counters or Borg phase. Unchanged counters do not prove forward progress;
|
|
173
|
+
before Borg reports a phase, the status only indicates that the process is
|
|
174
|
+
running. Warnings and statistics are not throttled.
|
|
170
175
|
|
|
171
176
|
## Configuration reference
|
|
172
177
|
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
import shutil
|
|
4
|
-
import subprocess
|
|
5
|
-
|
|
6
|
-
from ..environment import Environment
|
|
7
|
-
from ..errors import BorgWarningError, fail
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def ensure_borg_binary() -> None:
|
|
13
|
-
if shutil.which("borg") is None:
|
|
14
|
-
fail("Required executable 'borg' was not found on PATH.")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def borg_environment(settings: Environment) -> dict[str, str]:
|
|
18
|
-
environment = os.environ.copy()
|
|
19
|
-
environment.update(
|
|
20
|
-
{
|
|
21
|
-
"BORG_REPO": settings.borg_repo,
|
|
22
|
-
"BORG_BASE_DIR": str(settings.borg_base_dir),
|
|
23
|
-
"BORG_KEY_FILE": str(settings.key_file),
|
|
24
|
-
"BORG_RSH": settings.borg_rsh,
|
|
25
|
-
"BORG_PASSCOMMAND": settings.borg_passcommand,
|
|
26
|
-
}
|
|
27
|
-
)
|
|
28
|
-
if settings.borg_exit_codes is not None:
|
|
29
|
-
environment["BORG_EXIT_CODES"] = settings.borg_exit_codes
|
|
30
|
-
return environment
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def run_borg(
|
|
34
|
-
args: list[str],
|
|
35
|
-
settings: Environment,
|
|
36
|
-
operation: str,
|
|
37
|
-
context: str,
|
|
38
|
-
) -> None:
|
|
39
|
-
logger.info("Running Borg %s for %s.", operation, context)
|
|
40
|
-
command = ["borg"]
|
|
41
|
-
remote_path = settings.borg_remote_path
|
|
42
|
-
if remote_path is not None:
|
|
43
|
-
if not remote_path.strip() or "\0" in remote_path:
|
|
44
|
-
fail("BORG_REMOTE_PATH must be a nonempty Borg executable path.")
|
|
45
|
-
command.extend(("--remote-path", remote_path))
|
|
46
|
-
command.extend(args)
|
|
47
|
-
|
|
48
|
-
try:
|
|
49
|
-
result = subprocess.run(
|
|
50
|
-
command,
|
|
51
|
-
env=borg_environment(settings),
|
|
52
|
-
check=False,
|
|
53
|
-
)
|
|
54
|
-
except FileNotFoundError:
|
|
55
|
-
fail("Required executable 'borg' could not be started.")
|
|
56
|
-
except OSError as exc:
|
|
57
|
-
fail(
|
|
58
|
-
f"Unable to start Borg {operation} for {context}: {exc.strerror or 'I/O error'}."
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if result.returncode == 0:
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
if result.returncode == 1:
|
|
65
|
-
message = f"Borg {operation} reported warnings for {context} (exit code 1)."
|
|
66
|
-
logger.warning("%s", message)
|
|
67
|
-
raise BorgWarningError(message)
|
|
68
|
-
|
|
69
|
-
fail(f"Borg {operation} failed for {context} (exit code {result.returncode}).")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|