"""pull + push entry points. Both subprocess restic against ``rest:://:@//`` where the same password is the HTTP basic credential and the repo encryption key. cloud-svc provisions one password covering both. """ from __future__ import annotations import sys import urllib.parse from pathlib import Path from . import restic, scope as scopemod from .cli import Args from .creds import read_credentials def pull(args: Args) -> int: """Restore latest snapshot's files into pack_folder. If the repo has no snapshots yet, this is a no-op (first run on this machine; nothing to restore). """ discord_id, password = read_credentials(args.token_file) binary = restic.resolve_binary(args) repo = _restic_repo(args.url, discord_id, password) env = _restic_env(password) # Check whether any snapshots exist code, out = restic.run( binary, ["-r", repo, "snapshots", "--json", "--latest", "1"], env=env, ) if code != 0: print( f"cloud-sync: failed to list snapshots (restic exit {code})", file=sys.stderr, ) return 2 stripped = out.strip() if stripped in ("", "null", "[]"): print( "cloud-sync: no snapshots yet for this user " "(first run on this machine?); nothing to pull" ) return 0 scope = scopemod.load(args.pack_folder) _, exclude_from = scopemod.materialize_for_restic(args.pack_folder, scope) code, _ = restic.run( binary, [ "-r", repo, "restore", "latest", "--target", str(args.pack_folder), "--exclude-file", str(exclude_from), ], env=env, ) if code != 0: print(f"cloud-sync: restic restore failed (exit {code})", file=sys.stderr) return 2 print("cloud-sync: pull ok") return 0 def push(args: Args) -> int: """Snapshot the in-scope files into the user's repo.""" discord_id, password = read_credentials(args.token_file) binary = restic.resolve_binary(args) repo = _restic_repo(args.url, discord_id, password) env = _restic_env(password) scope = scopemod.load(args.pack_folder) files_from, exclude_from = scopemod.materialize_for_restic(args.pack_folder, scope) code, _ = restic.run( binary, [ "-r", repo, "backup", "--files-from", str(files_from), "--exclude-file", str(exclude_from), "--host", "cloud-sync", "--tag", "auto", ], env=env, cwd=args.pack_folder, ) if code != 0: print(f"cloud-sync: restic backup failed (exit {code})", file=sys.stderr) return 2 print("cloud-sync: push ok") return 0 # --------------------------------------------------------------------------- # helpers # --------------------------------------------------------------------------- def _restic_repo(base_url: str, discord_id: str, password: str) -> str: """Build rest:://:@// URL-embedded basic auth is universally supported by restic; alternative env vars (RESTIC_REST_USERNAME, RESTIC_REST_PASSWORD) require 0.16+. """ raw = base_url.strip() if raw.startswith("rest:"): raw = raw[len("rest:"):] raw = raw.rstrip("/") scheme_end = raw.find("://") if scheme_end <= 0: raise ValueError( f"--url must include scheme (http:// or https://): {base_url!r}" ) scheme = raw[: scheme_end + 3] host_and_path = raw[scheme_end + 3 :] u = urllib.parse.quote(discord_id, safe="") p = urllib.parse.quote(password, safe="") return f"rest:{scheme}{u}:{p}@{host_and_path}/{discord_id}/" def _restic_env(password: str) -> dict[str, str]: return { "RESTIC_PASSWORD": password, "RESTIC_PROGRESS_FPS": "0", }