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.
Files changed (35) hide show
  1. {safekeep-0.5.2/safekeep.egg-info → safekeep-0.7.0}/PKG-INFO +109 -22
  2. {safekeep-0.5.2 → safekeep-0.7.0}/README.md +106 -21
  3. {safekeep-0.5.2 → safekeep-0.7.0}/pyproject.toml +10 -1
  4. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/__init__.py +1 -1
  5. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/cli.py +123 -20
  6. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/config.py +115 -5
  7. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/copier.py +15 -4
  8. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/daemon.py +89 -4
  9. safekeep-0.7.0/safekeep/daemonctl.py +349 -0
  10. safekeep-0.7.0/safekeep/data/com.safekeep.agent.plist.tmpl +71 -0
  11. safekeep-0.7.0/safekeep/data/safekeep.example +91 -0
  12. safekeep-0.7.0/safekeep/data/safekeep.service.tmpl +47 -0
  13. safekeep-0.7.0/safekeep/s3dest.py +278 -0
  14. {safekeep-0.5.2 → safekeep-0.7.0/safekeep.egg-info}/PKG-INFO +109 -22
  15. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep.egg-info/SOURCES.txt +9 -0
  16. safekeep-0.7.0/safekeep.egg-info/requires.txt +3 -0
  17. {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_cli.py +96 -0
  18. {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_config.py +122 -0
  19. {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_daemon.py +164 -0
  20. safekeep-0.7.0/tests/test_daemonctl.py +651 -0
  21. {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_platform.py +48 -51
  22. safekeep-0.7.0/tests/test_s3_itest.py +157 -0
  23. safekeep-0.7.0/tests/test_s3dest.py +343 -0
  24. {safekeep-0.5.2 → safekeep-0.7.0}/LICENSE +0 -0
  25. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/matcher.py +0 -0
  26. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/platform.py +0 -0
  27. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep/volumes.py +0 -0
  28. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep.egg-info/dependency_links.txt +0 -0
  29. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep.egg-info/entry_points.txt +0 -0
  30. {safekeep-0.5.2 → safekeep-0.7.0}/safekeep.egg-info/top_level.txt +0 -0
  31. {safekeep-0.5.2 → safekeep-0.7.0}/setup.cfg +0 -0
  32. {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_copier.py +0 -0
  33. {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_matcher.py +0 -0
  34. {safekeep-0.5.2 → safekeep-0.7.0}/tests/test_trend.py +0 -0
  35. {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.5.2
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 # recommended for a CLI
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 a checkout of this repository with `./install.sh` — not from
80
- the wheel — see [Agent](#agent) below.
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
- git clone https://github.com/foxhound87/safekeep.git
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 script suggests the right command on Linux
97
+ brew install fswatch # macOS — the preflight suggests the right command on Linux
90
98
 
91
- # copies the example into ~/.safekeep, renders the plist / systemd unit,
92
- # runs preflight checks
93
- bash install.sh
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 (launchd)
108
- launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.safekeep.agent.plist
109
- # unload with: launchctl bootout gui/$(id -u)/com.safekeep.agent
110
-
111
- # Linux (systemd user unit)
112
- systemctl --user enable --now safekeep
113
- # status with: systemctl --user status safekeep
114
- # logs with: journalctl --user -u safekeep -f
115
- # unload with: systemctl --user disable --now safekeep
116
- # start at boot without a login session: loginctl enable-linger $USER
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: 221 tests covering the matcher,
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 # recommended for a CLI
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 a checkout of this repository with `./install.sh` — not from
55
- the wheel — see [Agent](#agent) below.
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
- git clone https://github.com/foxhound87/safekeep.git
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 script suggests the right command on Linux
70
+ brew install fswatch # macOS — the preflight suggests the right command on Linux
65
71
 
66
- # copies the example into ~/.safekeep, renders the plist / systemd unit,
67
- # runs preflight checks
68
- bash install.sh
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 (launchd)
83
- launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.safekeep.agent.plist
84
- # unload with: launchctl bootout gui/$(id -u)/com.safekeep.agent
85
-
86
- # Linux (systemd user unit)
87
- systemctl --user enable --now safekeep
88
- # status with: systemctl --user status safekeep
89
- # logs with: journalctl --user -u safekeep -f
90
- # unload with: systemctl --user disable --now safekeep
91
- # start at boot without a login session: loginctl enable-linger $USER
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: 221 tests covering the matcher,
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__"}
@@ -3,4 +3,4 @@ import logging
3
3
 
4
4
  logging.getLogger('safekeep').addHandler(logging.NullHandler())
5
5
 
6
- __version__ = '0.5.2'
6
+ __version__ = '0.7.0'