feat: add archive client foundation

This commit is contained in:
2026-07-22 14:55:02 +00:00
commit c11a7b5b5b
39 changed files with 2985 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
import os
import tempfile
import unittest
from pathlib import Path
from archive_clients.config import ClientConfig, ConfigError, RootMapping
class ConfigTests(unittest.TestCase):
def test_strict_config_mode_override_secrets_and_mapping(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for name in ("token", "qb-password", "syncthing-key"):
path = root / name
path.write_text(name, encoding="utf-8")
os.chmod(path, 0o600)
(root / "qb").mkdir()
(root / "sync").mkdir()
config_path = root / "client.toml"
config_path.write_text(
_config(root, role="cache"), encoding="utf-8"
)
config = ClientConfig.load(config_path, "archive")
self.assertEqual(config.role, "archive")
self.assertEqual(config.read_shared_token(), "token")
self.assertEqual(
config.qbittorrent.roots.api_to_local("/downloads/a/b"),
root / "qb/a/b",
)
self.assertEqual(config.jobs.stall_after, 30 * 60)
self.assertEqual(
config.syncthing.advertised_addresses, ("dynamic",)
)
def test_unknown_key_and_unsafe_secret_fail(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for name in ("token", "qb-password", "syncthing-key"):
path = root / name
path.write_text(name, encoding="utf-8")
os.chmod(path, 0o600)
config_path = root / "client.toml"
invalid = _config(root).replace(
"[qbittorrent]", "typo = true\n[qbittorrent]"
)
config_path.write_text(invalid, encoding="utf-8")
with self.assertRaisesRegex(ConfigError, "unknown root"):
ClientConfig.load(config_path)
os.chmod(root / "token", 0o640)
config_path.write_text(_config(root), encoding="utf-8")
with self.assertRaisesRegex(ConfigError, "permissions"):
ClientConfig.load(config_path).read_shared_token()
def test_mapping_rejects_escape(self):
mapping = RootMapping(Path("/api"), Path("/local"))
with self.assertRaises(ConfigError):
mapping.api_to_local("/elsewhere/file")
def test_endpoint_scheme_and_job_keys_are_strict(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
invalid_endpoint = _config(root).replace(
'control_endpoint = "ws://control/archive_control"',
'control_endpoint = "http://control/archive_control"',
)
path = root / "client.toml"
path.write_text(invalid_endpoint, encoding="utf-8")
with self.assertRaisesRegex(ConfigError, "ws/wss"):
ClientConfig.load(path)
path.write_text(
_config(root) + "\n[jobs]\nunknown = true\n", encoding="utf-8"
)
with self.assertRaisesRegex(ConfigError, "unknown jobs"):
ClientConfig.load(path)
def _config(root: Path, role: str = "cache") -> str:
return f'''client_id = "cache-1"
display_name = "Cache 1"
role = "{role}"
control_endpoint = "ws://control/archive_control"
shared_token_file = "{root / 'token'}"
state_db = "{root / 'state.db'}"
backup_dir = "{root / 'backups'}"
[qbittorrent]
endpoint = "http://qb"
username = "admin"
password_file = "{root / 'qb-password'}"
api_root = "/downloads"
local_root = "{root / 'qb'}"
[syncthing]
endpoint = "http://syncthing"
api_key_file = "{root / 'syncthing-key'}"
api_root = "/sync"
local_root = "{root / 'sync'}"
advertised_addresses = ["dynamic"]
'''
if __name__ == "__main__":
unittest.main()