safekeep 0.5.2__tar.gz → 0.7.0__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.
- {safekeep-0.5.2/safekeep.egg-info → safekeep-0.7.0}/PKG-INFO +109 -22
- {safekeep-0.5.2 → safekeep-0.7.0}/README.md +106 -21
- {safekeep-0.5.2 → safekeep-0.7.0}/pyproject.toml +10 -1
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/__init__.py +1 -1
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/cli.py +123 -20
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/config.py +115 -5
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/copier.py +15 -4
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/daemon.py +89 -4
- safekeep-0.7.0/safekeep/daemonctl.py +349 -0
- safekeep-0.7.0/safekeep/data/com.safekeep.agent.plist.tmpl +71 -0
- safekeep-0.7.0/safekeep/data/safekeep.example +91 -0
- safekeep-0.7.0/safekeep/data/safekeep.service.tmpl +47 -0
- safekeep-0.7.0/safekeep/s3dest.py +278 -0
- {safekeep-0.5.2 → safekeep-0.7.0/safekeep.egg-info}/PKG-INFO +109 -22
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep.egg-info/SOURCES.txt +9 -0
- safekeep-0.7.0/safekeep.egg-info/requires.txt +3 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_cli.py +96 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_config.py +122 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_daemon.py +164 -0
- safekeep-0.7.0/tests/test_daemonctl.py +651 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_platform.py +48 -51
- safekeep-0.7.0/tests/test_s3_itest.py +157 -0
- safekeep-0.7.0/tests/test_s3dest.py +343 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/LICENSE +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/matcher.py +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/platform.py +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/volumes.py +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep.egg-info/dependency_links.txt +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep.egg-info/entry_points.txt +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/safekeep.egg-info/top_level.txt +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/setup.cfg +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_copier.py +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_matcher.py +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_trend.py +0 -0
- {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_volumes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: safekeep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Selective always-on backups for macOS and Linux: fswatch-driven copy of allow-listed files, never deletes
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/foxhound87/safekeep
|
|
@@ -21,6 +21,8 @@ Classifier: Topic :: System :: Archiving :: Backup
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
+
Provides-Extra: s3
|
|
25
|
+
Requires-Dist: botocore<2,>=1.31; extra == "s3"
|
|
24
26
|
Dynamic: license-file
|
|
25
27
|
|
|
26
28
|
# safekeep
|
|
@@ -56,13 +58,19 @@ Documentation: https://foxhound87.github.io/safekeep/
|
|
|
56
58
|
`.sync` file can neither add nor remove destinations (a `dest:` line is an
|
|
57
59
|
invalid line), so a repo cloned from a third party can't redirect the backup
|
|
58
60
|
somewhere else.
|
|
61
|
+
- **S3-compatible destination** (optional `safekeep[s3]` extra): one
|
|
62
|
+
S3-compatible bucket alongside the filesystem destinations — same
|
|
63
|
+
size/mtime idempotency, metadata-based, and **never deletes anything**
|
|
64
|
+
(`--prune` only touches filesystem destinations).
|
|
59
65
|
|
|
60
66
|
## Installation
|
|
61
67
|
|
|
62
68
|
```bash
|
|
63
|
-
pipx install safekeep
|
|
69
|
+
pipx install safekeep # recommended for a CLI
|
|
70
|
+
pipx install "safekeep[s3]" # + S3-compatible destination (botocore)
|
|
64
71
|
# or
|
|
65
72
|
pip install safekeep
|
|
73
|
+
pip install "safekeep[s3]"
|
|
66
74
|
```
|
|
67
75
|
|
|
68
76
|
**Requirements**: macOS or Linux (POSIX with systemd), Python >= 3.9 and
|
|
@@ -76,21 +84,22 @@ sudo dnf install fswatch # Fedora
|
|
|
76
84
|
```
|
|
77
85
|
|
|
78
86
|
The **agent** (daemon at boot: launchd on macOS, a systemd *user unit* on Linux)
|
|
79
|
-
is installed from
|
|
80
|
-
the wheel — see
|
|
87
|
+
is installed from the installed package with `safekeep daemon install` — the
|
|
88
|
+
unit templates ship inside the wheel, no checkout needed — see
|
|
89
|
+
[Agent](#agent) below. The old `./install.sh` wrapper is deprecated.
|
|
81
90
|
|
|
82
91
|
## Agent
|
|
83
92
|
|
|
84
93
|
```bash
|
|
85
|
-
|
|
86
|
-
cd safekeep
|
|
94
|
+
pipx install safekeep # or: pip install --user safekeep
|
|
87
95
|
|
|
88
96
|
# only external dependency, if not installed yet (see the per-OS list above)
|
|
89
|
-
brew install fswatch # macOS — the
|
|
97
|
+
brew install fswatch # macOS — the preflight suggests the right command on Linux
|
|
90
98
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
99
|
+
# preflight (fswatch / shim on PATH / python / WSL systemd-on), then renders
|
|
100
|
+
# the plist or systemd user unit from the embedded template into place —
|
|
101
|
+
# idempotent, does NOT start anything yet
|
|
102
|
+
safekeep daemon install # --dry-run to preview, nothing is written
|
|
94
103
|
```
|
|
95
104
|
|
|
96
105
|
Then:
|
|
@@ -104,16 +113,18 @@ Then:
|
|
|
104
113
|
3. load the agent:
|
|
105
114
|
|
|
106
115
|
```bash
|
|
107
|
-
# macOS (
|
|
108
|
-
|
|
109
|
-
# unload
|
|
110
|
-
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
#
|
|
116
|
+
safekeep daemon start # launchctl bootstrap (macOS) / systemctl --user enable --now (Linux)
|
|
117
|
+
safekeep daemon status # exit 0 while running, 1 otherwise
|
|
118
|
+
safekeep daemon stop # unload
|
|
119
|
+
|
|
120
|
+
# logs (Linux): journalctl --user -u safekeep -f
|
|
121
|
+
# remove the unit (config and logs stay): safekeep daemon uninstall
|
|
122
|
+
# remove everything, incl. ~/.safekeep: safekeep daemon uninstall --purge
|
|
123
|
+
|
|
124
|
+
# Linux - start at boot without a login session (linger = user units active
|
|
125
|
+
# without a login), either pass
|
|
126
|
+
safekeep daemon install --enable-linger
|
|
127
|
+
# or run once: loginctl enable-linger $USER
|
|
117
128
|
```
|
|
118
129
|
|
|
119
130
|
On macOS, once you have granted TCC/FDA (Full Disk Access) permissions to the
|
|
@@ -144,7 +155,8 @@ exactly those entries, as always. Without it safekeep switches to
|
|
|
144
155
|
**auto-discovery**: it scans `$HOME` for `.sync` files (skipping hidden
|
|
145
156
|
directories, `Library`, `.Trash`, `.cache`, `node_modules`, `.git`, `.venv`,
|
|
146
157
|
`__pycache__`, `venv`), watches `$HOME`, and picks up any `.sync` created after
|
|
147
|
-
startup. `dest:` stays mandatory in both modes
|
|
158
|
+
startup. `dest:` stays mandatory in both modes — unless `s3_bucket:` is set
|
|
159
|
+
(at least one of the two must be present).
|
|
148
160
|
|
|
149
161
|
`~/Code/myapp/.sync` (rules only, no destinations):
|
|
150
162
|
|
|
@@ -171,6 +183,66 @@ safekeep sync-once --dry-run # prints what would be copied, copies nothing
|
|
|
171
183
|
safekeep run # daemon: watch + copy (foreground)
|
|
172
184
|
```
|
|
173
185
|
|
|
186
|
+
## S3-compatible destinations
|
|
187
|
+
|
|
188
|
+
safekeep can push every project to **one S3-compatible bucket**
|
|
189
|
+
(S3 = Simple Storage Service) in addition to (or instead of) the filesystem
|
|
190
|
+
destinations. The client is [botocore](https://botocore.amazonaws.com/),
|
|
191
|
+
installed via the optional extra — the core package stays stdlib-only:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pipx install "safekeep[s3]" # or: pip install "safekeep[s3]"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Without the extra the config still parses, but any `s3_*` key fails fast with
|
|
198
|
+
an install hint (`doctor` reports it as a **non-fatal** warning).
|
|
199
|
+
|
|
200
|
+
`~/.safekeep` (the `s3_*` keys are valid **only** in this file — a `.sync`
|
|
201
|
+
can't add them; keep the file at `chmod 600`):
|
|
202
|
+
|
|
203
|
+
```ini
|
|
204
|
+
s3_endpoint: https://s3.example.com:443 # http:// → cleartext warning
|
|
205
|
+
s3_bucket: my-backup-bucket # at least one dest: OR s3_bucket:
|
|
206
|
+
s3_prefix: safekeep # optional, no '..'
|
|
207
|
+
s3_region: us-east-1 # default
|
|
208
|
+
s3_access_key: REPLACEME_ACCESS_KEY # required when s3_bucket is set
|
|
209
|
+
s3_secret_key: REPLACEME_SECRET_KEY # chmod 600 — or use s3_secret_key_env:
|
|
210
|
+
# s3_secret_key_env: SAFEKEEP_S3_SECRET # secret from env, never in the file
|
|
211
|
+
s3_addressing: path # path (default) | virtual
|
|
212
|
+
s3_tls_check: true # false = self-signed certificates
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
With `s3_secret_key_env:` the variable must be in the **agent's** environment
|
|
216
|
+
(launchd `EnvironmentVariables`, systemd `Environment=`) — a shell `export`
|
|
217
|
+
alone doesn't reach a daemon started at boot.
|
|
218
|
+
|
|
219
|
+
Semantics (see [SPEC.md](SPEC.md) §18): upload-only with `needs_copy` based on
|
|
220
|
+
object metadata (`x-amz-meta-mtime` in nanoseconds, `x-amz-meta-mode`,
|
|
221
|
+
symlinks = zero-byte objects with `x-amz-meta-symlink`), same 1s mtime
|
|
222
|
+
tolerance as filesystem destinations, network errors → `pending` with
|
|
223
|
+
backoff, and **no deletions ever** — `--prune` never touches S3. A slow PUT
|
|
224
|
+
blocks the single-process daemon until it times out (connect 10s / read 60s);
|
|
225
|
+
that's an accepted v1 limitation.
|
|
226
|
+
|
|
227
|
+
Compatible services:
|
|
228
|
+
|
|
229
|
+
| Service | Example `s3_endpoint` | `s3_addressing` | `s3_region` | TLS |
|
|
230
|
+
|---|---|---|---|---|
|
|
231
|
+
| MinIO (self-hosted) | `https://minio.example.com:9000` | `path` | any (`us-east-1` default) | `https` + `s3_tls_check: false` if self-signed |
|
|
232
|
+
| Silo (fork of MinIO) | `https://s3.example.com:443` | `path` | any (`us-east-1` default) | as the endpoint's own certificate |
|
|
233
|
+
| Ceph RGW | `https://rgw.example.com` | `path` | any, must match the RGW zone (`us-east-1` default) | as configured on the RGW |
|
|
234
|
+
| Wasabi | `https://s3.wasabisys.com` | `path` | **required**, must match the bucket (`us-east-1` on that endpoint) | `https` |
|
|
235
|
+
| Backblaze B2 | `https://s3.<region>.backblazeb2.com` | `virtual` or `path` | **required**, must match the bucket region | `https` only — cleartext rejected |
|
|
236
|
+
| AWS S3 | *(omit `s3_endpoint`)* | `virtual` | must match the bucket (`us-east-1` default) | `https` |
|
|
237
|
+
|
|
238
|
+
All of them speak SigV4 (Signature Version 4) with the same client — no
|
|
239
|
+
service-specific code path. B2 wants an **Application Key** (not the Master
|
|
240
|
+
Key) as `s3_access_key`/`s3_secret_key`.
|
|
241
|
+
|
|
242
|
+
`safekeep status` shows the bucket as `[ok]`/`[offline]` (cached HeadBucket),
|
|
243
|
+
and `safekeep doctor` reports credentials, file permissions and bucket
|
|
244
|
+
reachability — all as **non-fatal** warnings.
|
|
245
|
+
|
|
174
246
|
## CLI commands
|
|
175
247
|
|
|
176
248
|
```
|
|
@@ -183,6 +255,7 @@ safekeep <command> [--config PATH] [-v]
|
|
|
183
255
|
| `sync-once [--dry-run] [--project PATH] [--prune]` | a single pass: walks the source and copies whatever differs (`--dry-run` only prints what it would copy; `--prune` also removes dest files whose source still exists but is no longer included) |
|
|
184
256
|
| `status` | read-only: config, sources, discovered projects with N rules, destination states |
|
|
185
257
|
| `doctor [--json]` | diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails; `--json` prints the same checks as one JSON document on stdout with the same exit code, including a structured `permission_errors` count of the last 24h |
|
|
258
|
+
| `daemon {install,start,stop,status,uninstall}` | manage the boot agent: `install` runs preflight (fswatch, shim on PATH, WSL systemd-on) and renders the launchd plist / systemd user unit from the embedded templates (idempotent, `--dry-run`, `--enable-linger`); `start`/`stop`/`status` load, unload and probe it; `uninstall` removes the unit (`--purge` also drops `~/.safekeep` and state) |
|
|
186
259
|
|
|
187
260
|
## Tests
|
|
188
261
|
|
|
@@ -190,7 +263,7 @@ safekeep <command> [--config PATH] [-v]
|
|
|
190
263
|
python3 -m unittest discover -s tests
|
|
191
264
|
```
|
|
192
265
|
|
|
193
|
-
Stdlib (`unittest`) suite, zero dependencies:
|
|
266
|
+
Stdlib (`unittest`) suite, zero dependencies: 287 tests covering the matcher,
|
|
194
267
|
config, atomic copy, volumes, daemon, auto-discovery, CLI, the permission-trend
|
|
195
268
|
wrapper and the POSIX portability layer (platform helper, per-OS fswatch
|
|
196
269
|
monitor, systemd template).
|
|
@@ -198,6 +271,20 @@ monitor, systemd template).
|
|
|
198
271
|
the real binary are skipped when it's missing — but CI installs it so the
|
|
199
272
|
Linux job exercises the real `inotify` monitor.
|
|
200
273
|
|
|
274
|
+
The S3 integration test talks to a real bucket and is **skipped** unless every
|
|
275
|
+
`SAFEKEEP_S3_TEST_*` variable is set (no credential is hard-coded; it uploads
|
|
276
|
+
under a throwaway timestamped prefix and performs **no** DeleteObject):
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
export SAFEKEEP_S3_TEST_ENDPOINT=https://s3.example.com:443 \
|
|
280
|
+
SAFEKEEP_S3_TEST_BUCKET=my-backup-bucket \
|
|
281
|
+
SAFEKEEP_S3_TEST_ACCESS_KEY=REPLACEME_ACCESS_KEY \
|
|
282
|
+
SAFEKEEP_S3_TEST_SECRET_KEY=REPLACEME_SECRET_KEY \
|
|
283
|
+
SAFEKEEP_S3_TEST_REGION=us-east-1 # optional, default us-east-1
|
|
284
|
+
# SAFEKEEP_S3_TEST_PREFIX=safekeep # optional, default safekeep-itest
|
|
285
|
+
python3 -m unittest tests.test_s3_itest -v
|
|
286
|
+
```
|
|
287
|
+
|
|
201
288
|
## Documentation
|
|
202
289
|
|
|
203
290
|
Everything in detail (config format, pattern semantics, event → sync flow,
|
|
@@ -31,13 +31,19 @@ Documentation: https://foxhound87.github.io/safekeep/
|
|
|
31
31
|
`.sync` file can neither add nor remove destinations (a `dest:` line is an
|
|
32
32
|
invalid line), so a repo cloned from a third party can't redirect the backup
|
|
33
33
|
somewhere else.
|
|
34
|
+
- **S3-compatible destination** (optional `safekeep[s3]` extra): one
|
|
35
|
+
S3-compatible bucket alongside the filesystem destinations — same
|
|
36
|
+
size/mtime idempotency, metadata-based, and **never deletes anything**
|
|
37
|
+
(`--prune` only touches filesystem destinations).
|
|
34
38
|
|
|
35
39
|
## Installation
|
|
36
40
|
|
|
37
41
|
```bash
|
|
38
|
-
pipx install safekeep
|
|
42
|
+
pipx install safekeep # recommended for a CLI
|
|
43
|
+
pipx install "safekeep[s3]" # + S3-compatible destination (botocore)
|
|
39
44
|
# or
|
|
40
45
|
pip install safekeep
|
|
46
|
+
pip install "safekeep[s3]"
|
|
41
47
|
```
|
|
42
48
|
|
|
43
49
|
**Requirements**: macOS or Linux (POSIX with systemd), Python >= 3.9 and
|
|
@@ -51,21 +57,22 @@ sudo dnf install fswatch # Fedora
|
|
|
51
57
|
```
|
|
52
58
|
|
|
53
59
|
The **agent** (daemon at boot: launchd on macOS, a systemd *user unit* on Linux)
|
|
54
|
-
is installed from
|
|
55
|
-
the wheel — see
|
|
60
|
+
is installed from the installed package with `safekeep daemon install` — the
|
|
61
|
+
unit templates ship inside the wheel, no checkout needed — see
|
|
62
|
+
[Agent](#agent) below. The old `./install.sh` wrapper is deprecated.
|
|
56
63
|
|
|
57
64
|
## Agent
|
|
58
65
|
|
|
59
66
|
```bash
|
|
60
|
-
|
|
61
|
-
cd safekeep
|
|
67
|
+
pipx install safekeep # or: pip install --user safekeep
|
|
62
68
|
|
|
63
69
|
# only external dependency, if not installed yet (see the per-OS list above)
|
|
64
|
-
brew install fswatch # macOS — the
|
|
70
|
+
brew install fswatch # macOS — the preflight suggests the right command on Linux
|
|
65
71
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
72
|
+
# preflight (fswatch / shim on PATH / python / WSL systemd-on), then renders
|
|
73
|
+
# the plist or systemd user unit from the embedded template into place —
|
|
74
|
+
# idempotent, does NOT start anything yet
|
|
75
|
+
safekeep daemon install # --dry-run to preview, nothing is written
|
|
69
76
|
```
|
|
70
77
|
|
|
71
78
|
Then:
|
|
@@ -79,16 +86,18 @@ Then:
|
|
|
79
86
|
3. load the agent:
|
|
80
87
|
|
|
81
88
|
```bash
|
|
82
|
-
# macOS (
|
|
83
|
-
|
|
84
|
-
# unload
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
#
|
|
91
|
-
#
|
|
89
|
+
safekeep daemon start # launchctl bootstrap (macOS) / systemctl --user enable --now (Linux)
|
|
90
|
+
safekeep daemon status # exit 0 while running, 1 otherwise
|
|
91
|
+
safekeep daemon stop # unload
|
|
92
|
+
|
|
93
|
+
# logs (Linux): journalctl --user -u safekeep -f
|
|
94
|
+
# remove the unit (config and logs stay): safekeep daemon uninstall
|
|
95
|
+
# remove everything, incl. ~/.safekeep: safekeep daemon uninstall --purge
|
|
96
|
+
|
|
97
|
+
# Linux - start at boot without a login session (linger = user units active
|
|
98
|
+
# without a login), either pass
|
|
99
|
+
safekeep daemon install --enable-linger
|
|
100
|
+
# or run once: loginctl enable-linger $USER
|
|
92
101
|
```
|
|
93
102
|
|
|
94
103
|
On macOS, once you have granted TCC/FDA (Full Disk Access) permissions to the
|
|
@@ -119,7 +128,8 @@ exactly those entries, as always. Without it safekeep switches to
|
|
|
119
128
|
**auto-discovery**: it scans `$HOME` for `.sync` files (skipping hidden
|
|
120
129
|
directories, `Library`, `.Trash`, `.cache`, `node_modules`, `.git`, `.venv`,
|
|
121
130
|
`__pycache__`, `venv`), watches `$HOME`, and picks up any `.sync` created after
|
|
122
|
-
startup. `dest:` stays mandatory in both modes
|
|
131
|
+
startup. `dest:` stays mandatory in both modes — unless `s3_bucket:` is set
|
|
132
|
+
(at least one of the two must be present).
|
|
123
133
|
|
|
124
134
|
`~/Code/myapp/.sync` (rules only, no destinations):
|
|
125
135
|
|
|
@@ -146,6 +156,66 @@ safekeep sync-once --dry-run # prints what would be copied, copies nothing
|
|
|
146
156
|
safekeep run # daemon: watch + copy (foreground)
|
|
147
157
|
```
|
|
148
158
|
|
|
159
|
+
## S3-compatible destinations
|
|
160
|
+
|
|
161
|
+
safekeep can push every project to **one S3-compatible bucket**
|
|
162
|
+
(S3 = Simple Storage Service) in addition to (or instead of) the filesystem
|
|
163
|
+
destinations. The client is [botocore](https://botocore.amazonaws.com/),
|
|
164
|
+
installed via the optional extra — the core package stays stdlib-only:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pipx install "safekeep[s3]" # or: pip install "safekeep[s3]"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Without the extra the config still parses, but any `s3_*` key fails fast with
|
|
171
|
+
an install hint (`doctor` reports it as a **non-fatal** warning).
|
|
172
|
+
|
|
173
|
+
`~/.safekeep` (the `s3_*` keys are valid **only** in this file — a `.sync`
|
|
174
|
+
can't add them; keep the file at `chmod 600`):
|
|
175
|
+
|
|
176
|
+
```ini
|
|
177
|
+
s3_endpoint: https://s3.example.com:443 # http:// → cleartext warning
|
|
178
|
+
s3_bucket: my-backup-bucket # at least one dest: OR s3_bucket:
|
|
179
|
+
s3_prefix: safekeep # optional, no '..'
|
|
180
|
+
s3_region: us-east-1 # default
|
|
181
|
+
s3_access_key: REPLACEME_ACCESS_KEY # required when s3_bucket is set
|
|
182
|
+
s3_secret_key: REPLACEME_SECRET_KEY # chmod 600 — or use s3_secret_key_env:
|
|
183
|
+
# s3_secret_key_env: SAFEKEEP_S3_SECRET # secret from env, never in the file
|
|
184
|
+
s3_addressing: path # path (default) | virtual
|
|
185
|
+
s3_tls_check: true # false = self-signed certificates
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With `s3_secret_key_env:` the variable must be in the **agent's** environment
|
|
189
|
+
(launchd `EnvironmentVariables`, systemd `Environment=`) — a shell `export`
|
|
190
|
+
alone doesn't reach a daemon started at boot.
|
|
191
|
+
|
|
192
|
+
Semantics (see [SPEC.md](SPEC.md) §18): upload-only with `needs_copy` based on
|
|
193
|
+
object metadata (`x-amz-meta-mtime` in nanoseconds, `x-amz-meta-mode`,
|
|
194
|
+
symlinks = zero-byte objects with `x-amz-meta-symlink`), same 1s mtime
|
|
195
|
+
tolerance as filesystem destinations, network errors → `pending` with
|
|
196
|
+
backoff, and **no deletions ever** — `--prune` never touches S3. A slow PUT
|
|
197
|
+
blocks the single-process daemon until it times out (connect 10s / read 60s);
|
|
198
|
+
that's an accepted v1 limitation.
|
|
199
|
+
|
|
200
|
+
Compatible services:
|
|
201
|
+
|
|
202
|
+
| Service | Example `s3_endpoint` | `s3_addressing` | `s3_region` | TLS |
|
|
203
|
+
|---|---|---|---|---|
|
|
204
|
+
| MinIO (self-hosted) | `https://minio.example.com:9000` | `path` | any (`us-east-1` default) | `https` + `s3_tls_check: false` if self-signed |
|
|
205
|
+
| Silo (fork of MinIO) | `https://s3.example.com:443` | `path` | any (`us-east-1` default) | as the endpoint's own certificate |
|
|
206
|
+
| Ceph RGW | `https://rgw.example.com` | `path` | any, must match the RGW zone (`us-east-1` default) | as configured on the RGW |
|
|
207
|
+
| Wasabi | `https://s3.wasabisys.com` | `path` | **required**, must match the bucket (`us-east-1` on that endpoint) | `https` |
|
|
208
|
+
| Backblaze B2 | `https://s3.<region>.backblazeb2.com` | `virtual` or `path` | **required**, must match the bucket region | `https` only — cleartext rejected |
|
|
209
|
+
| AWS S3 | *(omit `s3_endpoint`)* | `virtual` | must match the bucket (`us-east-1` default) | `https` |
|
|
210
|
+
|
|
211
|
+
All of them speak SigV4 (Signature Version 4) with the same client — no
|
|
212
|
+
service-specific code path. B2 wants an **Application Key** (not the Master
|
|
213
|
+
Key) as `s3_access_key`/`s3_secret_key`.
|
|
214
|
+
|
|
215
|
+
`safekeep status` shows the bucket as `[ok]`/`[offline]` (cached HeadBucket),
|
|
216
|
+
and `safekeep doctor` reports credentials, file permissions and bucket
|
|
217
|
+
reachability — all as **non-fatal** warnings.
|
|
218
|
+
|
|
149
219
|
## CLI commands
|
|
150
220
|
|
|
151
221
|
```
|
|
@@ -158,6 +228,7 @@ safekeep <command> [--config PATH] [-v]
|
|
|
158
228
|
| `sync-once [--dry-run] [--project PATH] [--prune]` | a single pass: walks the source and copies whatever differs (`--dry-run` only prints what it would copy; `--prune` also removes dest files whose source still exists but is no longer included) |
|
|
159
229
|
| `status` | read-only: config, sources, discovered projects with N rules, destination states |
|
|
160
230
|
| `doctor [--json]` | diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails; `--json` prints the same checks as one JSON document on stdout with the same exit code, including a structured `permission_errors` count of the last 24h |
|
|
231
|
+
| `daemon {install,start,stop,status,uninstall}` | manage the boot agent: `install` runs preflight (fswatch, shim on PATH, WSL systemd-on) and renders the launchd plist / systemd user unit from the embedded templates (idempotent, `--dry-run`, `--enable-linger`); `start`/`stop`/`status` load, unload and probe it; `uninstall` removes the unit (`--purge` also drops `~/.safekeep` and state) |
|
|
161
232
|
|
|
162
233
|
## Tests
|
|
163
234
|
|
|
@@ -165,7 +236,7 @@ safekeep <command> [--config PATH] [-v]
|
|
|
165
236
|
python3 -m unittest discover -s tests
|
|
166
237
|
```
|
|
167
238
|
|
|
168
|
-
Stdlib (`unittest`) suite, zero dependencies:
|
|
239
|
+
Stdlib (`unittest`) suite, zero dependencies: 287 tests covering the matcher,
|
|
169
240
|
config, atomic copy, volumes, daemon, auto-discovery, CLI, the permission-trend
|
|
170
241
|
wrapper and the POSIX portability layer (platform helper, per-OS fswatch
|
|
171
242
|
monitor, systemd template).
|
|
@@ -173,6 +244,20 @@ monitor, systemd template).
|
|
|
173
244
|
the real binary are skipped when it's missing — but CI installs it so the
|
|
174
245
|
Linux job exercises the real `inotify` monitor.
|
|
175
246
|
|
|
247
|
+
The S3 integration test talks to a real bucket and is **skipped** unless every
|
|
248
|
+
`SAFEKEEP_S3_TEST_*` variable is set (no credential is hard-coded; it uploads
|
|
249
|
+
under a throwaway timestamped prefix and performs **no** DeleteObject):
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
export SAFEKEEP_S3_TEST_ENDPOINT=https://s3.example.com:443 \
|
|
253
|
+
SAFEKEEP_S3_TEST_BUCKET=my-backup-bucket \
|
|
254
|
+
SAFEKEEP_S3_TEST_ACCESS_KEY=REPLACEME_ACCESS_KEY \
|
|
255
|
+
SAFEKEEP_S3_TEST_SECRET_KEY=REPLACEME_SECRET_KEY \
|
|
256
|
+
SAFEKEEP_S3_TEST_REGION=us-east-1 # optional, default us-east-1
|
|
257
|
+
# SAFEKEEP_S3_TEST_PREFIX=safekeep # optional, default safekeep-itest
|
|
258
|
+
python3 -m unittest tests.test_s3_itest -v
|
|
259
|
+
```
|
|
260
|
+
|
|
176
261
|
## Documentation
|
|
177
262
|
|
|
178
263
|
Everything in detail (config format, pattern semantics, event → sync flow,
|
|
@@ -25,7 +25,11 @@ classifiers = [
|
|
|
25
25
|
"Programming Language :: Python :: 3.14",
|
|
26
26
|
"Topic :: System :: Archiving :: Backup",
|
|
27
27
|
]
|
|
28
|
-
# zero runtime dependencies: solo stdlib (fswatch è un binario esterno)
|
|
28
|
+
# zero runtime dependencies: solo stdlib (fswatch è un binario esterno).
|
|
29
|
+
# L'extra `s3` (botocore, import lazy in s3dest.py) è l'unica dipendenza
|
|
30
|
+
# opzionale: serve SOLO se il config usa le chiavi `s3_*` (SPEC.md §18).
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
s3 = ["botocore>=1.31,<2"]
|
|
29
33
|
|
|
30
34
|
[project.urls]
|
|
31
35
|
Repository = "https://github.com/foxhound87/safekeep"
|
|
@@ -39,5 +43,10 @@ safekeep = "safekeep.cli:main"
|
|
|
39
43
|
# NO auto-discovery: bin/ e tests/ farebbero errore di build
|
|
40
44
|
packages = ["safekeep"]
|
|
41
45
|
|
|
46
|
+
[tool.setuptools.package-data]
|
|
47
|
+
# 0.7.0: template unit + example config embedded — `safekeep daemon install`
|
|
48
|
+
# funziona da PyPI senza git clone (sorgente unica: safekeep/data/)
|
|
49
|
+
safekeep = ["data/*"]
|
|
50
|
+
|
|
42
51
|
[tool.setuptools.dynamic]
|
|
43
52
|
version = {attr = "safekeep.__version__"}
|