safekeep 0.3.1__tar.gz → 0.4.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.3.1/safekeep.egg-info → safekeep-0.4.0}/PKG-INFO +1 -1
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep/__init__.py +1 -1
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep/cli.py +20 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep/platform.py +31 -3
- {safekeep-0.3.1 → safekeep-0.4.0/safekeep.egg-info}/PKG-INFO +1 -1
- {safekeep-0.3.1 → safekeep-0.4.0}/tests/test_cli.py +63 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/tests/test_platform.py +65 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/LICENSE +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/README.md +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/pyproject.toml +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep/config.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep/copier.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep/daemon.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep/matcher.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep/volumes.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep.egg-info/SOURCES.txt +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep.egg-info/dependency_links.txt +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep.egg-info/entry_points.txt +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/safekeep.egg-info/top_level.txt +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/setup.cfg +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/tests/test_config.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/tests/test_copier.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/tests/test_daemon.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.0}/tests/test_matcher.py +0 -0
- {safekeep-0.3.1 → safekeep-0.4.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.4.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
|
|
@@ -20,8 +20,11 @@ from safekeep.platform import (
|
|
|
20
20
|
install_hint,
|
|
21
21
|
is_darwin,
|
|
22
22
|
is_linux,
|
|
23
|
+
is_wsl,
|
|
23
24
|
linger_enabled,
|
|
24
25
|
recent_permission_errors,
|
|
26
|
+
systemd_user_available,
|
|
27
|
+
wsl_distro,
|
|
25
28
|
)
|
|
26
29
|
from safekeep.volumes import dest_state
|
|
27
30
|
|
|
@@ -244,6 +247,23 @@ def cmd_doctor(args):
|
|
|
244
247
|
else:
|
|
245
248
|
out(True, f'errori di permesso recenti: nessuno nelle ultime 24h — log: {log}')
|
|
246
249
|
|
|
250
|
+
# WSL (SPEC.md §17.2): tre righe informative — nessuna è mai fatale
|
|
251
|
+
if is_wsl():
|
|
252
|
+
out(True, f'WSL rilevato: {wsl_distro() or "sconosciuta"} '
|
|
253
|
+
'(supporto best-effort, SPEC §17)')
|
|
254
|
+
if not systemd_user_available():
|
|
255
|
+
out(False, 'systemd user non disponibile (WSL1 o systemd spento) — '
|
|
256
|
+
'abilita systemd in /etc/wsl.conf: [boot] systemd=true '
|
|
257
|
+
'(poi wsl --shutdown da Windows) oppure esegui '
|
|
258
|
+
'`safekeep run` a mano — warning non fatale')
|
|
259
|
+
if daemon is not None:
|
|
260
|
+
sotto_mnt = [p for p in list(daemon.cfg.sources)
|
|
261
|
+
+ list(daemon.cfg.dests) if p.startswith('/mnt/')]
|
|
262
|
+
if sotto_mnt:
|
|
263
|
+
out(True, f'drvfs: {len(sotto_mnt)} path sotto /mnt/ — I/O lento '
|
|
264
|
+
'e case-insensitive di default: valuta una dest su ext4 '
|
|
265
|
+
'nativa della distro (SPEC §17.2)')
|
|
266
|
+
|
|
247
267
|
# Linux: il limite di watch di inotify è il rischio #1 sugli alberi grandi
|
|
248
268
|
# (SPEC.md §14.4) — informativo, NON fatale
|
|
249
269
|
if is_linux():
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Rilevamento piattaforma (SPEC.md §14.1): l'unico posto che legge `sys.platform`.
|
|
2
2
|
|
|
3
3
|
Funzioni/constanti minime: monitor fswatch atteso, hint di installazione,
|
|
4
|
-
init system, limite di watch di inotify, log corrente, linger systemd
|
|
5
|
-
Tutto letto **a chiamata** (mai a import), così i test
|
|
6
|
-
ed esercitano entrambi i rami anche girando su una
|
|
4
|
+
init system, limite di watch di inotify, log corrente, linger systemd,
|
|
5
|
+
rilevamento WSL. Tutto letto **a chiamata** (mai a import), così i test
|
|
6
|
+
patchano `sys.platform` ed esercitano entrambi i rami anche girando su una
|
|
7
|
+
sola piattaforma.
|
|
7
8
|
|
|
8
9
|
I nomi dei monitor sono quelli stampati da `fswatch -M`: `fsevents_monitor`
|
|
9
10
|
(FSEvents, macOS) e `inotify_monitor` (inotify, Linux) — attenzione, su Linux
|
|
@@ -11,6 +12,7 @@ il nome NON è `inotify` (SPEC.md §14.2).
|
|
|
11
12
|
"""
|
|
12
13
|
import getpass
|
|
13
14
|
import os
|
|
15
|
+
import platform as _platform
|
|
14
16
|
import re
|
|
15
17
|
import shutil
|
|
16
18
|
import sys
|
|
@@ -20,6 +22,7 @@ INOTIFY_LIMIT_PATH = '/proc/sys/fs/inotify/max_user_watches'
|
|
|
20
22
|
INOTIFY_MIN_WATCHES = 16384 # sotto questa soglia un home medio lo supera
|
|
21
23
|
LOG_PATH = '~/.local/state/safekeep/safekeep.log' # log corrente (SPEC.md §3)
|
|
22
24
|
LINGER_DIR = '/var/lib/systemd/linger'
|
|
25
|
+
SYSTEMD_RUN_PATH = '/run/systemd/system' # assente su WSL1/systemd off
|
|
23
26
|
# EPERM (Errno 1) / EACCES (Errno 13) come li scrive il logger: codice errno
|
|
24
27
|
# + testo del kernel (SPEC.md §14.4)
|
|
25
28
|
PERM_ERR_RE = re.compile(r'\[Errno (?:1|13)\]|Operation not permitted|Permission denied')
|
|
@@ -117,3 +120,28 @@ def linger_enabled(user=None, linger_dir=None):
|
|
|
117
120
|
except OSError:
|
|
118
121
|
return None
|
|
119
122
|
return user in entries
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_wsl():
|
|
126
|
+
"""True dentro WSL (Windows Subsystem for Linux) — SPEC.md §17.1.
|
|
127
|
+
|
|
128
|
+
`sys.platform` resta `linux` su WSL: il rilevamento passa dall'env
|
|
129
|
+
`WSL_DISTRO_NAME` (ogni distro WSL lo settà) oppure dal kernel
|
|
130
|
+
(`platform.release()` contiene `microsoft` su WSL1 e WSL2)."""
|
|
131
|
+
if os.environ.get('WSL_DISTRO_NAME'):
|
|
132
|
+
return True
|
|
133
|
+
return 'microsoft' in _platform.release().lower()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def wsl_distro():
|
|
137
|
+
"""Nome della distro WSL (env `WSL_DISTRO_NAME`), vuoto se assente —
|
|
138
|
+
SPEC.md §17.1."""
|
|
139
|
+
return os.environ.get('WSL_DISTRO_NAME', '')
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def systemd_user_available(path=None):
|
|
143
|
+
"""True se c'è un init systemd vivo (`/run/systemd/system`): assente su
|
|
144
|
+
WSL1 o con systemd spento in WSL2 — SPEC.md §17.2."""
|
|
145
|
+
if path is None:
|
|
146
|
+
path = SYSTEMD_RUN_PATH
|
|
147
|
+
return os.path.exists(path)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: safekeep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -363,5 +363,68 @@ class DoctorLingerTest(CliTestCase):
|
|
|
363
363
|
self.assertEqual(code, 0, out)
|
|
364
364
|
|
|
365
365
|
|
|
366
|
+
class DoctorWslTest(CliTestCase):
|
|
367
|
+
"""SPEC.md §17.2: le tre righe WSL di `doctor` sono informative e non fatali."""
|
|
368
|
+
|
|
369
|
+
def doctor_wsl(self, distro='Ubuntu', systemd=False, dest='/mnt/c/backup'):
|
|
370
|
+
cfg = write(os.path.join(self.tmp, 'wsl.cfg'), f'dest: {dest}\n')
|
|
371
|
+
args = argparse.Namespace(config=cfg, v=False)
|
|
372
|
+
buf = io.StringIO()
|
|
373
|
+
with mock.patch.dict(os.environ, {'HOME': self.home}), \
|
|
374
|
+
mock.patch.object(cli, 'is_wsl', return_value=True), \
|
|
375
|
+
mock.patch.object(cli, 'wsl_distro', return_value=distro), \
|
|
376
|
+
mock.patch.object(cli, 'systemd_user_available',
|
|
377
|
+
return_value=systemd), \
|
|
378
|
+
contextlib.redirect_stdout(buf):
|
|
379
|
+
code = cli.cmd_doctor(args)
|
|
380
|
+
return code, buf.getvalue()
|
|
381
|
+
|
|
382
|
+
@unittest.skipUnless(shutil.which('fswatch'), 'fswatch non installato')
|
|
383
|
+
def test_wsle_righe_con_hint_systemd_e_drvfs(self):
|
|
384
|
+
code, out = self.doctor_wsl()
|
|
385
|
+
self.assertIn('✔ WSL rilevato: Ubuntu', out)
|
|
386
|
+
hint = next(l for l in out.splitlines()
|
|
387
|
+
if 'systemd user non disponibile' in l)
|
|
388
|
+
self.assertTrue(hint.startswith('✗'), hint)
|
|
389
|
+
for needle in ('/etc/wsl.conf', 'systemd=true', 'safekeep run',
|
|
390
|
+
'warning non fatale'):
|
|
391
|
+
self.assertIn(needle, hint, needle)
|
|
392
|
+
drvfs = next(l for l in out.splitlines() if 'drvfs' in l)
|
|
393
|
+
self.assertIn('/mnt/', drvfs)
|
|
394
|
+
self.assertEqual(code, 0, out) # mai fatale
|
|
395
|
+
|
|
396
|
+
@unittest.skipUnless(shutil.which('fswatch'), 'fswatch non installato')
|
|
397
|
+
def test_systemd_attivo_nessun_hint(self):
|
|
398
|
+
code, out = self.doctor_wsl(systemd=True)
|
|
399
|
+
self.assertNotIn('systemd user non disponibile', out)
|
|
400
|
+
self.assertIn('WSL rilevato', out)
|
|
401
|
+
self.assertEqual(code, 0, out)
|
|
402
|
+
|
|
403
|
+
@unittest.skipUnless(shutil.which('fswatch'), 'fswatch non installato')
|
|
404
|
+
def test_nessun_path_sotto_mnt_niente_drvfs(self):
|
|
405
|
+
code, out = self.doctor_wsl(dest=os.path.join(self.tmp, 'dst'))
|
|
406
|
+
self.assertNotIn('drvfs', out)
|
|
407
|
+
self.assertEqual(code, 0, out)
|
|
408
|
+
|
|
409
|
+
def test_detection_reale_via_env_fino_a_doctor(self):
|
|
410
|
+
"""Detection reale (env) fino all'output di doctor: niente patch."""
|
|
411
|
+
env = dict(os.environ, HOME=self.home, WSL_DISTRO_NAME='Debian')
|
|
412
|
+
r = subprocess.run([sys.executable, CLI, 'doctor', '--config', self.cfg],
|
|
413
|
+
capture_output=True, text=True, env=env, cwd=REPO,
|
|
414
|
+
timeout=60)
|
|
415
|
+
self.assertIn('WSL rilevato: Debian', r.stdout)
|
|
416
|
+
|
|
417
|
+
@unittest.skipUnless(shutil.which('fswatch'), 'fswatch non installato')
|
|
418
|
+
def test_senza_wsl_nulla_cambia(self):
|
|
419
|
+
"""Su Linux/macOS non-WSL nessuna riga WSL compare (SPEC §17)."""
|
|
420
|
+
env = {k: v for k, v in os.environ.items() if k != 'WSL_DISTRO_NAME'}
|
|
421
|
+
env['HOME'] = self.home
|
|
422
|
+
r = subprocess.run([sys.executable, CLI, 'doctor', '--config', self.cfg],
|
|
423
|
+
capture_output=True, text=True, env=env, cwd=REPO,
|
|
424
|
+
timeout=60)
|
|
425
|
+
self.assertNotIn('WSL', r.stdout)
|
|
426
|
+
self.assertNotIn('drvfs', r.stdout)
|
|
427
|
+
|
|
428
|
+
|
|
366
429
|
if __name__ == '__main__':
|
|
367
430
|
unittest.main()
|
|
@@ -227,5 +227,70 @@ class LingerTest(unittest.TestCase):
|
|
|
227
227
|
self.assertFalse(plat.linger_enabled(linger_dir=self._dir()))
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
class WslTest(unittest.TestCase):
|
|
231
|
+
"""SPEC.md §17.1: rilevamento WSL via env o kernel, letto a chiamata."""
|
|
232
|
+
|
|
233
|
+
def test_env_wsl_distro_name(self):
|
|
234
|
+
with mock.patch.dict(os.environ, {'WSL_DISTRO_NAME': 'Ubuntu'}):
|
|
235
|
+
self.assertTrue(plat.is_wsl())
|
|
236
|
+
self.assertEqual(plat.wsl_distro(), 'Ubuntu')
|
|
237
|
+
|
|
238
|
+
def test_kernel_microsoft_senza_env(self):
|
|
239
|
+
for release in ('5.15.167.4-microsoft-standard-WSL2', # WSL2
|
|
240
|
+
'4.4.0-19041-Microsoft'): # WSL1 (maiuscolo)
|
|
241
|
+
with self.subTest(release=release), \
|
|
242
|
+
mock.patch.dict(os.environ, {'WSL_DISTRO_NAME': ''}), \
|
|
243
|
+
mock.patch.object(plat, '_platform') as p:
|
|
244
|
+
p.release.return_value = release
|
|
245
|
+
self.assertTrue(plat.is_wsl())
|
|
246
|
+
self.assertEqual(plat.wsl_distro(), '')
|
|
247
|
+
|
|
248
|
+
def test_non_wsl(self):
|
|
249
|
+
with mock.patch.dict(os.environ, {'WSL_DISTRO_NAME': ''}), \
|
|
250
|
+
mock.patch.object(plat, '_platform') as p:
|
|
251
|
+
p.release.return_value = '24.6.0' # macOS, niente microsoft
|
|
252
|
+
self.assertFalse(plat.is_wsl())
|
|
253
|
+
self.assertEqual(plat.wsl_distro(), '')
|
|
254
|
+
|
|
255
|
+
def test_systemd_user_available(self):
|
|
256
|
+
with tempfile.TemporaryDirectory() as d:
|
|
257
|
+
self.assertTrue(plat.systemd_user_available(path=d))
|
|
258
|
+
self.assertFalse(
|
|
259
|
+
plat.systemd_user_available(path='/non/esiste/qui'),
|
|
260
|
+
'/run/systemd/system assente = WSL1 o systemd spento (SPEC §17.2)')
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@unittest.skipUnless(shutil.which('fswatch'), 'fswatch non installato')
|
|
264
|
+
class InstallShSenzaSystemdTest(unittest.TestCase):
|
|
265
|
+
"""SPEC.md §17.3: `install.sh` su Linux senza systemd → unit NON installata,
|
|
266
|
+
istruzioni di avvio manuale ed exit 0 (nessun errore duro)."""
|
|
267
|
+
|
|
268
|
+
def test_ramo_linux_senza_systemd_esce_0(self):
|
|
269
|
+
if os.path.isdir('/run/systemd/system'):
|
|
270
|
+
self.skipTest('questa macchina ha systemd: ramo diverso')
|
|
271
|
+
tmp = tempfile.mkdtemp()
|
|
272
|
+
self.addCleanup(shutil.rmtree, tmp, ignore_errors=True)
|
|
273
|
+
home, fake_bin = os.path.join(tmp, 'home'), os.path.join(tmp, 'bin')
|
|
274
|
+
os.makedirs(home)
|
|
275
|
+
os.makedirs(fake_bin)
|
|
276
|
+
uname = os.path.join(fake_bin, 'uname') # uname finto → Linux
|
|
277
|
+
with open(uname, 'w') as fh:
|
|
278
|
+
fh.write('#!/bin/bash\necho Linux\n')
|
|
279
|
+
os.chmod(uname, 0o755)
|
|
280
|
+
env = dict(os.environ, HOME=home,
|
|
281
|
+
PATH=fake_bin + os.pathsep + os.environ['PATH'])
|
|
282
|
+
r = subprocess.run(['bash', os.path.join(REPO, 'install.sh')],
|
|
283
|
+
capture_output=True, text=True, env=env, timeout=60)
|
|
284
|
+
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
|
285
|
+
self.assertIn('systemd non disponibile', r.stdout)
|
|
286
|
+
self.assertIn('safekeep.py run', r.stdout) # avvio manuale
|
|
287
|
+
self.assertIn('systemd=true', r.stdout) # hint /etc/wsl.conf
|
|
288
|
+
self.assertTrue(os.path.exists(os.path.join(home, '.safekeep')),
|
|
289
|
+
'la config viene creata comunque')
|
|
290
|
+
self.assertFalse(os.path.exists(
|
|
291
|
+
os.path.join(home, '.config/systemd/user/safekeep.service')),
|
|
292
|
+
'niente unit senza systemd')
|
|
293
|
+
|
|
294
|
+
|
|
230
295
|
if __name__ == '__main__':
|
|
231
296
|
unittest.main()
|
|
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
|