ffdfb1f9b6
Reasons stacked up:
- AV: unsigned JARs that auto-download binaries + upload files trigger
Windows Defender false-positives more often than Python scripts
invoked by code-signed python.exe.
- Qt UI option: PySide6 opens a path to a real Qt UI (matching Prism's
look) if needed later. JVM Qt bindings are abandoned.
- frazclient already needs Python; inlining as 'import cloud_sync' is
zero overhead vs the launcher always shelling out to java.
Implementation:
- cloud_sync package: cli.py (argparse), creds.py, scope.py,
restic.py (binary discovery + auto-download + sha256 verify),
sync.py (pull/push subprocess restic).
- pyproject.toml with hatchling backend; pip-installable.
- Makefile builds cloud-sync.pyz via python -m zipapp (~53 KB).
- 33 pytest tests, stdlib only on runtime.
- CI workflow runs pytest matrix (3.10/3.11/3.12) + builds pyz.
- DESIGN.md + README.md updated to reflect Python.
E2E verified against local restic-rest-server:
pull empty → push initial → rm -rf local → pull restores → modify+push
creates second snapshot → client forget --prune blocked by --append-only.
Throws away ~565 LOC of Kotlin (and 18 jar tests) committed earlier in
this same session. Net result is ~250 LOC Python + 33 tests = smaller
and more aligned with the rest of the stack.
71 lines
2.0 KiB
Python
71 lines
2.0 KiB
Python
"""Scope file reader + materializer tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from cloud_sync.scope import (
|
|
DEFAULT_EXCLUDE,
|
|
DEFAULT_INCLUDE,
|
|
Scope,
|
|
load,
|
|
materialize_for_restic,
|
|
)
|
|
|
|
|
|
def test_load_missing_returns_defaults(tmp_path):
|
|
s = load(tmp_path)
|
|
assert s.include == DEFAULT_INCLUDE
|
|
assert s.exclude == DEFAULT_EXCLUDE
|
|
|
|
|
|
def test_load_valid_overrides_defaults(tmp_path):
|
|
state = tmp_path / ".cloud-sync"
|
|
state.mkdir()
|
|
(state / "scope.json").write_text(json.dumps({
|
|
"include": ["foo/", "bar.txt"],
|
|
"exclude": ["**/*.log"],
|
|
}))
|
|
s = load(tmp_path)
|
|
assert s.include == ["foo/", "bar.txt"]
|
|
assert s.exclude == ["**/*.log"]
|
|
|
|
|
|
def test_load_partial_keeps_defaults_for_missing(tmp_path):
|
|
state = tmp_path / ".cloud-sync"
|
|
state.mkdir()
|
|
(state / "scope.json").write_text(json.dumps({"include": ["just-this"]}))
|
|
s = load(tmp_path)
|
|
assert s.include == ["just-this"]
|
|
assert s.exclude == DEFAULT_EXCLUDE
|
|
|
|
|
|
def test_load_invalid_falls_back(tmp_path, capsys):
|
|
state = tmp_path / ".cloud-sync"
|
|
state.mkdir()
|
|
(state / "scope.json").write_text("{not valid json")
|
|
s = load(tmp_path)
|
|
assert s.include == DEFAULT_INCLUDE
|
|
captured = capsys.readouterr()
|
|
assert "invalid" in captured.err.lower()
|
|
|
|
|
|
def test_materialize_writes_files(tmp_path):
|
|
scope = Scope(include=["config/", "options.txt"], exclude=["**/*.log"])
|
|
files_from, exclude_from = materialize_for_restic(tmp_path, scope)
|
|
assert files_from.exists()
|
|
assert exclude_from.exists()
|
|
body_in = files_from.read_text().splitlines()
|
|
body_ex = exclude_from.read_text().splitlines()
|
|
# trailing slash stripped on include entries
|
|
assert "config" in body_in
|
|
assert "options.txt" in body_in
|
|
assert "**/*.log" in body_ex
|
|
|
|
|
|
def test_materialize_creates_state_dir(tmp_path):
|
|
scope = Scope(include=["x"], exclude=["y"])
|
|
files_from, _ = materialize_for_restic(tmp_path, scope)
|
|
assert files_from.parent.name == ".cloud-sync"
|
|
assert files_from.parent.exists()
|