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
+5
View File
@@ -0,0 +1,5 @@
.git
__pycache__
*.pyc
state
backups
+10
View File
@@ -0,0 +1,10 @@
__pycache__/
*.py[cod]
.venv/
dist/
build/
*.egg-info/
.pytest_cache/
state/
backups/
+20
View File
@@ -0,0 +1,20 @@
FROM python:3.11-slim AS builder
WORKDIR /build
COPY pyproject.toml ./
COPY src ./src
RUN pip wheel --no-cache-dir --wheel-dir /wheels .
FROM builder AS test
RUN pip install --no-cache-dir /wheels/*.whl
COPY tests ./tests
CMD ["python", "-m", "unittest", "discover", "-s", "tests", "-v"]
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN groupadd --gid 1001 archive-control \
&& useradd --uid 1001 --gid 1001 --no-create-home archive-control
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*.whl && rm -rf /wheels
USER 1001:1001
ENTRYPOINT ["archive-client"]
CMD ["--config", "/etc/archive-control/client.toml"]
+35
View File
@@ -0,0 +1,35 @@
# Archive Clients
One Python daemon runs as either an `archive` or `cache` node for Archive
Control. Both roles use the same code and `sodium/archive-clients` image.
The current foundation provides strict TOML configuration, file-backed secrets,
API/local root mapping, fail-fast permission and sparse-file probes, a durable
SQLite command inbox, bounded one-writer WebSocket output, registration-first
authentication, heartbeat handling, duplicate-command acknowledgements, and
indefinite capped exponential reconnect with optional jitter.
```bash
archive-client --config /etc/archive-control/client.toml --check-config
archive-client --config /etc/archive-control/client.toml --mode archive
```
`--mode` accepts only `archive` or `cache` and overrides the configured role.
Secrets must be regular files without group/world permissions. The daemon never
stores them in SQLite or sends the shared token after registration.
This foundation currently executes heartbeat and state-snapshot commands.
Other mutation commands are durably rejected as unsupported until their
service and file-operation executors are added; they are never falsely
acknowledged as accepted.
Run tests and build using containers:
```bash
docker build --target test -t archive-clients-test .
docker run --rm archive-clients-test
docker build -t sodium/archive-clients:dev .
```
Generated bindings are pinned to archive-control-proto commit
`4ec852014dad74606d4078b3ae1aa208c814b033`.
+13
View File
@@ -0,0 +1,13 @@
services:
client:
image: sodium/archive-clients:dev
build: .
user: "1001:1001"
command: ["--config", "/etc/archive-control/client.toml", "--check-config"]
volumes:
- ./examples/client.toml:/etc/archive-control/client.toml:ro
- ./state:/var/lib/archive-control
- ./backups:/var/backups/archive-control
- ./secrets:/run/secrets:ro
- ./data/qb:/data/qb
- ./data/sync:/data/sync
+29
View File
@@ -0,0 +1,29 @@
client_id = "cache-1"
display_name = "Cache 1"
role = "cache"
control_endpoint = "ws://control:8765/archive_control"
shared_token_file = "/run/secrets/archive_control_token"
state_db = "/var/lib/archive-control/client.db"
backup_dir = "/var/backups/archive-control"
[connection]
registration_timeout = "10s"
reconnect_initial = "1s"
reconnect_max = "60s"
reconnect_reset_after = "60s"
reconnect_jitter = true
[qbittorrent]
endpoint = "http://qbittorrent:8080"
username = "admin"
password_file = "/run/secrets/qb_password"
api_root = "/downloads"
local_root = "/data/qb"
[syncthing]
endpoint = "http://syncthing:8384"
api_key_file = "/run/secrets/syncthing_api_key"
api_root = "/sync"
local_root = "/data/sync"
advertised_addresses = ["dynamic"]
+16
View File
@@ -0,0 +1,16 @@
[build-system]
requires = ["setuptools==80.9.0", "wheel==0.46.3"]
build-backend = "setuptools.build_meta"
[project]
name = "archive-clients"
version = "0.1.0.dev0"
requires-python = ">=3.11"
dependencies = ["protobuf==7.35.1", "websockets==16.0"]
[project.scripts]
archive-client = "archive_clients.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
+4
View File
@@ -0,0 +1,4 @@
"""Archive Control data-node daemon."""
PROTO_COMMIT = "4ec852014dad74606d4078b3ae1aa208c814b033"
+40
View File
@@ -0,0 +1,40 @@
"""Archive client command-line entry point."""
from __future__ import annotations
import argparse
import asyncio
import json
import logging
from pathlib import Path
from typing import Sequence
from archive_clients.config import ClientConfig
from archive_clients.daemon import ArchiveClientDaemon
from archive_clients.probes import probe_root
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="archive-client")
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--mode", choices=("archive", "cache"))
parser.add_argument("--check-config", action="store_true")
arguments = parser.parse_args(argv)
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
config = ClientConfig.load(arguments.config, arguments.mode)
probes = [
probe_root(config.qbittorrent.local_root),
probe_root(config.syncthing.local_root),
]
config.read_shared_token()
config.qbittorrent.read_password()
config.syncthing.read_api_key()
if arguments.check_config:
print(json.dumps({
"client_id": config.client_id,
"role": config.role,
"filesystems": [probe.__dict__ | {"root": str(probe.root)} for probe in probes],
}, sort_keys=True))
return 0
asyncio.run(ArchiveClientDaemon(config, probes).run())
return 0
+259
View File
@@ -0,0 +1,259 @@
"""Strict TOML configuration with file-backed secrets."""
from __future__ import annotations
import os
import re
import stat
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Any
from urllib.parse import urlsplit
import tomllib
class ConfigError(ValueError):
pass
_CLIENT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
_DURATION = re.compile(r"^([1-9][0-9]*)(ms|s|m|h)$")
_FACTORS = {"ms": 0.001, "s": 1, "m": 60, "h": 3600}
_ENV = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
@dataclass(frozen=True)
class RootMapping:
api_root: PurePosixPath
local_root: Path
def api_to_local(self, api_path: str) -> Path:
candidate = PurePosixPath(api_path)
try:
relative = candidate.relative_to(self.api_root)
except ValueError as exc:
raise ConfigError("API path is outside its configured root") from exc
if any(part in {"", ".", ".."} for part in relative.parts):
raise ConfigError("API path contains an unsafe component")
return self.local_root.joinpath(*relative.parts)
@dataclass(frozen=True)
class ServiceConfig:
endpoint: str
api_root: PurePosixPath
local_root: Path
username: str | None = None
password_file: Path | None = None
api_key_file: Path | None = None
advertised_addresses: tuple[str, ...] = ()
@property
def roots(self) -> RootMapping:
return RootMapping(self.api_root, self.local_root)
def read_password(self) -> str | None:
return _secret(self.password_file, "service password") if self.password_file else None
def read_api_key(self) -> str | None:
return _secret(self.api_key_file, "service API key") if self.api_key_file else None
@dataclass(frozen=True)
class ConnectionConfig:
registration_timeout: float = 10
heartbeat_interval: float = 15
offline_timeout: float = 45
reconnect_initial: float = 1
reconnect_max: float = 60
reconnect_reset_after: float = 60
reconnect_jitter: bool = True
@dataclass(frozen=True)
class JobsConfig:
stall_after: float = 30 * 60
@dataclass(frozen=True)
class ClientConfig:
client_id: str
display_name: str
role: str
control_endpoint: str
shared_token_file: Path
state_db: Path
backup_dir: Path
qbittorrent: ServiceConfig
syncthing: ServiceConfig
connection: ConnectionConfig = ConnectionConfig()
jobs: JobsConfig = JobsConfig()
@classmethod
def load(cls, path: Path, mode_override: str | None = None) -> "ClientConfig":
try:
with path.open("rb") as source:
raw = tomllib.load(source)
except (OSError, tomllib.TOMLDecodeError) as exc:
raise ConfigError("configuration cannot be read") from exc
_keys(raw, {
"client_id", "display_name", "role", "control_endpoint",
"shared_token_file", "state_db", "backup_dir", "connection",
"jobs", "qbittorrent", "syncthing",
}, "root")
role = mode_override or raw.get("role")
if role not in {"archive", "cache"}:
raise ConfigError("role/--mode must be archive or cache")
client_id = raw.get("client_id")
if not isinstance(client_id, str) or not _CLIENT_ID.fullmatch(client_id):
raise ConfigError("client_id is invalid")
display_name = raw.get("display_name")
if not isinstance(display_name, str) or not 1 <= len(display_name) <= 128:
raise ConfigError("display_name is invalid")
connection = _connection(raw.get("connection", {}))
jobs = _jobs(raw.get("jobs", {}))
return cls(
client_id, display_name, role,
_endpoint(raw, "control_endpoint", {"ws", "wss"}),
_absolute_path(raw, "shared_token_file"),
_absolute_path(raw, "state_db"),
_absolute_path(raw, "backup_dir"),
_service(raw.get("qbittorrent"), "qbittorrent"),
_service(raw.get("syncthing"), "syncthing"), connection, jobs,
)
def read_shared_token(self) -> str:
return _secret(self.shared_token_file, "shared token")
def _service(value: Any, name: str) -> ServiceConfig:
if not isinstance(value, dict):
raise ConfigError(f"{name} must be a table")
allowed = {
"endpoint", "api_root", "local_root", "username", "password_file",
"api_key_file", "advertised_addresses",
}
_keys(value, allowed, name)
api_root = PurePosixPath(_string(value, "api_root"))
if not api_root.is_absolute() or ".." in api_root.parts:
raise ConfigError(f"{name}.api_root must be absolute and normalized")
username = value.get("username")
if username is not None and (not isinstance(username, str) or not username):
raise ConfigError(f"{name}.username must be a non-empty string")
addresses = value.get("advertised_addresses", [])
if not isinstance(addresses, list) or any(
not isinstance(address, str) or not address for address in addresses
):
raise ConfigError(f"{name}.advertised_addresses must be a string array")
return ServiceConfig(
_endpoint(value, "endpoint", {"http", "https"}), api_root,
_absolute_path(value, "local_root"), username,
_absolute_path(value, "password_file")
if "password_file" in value else None,
_absolute_path(value, "api_key_file")
if "api_key_file" in value else None,
tuple(addresses),
)
def _connection(value: Any) -> ConnectionConfig:
if not isinstance(value, dict):
raise ConfigError("connection must be a table")
_keys(value, {"registration_timeout", "heartbeat_interval", "offline_timeout", "reconnect_initial", "reconnect_max", "reconnect_reset_after", "reconnect_jitter"}, "connection")
result = ConnectionConfig(
registration_timeout=_duration(value.get("registration_timeout", "10s")),
heartbeat_interval=_duration(value.get("heartbeat_interval", "15s")),
offline_timeout=_duration(value.get("offline_timeout", "45s")),
reconnect_initial=_duration(value.get("reconnect_initial", "1s")),
reconnect_max=_duration(value.get("reconnect_max", "60s")),
reconnect_reset_after=_duration(value.get("reconnect_reset_after", "60s")),
reconnect_jitter=value.get("reconnect_jitter", True),
)
if result.reconnect_initial > result.reconnect_max:
raise ConfigError("reconnect_initial cannot exceed reconnect_max")
if result.offline_timeout <= result.heartbeat_interval:
raise ConfigError("offline_timeout must exceed heartbeat_interval")
if not isinstance(result.reconnect_jitter, bool):
raise ConfigError("reconnect_jitter must be boolean")
return result
def _jobs(value: Any) -> JobsConfig:
if not isinstance(value, dict):
raise ConfigError("jobs must be a table")
_keys(value, {"stall_after"}, "jobs")
return JobsConfig(stall_after=_duration(value.get("stall_after", "30m")))
def _duration(value: Any) -> float:
if not isinstance(value, str) or not (match := _DURATION.fullmatch(value)):
raise ConfigError("duration must look like 15s or 30m")
return int(match.group(1)) * _FACTORS[match.group(2)]
def _string(value: dict[str, Any], key: str) -> str:
item = value.get(key)
if not isinstance(item, str) or not item:
raise ConfigError(f"{key} must be a non-empty string")
return _expand(item)
def _path(value: dict[str, Any], key: str) -> Path:
return Path(_string(value, key))
def _absolute_path(value: dict[str, Any], key: str) -> Path:
path = _path(value, key)
if not path.is_absolute():
raise ConfigError(f"{key} must be an absolute path")
return path
def _endpoint(
value: dict[str, Any], key: str, allowed_schemes: set[str]
) -> str:
endpoint = _string(value, key)
parsed = urlsplit(endpoint)
if (
parsed.scheme not in allowed_schemes
or not parsed.hostname
or parsed.username is not None
or parsed.password is not None
or parsed.fragment
):
schemes = "/".join(sorted(allowed_schemes))
raise ConfigError(f"{key} must be a credential-free {schemes} endpoint")
return endpoint
def _expand(value: str) -> str:
def replace(match: re.Match[str]) -> str:
name = match.group(1)
if name not in os.environ:
raise ConfigError(f"environment variable {name} is not set")
return os.environ[name]
expanded = _ENV.sub(replace, value)
if "$" in expanded:
raise ConfigError("only ${NAME} environment interpolation is supported")
return expanded
def _secret(path: Path, name: str) -> str:
try:
metadata = path.stat()
exposed = metadata.st_mode & (stat.S_IRWXG | stat.S_IRWXO)
if not stat.S_ISREG(metadata.st_mode) or exposed:
raise ConfigError(f"{name} file permissions are unsafe")
value = path.read_text(encoding="utf-8").strip()
except OSError as exc:
raise ConfigError(f"{name} file cannot be read") from exc
if not value:
raise ConfigError(f"{name} is empty")
return value
def _keys(value: dict[str, Any], allowed: set[str], name: str) -> None:
unknown = sorted(set(value) - allowed)
if unknown:
raise ConfigError(f"unknown {name} keys: {', '.join(unknown)}")
+198
View File
@@ -0,0 +1,198 @@
"""Reconnectable Archive Control client transport."""
from __future__ import annotations
import asyncio
import logging
import random
import time
import uuid
from typing import Any
from websockets.asyncio.client import connect
from archive_clients.config import ClientConfig
from archive_clients.probes import FilesystemProbe
from archive_clients.protocol import (
decode,
decode_message,
encode,
encode_message,
new_envelope,
)
from archive_clients.state import ClientStore, CommandConflict
from archive_control.v1 import client_pb2, common_pb2, control_pb2, job_pb2
logger = logging.getLogger(__name__)
class ArchiveClientDaemon:
def __init__(self, config: ClientConfig, probes: list[FilesystemProbe]):
self.config = config
self.probes = probes
self.store = ClientStore(config.state_db)
async def run(self) -> None:
await asyncio.to_thread(self.store.initialize)
delay = self.config.connection.reconnect_initial
while True:
started = time.monotonic()
try:
await self._connection()
except asyncio.CancelledError:
raise
except Exception as exc:
logger.warning("control connection ended: %s", type(exc).__name__)
if time.monotonic() - started >= self.config.connection.reconnect_reset_after:
delay = self.config.connection.reconnect_initial
wait = random.uniform(0, delay) if self.config.connection.reconnect_jitter else delay
await asyncio.sleep(wait)
delay = min(delay * 2, self.config.connection.reconnect_max)
async def _connection(self) -> None:
async with connect(
self.config.control_endpoint, ping_interval=None, compression=None,
max_size=1024 * 1024,
) as websocket:
registration = self._registration()
await websocket.send(encode(registration))
registration.register_request.shared_token = ""
response = decode(await asyncio.wait_for(
websocket.recv(), self.config.connection.registration_timeout
))
if response.WhichOneof("payload") != "register_response":
raise RuntimeError("control did not answer registration")
if response.correlation_id != registration.message_id:
raise RuntimeError("registration response correlation mismatch")
if response.register_response.status != client_pb2.REGISTRATION_STATUS_ACCEPTED:
raise RuntimeError("control rejected registration")
if response.register_response.negotiated_version.major != 1:
raise RuntimeError("control negotiated an unsupported protocol version")
outbound: asyncio.Queue[str] = asyncio.Queue(maxsize=100)
writer = asyncio.create_task(self._writer(websocket, outbound))
try:
async for frame in websocket:
await self._handle(decode(frame), outbound)
finally:
writer.cancel()
await asyncio.gather(writer, return_exceptions=True)
def _registration(self):
envelope = new_envelope()
request = envelope.register_request
request.protocol_version.CopyFrom(envelope.protocol_version)
request.client.client_id = self.config.client_id
request.client.display_name = self.config.display_name
request.client.role = (
common_pb2.CLIENT_ROLE_ARCHIVE
if self.config.role == "archive" else common_pb2.CLIENT_ROLE_CACHE
)
request.connection_instance_id = str(uuid.uuid4())
request.shared_token = self.config.read_shared_token()
request.capabilities.max_envelope_bytes = 1024 * 1024
request.capabilities.syncthing_advertised_addresses.extend(
self.config.syncthing.advertised_addresses
)
for root_name, probe in zip(
("qbittorrent", "syncthing"), self.probes, strict=True
):
filesystem = request.capabilities.filesystems.add()
filesystem.root_name = root_name
filesystem.readable = probe.readable
filesystem.writable = probe.writable
filesystem.hard_link = probe.hard_link
filesystem.sparse_files = probe.sparse_files
if all(probe.sparse_files for probe in self.probes):
request.capabilities.features.append(client_pb2.CLIENT_FEATURE_SPARSE_FILES)
for cursor in self.store.list_active_job_cursors():
active = request.active_jobs.add()
active.job_id = str(cursor["job_id"])
active.job_revision = int(cursor["revision"])
active.last_event_sequence = int(cursor["last_event_sequence"])
active.state = job_pb2.JobState.Value(str(cursor["state"]))
active.committed = bool(cursor["committed"])
return envelope
async def _writer(
self, websocket: Any, outbound: asyncio.Queue[str]
) -> None:
while True:
await websocket.send(await outbound.get())
async def _handle(
self, envelope: Any, outbound: asyncio.Queue[str]
) -> None:
payload = envelope.WhichOneof("payload")
if payload == "heartbeat":
response = new_envelope()
response.correlation_id = envelope.message_id
response.heartbeat_ack.sequence = envelope.heartbeat.sequence
await outbound.put(encode(response))
elif payload == "command":
await self._accept_command(envelope, outbound)
elif payload == "protocol_error":
logger.warning("control reported protocol error code=%s", envelope.protocol_error.error.code)
async def _accept_command(
self, envelope: Any, outbound: asyncio.Queue[str]
) -> None:
command = envelope.command
acknowledgement = self._initial_acknowledgement(command)
accepted = None
try:
accepted = await asyncio.to_thread(
self.store.accept_command,
command.command_id,
encode_message(command),
encode_message(acknowledgement),
)
if accepted.duplicate:
acknowledgement = decode_message(
accepted.acknowledgement_json, control_pb2.CommandAck()
)
if (
acknowledgement.status
== control_pb2.COMMAND_ACK_STATUS_ACCEPTED
):
acknowledgement.status = (
control_pb2.COMMAND_ACK_STATUS_DUPLICATE
)
except CommandConflict:
acknowledgement.status = control_pb2.COMMAND_ACK_STATUS_REJECTED
acknowledgement.error.code = common_pb2.ERROR_CODE_CONFLICT
acknowledgement.error.message = "command ID content conflict"
response = new_envelope()
response.correlation_id = envelope.message_id
response.command_ack.CopyFrom(acknowledgement)
await outbound.put(encode(response))
if (
accepted is not None
and not accepted.duplicate
and acknowledgement.status
== control_pb2.COMMAND_ACK_STATUS_ACCEPTED
and command.WhichOneof("payload") == "request_job_snapshot"
):
snapshot = new_envelope()
snapshot.correlation_id = envelope.message_id
snapshot.client_state_snapshot.snapshot_id = str(uuid.uuid4())
snapshot.client_state_snapshot.observed_at.CopyFrom(snapshot.sent_at)
await outbound.put(encode(snapshot))
@staticmethod
def _initial_acknowledgement(command: Any) -> control_pb2.CommandAck:
acknowledgement = control_pb2.CommandAck(command_id=command.command_id)
if not command.command_id or command.WhichOneof("payload") is None:
acknowledgement.status = control_pb2.COMMAND_ACK_STATUS_REJECTED
acknowledgement.error.code = common_pb2.ERROR_CODE_INVALID_ARGUMENT
acknowledgement.error.message = "command ID and payload are required"
elif command.WhichOneof("payload") == "request_job_snapshot":
acknowledgement.status = control_pb2.COMMAND_ACK_STATUS_ACCEPTED
else:
acknowledgement.status = control_pb2.COMMAND_ACK_STATUS_REJECTED
acknowledgement.error.code = common_pb2.ERROR_CODE_UNSUPPORTED
acknowledgement.error.message = (
"command is not supported by this client build"
)
return acknowledgement
+54
View File
@@ -0,0 +1,54 @@
"""Fail-fast root and filesystem capability probes."""
from __future__ import annotations
import os
import tempfile
from dataclasses import dataclass
from pathlib import Path
class ProbeError(RuntimeError):
pass
@dataclass(frozen=True)
class FilesystemProbe:
root: Path
readable: bool
writable: bool
hard_link: bool
sparse_files: bool
def probe_root(root: Path) -> FilesystemProbe:
if not root.is_absolute() or not root.is_dir():
raise ProbeError(f"configured root is not an existing absolute directory: {root}")
if not os.access(root, os.R_OK | os.X_OK | os.W_OK):
raise ProbeError(f"configured root permissions are insufficient: {root}")
source: Path | None = None
linked: Path | None = None
try:
descriptor, raw_path = tempfile.mkstemp(prefix=".archive-control-probe-", dir=root)
source = Path(raw_path)
with os.fdopen(descriptor, "wb") as probe:
probe.seek(1024 * 1024)
probe.write(b"x")
probe.flush()
os.fsync(probe.fileno())
allocated = source.stat().st_blocks * 512
sparse = allocated < source.stat().st_size
linked = source.with_name(f"{source.name}.link")
try:
os.link(source, linked)
hard_link = linked.stat().st_ino == source.stat().st_ino
except OSError:
hard_link = False
return FilesystemProbe(root, True, True, hard_link, sparse)
except OSError as exc:
raise ProbeError(f"filesystem capability probe failed for {root}") from exc
finally:
if linked is not None:
linked.unlink(missing_ok=True)
if source is not None:
source.unlink(missing_ok=True)
+93
View File
@@ -0,0 +1,93 @@
"""Strict protobuf-JSON envelope helpers."""
from __future__ import annotations
import json
import uuid
from datetime import datetime, timezone
from typing import Any
from google.protobuf import json_format
from archive_control.v1 import envelope_pb2
class ProtocolError(ValueError):
pass
def new_envelope() -> envelope_pb2.Envelope:
envelope = envelope_pb2.Envelope()
envelope.protocol_version.major = 1
envelope.message_id = str(uuid.uuid4())
envelope.sent_at.FromDatetime(datetime.now(timezone.utc))
return envelope
def encode(envelope: envelope_pb2.Envelope) -> str:
return json_format.MessageToJson(
envelope, preserving_proto_field_name=False, indent=None,
sort_keys=True, ensure_ascii=False,
)
def encode_message(message: Any) -> str:
return json_format.MessageToJson(
message, preserving_proto_field_name=False, indent=None,
sort_keys=True, ensure_ascii=False,
)
def decode_message(data: str, message: Any) -> Any:
try:
raw = json.loads(data, object_pairs_hook=_unique)
return json_format.ParseDict(raw, message)
except (ValueError, json.JSONDecodeError, json_format.ParseError) as exc:
raise ProtocolError("invalid protobuf JSON message") from exc
def decode(data: str | bytes, max_bytes: int = 1024 * 1024) -> envelope_pb2.Envelope:
if not isinstance(data, str) or len(data.encode()) > max_bytes:
raise ProtocolError("control envelope must be bounded text")
try:
raw = json.loads(data, object_pairs_hook=_unique)
envelope = json_format.ParseDict(raw, envelope_pb2.Envelope())
except (
TypeError,
ValueError,
json.JSONDecodeError,
json_format.ParseError,
) as exc:
raise ProtocolError("invalid control envelope") from exc
if envelope.protocol_version.major != 1:
raise ProtocolError("unsupported protocol major version")
_canonical_uuid(envelope.message_id, "message_id")
if envelope.correlation_id:
_canonical_uuid(envelope.correlation_id, "correlation_id")
if not envelope.HasField("sent_at"):
raise ProtocolError("sent_at is required")
try:
envelope.sent_at.ToDatetime(tzinfo=timezone.utc)
except (ValueError, OverflowError) as exc:
raise ProtocolError("sent_at is invalid") from exc
if envelope.WhichOneof("payload") is None:
raise ProtocolError("unsupported or empty control envelope")
return envelope
def _unique(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate JSON key")
result[key] = value
return result
def _canonical_uuid(value: str, name: str) -> None:
try:
parsed = uuid.UUID(value)
except (ValueError, AttributeError) as exc:
raise ProtocolError(f"{name} must be a canonical UUID") from exc
if str(parsed) != value:
raise ProtocolError(f"{name} must be a lowercase canonical UUID")
+122
View File
@@ -0,0 +1,122 @@
"""Durable command inbox and client recovery cursors."""
from __future__ import annotations
import hashlib
import json
import sqlite3
from dataclasses import dataclass
from pathlib import Path
class CommandConflict(RuntimeError):
pass
@dataclass(frozen=True)
class CommandAcceptance:
duplicate: bool
acknowledgement_json: str
class ClientStore:
def __init__(self, database: Path):
self.database = database
def initialize(self) -> None:
self.database.parent.mkdir(parents=True, exist_ok=True)
with self._connect() as connection:
version = connection.execute("PRAGMA user_version").fetchone()[0]
if version > 1:
raise RuntimeError(
f"client database schema {version} is newer than supported"
)
connection.executescript(_SCHEMA)
connection.execute("PRAGMA user_version = 1")
if connection.execute("PRAGMA foreign_key_check").fetchall():
raise RuntimeError("client database foreign-key check failed")
def accept_command(
self, command_id: str, command_json: str, acknowledgement_json: str
) -> CommandAcceptance:
payload = _canonical(json.loads(command_json))
acknowledgement = _canonical(json.loads(acknowledgement_json))
digest = hashlib.sha256(payload.encode()).hexdigest()
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute(
"SELECT payload_sha256, command_json, acknowledgement_json FROM commands WHERE command_id = ?",
(command_id,),
).fetchone()
if existing:
if existing["payload_sha256"] != digest or existing["command_json"] != payload:
raise CommandConflict("command ID was reused with different content")
return CommandAcceptance(True, existing["acknowledgement_json"])
connection.execute(
"""
INSERT INTO commands (
command_id, payload_sha256, command_json,
acknowledgement_json, state
) VALUES (?, ?, ?, ?, 'received')
""",
(command_id, digest, payload, acknowledgement),
)
return CommandAcceptance(False, acknowledgement)
def list_active_job_cursors(self) -> list[dict[str, object]]:
with self._connect() as connection:
rows = connection.execute(
"""
SELECT job_id, revision, last_event_sequence, state, committed
FROM jobs WHERE state NOT IN ('JOB_STATE_SUCCEEDED',
'JOB_STATE_FAILED', 'JOB_STATE_CANCELLED')
ORDER BY job_id
"""
).fetchall()
return [dict(row) for row in rows]
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.database, isolation_level=None, timeout=5)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys = ON")
connection.execute("PRAGMA journal_mode = WAL")
connection.execute("PRAGMA synchronous = FULL")
connection.execute("PRAGMA busy_timeout = 5000")
return connection
def _canonical(value: object) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
_SCHEMA = """
CREATE TABLE IF NOT EXISTS commands (
command_id TEXT PRIMARY KEY,
payload_sha256 TEXT NOT NULL,
command_json TEXT NOT NULL,
acknowledgement_json TEXT NOT NULL,
state TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS jobs (
job_id TEXT PRIMARY KEY,
definition_json TEXT NOT NULL,
state TEXT NOT NULL,
revision INTEGER NOT NULL,
last_event_sequence INTEGER NOT NULL,
committed INTEGER NOT NULL CHECK(committed IN (0, 1))
);
CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY,
job_id TEXT NOT NULL REFERENCES jobs(job_id),
sequence INTEGER NOT NULL,
event_json TEXT NOT NULL,
UNIQUE(job_id, sequence)
);
CREATE TABLE IF NOT EXISTS file_journal (
operation_id TEXT PRIMARY KEY,
job_id TEXT NOT NULL,
intent_json TEXT NOT NULL,
result_json TEXT,
state TEXT NOT NULL
);
"""
+2
View File
@@ -0,0 +1,2 @@
"""Generated Archive Control protocol package."""
+2
View File
@@ -0,0 +1,2 @@
"""Generated archive_control.v1 bindings."""
+57
View File
@@ -0,0 +1,57 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: archive_control/v1/client.proto
# Protobuf Python Version: 7.35.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
7,
35,
1,
'',
'archive_control/v1/client.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from archive_control.v1 import common_pb2 as archive__control_dot_v1_dot_common__pb2
from archive_control.v1 import job_pb2 as archive__control_dot_v1_dot_job__pb2
from archive_control.v1 import route_pb2 as archive__control_dot_v1_dot_route__pb2
from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x61rchive_control/v1/client.proto\x12\x12\x61rchive_control.v1\x1a\x1f\x61rchive_control/v1/common.proto\x1a\x1c\x61rchive_control/v1/job.proto\x1a\x1e\x61rchive_control/v1/route.proto\x1a\x1egoogle/protobuf/duration.proto\"\xdf\x01\n\x16\x46ilesystemCapabilities\x12\x1b\n\troot_name\x18\x01 \x01(\tR\x08rootName\x12\x1a\n\x08readable\x18\x02 \x01(\x08R\x08readable\x12\x1a\n\x08writable\x18\x03 \x01(\x08R\x08writable\x12\x1b\n\thard_link\x18\x04 \x01(\x08R\x08hardLink\x12\x18\n\x07reflink\x18\x05 \x01(\x08R\x07reflink\x12!\n\x0csparse_files\x18\x06 \x01(\x08R\x0bsparseFiles\x12\x16\n\x06\x64\x65tail\x18\x07 \x01(\tR\x06\x64\x65tail\"\xc6\x05\n\x12\x43lientCapabilities\x12=\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0e\x32!.archive_control.v1.ClientFeatureR\x08\x66\x65\x61tures\x12=\n\x08services\x18\x02 \x03(\x0b\x32!.archive_control.v1.ServiceHealthR\x08services\x12L\n\x0b\x66ilesystems\x18\x03 \x03(\x0b\x32*.archive_control.v1.FilesystemCapabilitiesR\x0b\x66ilesystems\x12\x36\n\x06routes\x18\x04 \x03(\x0b\x32\x1e.archive_control.v1.LocalRouteR\x06routes\x12.\n\x13syncthing_device_id\x18\x05 \x01(\tR\x11syncthingDeviceId\x12\x44\n\x1esyncthing_advertised_addresses\x18\x06 \x03(\tR\x1csyncthingAdvertisedAddresses\x12/\n\x13qbittorrent_version\x18\x07 \x01(\tR\x12qbittorrentVersion\x12=\n\x1bqbittorrent_web_api_version\x18\x08 \x01(\tR\x18qbittorrentWebApiVersion\x12-\n\x12libtorrent_version\x18\t \x01(\tR\x11libtorrentVersion\x12+\n\x11syncthing_version\x18\n \x01(\tR\x10syncthingVersion\x12<\n\x1asupported_partfile_formats\x18\x0b \x03(\tR\x18supportedPartfileFormats\x12,\n\x12max_envelope_bytes\x18\x0c \x01(\rR\x10maxEnvelopeBytes\"\xcd\x01\n\x0f\x41\x63tiveJobCursor\x12\x15\n\x06job_id\x18\x01 \x01(\tR\x05jobId\x12!\n\x0cjob_revision\x18\x02 \x01(\x04R\x0bjobRevision\x12.\n\x13last_event_sequence\x18\x03 \x01(\x04R\x11lastEventSequence\x12\x32\n\x05state\x18\x04 \x01(\x0e\x32\x1c.archive_control.v1.JobStateR\x05state\x12\x1c\n\tcommitted\x18\x05 \x01(\x08R\tcommitted\"\x88\x03\n\x0fRegisterRequest\x12N\n\x10protocol_version\x18\x01 \x01(\x0b\x32#.archive_control.v1.ProtocolVersionR\x0fprotocolVersion\x12:\n\x06\x63lient\x18\x02 \x01(\x0b\x32\".archive_control.v1.ClientIdentityR\x06\x63lient\x12\x34\n\x16\x63onnection_instance_id\x18\x03 \x01(\tR\x14\x63onnectionInstanceId\x12!\n\x0cshared_token\x18\x04 \x01(\tR\x0bsharedToken\x12J\n\x0c\x63\x61pabilities\x18\x05 \x01(\x0b\x32&.archive_control.v1.ClientCapabilitiesR\x0c\x63\x61pabilities\x12\x44\n\x0b\x61\x63tive_jobs\x18\x06 \x03(\x0b\x32#.archive_control.v1.ActiveJobCursorR\nactiveJobs\"\x8e\x03\n\x10RegisterResponse\x12>\n\x06status\x18\x01 \x01(\x0e\x32&.archive_control.v1.RegistrationStatusR\x06status\x12R\n\x12negotiated_version\x18\x02 \x01(\x0b\x32#.archive_control.v1.ProtocolVersionR\x11negotiatedVersion\x12#\n\rconnection_id\x18\x03 \x01(\tR\x0c\x63onnectionId\x12H\n\x12heartbeat_interval\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationR\x11heartbeatInterval\x12\x46\n\x11heartbeat_timeout\x18\x05 \x01(\x0b\x32\x19.google.protobuf.DurationR\x10heartbeatTimeout\x12/\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\"\'\n\tHeartbeat\x12\x1a\n\x08sequence\x18\x01 \x01(\x04R\x08sequence\"*\n\x0cHeartbeatAck\x12\x1a\n\x08sequence\x18\x01 \x01(\x04R\x08sequence*\x9f\x03\n\rClientFeature\x12\x1e\n\x1a\x43LIENT_FEATURE_UNSPECIFIED\x10\x00\x12#\n\x1f\x43LIENT_FEATURE_INVENTORY_CHUNKS\x10\x01\x12\x1f\n\x1b\x43LIENT_FEATURE_CONTENT_TREE\x10\x02\x12%\n!CLIENT_FEATURE_ROUTE_PROVISIONING\x10\x03\x12\x1c\n\x18\x43LIENT_FEATURE_HARD_LINK\x10\x04\x12\x1a\n\x16\x43LIENT_FEATURE_REFLINK\x10\x05\x12\x1f\n\x1b\x43LIENT_FEATURE_SPARSE_FILES\x10\x06\x12%\n!CLIENT_FEATURE_PARTFILE_MIGRATION\x10\x07\x12!\n\x1d\x43LIENT_FEATURE_PARTFILE_MERGE\x10\x08\x12!\n\x1d\x43LIENT_FEATURE_QB_STOPPED_ADD\x10\t\x12\x1b\n\x17\x43LIENT_FEATURE_EVICTION\x10\n\x12\x1c\n\x18\x43LIENT_FEATURE_DB_BACKUP\x10\x0b*}\n\x12RegistrationStatus\x12#\n\x1fREGISTRATION_STATUS_UNSPECIFIED\x10\x00\x12 \n\x1cREGISTRATION_STATUS_ACCEPTED\x10\x01\x12 \n\x1cREGISTRATION_STATUS_REJECTED\x10\x02\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.client_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_CLIENTFEATURE']._serialized_start=2211
_globals['_CLIENTFEATURE']._serialized_end=2626
_globals['_REGISTRATIONSTATUS']._serialized_start=2628
_globals['_REGISTRATIONSTATUS']._serialized_end=2753
_globals['_FILESYSTEMCAPABILITIES']._serialized_start=183
_globals['_FILESYSTEMCAPABILITIES']._serialized_end=406
_globals['_CLIENTCAPABILITIES']._serialized_start=409
_globals['_CLIENTCAPABILITIES']._serialized_end=1119
_globals['_ACTIVEJOBCURSOR']._serialized_start=1122
_globals['_ACTIVEJOBCURSOR']._serialized_end=1327
_globals['_REGISTERREQUEST']._serialized_start=1330
_globals['_REGISTERREQUEST']._serialized_end=1722
_globals['_REGISTERRESPONSE']._serialized_start=1725
_globals['_REGISTERRESPONSE']._serialized_end=2123
_globals['_HEARTBEAT']._serialized_start=2125
_globals['_HEARTBEAT']._serialized_end=2164
_globals['_HEARTBEATACK']._serialized_start=2166
_globals['_HEARTBEATACK']._serialized_end=2208
# @@protoc_insertion_point(module_scope)
+155
View File
@@ -0,0 +1,155 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
import datetime
from archive_control.v1 import common_pb2 as _common_pb2
from archive_control.v1 import job_pb2 as _job_pb2
from archive_control.v1 import route_pb2 as _route_pb2
from google.protobuf import duration_pb2 as _duration_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class ClientFeature(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
CLIENT_FEATURE_UNSPECIFIED: _ClassVar[ClientFeature]
CLIENT_FEATURE_INVENTORY_CHUNKS: _ClassVar[ClientFeature]
CLIENT_FEATURE_CONTENT_TREE: _ClassVar[ClientFeature]
CLIENT_FEATURE_ROUTE_PROVISIONING: _ClassVar[ClientFeature]
CLIENT_FEATURE_HARD_LINK: _ClassVar[ClientFeature]
CLIENT_FEATURE_REFLINK: _ClassVar[ClientFeature]
CLIENT_FEATURE_SPARSE_FILES: _ClassVar[ClientFeature]
CLIENT_FEATURE_PARTFILE_MIGRATION: _ClassVar[ClientFeature]
CLIENT_FEATURE_PARTFILE_MERGE: _ClassVar[ClientFeature]
CLIENT_FEATURE_QB_STOPPED_ADD: _ClassVar[ClientFeature]
CLIENT_FEATURE_EVICTION: _ClassVar[ClientFeature]
CLIENT_FEATURE_DB_BACKUP: _ClassVar[ClientFeature]
class RegistrationStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
REGISTRATION_STATUS_UNSPECIFIED: _ClassVar[RegistrationStatus]
REGISTRATION_STATUS_ACCEPTED: _ClassVar[RegistrationStatus]
REGISTRATION_STATUS_REJECTED: _ClassVar[RegistrationStatus]
CLIENT_FEATURE_UNSPECIFIED: ClientFeature
CLIENT_FEATURE_INVENTORY_CHUNKS: ClientFeature
CLIENT_FEATURE_CONTENT_TREE: ClientFeature
CLIENT_FEATURE_ROUTE_PROVISIONING: ClientFeature
CLIENT_FEATURE_HARD_LINK: ClientFeature
CLIENT_FEATURE_REFLINK: ClientFeature
CLIENT_FEATURE_SPARSE_FILES: ClientFeature
CLIENT_FEATURE_PARTFILE_MIGRATION: ClientFeature
CLIENT_FEATURE_PARTFILE_MERGE: ClientFeature
CLIENT_FEATURE_QB_STOPPED_ADD: ClientFeature
CLIENT_FEATURE_EVICTION: ClientFeature
CLIENT_FEATURE_DB_BACKUP: ClientFeature
REGISTRATION_STATUS_UNSPECIFIED: RegistrationStatus
REGISTRATION_STATUS_ACCEPTED: RegistrationStatus
REGISTRATION_STATUS_REJECTED: RegistrationStatus
class FilesystemCapabilities(_message.Message):
__slots__ = ("root_name", "readable", "writable", "hard_link", "reflink", "sparse_files", "detail")
ROOT_NAME_FIELD_NUMBER: _ClassVar[int]
READABLE_FIELD_NUMBER: _ClassVar[int]
WRITABLE_FIELD_NUMBER: _ClassVar[int]
HARD_LINK_FIELD_NUMBER: _ClassVar[int]
REFLINK_FIELD_NUMBER: _ClassVar[int]
SPARSE_FILES_FIELD_NUMBER: _ClassVar[int]
DETAIL_FIELD_NUMBER: _ClassVar[int]
root_name: str
readable: bool
writable: bool
hard_link: bool
reflink: bool
sparse_files: bool
detail: str
def __init__(self, root_name: _Optional[str] = ..., readable: _Optional[bool] = ..., writable: _Optional[bool] = ..., hard_link: _Optional[bool] = ..., reflink: _Optional[bool] = ..., sparse_files: _Optional[bool] = ..., detail: _Optional[str] = ...) -> None: ...
class ClientCapabilities(_message.Message):
__slots__ = ("features", "services", "filesystems", "routes", "syncthing_device_id", "syncthing_advertised_addresses", "qbittorrent_version", "qbittorrent_web_api_version", "libtorrent_version", "syncthing_version", "supported_partfile_formats", "max_envelope_bytes")
FEATURES_FIELD_NUMBER: _ClassVar[int]
SERVICES_FIELD_NUMBER: _ClassVar[int]
FILESYSTEMS_FIELD_NUMBER: _ClassVar[int]
ROUTES_FIELD_NUMBER: _ClassVar[int]
SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
SYNCTHING_ADVERTISED_ADDRESSES_FIELD_NUMBER: _ClassVar[int]
QBITTORRENT_VERSION_FIELD_NUMBER: _ClassVar[int]
QBITTORRENT_WEB_API_VERSION_FIELD_NUMBER: _ClassVar[int]
LIBTORRENT_VERSION_FIELD_NUMBER: _ClassVar[int]
SYNCTHING_VERSION_FIELD_NUMBER: _ClassVar[int]
SUPPORTED_PARTFILE_FORMATS_FIELD_NUMBER: _ClassVar[int]
MAX_ENVELOPE_BYTES_FIELD_NUMBER: _ClassVar[int]
features: _containers.RepeatedScalarFieldContainer[ClientFeature]
services: _containers.RepeatedCompositeFieldContainer[_common_pb2.ServiceHealth]
filesystems: _containers.RepeatedCompositeFieldContainer[FilesystemCapabilities]
routes: _containers.RepeatedCompositeFieldContainer[_route_pb2.LocalRoute]
syncthing_device_id: str
syncthing_advertised_addresses: _containers.RepeatedScalarFieldContainer[str]
qbittorrent_version: str
qbittorrent_web_api_version: str
libtorrent_version: str
syncthing_version: str
supported_partfile_formats: _containers.RepeatedScalarFieldContainer[str]
max_envelope_bytes: int
def __init__(self, features: _Optional[_Iterable[_Union[ClientFeature, str]]] = ..., services: _Optional[_Iterable[_Union[_common_pb2.ServiceHealth, _Mapping]]] = ..., filesystems: _Optional[_Iterable[_Union[FilesystemCapabilities, _Mapping]]] = ..., routes: _Optional[_Iterable[_Union[_route_pb2.LocalRoute, _Mapping]]] = ..., syncthing_device_id: _Optional[str] = ..., syncthing_advertised_addresses: _Optional[_Iterable[str]] = ..., qbittorrent_version: _Optional[str] = ..., qbittorrent_web_api_version: _Optional[str] = ..., libtorrent_version: _Optional[str] = ..., syncthing_version: _Optional[str] = ..., supported_partfile_formats: _Optional[_Iterable[str]] = ..., max_envelope_bytes: _Optional[int] = ...) -> None: ...
class ActiveJobCursor(_message.Message):
__slots__ = ("job_id", "job_revision", "last_event_sequence", "state", "committed")
JOB_ID_FIELD_NUMBER: _ClassVar[int]
JOB_REVISION_FIELD_NUMBER: _ClassVar[int]
LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
COMMITTED_FIELD_NUMBER: _ClassVar[int]
job_id: str
job_revision: int
last_event_sequence: int
state: _job_pb2.JobState
committed: bool
def __init__(self, job_id: _Optional[str] = ..., job_revision: _Optional[int] = ..., last_event_sequence: _Optional[int] = ..., state: _Optional[_Union[_job_pb2.JobState, str]] = ..., committed: _Optional[bool] = ...) -> None: ...
class RegisterRequest(_message.Message):
__slots__ = ("protocol_version", "client", "connection_instance_id", "shared_token", "capabilities", "active_jobs")
PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int]
CLIENT_FIELD_NUMBER: _ClassVar[int]
CONNECTION_INSTANCE_ID_FIELD_NUMBER: _ClassVar[int]
SHARED_TOKEN_FIELD_NUMBER: _ClassVar[int]
CAPABILITIES_FIELD_NUMBER: _ClassVar[int]
ACTIVE_JOBS_FIELD_NUMBER: _ClassVar[int]
protocol_version: _common_pb2.ProtocolVersion
client: _common_pb2.ClientIdentity
connection_instance_id: str
shared_token: str
capabilities: ClientCapabilities
active_jobs: _containers.RepeatedCompositeFieldContainer[ActiveJobCursor]
def __init__(self, protocol_version: _Optional[_Union[_common_pb2.ProtocolVersion, _Mapping]] = ..., client: _Optional[_Union[_common_pb2.ClientIdentity, _Mapping]] = ..., connection_instance_id: _Optional[str] = ..., shared_token: _Optional[str] = ..., capabilities: _Optional[_Union[ClientCapabilities, _Mapping]] = ..., active_jobs: _Optional[_Iterable[_Union[ActiveJobCursor, _Mapping]]] = ...) -> None: ...
class RegisterResponse(_message.Message):
__slots__ = ("status", "negotiated_version", "connection_id", "heartbeat_interval", "heartbeat_timeout", "error")
STATUS_FIELD_NUMBER: _ClassVar[int]
NEGOTIATED_VERSION_FIELD_NUMBER: _ClassVar[int]
CONNECTION_ID_FIELD_NUMBER: _ClassVar[int]
HEARTBEAT_INTERVAL_FIELD_NUMBER: _ClassVar[int]
HEARTBEAT_TIMEOUT_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
status: RegistrationStatus
negotiated_version: _common_pb2.ProtocolVersion
connection_id: str
heartbeat_interval: _duration_pb2.Duration
heartbeat_timeout: _duration_pb2.Duration
error: _common_pb2.Error
def __init__(self, status: _Optional[_Union[RegistrationStatus, str]] = ..., negotiated_version: _Optional[_Union[_common_pb2.ProtocolVersion, _Mapping]] = ..., connection_id: _Optional[str] = ..., heartbeat_interval: _Optional[_Union[datetime.timedelta, _duration_pb2.Duration, _Mapping]] = ..., heartbeat_timeout: _Optional[_Union[datetime.timedelta, _duration_pb2.Duration, _Mapping]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ...) -> None: ...
class Heartbeat(_message.Message):
__slots__ = ("sequence",)
SEQUENCE_FIELD_NUMBER: _ClassVar[int]
sequence: int
def __init__(self, sequence: _Optional[int] = ...) -> None: ...
class HeartbeatAck(_message.Message):
__slots__ = ("sequence",)
SEQUENCE_FIELD_NUMBER: _ClassVar[int]
sequence: int
def __init__(self, sequence: _Optional[int] = ...) -> None: ...
+56
View File
@@ -0,0 +1,56 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: archive_control/v1/common.proto
# Protobuf Python Version: 7.35.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
7,
35,
1,
'',
'archive_control/v1/common.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x61rchive_control/v1/common.proto\x12\x12\x61rchive_control.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"=\n\x0fProtocolVersion\x12\x14\n\x05major\x18\x01 \x01(\rR\x05major\x12\x14\n\x05minor\x18\x02 \x01(\rR\x05minor\"\xf0\x01\n\x05\x45rror\x12\x31\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1d.archive_control.v1.ErrorCodeR\x04\x63ode\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\x12\x1c\n\tretryable\x18\x03 \x01(\x08R\tretryable\x12@\n\x07\x64\x65tails\x18\x04 \x03(\x0b\x32&.archive_control.v1.Error.DetailsEntryR\x07\x64\x65tails\x1a:\n\x0c\x44\x65tailsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xee\x01\n\rServiceHealth\x12\x18\n\x07service\x18\x01 \x01(\tR\x07service\x12\x35\n\x05state\x18\x02 \x01(\x0e\x32\x1f.archive_control.v1.HealthStateR\x05state\x12\x18\n\x07version\x18\x03 \x01(\tR\x07version\x12\x1f\n\x0b\x61pi_version\x18\x04 \x01(\tR\napiVersion\x12\x16\n\x06\x64\x65tail\x18\x05 \x01(\tR\x06\x64\x65tail\x12\x39\n\nchecked_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcheckedAt\"\x84\x01\n\x0e\x43lientIdentity\x12\x1b\n\tclient_id\x18\x01 \x01(\tR\x08\x63lientId\x12!\n\x0c\x64isplay_name\x18\x02 \x01(\tR\x0b\x64isplayName\x12\x32\n\x04role\x18\x03 \x01(\x0e\x32\x1e.archive_control.v1.ClientRoleR\x04role\"I\n\x0bPageRequest\x12\x1b\n\tpage_size\x18\x01 \x01(\rR\x08pageSize\x12\x1d\n\npage_token\x18\x02 \x01(\tR\tpageToken*Y\n\nClientRole\x12\x1b\n\x17\x43LIENT_ROLE_UNSPECIFIED\x10\x00\x12\x17\n\x13\x43LIENT_ROLE_ARCHIVE\x10\x01\x12\x15\n\x11\x43LIENT_ROLE_CACHE\x10\x02*\x96\x01\n\x0bHealthState\x12\x1c\n\x18HEALTH_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14HEALTH_STATE_HEALTHY\x10\x01\x12\x19\n\x15HEALTH_STATE_DEGRADED\x10\x02\x12\x1a\n\x16HEALTH_STATE_UNHEALTHY\x10\x03\x12\x18\n\x14HEALTH_STATE_UNKNOWN\x10\x04*\xe2\x04\n\tErrorCode\x12\x1a\n\x16\x45RROR_CODE_UNSPECIFIED\x10\x00\x12\x1f\n\x1b\x45RROR_CODE_INVALID_ARGUMENT\x10\x01\x12\x1e\n\x1a\x45RROR_CODE_UNAUTHENTICATED\x10\x02\x12(\n$ERROR_CODE_PROTOCOL_VERSION_MISMATCH\x10\x03\x12!\n\x1d\x45RROR_CODE_CLIENT_ID_CONFLICT\x10\x04\x12\x18\n\x14\x45RROR_CODE_NOT_FOUND\x10\x05\x12\x17\n\x13\x45RROR_CODE_CONFLICT\x10\x06\x12\x1a\n\x16\x45RROR_CODE_STALE_STATE\x10\x07\x12\x1a\n\x16\x45RROR_CODE_UNSUPPORTED\x10\x08\x12\"\n\x1e\x45RROR_CODE_PRECONDITION_FAILED\x10\t\x12!\n\x1d\x45RROR_CODE_RESOURCE_EXHAUSTED\x10\n\x12\x1a\n\x16\x45RROR_CODE_UNAVAILABLE\x10\x0b\x12\x16\n\x12\x45RROR_CODE_TIMEOUT\x10\x0c\x12\x18\n\x14\x45RROR_CODE_CANCELLED\x10\r\x12%\n!ERROR_CODE_INTEGRITY_CHECK_FAILED\x10\x0e\x12\x1c\n\x18\x45RROR_CODE_PATH_CONFLICT\x10\x0f\x12 \n\x1c\x45RROR_CODE_PERMISSION_DENIED\x10\x10\x12+\n\'ERROR_CODE_MANUAL_INTERVENTION_REQUIRED\x10\x11\x12\x17\n\x13\x45RROR_CODE_INTERNAL\x10\x12\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.common_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_ERROR_DETAILSENTRY']._loaded_options = None
_globals['_ERROR_DETAILSENTRY']._serialized_options = b'8\001'
_globals['_CLIENTROLE']._serialized_start=845
_globals['_CLIENTROLE']._serialized_end=934
_globals['_HEALTHSTATE']._serialized_start=937
_globals['_HEALTHSTATE']._serialized_end=1087
_globals['_ERRORCODE']._serialized_start=1090
_globals['_ERRORCODE']._serialized_end=1700
_globals['_PROTOCOLVERSION']._serialized_start=88
_globals['_PROTOCOLVERSION']._serialized_end=149
_globals['_ERROR']._serialized_start=152
_globals['_ERROR']._serialized_end=392
_globals['_ERROR_DETAILSENTRY']._serialized_start=334
_globals['_ERROR_DETAILSENTRY']._serialized_end=392
_globals['_SERVICEHEALTH']._serialized_start=395
_globals['_SERVICEHEALTH']._serialized_end=633
_globals['_CLIENTIDENTITY']._serialized_start=636
_globals['_CLIENTIDENTITY']._serialized_end=768
_globals['_PAGEREQUEST']._serialized_start=770
_globals['_PAGEREQUEST']._serialized_end=843
# @@protoc_insertion_point(module_scope)
+136
View File
@@ -0,0 +1,136 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
import datetime
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class ClientRole(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
CLIENT_ROLE_UNSPECIFIED: _ClassVar[ClientRole]
CLIENT_ROLE_ARCHIVE: _ClassVar[ClientRole]
CLIENT_ROLE_CACHE: _ClassVar[ClientRole]
class HealthState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
HEALTH_STATE_UNSPECIFIED: _ClassVar[HealthState]
HEALTH_STATE_HEALTHY: _ClassVar[HealthState]
HEALTH_STATE_DEGRADED: _ClassVar[HealthState]
HEALTH_STATE_UNHEALTHY: _ClassVar[HealthState]
HEALTH_STATE_UNKNOWN: _ClassVar[HealthState]
class ErrorCode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
ERROR_CODE_UNSPECIFIED: _ClassVar[ErrorCode]
ERROR_CODE_INVALID_ARGUMENT: _ClassVar[ErrorCode]
ERROR_CODE_UNAUTHENTICATED: _ClassVar[ErrorCode]
ERROR_CODE_PROTOCOL_VERSION_MISMATCH: _ClassVar[ErrorCode]
ERROR_CODE_CLIENT_ID_CONFLICT: _ClassVar[ErrorCode]
ERROR_CODE_NOT_FOUND: _ClassVar[ErrorCode]
ERROR_CODE_CONFLICT: _ClassVar[ErrorCode]
ERROR_CODE_STALE_STATE: _ClassVar[ErrorCode]
ERROR_CODE_UNSUPPORTED: _ClassVar[ErrorCode]
ERROR_CODE_PRECONDITION_FAILED: _ClassVar[ErrorCode]
ERROR_CODE_RESOURCE_EXHAUSTED: _ClassVar[ErrorCode]
ERROR_CODE_UNAVAILABLE: _ClassVar[ErrorCode]
ERROR_CODE_TIMEOUT: _ClassVar[ErrorCode]
ERROR_CODE_CANCELLED: _ClassVar[ErrorCode]
ERROR_CODE_INTEGRITY_CHECK_FAILED: _ClassVar[ErrorCode]
ERROR_CODE_PATH_CONFLICT: _ClassVar[ErrorCode]
ERROR_CODE_PERMISSION_DENIED: _ClassVar[ErrorCode]
ERROR_CODE_MANUAL_INTERVENTION_REQUIRED: _ClassVar[ErrorCode]
ERROR_CODE_INTERNAL: _ClassVar[ErrorCode]
CLIENT_ROLE_UNSPECIFIED: ClientRole
CLIENT_ROLE_ARCHIVE: ClientRole
CLIENT_ROLE_CACHE: ClientRole
HEALTH_STATE_UNSPECIFIED: HealthState
HEALTH_STATE_HEALTHY: HealthState
HEALTH_STATE_DEGRADED: HealthState
HEALTH_STATE_UNHEALTHY: HealthState
HEALTH_STATE_UNKNOWN: HealthState
ERROR_CODE_UNSPECIFIED: ErrorCode
ERROR_CODE_INVALID_ARGUMENT: ErrorCode
ERROR_CODE_UNAUTHENTICATED: ErrorCode
ERROR_CODE_PROTOCOL_VERSION_MISMATCH: ErrorCode
ERROR_CODE_CLIENT_ID_CONFLICT: ErrorCode
ERROR_CODE_NOT_FOUND: ErrorCode
ERROR_CODE_CONFLICT: ErrorCode
ERROR_CODE_STALE_STATE: ErrorCode
ERROR_CODE_UNSUPPORTED: ErrorCode
ERROR_CODE_PRECONDITION_FAILED: ErrorCode
ERROR_CODE_RESOURCE_EXHAUSTED: ErrorCode
ERROR_CODE_UNAVAILABLE: ErrorCode
ERROR_CODE_TIMEOUT: ErrorCode
ERROR_CODE_CANCELLED: ErrorCode
ERROR_CODE_INTEGRITY_CHECK_FAILED: ErrorCode
ERROR_CODE_PATH_CONFLICT: ErrorCode
ERROR_CODE_PERMISSION_DENIED: ErrorCode
ERROR_CODE_MANUAL_INTERVENTION_REQUIRED: ErrorCode
ERROR_CODE_INTERNAL: ErrorCode
class ProtocolVersion(_message.Message):
__slots__ = ("major", "minor")
MAJOR_FIELD_NUMBER: _ClassVar[int]
MINOR_FIELD_NUMBER: _ClassVar[int]
major: int
minor: int
def __init__(self, major: _Optional[int] = ..., minor: _Optional[int] = ...) -> None: ...
class Error(_message.Message):
__slots__ = ("code", "message", "retryable", "details")
class DetailsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: str
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
CODE_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
RETRYABLE_FIELD_NUMBER: _ClassVar[int]
DETAILS_FIELD_NUMBER: _ClassVar[int]
code: ErrorCode
message: str
retryable: bool
details: _containers.ScalarMap[str, str]
def __init__(self, code: _Optional[_Union[ErrorCode, str]] = ..., message: _Optional[str] = ..., retryable: _Optional[bool] = ..., details: _Optional[_Mapping[str, str]] = ...) -> None: ...
class ServiceHealth(_message.Message):
__slots__ = ("service", "state", "version", "api_version", "detail", "checked_at")
SERVICE_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
VERSION_FIELD_NUMBER: _ClassVar[int]
API_VERSION_FIELD_NUMBER: _ClassVar[int]
DETAIL_FIELD_NUMBER: _ClassVar[int]
CHECKED_AT_FIELD_NUMBER: _ClassVar[int]
service: str
state: HealthState
version: str
api_version: str
detail: str
checked_at: _timestamp_pb2.Timestamp
def __init__(self, service: _Optional[str] = ..., state: _Optional[_Union[HealthState, str]] = ..., version: _Optional[str] = ..., api_version: _Optional[str] = ..., detail: _Optional[str] = ..., checked_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class ClientIdentity(_message.Message):
__slots__ = ("client_id", "display_name", "role")
CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int]
ROLE_FIELD_NUMBER: _ClassVar[int]
client_id: str
display_name: str
role: ClientRole
def __init__(self, client_id: _Optional[str] = ..., display_name: _Optional[str] = ..., role: _Optional[_Union[ClientRole, str]] = ...) -> None: ...
class PageRequest(_message.Message):
__slots__ = ("page_size", "page_token")
PAGE_SIZE_FIELD_NUMBER: _ClassVar[int]
PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int]
page_size: int
page_token: str
def __init__(self, page_size: _Optional[int] = ..., page_token: _Optional[str] = ...) -> None: ...
File diff suppressed because one or more lines are too long
+215
View File
@@ -0,0 +1,215 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
import datetime
from archive_control.v1 import common_pb2 as _common_pb2
from archive_control.v1 import inventory_pb2 as _inventory_pb2
from archive_control.v1 import job_pb2 as _job_pb2
from archive_control.v1 import resource_pb2 as _resource_pb2
from archive_control.v1 import route_pb2 as _route_pb2
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class CommandAckStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
COMMAND_ACK_STATUS_UNSPECIFIED: _ClassVar[CommandAckStatus]
COMMAND_ACK_STATUS_ACCEPTED: _ClassVar[CommandAckStatus]
COMMAND_ACK_STATUS_DUPLICATE: _ClassVar[CommandAckStatus]
COMMAND_ACK_STATUS_REJECTED: _ClassVar[CommandAckStatus]
class JobEventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
JOB_EVENT_TYPE_UNSPECIFIED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_ASSIGNED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_STEP_STARTED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_PROGRESS: _ClassVar[JobEventType]
JOB_EVENT_TYPE_WAITING: _ClassVar[JobEventType]
JOB_EVENT_TYPE_STALLED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_STEP_SUCCEEDED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_COMMITTED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_CANCELLING: _ClassVar[JobEventType]
JOB_EVENT_TYPE_ROLLBACK_STARTED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_ROLLBACK_SUCCEEDED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_CLEANUP_REQUIRED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_SUCCEEDED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_FAILED: _ClassVar[JobEventType]
JOB_EVENT_TYPE_CANCELLED: _ClassVar[JobEventType]
COMMAND_ACK_STATUS_UNSPECIFIED: CommandAckStatus
COMMAND_ACK_STATUS_ACCEPTED: CommandAckStatus
COMMAND_ACK_STATUS_DUPLICATE: CommandAckStatus
COMMAND_ACK_STATUS_REJECTED: CommandAckStatus
JOB_EVENT_TYPE_UNSPECIFIED: JobEventType
JOB_EVENT_TYPE_ASSIGNED: JobEventType
JOB_EVENT_TYPE_STEP_STARTED: JobEventType
JOB_EVENT_TYPE_PROGRESS: JobEventType
JOB_EVENT_TYPE_WAITING: JobEventType
JOB_EVENT_TYPE_STALLED: JobEventType
JOB_EVENT_TYPE_STEP_SUCCEEDED: JobEventType
JOB_EVENT_TYPE_COMMITTED: JobEventType
JOB_EVENT_TYPE_CANCELLING: JobEventType
JOB_EVENT_TYPE_ROLLBACK_STARTED: JobEventType
JOB_EVENT_TYPE_ROLLBACK_SUCCEEDED: JobEventType
JOB_EVENT_TYPE_CLEANUP_REQUIRED: JobEventType
JOB_EVENT_TYPE_SUCCEEDED: JobEventType
JOB_EVENT_TYPE_FAILED: JobEventType
JOB_EVENT_TYPE_CANCELLED: JobEventType
class AssignJobCommand(_message.Message):
__slots__ = ("job", "expected_job_revision", "expected_last_event_sequence")
JOB_FIELD_NUMBER: _ClassVar[int]
EXPECTED_JOB_REVISION_FIELD_NUMBER: _ClassVar[int]
EXPECTED_LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
job: _job_pb2.JobDefinition
expected_job_revision: int
expected_last_event_sequence: int
def __init__(self, job: _Optional[_Union[_job_pb2.JobDefinition, _Mapping]] = ..., expected_job_revision: _Optional[int] = ..., expected_last_event_sequence: _Optional[int] = ...) -> None: ...
class ExecuteStepCommand(_message.Message):
__slots__ = ("job_id", "expected_job_revision", "step", "attempt", "expected_last_event_sequence")
JOB_ID_FIELD_NUMBER: _ClassVar[int]
EXPECTED_JOB_REVISION_FIELD_NUMBER: _ClassVar[int]
STEP_FIELD_NUMBER: _ClassVar[int]
ATTEMPT_FIELD_NUMBER: _ClassVar[int]
EXPECTED_LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
job_id: str
expected_job_revision: int
step: _job_pb2.JobStepKind
attempt: int
expected_last_event_sequence: int
def __init__(self, job_id: _Optional[str] = ..., expected_job_revision: _Optional[int] = ..., step: _Optional[_Union[_job_pb2.JobStepKind, str]] = ..., attempt: _Optional[int] = ..., expected_last_event_sequence: _Optional[int] = ...) -> None: ...
class CancelJobCommand(_message.Message):
__slots__ = ("job_id", "expected_job_revision", "reason", "expected_last_event_sequence")
JOB_ID_FIELD_NUMBER: _ClassVar[int]
EXPECTED_JOB_REVISION_FIELD_NUMBER: _ClassVar[int]
REASON_FIELD_NUMBER: _ClassVar[int]
EXPECTED_LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
job_id: str
expected_job_revision: int
reason: str
expected_last_event_sequence: int
def __init__(self, job_id: _Optional[str] = ..., expected_job_revision: _Optional[int] = ..., reason: _Optional[str] = ..., expected_last_event_sequence: _Optional[int] = ...) -> None: ...
class EnsureRouteCommand(_message.Message):
__slots__ = ("route",)
ROUTE_FIELD_NUMBER: _ClassVar[int]
route: _route_pb2.EnsureRouteSpec
def __init__(self, route: _Optional[_Union[_route_pb2.EnsureRouteSpec, _Mapping]] = ...) -> None: ...
class RequestJobSnapshotCommand(_message.Message):
__slots__ = ("job_ids",)
JOB_IDS_FIELD_NUMBER: _ClassVar[int]
job_ids: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, job_ids: _Optional[_Iterable[str]] = ...) -> None: ...
class Command(_message.Message):
__slots__ = ("command_id", "created_at", "assign_job", "execute_step", "cancel_job", "ensure_route", "inventory_query", "request_job_snapshot")
COMMAND_ID_FIELD_NUMBER: _ClassVar[int]
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
ASSIGN_JOB_FIELD_NUMBER: _ClassVar[int]
EXECUTE_STEP_FIELD_NUMBER: _ClassVar[int]
CANCEL_JOB_FIELD_NUMBER: _ClassVar[int]
ENSURE_ROUTE_FIELD_NUMBER: _ClassVar[int]
INVENTORY_QUERY_FIELD_NUMBER: _ClassVar[int]
REQUEST_JOB_SNAPSHOT_FIELD_NUMBER: _ClassVar[int]
command_id: str
created_at: _timestamp_pb2.Timestamp
assign_job: AssignJobCommand
execute_step: ExecuteStepCommand
cancel_job: CancelJobCommand
ensure_route: EnsureRouteCommand
inventory_query: _inventory_pb2.InventoryQuery
request_job_snapshot: RequestJobSnapshotCommand
def __init__(self, command_id: _Optional[str] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., assign_job: _Optional[_Union[AssignJobCommand, _Mapping]] = ..., execute_step: _Optional[_Union[ExecuteStepCommand, _Mapping]] = ..., cancel_job: _Optional[_Union[CancelJobCommand, _Mapping]] = ..., ensure_route: _Optional[_Union[EnsureRouteCommand, _Mapping]] = ..., inventory_query: _Optional[_Union[_inventory_pb2.InventoryQuery, _Mapping]] = ..., request_job_snapshot: _Optional[_Union[RequestJobSnapshotCommand, _Mapping]] = ...) -> None: ...
class CommandAck(_message.Message):
__slots__ = ("command_id", "status", "error")
COMMAND_ID_FIELD_NUMBER: _ClassVar[int]
STATUS_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
command_id: str
status: CommandAckStatus
error: _common_pb2.Error
def __init__(self, command_id: _Optional[str] = ..., status: _Optional[_Union[CommandAckStatus, str]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ...) -> None: ...
class JobEvent(_message.Message):
__slots__ = ("event_id", "job_id", "sequence", "job_revision", "type", "state", "committed", "progress", "error", "observed_resource", "observed_placement", "occurred_at")
EVENT_ID_FIELD_NUMBER: _ClassVar[int]
JOB_ID_FIELD_NUMBER: _ClassVar[int]
SEQUENCE_FIELD_NUMBER: _ClassVar[int]
JOB_REVISION_FIELD_NUMBER: _ClassVar[int]
TYPE_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
COMMITTED_FIELD_NUMBER: _ClassVar[int]
PROGRESS_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
OBSERVED_RESOURCE_FIELD_NUMBER: _ClassVar[int]
OBSERVED_PLACEMENT_FIELD_NUMBER: _ClassVar[int]
OCCURRED_AT_FIELD_NUMBER: _ClassVar[int]
event_id: str
job_id: str
sequence: int
job_revision: int
type: JobEventType
state: _job_pb2.JobState
committed: bool
progress: _job_pb2.JobProgress
error: _common_pb2.Error
observed_resource: _resource_pb2.ResourceStateFingerprint
observed_placement: _resource_pb2.Placement
occurred_at: _timestamp_pb2.Timestamp
def __init__(self, event_id: _Optional[str] = ..., job_id: _Optional[str] = ..., sequence: _Optional[int] = ..., job_revision: _Optional[int] = ..., type: _Optional[_Union[JobEventType, str]] = ..., state: _Optional[_Union[_job_pb2.JobState, str]] = ..., committed: _Optional[bool] = ..., progress: _Optional[_Union[_job_pb2.JobProgress, _Mapping]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., observed_resource: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., observed_placement: _Optional[_Union[_resource_pb2.Placement, _Mapping]] = ..., occurred_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class JobSnapshot(_message.Message):
__slots__ = ("job", "last_event_sequence", "in_flight_command_ids")
JOB_FIELD_NUMBER: _ClassVar[int]
LAST_EVENT_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
IN_FLIGHT_COMMAND_IDS_FIELD_NUMBER: _ClassVar[int]
job: _job_pb2.JobRecord
last_event_sequence: int
in_flight_command_ids: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, job: _Optional[_Union[_job_pb2.JobRecord, _Mapping]] = ..., last_event_sequence: _Optional[int] = ..., in_flight_command_ids: _Optional[_Iterable[str]] = ...) -> None: ...
class ClientStateSnapshot(_message.Message):
__slots__ = ("snapshot_id", "active_jobs", "routes", "services", "observed_at")
SNAPSHOT_ID_FIELD_NUMBER: _ClassVar[int]
ACTIVE_JOBS_FIELD_NUMBER: _ClassVar[int]
ROUTES_FIELD_NUMBER: _ClassVar[int]
SERVICES_FIELD_NUMBER: _ClassVar[int]
OBSERVED_AT_FIELD_NUMBER: _ClassVar[int]
snapshot_id: str
active_jobs: _containers.RepeatedCompositeFieldContainer[JobSnapshot]
routes: _containers.RepeatedCompositeFieldContainer[_route_pb2.LocalRoute]
services: _containers.RepeatedCompositeFieldContainer[_common_pb2.ServiceHealth]
observed_at: _timestamp_pb2.Timestamp
def __init__(self, snapshot_id: _Optional[str] = ..., active_jobs: _Optional[_Iterable[_Union[JobSnapshot, _Mapping]]] = ..., routes: _Optional[_Iterable[_Union[_route_pb2.LocalRoute, _Mapping]]] = ..., services: _Optional[_Iterable[_Union[_common_pb2.ServiceHealth, _Mapping]]] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class RouteUpdate(_message.Message):
__slots__ = ("command_id", "route", "verification", "error", "update_id", "sequence")
COMMAND_ID_FIELD_NUMBER: _ClassVar[int]
ROUTE_FIELD_NUMBER: _ClassVar[int]
VERIFICATION_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
UPDATE_ID_FIELD_NUMBER: _ClassVar[int]
SEQUENCE_FIELD_NUMBER: _ClassVar[int]
command_id: str
route: _route_pb2.LocalRoute
verification: _route_pb2.RouteVerification
error: _common_pb2.Error
update_id: str
sequence: int
def __init__(self, command_id: _Optional[str] = ..., route: _Optional[_Union[_route_pb2.LocalRoute, _Mapping]] = ..., verification: _Optional[_Union[_route_pb2.RouteVerification, _Mapping]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., update_id: _Optional[str] = ..., sequence: _Optional[int] = ...) -> None: ...
class ProtocolError(_message.Message):
__slots__ = ("error", "offending_message_id")
ERROR_FIELD_NUMBER: _ClassVar[int]
OFFENDING_MESSAGE_ID_FIELD_NUMBER: _ClassVar[int]
error: _common_pb2.Error
offending_message_id: str
def __init__(self, error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., offending_message_id: _Optional[str] = ...) -> None: ...
+42
View File
@@ -0,0 +1,42 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: archive_control/v1/envelope.proto
# Protobuf Python Version: 7.35.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
7,
35,
1,
'',
'archive_control/v1/envelope.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from archive_control.v1 import client_pb2 as archive__control_dot_v1_dot_client__pb2
from archive_control.v1 import common_pb2 as archive__control_dot_v1_dot_common__pb2
from archive_control.v1 import control_pb2 as archive__control_dot_v1_dot_control__pb2
from archive_control.v1 import inventory_pb2 as archive__control_dot_v1_dot_inventory__pb2
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!archive_control/v1/envelope.proto\x12\x12\x61rchive_control.v1\x1a\x1f\x61rchive_control/v1/client.proto\x1a\x1f\x61rchive_control/v1/common.proto\x1a archive_control/v1/control.proto\x1a\"archive_control/v1/inventory.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xce\x08\n\x08\x45nvelope\x12N\n\x10protocol_version\x18\x01 \x01(\x0b\x32#.archive_control.v1.ProtocolVersionR\x0fprotocolVersion\x12\x1d\n\nmessage_id\x18\x02 \x01(\tR\tmessageId\x12%\n\x0e\x63orrelation_id\x18\x03 \x01(\tR\rcorrelationId\x12\x33\n\x07sent_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x06sentAt\x12P\n\x10register_request\x18\n \x01(\x0b\x32#.archive_control.v1.RegisterRequestH\x00R\x0fregisterRequest\x12S\n\x11register_response\x18\x0b \x01(\x0b\x32$.archive_control.v1.RegisterResponseH\x00R\x10registerResponse\x12=\n\theartbeat\x18\x0c \x01(\x0b\x32\x1d.archive_control.v1.HeartbeatH\x00R\theartbeat\x12G\n\rheartbeat_ack\x18\r \x01(\x0b\x32 .archive_control.v1.HeartbeatAckH\x00R\x0cheartbeatAck\x12\x37\n\x07\x63ommand\x18\x0e \x01(\x0b\x32\x1b.archive_control.v1.CommandH\x00R\x07\x63ommand\x12\x41\n\x0b\x63ommand_ack\x18\x0f \x01(\x0b\x32\x1e.archive_control.v1.CommandAckH\x00R\ncommandAck\x12M\n\x0finventory_chunk\x18\x10 \x01(\x0b\x32\".archive_control.v1.InventoryChunkH\x00R\x0einventoryChunk\x12;\n\tjob_event\x18\x11 \x01(\x0b\x32\x1c.archive_control.v1.JobEventH\x00R\x08jobEvent\x12\x44\n\x0cjob_snapshot\x18\x12 \x01(\x0b\x32\x1f.archive_control.v1.JobSnapshotH\x00R\x0bjobSnapshot\x12]\n\x15\x63lient_state_snapshot\x18\x13 \x01(\x0b\x32\'.archive_control.v1.ClientStateSnapshotH\x00R\x13\x63lientStateSnapshot\x12\x44\n\x0croute_update\x18\x14 \x01(\x0b\x32\x1f.archive_control.v1.RouteUpdateH\x00R\x0brouteUpdate\x12J\n\x0eprotocol_error\x18\x15 \x01(\x0b\x32!.archive_control.v1.ProtocolErrorH\x00R\rprotocolErrorB\t\n\x07payloadb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.envelope_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_ENVELOPE']._serialized_start=227
_globals['_ENVELOPE']._serialized_end=1329
# @@protoc_insertion_point(module_scope)
+50
View File
@@ -0,0 +1,50 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
import datetime
from archive_control.v1 import client_pb2 as _client_pb2
from archive_control.v1 import common_pb2 as _common_pb2
from archive_control.v1 import control_pb2 as _control_pb2
from archive_control.v1 import inventory_pb2 as _inventory_pb2
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class Envelope(_message.Message):
__slots__ = ("protocol_version", "message_id", "correlation_id", "sent_at", "register_request", "register_response", "heartbeat", "heartbeat_ack", "command", "command_ack", "inventory_chunk", "job_event", "job_snapshot", "client_state_snapshot", "route_update", "protocol_error")
PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int]
MESSAGE_ID_FIELD_NUMBER: _ClassVar[int]
CORRELATION_ID_FIELD_NUMBER: _ClassVar[int]
SENT_AT_FIELD_NUMBER: _ClassVar[int]
REGISTER_REQUEST_FIELD_NUMBER: _ClassVar[int]
REGISTER_RESPONSE_FIELD_NUMBER: _ClassVar[int]
HEARTBEAT_FIELD_NUMBER: _ClassVar[int]
HEARTBEAT_ACK_FIELD_NUMBER: _ClassVar[int]
COMMAND_FIELD_NUMBER: _ClassVar[int]
COMMAND_ACK_FIELD_NUMBER: _ClassVar[int]
INVENTORY_CHUNK_FIELD_NUMBER: _ClassVar[int]
JOB_EVENT_FIELD_NUMBER: _ClassVar[int]
JOB_SNAPSHOT_FIELD_NUMBER: _ClassVar[int]
CLIENT_STATE_SNAPSHOT_FIELD_NUMBER: _ClassVar[int]
ROUTE_UPDATE_FIELD_NUMBER: _ClassVar[int]
PROTOCOL_ERROR_FIELD_NUMBER: _ClassVar[int]
protocol_version: _common_pb2.ProtocolVersion
message_id: str
correlation_id: str
sent_at: _timestamp_pb2.Timestamp
register_request: _client_pb2.RegisterRequest
register_response: _client_pb2.RegisterResponse
heartbeat: _client_pb2.Heartbeat
heartbeat_ack: _client_pb2.HeartbeatAck
command: _control_pb2.Command
command_ack: _control_pb2.CommandAck
inventory_chunk: _inventory_pb2.InventoryChunk
job_event: _control_pb2.JobEvent
job_snapshot: _control_pb2.JobSnapshot
client_state_snapshot: _control_pb2.ClientStateSnapshot
route_update: _control_pb2.RouteUpdate
protocol_error: _control_pb2.ProtocolError
def __init__(self, protocol_version: _Optional[_Union[_common_pb2.ProtocolVersion, _Mapping]] = ..., message_id: _Optional[str] = ..., correlation_id: _Optional[str] = ..., sent_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., register_request: _Optional[_Union[_client_pb2.RegisterRequest, _Mapping]] = ..., register_response: _Optional[_Union[_client_pb2.RegisterResponse, _Mapping]] = ..., heartbeat: _Optional[_Union[_client_pb2.Heartbeat, _Mapping]] = ..., heartbeat_ack: _Optional[_Union[_client_pb2.HeartbeatAck, _Mapping]] = ..., command: _Optional[_Union[_control_pb2.Command, _Mapping]] = ..., command_ack: _Optional[_Union[_control_pb2.CommandAck, _Mapping]] = ..., inventory_chunk: _Optional[_Union[_inventory_pb2.InventoryChunk, _Mapping]] = ..., job_event: _Optional[_Union[_control_pb2.JobEvent, _Mapping]] = ..., job_snapshot: _Optional[_Union[_control_pb2.JobSnapshot, _Mapping]] = ..., client_state_snapshot: _Optional[_Union[_control_pb2.ClientStateSnapshot, _Mapping]] = ..., route_update: _Optional[_Union[_control_pb2.RouteUpdate, _Mapping]] = ..., protocol_error: _Optional[_Union[_control_pb2.ProtocolError, _Mapping]] = ...) -> None: ...
+49
View File
@@ -0,0 +1,49 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: archive_control/v1/inventory.proto
# Protobuf Python Version: 7.35.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
7,
35,
1,
'',
'archive_control/v1/inventory.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from archive_control.v1 import common_pb2 as archive__control_dot_v1_dot_common__pb2
from archive_control.v1 import resource_pb2 as archive__control_dot_v1_dot_resource__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"archive_control/v1/inventory.proto\x12\x12\x61rchive_control.v1\x1a\x1f\x61rchive_control/v1/common.proto\x1a!archive_control/v1/resource.proto\"\xe1\x02\n\x0eInventoryQuery\x12\x19\n\x08query_id\x18\x01 \x01(\tR\x07queryId\x12\x38\n\x05scope\x18\x02 \x01(\x0e\x32\".archive_control.v1.InventoryScopeR\x05scope\x12\x41\n\x0cresource_ids\x18\x03 \x03(\x0b\x32\x1e.archive_control.v1.ResourceIdR\x0bresourceIds\x12\x33\n\x04page\x18\x04 \x01(\x0b\x32\x1f.archive_control.v1.PageRequestR\x04page\x12\x1f\n\x0bpage_filter\x18\x05 \x01(\tR\npageFilter\x12\x34\n\x16selected_complete_only\x18\x06 \x01(\x08R\x14selectedCompleteOnly\x12+\n\x11\x65xpected_revision\x18\x07 \x01(\tR\x10\x65xpectedRevision\"Y\n\x14ResourceSummaryChunk\x12\x41\n\tresources\x18\x01 \x03(\x0b\x32#.archive_control.v1.ResourceSummaryR\tresources\"\xcf\x01\n\x10\x43ontentTreeChunk\x12?\n\x0bresource_id\x18\x01 \x01(\x0b\x32\x1e.archive_control.v1.ResourceIdR\nresourceId\x12>\n\x07\x65ntries\x18\x02 \x03(\x0b\x32$.archive_control.v1.ContentTreeEntryR\x07\x65ntries\x12:\n\x19resource_content_revision\x18\x03 \x01(\tR\x17resourceContentRevision\"O\n\x0ePlacementChunk\x12=\n\nplacements\x18\x01 \x03(\x0b\x32\x1d.archive_control.v1.PlacementR\nplacements\"\xf8\x03\n\x0eInventoryChunk\x12\x19\n\x08query_id\x18\x01 \x01(\tR\x07queryId\x12\x1f\n\x0bsnapshot_id\x18\x02 \x01(\tR\nsnapshotId\x12\x1a\n\x08revision\x18\x03 \x01(\tR\x08revision\x12\x1f\n\x0b\x63hunk_index\x18\x04 \x01(\rR\nchunkIndex\x12\x1d\n\nlast_chunk\x18\x05 \x01(\x08R\tlastChunk\x12&\n\x0fnext_page_token\x18\x06 \x01(\tR\rnextPageToken\x12/\n\x05\x65rror\x18\x07 \x01(\x0b\x32\x19.archive_control.v1.ErrorR\x05\x65rror\x12Y\n\x12resource_summaries\x18\n \x01(\x0b\x32(.archive_control.v1.ResourceSummaryChunkH\x00R\x11resourceSummaries\x12I\n\x0c\x63ontent_tree\x18\x0b \x01(\x0b\x32$.archive_control.v1.ContentTreeChunkH\x00R\x0b\x63ontentTree\x12\x44\n\nplacements\x18\x0c \x01(\x0b\x32\".archive_control.v1.PlacementChunkH\x00R\nplacementsB\t\n\x07payload*\xc0\x01\n\x0eInventoryScope\x12\x1f\n\x1bINVENTORY_SCOPE_UNSPECIFIED\x10\x00\x12&\n\"INVENTORY_SCOPE_RESOURCE_SUMMARIES\x10\x01\x12#\n\x1fINVENTORY_SCOPE_RESOURCE_LOOKUP\x10\x02\x12 \n\x1cINVENTORY_SCOPE_CONTENT_TREE\x10\x03\x12\x1e\n\x1aINVENTORY_SCOPE_PLACEMENTS\x10\x04\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.inventory_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_INVENTORYSCOPE']._serialized_start=1372
_globals['_INVENTORYSCOPE']._serialized_end=1564
_globals['_INVENTORYQUERY']._serialized_start=127
_globals['_INVENTORYQUERY']._serialized_end=480
_globals['_RESOURCESUMMARYCHUNK']._serialized_start=482
_globals['_RESOURCESUMMARYCHUNK']._serialized_end=571
_globals['_CONTENTTREECHUNK']._serialized_start=574
_globals['_CONTENTTREECHUNK']._serialized_end=781
_globals['_PLACEMENTCHUNK']._serialized_start=783
_globals['_PLACEMENTCHUNK']._serialized_end=862
_globals['_INVENTORYCHUNK']._serialized_start=865
_globals['_INVENTORYCHUNK']._serialized_end=1369
# @@protoc_insertion_point(module_scope)
+88
View File
@@ -0,0 +1,88 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
from archive_control.v1 import common_pb2 as _common_pb2
from archive_control.v1 import resource_pb2 as _resource_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class InventoryScope(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
INVENTORY_SCOPE_UNSPECIFIED: _ClassVar[InventoryScope]
INVENTORY_SCOPE_RESOURCE_SUMMARIES: _ClassVar[InventoryScope]
INVENTORY_SCOPE_RESOURCE_LOOKUP: _ClassVar[InventoryScope]
INVENTORY_SCOPE_CONTENT_TREE: _ClassVar[InventoryScope]
INVENTORY_SCOPE_PLACEMENTS: _ClassVar[InventoryScope]
INVENTORY_SCOPE_UNSPECIFIED: InventoryScope
INVENTORY_SCOPE_RESOURCE_SUMMARIES: InventoryScope
INVENTORY_SCOPE_RESOURCE_LOOKUP: InventoryScope
INVENTORY_SCOPE_CONTENT_TREE: InventoryScope
INVENTORY_SCOPE_PLACEMENTS: InventoryScope
class InventoryQuery(_message.Message):
__slots__ = ("query_id", "scope", "resource_ids", "page", "page_filter", "selected_complete_only", "expected_revision")
QUERY_ID_FIELD_NUMBER: _ClassVar[int]
SCOPE_FIELD_NUMBER: _ClassVar[int]
RESOURCE_IDS_FIELD_NUMBER: _ClassVar[int]
PAGE_FIELD_NUMBER: _ClassVar[int]
PAGE_FILTER_FIELD_NUMBER: _ClassVar[int]
SELECTED_COMPLETE_ONLY_FIELD_NUMBER: _ClassVar[int]
EXPECTED_REVISION_FIELD_NUMBER: _ClassVar[int]
query_id: str
scope: InventoryScope
resource_ids: _containers.RepeatedCompositeFieldContainer[_resource_pb2.ResourceId]
page: _common_pb2.PageRequest
page_filter: str
selected_complete_only: bool
expected_revision: str
def __init__(self, query_id: _Optional[str] = ..., scope: _Optional[_Union[InventoryScope, str]] = ..., resource_ids: _Optional[_Iterable[_Union[_resource_pb2.ResourceId, _Mapping]]] = ..., page: _Optional[_Union[_common_pb2.PageRequest, _Mapping]] = ..., page_filter: _Optional[str] = ..., selected_complete_only: _Optional[bool] = ..., expected_revision: _Optional[str] = ...) -> None: ...
class ResourceSummaryChunk(_message.Message):
__slots__ = ("resources",)
RESOURCES_FIELD_NUMBER: _ClassVar[int]
resources: _containers.RepeatedCompositeFieldContainer[_resource_pb2.ResourceSummary]
def __init__(self, resources: _Optional[_Iterable[_Union[_resource_pb2.ResourceSummary, _Mapping]]] = ...) -> None: ...
class ContentTreeChunk(_message.Message):
__slots__ = ("resource_id", "entries", "resource_content_revision")
RESOURCE_ID_FIELD_NUMBER: _ClassVar[int]
ENTRIES_FIELD_NUMBER: _ClassVar[int]
RESOURCE_CONTENT_REVISION_FIELD_NUMBER: _ClassVar[int]
resource_id: _resource_pb2.ResourceId
entries: _containers.RepeatedCompositeFieldContainer[_resource_pb2.ContentTreeEntry]
resource_content_revision: str
def __init__(self, resource_id: _Optional[_Union[_resource_pb2.ResourceId, _Mapping]] = ..., entries: _Optional[_Iterable[_Union[_resource_pb2.ContentTreeEntry, _Mapping]]] = ..., resource_content_revision: _Optional[str] = ...) -> None: ...
class PlacementChunk(_message.Message):
__slots__ = ("placements",)
PLACEMENTS_FIELD_NUMBER: _ClassVar[int]
placements: _containers.RepeatedCompositeFieldContainer[_resource_pb2.Placement]
def __init__(self, placements: _Optional[_Iterable[_Union[_resource_pb2.Placement, _Mapping]]] = ...) -> None: ...
class InventoryChunk(_message.Message):
__slots__ = ("query_id", "snapshot_id", "revision", "chunk_index", "last_chunk", "next_page_token", "error", "resource_summaries", "content_tree", "placements")
QUERY_ID_FIELD_NUMBER: _ClassVar[int]
SNAPSHOT_ID_FIELD_NUMBER: _ClassVar[int]
REVISION_FIELD_NUMBER: _ClassVar[int]
CHUNK_INDEX_FIELD_NUMBER: _ClassVar[int]
LAST_CHUNK_FIELD_NUMBER: _ClassVar[int]
NEXT_PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
RESOURCE_SUMMARIES_FIELD_NUMBER: _ClassVar[int]
CONTENT_TREE_FIELD_NUMBER: _ClassVar[int]
PLACEMENTS_FIELD_NUMBER: _ClassVar[int]
query_id: str
snapshot_id: str
revision: str
chunk_index: int
last_chunk: bool
next_page_token: str
error: _common_pb2.Error
resource_summaries: ResourceSummaryChunk
content_tree: ContentTreeChunk
placements: PlacementChunk
def __init__(self, query_id: _Optional[str] = ..., snapshot_id: _Optional[str] = ..., revision: _Optional[str] = ..., chunk_index: _Optional[int] = ..., last_chunk: _Optional[bool] = ..., next_page_token: _Optional[str] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., resource_summaries: _Optional[_Union[ResourceSummaryChunk, _Mapping]] = ..., content_tree: _Optional[_Union[ContentTreeChunk, _Mapping]] = ..., placements: _Optional[_Union[PlacementChunk, _Mapping]] = ...) -> None: ...
File diff suppressed because one or more lines are too long
+220
View File
@@ -0,0 +1,220 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
import datetime
from archive_control.v1 import common_pb2 as _common_pb2
from archive_control.v1 import resource_pb2 as _resource_pb2
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class JobOperation(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
JOB_OPERATION_UNSPECIFIED: _ClassVar[JobOperation]
JOB_OPERATION_ARCHIVE: _ClassVar[JobOperation]
JOB_OPERATION_UNARCHIVE: _ClassVar[JobOperation]
JOB_OPERATION_EVICT_CACHE: _ClassVar[JobOperation]
class JobState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
JOB_STATE_UNSPECIFIED: _ClassVar[JobState]
JOB_STATE_QUEUED: _ClassVar[JobState]
JOB_STATE_PREPARING: _ClassVar[JobState]
JOB_STATE_RUNNING: _ClassVar[JobState]
JOB_STATE_WAITING: _ClassVar[JobState]
JOB_STATE_STALLED: _ClassVar[JobState]
JOB_STATE_CANCELLING: _ClassVar[JobState]
JOB_STATE_ROLLING_BACK: _ClassVar[JobState]
JOB_STATE_CLEANUP_REQUIRED: _ClassVar[JobState]
JOB_STATE_SUCCEEDED: _ClassVar[JobState]
JOB_STATE_FAILED: _ClassVar[JobState]
JOB_STATE_CANCELLED: _ClassVar[JobState]
class JobStepKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
JOB_STEP_KIND_UNSPECIFIED: _ClassVar[JobStepKind]
JOB_STEP_KIND_ROUTE_SETUP: _ClassVar[JobStepKind]
JOB_STEP_KIND_PREFLIGHT: _ClassVar[JobStepKind]
JOB_STEP_KIND_SOURCE_STAGE: _ClassVar[JobStepKind]
JOB_STEP_KIND_SYNCTHING_TRANSFER: _ClassVar[JobStepKind]
JOB_STEP_KIND_TARGET_MATERIALIZE: _ClassVar[JobStepKind]
JOB_STEP_KIND_QB_VERIFY: _ClassVar[JobStepKind]
JOB_STEP_KIND_STAGING_CLEANUP: _ClassVar[JobStepKind]
JOB_STEP_KIND_VERIFY_ARCHIVE_COVERAGE: _ClassVar[JobStepKind]
JOB_STEP_KIND_QB_REMOVE_ENTRY: _ClassVar[JobStepKind]
JOB_STEP_KIND_SAFE_FILE_UNLINK: _ClassVar[JobStepKind]
JOB_STEP_KIND_ROLLBACK: _ClassVar[JobStepKind]
class StepState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
STEP_STATE_UNSPECIFIED: _ClassVar[StepState]
STEP_STATE_NOT_STARTED: _ClassVar[StepState]
STEP_STATE_QUEUED: _ClassVar[StepState]
STEP_STATE_RUNNING: _ClassVar[StepState]
STEP_STATE_WAITING: _ClassVar[StepState]
STEP_STATE_STALLED: _ClassVar[StepState]
STEP_STATE_SUCCEEDED: _ClassVar[StepState]
STEP_STATE_FAILED: _ClassVar[StepState]
STEP_STATE_CANCELLED: _ClassVar[StepState]
JOB_OPERATION_UNSPECIFIED: JobOperation
JOB_OPERATION_ARCHIVE: JobOperation
JOB_OPERATION_UNARCHIVE: JobOperation
JOB_OPERATION_EVICT_CACHE: JobOperation
JOB_STATE_UNSPECIFIED: JobState
JOB_STATE_QUEUED: JobState
JOB_STATE_PREPARING: JobState
JOB_STATE_RUNNING: JobState
JOB_STATE_WAITING: JobState
JOB_STATE_STALLED: JobState
JOB_STATE_CANCELLING: JobState
JOB_STATE_ROLLING_BACK: JobState
JOB_STATE_CLEANUP_REQUIRED: JobState
JOB_STATE_SUCCEEDED: JobState
JOB_STATE_FAILED: JobState
JOB_STATE_CANCELLED: JobState
JOB_STEP_KIND_UNSPECIFIED: JobStepKind
JOB_STEP_KIND_ROUTE_SETUP: JobStepKind
JOB_STEP_KIND_PREFLIGHT: JobStepKind
JOB_STEP_KIND_SOURCE_STAGE: JobStepKind
JOB_STEP_KIND_SYNCTHING_TRANSFER: JobStepKind
JOB_STEP_KIND_TARGET_MATERIALIZE: JobStepKind
JOB_STEP_KIND_QB_VERIFY: JobStepKind
JOB_STEP_KIND_STAGING_CLEANUP: JobStepKind
JOB_STEP_KIND_VERIFY_ARCHIVE_COVERAGE: JobStepKind
JOB_STEP_KIND_QB_REMOVE_ENTRY: JobStepKind
JOB_STEP_KIND_SAFE_FILE_UNLINK: JobStepKind
JOB_STEP_KIND_ROLLBACK: JobStepKind
STEP_STATE_UNSPECIFIED: StepState
STEP_STATE_NOT_STARTED: StepState
STEP_STATE_QUEUED: StepState
STEP_STATE_RUNNING: StepState
STEP_STATE_WAITING: StepState
STEP_STATE_STALLED: StepState
STEP_STATE_SUCCEEDED: StepState
STEP_STATE_FAILED: StepState
STEP_STATE_CANCELLED: StepState
class TransferJobSpec(_message.Message):
__slots__ = ("source_client_id", "target_client_id", "route_id", "requested_files", "source_fingerprint", "target_baseline_fingerprint", "expected_target_placement_generation", "target_baseline_files", "transfer_delta_files", "requested_logical_bytes", "transfer_delta_logical_bytes")
SOURCE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
TARGET_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
ROUTE_ID_FIELD_NUMBER: _ClassVar[int]
REQUESTED_FILES_FIELD_NUMBER: _ClassVar[int]
SOURCE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int]
TARGET_BASELINE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int]
EXPECTED_TARGET_PLACEMENT_GENERATION_FIELD_NUMBER: _ClassVar[int]
TARGET_BASELINE_FILES_FIELD_NUMBER: _ClassVar[int]
TRANSFER_DELTA_FILES_FIELD_NUMBER: _ClassVar[int]
REQUESTED_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
TRANSFER_DELTA_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
source_client_id: str
target_client_id: str
route_id: str
requested_files: _resource_pb2.SelectionSet
source_fingerprint: _resource_pb2.ResourceStateFingerprint
target_baseline_fingerprint: _resource_pb2.ResourceStateFingerprint
expected_target_placement_generation: int
target_baseline_files: _resource_pb2.SelectionSet
transfer_delta_files: _resource_pb2.SelectionSet
requested_logical_bytes: int
transfer_delta_logical_bytes: int
def __init__(self, source_client_id: _Optional[str] = ..., target_client_id: _Optional[str] = ..., route_id: _Optional[str] = ..., requested_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., source_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., target_baseline_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., expected_target_placement_generation: _Optional[int] = ..., target_baseline_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., transfer_delta_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., requested_logical_bytes: _Optional[int] = ..., transfer_delta_logical_bytes: _Optional[int] = ...) -> None: ...
class ArchiveCoverage(_message.Message):
__slots__ = ("archive_client_id", "covered_files", "placement_generation", "fingerprint")
ARCHIVE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
COVERED_FILES_FIELD_NUMBER: _ClassVar[int]
PLACEMENT_GENERATION_FIELD_NUMBER: _ClassVar[int]
FINGERPRINT_FIELD_NUMBER: _ClassVar[int]
archive_client_id: str
covered_files: _resource_pb2.SelectionSet
placement_generation: int
fingerprint: _resource_pb2.ResourceStateFingerprint
def __init__(self, archive_client_id: _Optional[str] = ..., covered_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., placement_generation: _Optional[int] = ..., fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ...) -> None: ...
class EvictionJobSpec(_message.Message):
__slots__ = ("cache_client_id", "files_to_evict", "cache_placement_generation", "cache_fingerprint", "archive_coverage")
CACHE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
FILES_TO_EVICT_FIELD_NUMBER: _ClassVar[int]
CACHE_PLACEMENT_GENERATION_FIELD_NUMBER: _ClassVar[int]
CACHE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int]
ARCHIVE_COVERAGE_FIELD_NUMBER: _ClassVar[int]
cache_client_id: str
files_to_evict: _resource_pb2.SelectionSet
cache_placement_generation: int
cache_fingerprint: _resource_pb2.ResourceStateFingerprint
archive_coverage: _containers.RepeatedCompositeFieldContainer[ArchiveCoverage]
def __init__(self, cache_client_id: _Optional[str] = ..., files_to_evict: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., cache_placement_generation: _Optional[int] = ..., cache_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., archive_coverage: _Optional[_Iterable[_Union[ArchiveCoverage, _Mapping]]] = ...) -> None: ...
class JobDefinition(_message.Message):
__slots__ = ("job_id", "idempotency_key", "operation", "resource_id", "resource_display_name", "created_at", "transfer", "eviction")
JOB_ID_FIELD_NUMBER: _ClassVar[int]
IDEMPOTENCY_KEY_FIELD_NUMBER: _ClassVar[int]
OPERATION_FIELD_NUMBER: _ClassVar[int]
RESOURCE_ID_FIELD_NUMBER: _ClassVar[int]
RESOURCE_DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int]
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
TRANSFER_FIELD_NUMBER: _ClassVar[int]
EVICTION_FIELD_NUMBER: _ClassVar[int]
job_id: str
idempotency_key: str
operation: JobOperation
resource_id: _resource_pb2.ResourceId
resource_display_name: str
created_at: _timestamp_pb2.Timestamp
transfer: TransferJobSpec
eviction: EvictionJobSpec
def __init__(self, job_id: _Optional[str] = ..., idempotency_key: _Optional[str] = ..., operation: _Optional[_Union[JobOperation, str]] = ..., resource_id: _Optional[_Union[_resource_pb2.ResourceId, _Mapping]] = ..., resource_display_name: _Optional[str] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., transfer: _Optional[_Union[TransferJobSpec, _Mapping]] = ..., eviction: _Optional[_Union[EvictionJobSpec, _Mapping]] = ...) -> None: ...
class JobProgress(_message.Message):
__slots__ = ("step", "display_step_number", "display_step_total", "state", "fraction_complete", "bytes_complete", "bytes_total", "approximate_bytes_per_second", "detail", "last_progress_at", "overall_fraction_complete")
STEP_FIELD_NUMBER: _ClassVar[int]
DISPLAY_STEP_NUMBER_FIELD_NUMBER: _ClassVar[int]
DISPLAY_STEP_TOTAL_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
FRACTION_COMPLETE_FIELD_NUMBER: _ClassVar[int]
BYTES_COMPLETE_FIELD_NUMBER: _ClassVar[int]
BYTES_TOTAL_FIELD_NUMBER: _ClassVar[int]
APPROXIMATE_BYTES_PER_SECOND_FIELD_NUMBER: _ClassVar[int]
DETAIL_FIELD_NUMBER: _ClassVar[int]
LAST_PROGRESS_AT_FIELD_NUMBER: _ClassVar[int]
OVERALL_FRACTION_COMPLETE_FIELD_NUMBER: _ClassVar[int]
step: JobStepKind
display_step_number: int
display_step_total: int
state: StepState
fraction_complete: float
bytes_complete: int
bytes_total: int
approximate_bytes_per_second: float
detail: str
last_progress_at: _timestamp_pb2.Timestamp
overall_fraction_complete: float
def __init__(self, step: _Optional[_Union[JobStepKind, str]] = ..., display_step_number: _Optional[int] = ..., display_step_total: _Optional[int] = ..., state: _Optional[_Union[StepState, str]] = ..., fraction_complete: _Optional[float] = ..., bytes_complete: _Optional[int] = ..., bytes_total: _Optional[int] = ..., approximate_bytes_per_second: _Optional[float] = ..., detail: _Optional[str] = ..., last_progress_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., overall_fraction_complete: _Optional[float] = ...) -> None: ...
class JobRecord(_message.Message):
__slots__ = ("definition", "state", "committed", "revision", "progress", "error", "updated_at", "committed_at", "finished_at")
DEFINITION_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
COMMITTED_FIELD_NUMBER: _ClassVar[int]
REVISION_FIELD_NUMBER: _ClassVar[int]
PROGRESS_FIELD_NUMBER: _ClassVar[int]
ERROR_FIELD_NUMBER: _ClassVar[int]
UPDATED_AT_FIELD_NUMBER: _ClassVar[int]
COMMITTED_AT_FIELD_NUMBER: _ClassVar[int]
FINISHED_AT_FIELD_NUMBER: _ClassVar[int]
definition: JobDefinition
state: JobState
committed: bool
revision: int
progress: JobProgress
error: _common_pb2.Error
updated_at: _timestamp_pb2.Timestamp
committed_at: _timestamp_pb2.Timestamp
finished_at: _timestamp_pb2.Timestamp
def __init__(self, definition: _Optional[_Union[JobDefinition, _Mapping]] = ..., state: _Optional[_Union[JobState, str]] = ..., committed: _Optional[bool] = ..., revision: _Optional[int] = ..., progress: _Optional[_Union[JobProgress, _Mapping]] = ..., error: _Optional[_Union[_common_pb2.Error, _Mapping]] = ..., updated_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., committed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., finished_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
File diff suppressed because one or more lines are too long
+199
View File
@@ -0,0 +1,199 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
import datetime
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class TorrentRuntimeState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
TORRENT_RUNTIME_STATE_UNSPECIFIED: _ClassVar[TorrentRuntimeState]
TORRENT_RUNTIME_STATE_STOPPED: _ClassVar[TorrentRuntimeState]
TORRENT_RUNTIME_STATE_QUEUED: _ClassVar[TorrentRuntimeState]
TORRENT_RUNTIME_STATE_CHECKING: _ClassVar[TorrentRuntimeState]
TORRENT_RUNTIME_STATE_DOWNLOADING: _ClassVar[TorrentRuntimeState]
TORRENT_RUNTIME_STATE_SEEDING: _ClassVar[TorrentRuntimeState]
TORRENT_RUNTIME_STATE_STALLED: _ClassVar[TorrentRuntimeState]
TORRENT_RUNTIME_STATE_ERROR: _ClassVar[TorrentRuntimeState]
TORRENT_RUNTIME_STATE_MISSING: _ClassVar[TorrentRuntimeState]
class ContentEntryType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
CONTENT_ENTRY_TYPE_UNSPECIFIED: _ClassVar[ContentEntryType]
CONTENT_ENTRY_TYPE_FILE: _ClassVar[ContentEntryType]
CONTENT_ENTRY_TYPE_DIRECTORY: _ClassVar[ContentEntryType]
CONTENT_ENTRY_TYPE_PADDING_FILE: _ClassVar[ContentEntryType]
class PlacementState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
PLACEMENT_STATE_UNSPECIFIED: _ClassVar[PlacementState]
PLACEMENT_STATE_PRESENT: _ClassVar[PlacementState]
PLACEMENT_STATE_MERGING: _ClassVar[PlacementState]
PLACEMENT_STATE_EVICTING: _ClassVar[PlacementState]
PLACEMENT_STATE_ABSENT: _ClassVar[PlacementState]
PLACEMENT_STATE_ERROR: _ClassVar[PlacementState]
PLACEMENT_STATE_UNKNOWN: _ClassVar[PlacementState]
TORRENT_RUNTIME_STATE_UNSPECIFIED: TorrentRuntimeState
TORRENT_RUNTIME_STATE_STOPPED: TorrentRuntimeState
TORRENT_RUNTIME_STATE_QUEUED: TorrentRuntimeState
TORRENT_RUNTIME_STATE_CHECKING: TorrentRuntimeState
TORRENT_RUNTIME_STATE_DOWNLOADING: TorrentRuntimeState
TORRENT_RUNTIME_STATE_SEEDING: TorrentRuntimeState
TORRENT_RUNTIME_STATE_STALLED: TorrentRuntimeState
TORRENT_RUNTIME_STATE_ERROR: TorrentRuntimeState
TORRENT_RUNTIME_STATE_MISSING: TorrentRuntimeState
CONTENT_ENTRY_TYPE_UNSPECIFIED: ContentEntryType
CONTENT_ENTRY_TYPE_FILE: ContentEntryType
CONTENT_ENTRY_TYPE_DIRECTORY: ContentEntryType
CONTENT_ENTRY_TYPE_PADDING_FILE: ContentEntryType
PLACEMENT_STATE_UNSPECIFIED: PlacementState
PLACEMENT_STATE_PRESENT: PlacementState
PLACEMENT_STATE_MERGING: PlacementState
PLACEMENT_STATE_EVICTING: PlacementState
PLACEMENT_STATE_ABSENT: PlacementState
PLACEMENT_STATE_ERROR: PlacementState
PLACEMENT_STATE_UNKNOWN: PlacementState
class ResourceId(_message.Message):
__slots__ = ("info_hash_v1_hex", "info_hash_v2_hex")
INFO_HASH_V1_HEX_FIELD_NUMBER: _ClassVar[int]
INFO_HASH_V2_HEX_FIELD_NUMBER: _ClassVar[int]
info_hash_v1_hex: str
info_hash_v2_hex: str
def __init__(self, info_hash_v1_hex: _Optional[str] = ..., info_hash_v2_hex: _Optional[str] = ...) -> None: ...
class FileIndexRange(_message.Message):
__slots__ = ("first", "last")
FIRST_FIELD_NUMBER: _ClassVar[int]
LAST_FIELD_NUMBER: _ClassVar[int]
first: int
last: int
def __init__(self, first: _Optional[int] = ..., last: _Optional[int] = ...) -> None: ...
class SelectionSet(_message.Message):
__slots__ = ("ranges",)
RANGES_FIELD_NUMBER: _ClassVar[int]
ranges: _containers.RepeatedCompositeFieldContainer[FileIndexRange]
def __init__(self, ranges: _Optional[_Iterable[_Union[FileIndexRange, _Mapping]]] = ...) -> None: ...
class TorrentFile(_message.Message):
__slots__ = ("file_index", "canonical_path", "logical_bytes", "allocated_bytes", "completed_bytes", "selected", "sparse", "padding")
FILE_INDEX_FIELD_NUMBER: _ClassVar[int]
CANONICAL_PATH_FIELD_NUMBER: _ClassVar[int]
LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
ALLOCATED_BYTES_FIELD_NUMBER: _ClassVar[int]
COMPLETED_BYTES_FIELD_NUMBER: _ClassVar[int]
SELECTED_FIELD_NUMBER: _ClassVar[int]
SPARSE_FIELD_NUMBER: _ClassVar[int]
PADDING_FIELD_NUMBER: _ClassVar[int]
file_index: int
canonical_path: str
logical_bytes: int
allocated_bytes: int
completed_bytes: int
selected: bool
sparse: bool
padding: bool
def __init__(self, file_index: _Optional[int] = ..., canonical_path: _Optional[str] = ..., logical_bytes: _Optional[int] = ..., allocated_bytes: _Optional[int] = ..., completed_bytes: _Optional[int] = ..., selected: _Optional[bool] = ..., sparse: _Optional[bool] = ..., padding: _Optional[bool] = ...) -> None: ...
class ContentTreeEntry(_message.Message):
__slots__ = ("canonical_path", "parent_path", "display_name", "type", "file_index", "available_file_indices", "available_logical_bytes", "available_file_count", "file")
CANONICAL_PATH_FIELD_NUMBER: _ClassVar[int]
PARENT_PATH_FIELD_NUMBER: _ClassVar[int]
DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int]
TYPE_FIELD_NUMBER: _ClassVar[int]
FILE_INDEX_FIELD_NUMBER: _ClassVar[int]
AVAILABLE_FILE_INDICES_FIELD_NUMBER: _ClassVar[int]
AVAILABLE_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
AVAILABLE_FILE_COUNT_FIELD_NUMBER: _ClassVar[int]
FILE_FIELD_NUMBER: _ClassVar[int]
canonical_path: str
parent_path: str
display_name: str
type: ContentEntryType
file_index: int
available_file_indices: SelectionSet
available_logical_bytes: int
available_file_count: int
file: TorrentFile
def __init__(self, canonical_path: _Optional[str] = ..., parent_path: _Optional[str] = ..., display_name: _Optional[str] = ..., type: _Optional[_Union[ContentEntryType, str]] = ..., file_index: _Optional[int] = ..., available_file_indices: _Optional[_Union[SelectionSet, _Mapping]] = ..., available_logical_bytes: _Optional[int] = ..., available_file_count: _Optional[int] = ..., file: _Optional[_Union[TorrentFile, _Mapping]] = ...) -> None: ...
class ResourceSummary(_message.Message):
__slots__ = ("resource_id", "qb_torrent_id", "display_name", "runtime_state", "selected_files", "selected_complete_files", "selected_logical_bytes", "selected_complete_bytes", "total_logical_bytes", "total_file_count", "content_revision", "canonical_paths", "observed_at")
RESOURCE_ID_FIELD_NUMBER: _ClassVar[int]
QB_TORRENT_ID_FIELD_NUMBER: _ClassVar[int]
DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int]
RUNTIME_STATE_FIELD_NUMBER: _ClassVar[int]
SELECTED_FILES_FIELD_NUMBER: _ClassVar[int]
SELECTED_COMPLETE_FILES_FIELD_NUMBER: _ClassVar[int]
SELECTED_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
SELECTED_COMPLETE_BYTES_FIELD_NUMBER: _ClassVar[int]
TOTAL_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
TOTAL_FILE_COUNT_FIELD_NUMBER: _ClassVar[int]
CONTENT_REVISION_FIELD_NUMBER: _ClassVar[int]
CANONICAL_PATHS_FIELD_NUMBER: _ClassVar[int]
OBSERVED_AT_FIELD_NUMBER: _ClassVar[int]
resource_id: ResourceId
qb_torrent_id: str
display_name: str
runtime_state: TorrentRuntimeState
selected_files: SelectionSet
selected_complete_files: SelectionSet
selected_logical_bytes: int
selected_complete_bytes: int
total_logical_bytes: int
total_file_count: int
content_revision: str
canonical_paths: bool
observed_at: _timestamp_pb2.Timestamp
def __init__(self, resource_id: _Optional[_Union[ResourceId, _Mapping]] = ..., qb_torrent_id: _Optional[str] = ..., display_name: _Optional[str] = ..., runtime_state: _Optional[_Union[TorrentRuntimeState, str]] = ..., selected_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., selected_complete_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., selected_logical_bytes: _Optional[int] = ..., selected_complete_bytes: _Optional[int] = ..., total_logical_bytes: _Optional[int] = ..., total_file_count: _Optional[int] = ..., content_revision: _Optional[str] = ..., canonical_paths: _Optional[bool] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class ResourceStateFingerprint(_message.Message):
__slots__ = ("resource_id", "client_id", "qb_torrent_id", "content_revision", "selected_files", "selected_complete_files", "runtime_state", "save_path_fingerprint", "observed_at")
RESOURCE_ID_FIELD_NUMBER: _ClassVar[int]
CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
QB_TORRENT_ID_FIELD_NUMBER: _ClassVar[int]
CONTENT_REVISION_FIELD_NUMBER: _ClassVar[int]
SELECTED_FILES_FIELD_NUMBER: _ClassVar[int]
SELECTED_COMPLETE_FILES_FIELD_NUMBER: _ClassVar[int]
RUNTIME_STATE_FIELD_NUMBER: _ClassVar[int]
SAVE_PATH_FINGERPRINT_FIELD_NUMBER: _ClassVar[int]
OBSERVED_AT_FIELD_NUMBER: _ClassVar[int]
resource_id: ResourceId
client_id: str
qb_torrent_id: str
content_revision: str
selected_files: SelectionSet
selected_complete_files: SelectionSet
runtime_state: TorrentRuntimeState
save_path_fingerprint: str
observed_at: _timestamp_pb2.Timestamp
def __init__(self, resource_id: _Optional[_Union[ResourceId, _Mapping]] = ..., client_id: _Optional[str] = ..., qb_torrent_id: _Optional[str] = ..., content_revision: _Optional[str] = ..., selected_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., selected_complete_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., runtime_state: _Optional[_Union[TorrentRuntimeState, str]] = ..., save_path_fingerprint: _Optional[str] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class Placement(_message.Message):
__slots__ = ("resource_id", "client_id", "state", "verified_files", "verified_logical_bytes", "generation", "fingerprint", "verified_at", "created_by_job_id")
RESOURCE_ID_FIELD_NUMBER: _ClassVar[int]
CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
VERIFIED_FILES_FIELD_NUMBER: _ClassVar[int]
VERIFIED_LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
GENERATION_FIELD_NUMBER: _ClassVar[int]
FINGERPRINT_FIELD_NUMBER: _ClassVar[int]
VERIFIED_AT_FIELD_NUMBER: _ClassVar[int]
CREATED_BY_JOB_ID_FIELD_NUMBER: _ClassVar[int]
resource_id: ResourceId
client_id: str
state: PlacementState
verified_files: SelectionSet
verified_logical_bytes: int
generation: int
fingerprint: ResourceStateFingerprint
verified_at: _timestamp_pb2.Timestamp
created_by_job_id: str
def __init__(self, resource_id: _Optional[_Union[ResourceId, _Mapping]] = ..., client_id: _Optional[str] = ..., state: _Optional[_Union[PlacementState, str]] = ..., verified_files: _Optional[_Union[SelectionSet, _Mapping]] = ..., verified_logical_bytes: _Optional[int] = ..., generation: _Optional[int] = ..., fingerprint: _Optional[_Union[ResourceStateFingerprint, _Mapping]] = ..., verified_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., created_by_job_id: _Optional[str] = ...) -> None: ...
+48
View File
@@ -0,0 +1,48 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: archive_control/v1/route.proto
# Protobuf Python Version: 7.35.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
7,
35,
1,
'',
'archive_control/v1/route.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x61rchive_control/v1/route.proto\x12\x12\x61rchive_control.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa1\x04\n\nLocalRoute\x12\x19\n\x08route_id\x18\x01 \x01(\tR\x07routeId\x12.\n\x13local_relative_path\x18\x02 \x01(\tR\x11localRelativePath\x12H\n\x0b\x66older_type\x18\x03 \x01(\x0e\x32\'.archive_control.v1.SyncthingFolderTypeR\nfolderType\x12\x39\n\x19local_syncthing_device_id\x18\x04 \x01(\tR\x16localSyncthingDeviceId\x12\x39\n\x19peer_syncthing_device_ids\x18\x05 \x03(\tR\x16peerSyncthingDeviceIds\x12\x34\n\x05state\x18\x06 \x01(\x0e\x32\x1e.archive_control.v1.RouteStateR\x05state\x12\x1a\n\x08writable\x18\x07 \x01(\x08R\x08writable\x12)\n\x10sparse_supported\x18\x08 \x01(\x08R\x0fsparseSupported\x12\x36\n\x17\x61rchive_control_created\x18\t \x01(\x08R\x15\x61rchiveControlCreated\x12\x16\n\x06\x64\x65tail\x18\n \x01(\tR\x06\x64\x65tail\x12;\n\x0bobserved_at\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nobservedAt\"\x85\x03\n\x0fRouteDescriptor\x12\x19\n\x08route_id\x18\x01 \x01(\tR\x07routeId\x12*\n\x11\x61rchive_client_id\x18\x02 \x01(\tR\x0f\x61rchiveClientId\x12&\n\x0f\x63\x61\x63he_client_id\x18\x03 \x01(\tR\rcacheClientId\x12=\n\x1b\x61rchive_syncthing_device_id\x18\x04 \x01(\tR\x18\x61rchiveSyncthingDeviceId\x12\x39\n\x19\x63\x61\x63he_syncthing_device_id\x18\x05 \x01(\tR\x16\x63\x61\x63heSyncthingDeviceId\x12\x34\n\x05state\x18\x06 \x01(\x0e\x32\x1e.archive_control.v1.RouteStateR\x05state\x12;\n\x0bverified_at\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nverifiedAt\x12\x16\n\x06\x64\x65tail\x18\x08 \x01(\tR\x06\x64\x65tail\"\x96\x02\n\x0f\x45nsureRouteSpec\x12\x19\n\x08route_id\x18\x01 \x01(\tR\x07routeId\x12$\n\x0epeer_client_id\x18\x02 \x01(\tR\x0cpeerClientId\x12\x37\n\x18peer_syncthing_device_id\x18\x03 \x01(\tR\x15peerSyncthingDeviceId\x12%\n\x0epeer_addresses\x18\x04 \x03(\tR\rpeerAddresses\x12.\n\x13local_relative_path\x18\x05 \x01(\tR\x11localRelativePath\x12\x32\n\x15setup_timeout_seconds\x18\x06 \x01(\rR\x13setupTimeoutSeconds\"\xbe\x02\n\x11RouteVerification\x12\x19\n\x08route_id\x18\x01 \x01(\tR\x07routeId\x12\x14\n\x05nonce\x18\x02 \x01(\tR\x05nonce\x12\x36\n\x18local_nonce_seen_by_peer\x18\x03 \x01(\x08R\x14localNonceSeenByPeer\x12\x35\n\x17peer_nonce_seen_locally\x18\x04 \x01(\x08R\x14peerNonceSeenLocally\x12\x34\n\x05state\x18\x05 \x01(\x0e\x32\x1e.archive_control.v1.RouteStateR\x05state\x12\x16\n\x06\x64\x65tail\x18\x06 \x01(\tR\x06\x64\x65tail\x12;\n\x0bobserved_at\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nobservedAt*\xd2\x01\n\x13SyncthingFolderType\x12%\n!SYNCTHING_FOLDER_TYPE_UNSPECIFIED\x10\x00\x12&\n\"SYNCTHING_FOLDER_TYPE_SEND_RECEIVE\x10\x01\x12#\n\x1fSYNCTHING_FOLDER_TYPE_SEND_ONLY\x10\x02\x12&\n\"SYNCTHING_FOLDER_TYPE_RECEIVE_ONLY\x10\x03\x12\x1f\n\x1bSYNCTHING_FOLDER_TYPE_OTHER\x10\x04*\xe5\x01\n\nRouteState\x12\x1b\n\x17ROUTE_STATE_UNSPECIFIED\x10\x00\x12\x1a\n\x16ROUTE_STATE_DISCOVERED\x10\x01\x12\x1c\n\x18ROUTE_STATE_PROVISIONING\x10\x02\x12\x19\n\x15ROUTE_STATE_VERIFYING\x10\x03\x12\x15\n\x11ROUTE_STATE_READY\x10\x04\x12\x16\n\x12ROUTE_STATE_PAUSED\x10\x05\x12\x19\n\x15ROUTE_STATE_UNHEALTHY\x10\x06\x12\x1b\n\x17ROUTE_STATE_UNSUPPORTED\x10\x07\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.route_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_SYNCTHINGFOLDERTYPE']._serialized_start=1630
_globals['_SYNCTHINGFOLDERTYPE']._serialized_end=1840
_globals['_ROUTESTATE']._serialized_start=1843
_globals['_ROUTESTATE']._serialized_end=2072
_globals['_LOCALROUTE']._serialized_start=88
_globals['_LOCALROUTE']._serialized_end=633
_globals['_ROUTEDESCRIPTOR']._serialized_start=636
_globals['_ROUTEDESCRIPTOR']._serialized_end=1025
_globals['_ENSUREROUTESPEC']._serialized_start=1028
_globals['_ENSUREROUTESPEC']._serialized_end=1306
_globals['_ROUTEVERIFICATION']._serialized_start=1309
_globals['_ROUTEVERIFICATION']._serialized_end=1627
# @@protoc_insertion_point(module_scope)
+124
View File
@@ -0,0 +1,124 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
import datetime
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class SyncthingFolderType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
SYNCTHING_FOLDER_TYPE_UNSPECIFIED: _ClassVar[SyncthingFolderType]
SYNCTHING_FOLDER_TYPE_SEND_RECEIVE: _ClassVar[SyncthingFolderType]
SYNCTHING_FOLDER_TYPE_SEND_ONLY: _ClassVar[SyncthingFolderType]
SYNCTHING_FOLDER_TYPE_RECEIVE_ONLY: _ClassVar[SyncthingFolderType]
SYNCTHING_FOLDER_TYPE_OTHER: _ClassVar[SyncthingFolderType]
class RouteState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
ROUTE_STATE_UNSPECIFIED: _ClassVar[RouteState]
ROUTE_STATE_DISCOVERED: _ClassVar[RouteState]
ROUTE_STATE_PROVISIONING: _ClassVar[RouteState]
ROUTE_STATE_VERIFYING: _ClassVar[RouteState]
ROUTE_STATE_READY: _ClassVar[RouteState]
ROUTE_STATE_PAUSED: _ClassVar[RouteState]
ROUTE_STATE_UNHEALTHY: _ClassVar[RouteState]
ROUTE_STATE_UNSUPPORTED: _ClassVar[RouteState]
SYNCTHING_FOLDER_TYPE_UNSPECIFIED: SyncthingFolderType
SYNCTHING_FOLDER_TYPE_SEND_RECEIVE: SyncthingFolderType
SYNCTHING_FOLDER_TYPE_SEND_ONLY: SyncthingFolderType
SYNCTHING_FOLDER_TYPE_RECEIVE_ONLY: SyncthingFolderType
SYNCTHING_FOLDER_TYPE_OTHER: SyncthingFolderType
ROUTE_STATE_UNSPECIFIED: RouteState
ROUTE_STATE_DISCOVERED: RouteState
ROUTE_STATE_PROVISIONING: RouteState
ROUTE_STATE_VERIFYING: RouteState
ROUTE_STATE_READY: RouteState
ROUTE_STATE_PAUSED: RouteState
ROUTE_STATE_UNHEALTHY: RouteState
ROUTE_STATE_UNSUPPORTED: RouteState
class LocalRoute(_message.Message):
__slots__ = ("route_id", "local_relative_path", "folder_type", "local_syncthing_device_id", "peer_syncthing_device_ids", "state", "writable", "sparse_supported", "archive_control_created", "detail", "observed_at")
ROUTE_ID_FIELD_NUMBER: _ClassVar[int]
LOCAL_RELATIVE_PATH_FIELD_NUMBER: _ClassVar[int]
FOLDER_TYPE_FIELD_NUMBER: _ClassVar[int]
LOCAL_SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
PEER_SYNCTHING_DEVICE_IDS_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
WRITABLE_FIELD_NUMBER: _ClassVar[int]
SPARSE_SUPPORTED_FIELD_NUMBER: _ClassVar[int]
ARCHIVE_CONTROL_CREATED_FIELD_NUMBER: _ClassVar[int]
DETAIL_FIELD_NUMBER: _ClassVar[int]
OBSERVED_AT_FIELD_NUMBER: _ClassVar[int]
route_id: str
local_relative_path: str
folder_type: SyncthingFolderType
local_syncthing_device_id: str
peer_syncthing_device_ids: _containers.RepeatedScalarFieldContainer[str]
state: RouteState
writable: bool
sparse_supported: bool
archive_control_created: bool
detail: str
observed_at: _timestamp_pb2.Timestamp
def __init__(self, route_id: _Optional[str] = ..., local_relative_path: _Optional[str] = ..., folder_type: _Optional[_Union[SyncthingFolderType, str]] = ..., local_syncthing_device_id: _Optional[str] = ..., peer_syncthing_device_ids: _Optional[_Iterable[str]] = ..., state: _Optional[_Union[RouteState, str]] = ..., writable: _Optional[bool] = ..., sparse_supported: _Optional[bool] = ..., archive_control_created: _Optional[bool] = ..., detail: _Optional[str] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class RouteDescriptor(_message.Message):
__slots__ = ("route_id", "archive_client_id", "cache_client_id", "archive_syncthing_device_id", "cache_syncthing_device_id", "state", "verified_at", "detail")
ROUTE_ID_FIELD_NUMBER: _ClassVar[int]
ARCHIVE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
CACHE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
ARCHIVE_SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
CACHE_SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
VERIFIED_AT_FIELD_NUMBER: _ClassVar[int]
DETAIL_FIELD_NUMBER: _ClassVar[int]
route_id: str
archive_client_id: str
cache_client_id: str
archive_syncthing_device_id: str
cache_syncthing_device_id: str
state: RouteState
verified_at: _timestamp_pb2.Timestamp
detail: str
def __init__(self, route_id: _Optional[str] = ..., archive_client_id: _Optional[str] = ..., cache_client_id: _Optional[str] = ..., archive_syncthing_device_id: _Optional[str] = ..., cache_syncthing_device_id: _Optional[str] = ..., state: _Optional[_Union[RouteState, str]] = ..., verified_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., detail: _Optional[str] = ...) -> None: ...
class EnsureRouteSpec(_message.Message):
__slots__ = ("route_id", "peer_client_id", "peer_syncthing_device_id", "peer_addresses", "local_relative_path", "setup_timeout_seconds")
ROUTE_ID_FIELD_NUMBER: _ClassVar[int]
PEER_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
PEER_SYNCTHING_DEVICE_ID_FIELD_NUMBER: _ClassVar[int]
PEER_ADDRESSES_FIELD_NUMBER: _ClassVar[int]
LOCAL_RELATIVE_PATH_FIELD_NUMBER: _ClassVar[int]
SETUP_TIMEOUT_SECONDS_FIELD_NUMBER: _ClassVar[int]
route_id: str
peer_client_id: str
peer_syncthing_device_id: str
peer_addresses: _containers.RepeatedScalarFieldContainer[str]
local_relative_path: str
setup_timeout_seconds: int
def __init__(self, route_id: _Optional[str] = ..., peer_client_id: _Optional[str] = ..., peer_syncthing_device_id: _Optional[str] = ..., peer_addresses: _Optional[_Iterable[str]] = ..., local_relative_path: _Optional[str] = ..., setup_timeout_seconds: _Optional[int] = ...) -> None: ...
class RouteVerification(_message.Message):
__slots__ = ("route_id", "nonce", "local_nonce_seen_by_peer", "peer_nonce_seen_locally", "state", "detail", "observed_at")
ROUTE_ID_FIELD_NUMBER: _ClassVar[int]
NONCE_FIELD_NUMBER: _ClassVar[int]
LOCAL_NONCE_SEEN_BY_PEER_FIELD_NUMBER: _ClassVar[int]
PEER_NONCE_SEEN_LOCALLY_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
DETAIL_FIELD_NUMBER: _ClassVar[int]
OBSERVED_AT_FIELD_NUMBER: _ClassVar[int]
route_id: str
nonce: str
local_nonce_seen_by_peer: bool
peer_nonce_seen_locally: bool
state: RouteState
detail: str
observed_at: _timestamp_pb2.Timestamp
def __init__(self, route_id: _Optional[str] = ..., nonce: _Optional[str] = ..., local_nonce_seen_by_peer: _Optional[bool] = ..., peer_nonce_seen_locally: _Optional[bool] = ..., state: _Optional[_Union[RouteState, str]] = ..., detail: _Optional[str] = ..., observed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
+49
View File
@@ -0,0 +1,49 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: archive_control/v1/transfer.proto
# Protobuf Python Version: 7.35.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
7,
35,
1,
'',
'archive_control/v1/transfer.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from archive_control.v1 import resource_pb2 as archive__control_dot_v1_dot_resource__pb2
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!archive_control/v1/transfer.proto\x12\x12\x61rchive_control.v1\x1a!archive_control/v1/resource.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd7\x03\n\x0cManifestFile\x12\x1d\n\nfile_index\x18\x01 \x01(\rR\tfileIndex\x12\x32\n\x15payload_relative_path\x18\x02 \x01(\tR\x13payloadRelativePath\x12\x32\n\x15target_canonical_path\x18\x03 \x01(\tR\x13targetCanonicalPath\x12#\n\rlogical_bytes\x18\x04 \x01(\x04R\x0clogicalBytes\x12\'\n\x0f\x61llocated_bytes\x18\x05 \x01(\x04R\x0e\x61llocatedBytes\x12\x16\n\x06sparse\x18\x06 \x01(\x08R\x06sparse\x12]\n\x15source_staging_method\x18\x07 \x01(\x0e\x32).archive_control.v1.MaterializationMethodR\x13sourceStagingMethod\x12N\n\rtarget_method\x18\x08 \x01(\x0e\x32).archive_control.v1.MaterializationMethodR\x0ctargetMethod\x12+\n\x11target_preexisted\x18\t \x01(\x08R\x10targetPreexisted\"\x99\x02\n\x10ManifestArtifact\x12\x34\n\x04kind\x18\x01 \x01(\x0e\x32 .archive_control.v1.ArtifactKindR\x04kind\x12\x32\n\x15payload_relative_path\x18\x02 \x01(\tR\x13payloadRelativePath\x12#\n\rlogical_bytes\x18\x03 \x01(\x04R\x0clogicalBytes\x12\'\n\x0f\x61llocated_bytes\x18\x04 \x01(\x04R\x0e\x61llocatedBytes\x12\x16\n\x06sparse\x18\x05 \x01(\x08R\x06sparse\x12\x16\n\x06\x66ormat\x18\x06 \x01(\tR\x06\x66ormat\x12\x1d\n\nsha256_hex\x18\x07 \x01(\tR\tsha256Hex\"\xfb\x06\n\x10TransferManifest\x12)\n\x10manifest_version\x18\x01 \x01(\rR\x0fmanifestVersion\x12\x15\n\x06job_id\x18\x02 \x01(\tR\x05jobId\x12?\n\x0bresource_id\x18\x03 \x01(\x0b\x32\x1e.archive_control.v1.ResourceIdR\nresourceId\x12(\n\x10source_client_id\x18\x04 \x01(\tR\x0esourceClientId\x12(\n\x10target_client_id\x18\x05 \x01(\tR\x0etargetClientId\x12\x19\n\x08route_id\x18\x06 \x01(\tR\x07routeId\x12I\n\x0frequested_files\x18\x07 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x0erequestedFiles\x12T\n\x15target_baseline_files\x18\x08 \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x13targetBaselineFiles\x12R\n\x14transfer_delta_files\x18\t \x01(\x0b\x32 .archive_control.v1.SelectionSetR\x12transferDeltaFiles\x12\x36\n\x05\x66iles\x18\n \x03(\x0b\x32 .archive_control.v1.ManifestFileR\x05\x66iles\x12\x42\n\tartifacts\x18\x0b \x03(\x0b\x32$.archive_control.v1.ManifestArtifactR\tartifacts\x12[\n\x12source_fingerprint\x18\x0c \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x11sourceFingerprint\x12l\n\x1btarget_baseline_fingerprint\x18\r \x01(\x0b\x32,.archive_control.v1.ResourceStateFingerprintR\x19targetBaselineFingerprint\x12\x39\n\ncreated_at\x18\x0e \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\"\x8b\x01\n\x0bReadyMarker\x12\x15\n\x06job_id\x18\x01 \x01(\tR\x05jobId\x12.\n\x13manifest_sha256_hex\x18\x02 \x01(\tR\x11manifestSha256Hex\x12\x35\n\x08ready_at\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x07readyAt*\xd9\x01\n\x15MaterializationMethod\x12&\n\"MATERIALIZATION_METHOD_UNSPECIFIED\x10\x00\x12$\n MATERIALIZATION_METHOD_HARD_LINK\x10\x01\x12\"\n\x1eMATERIALIZATION_METHOD_REFLINK\x10\x02\x12\x1f\n\x1bMATERIALIZATION_METHOD_COPY\x10\x03\x12-\n)MATERIALIZATION_METHOD_PREEXISTING_REUSED\x10\x04*\x97\x01\n\x0c\x41rtifactKind\x12\x1d\n\x19\x41RTIFACT_KIND_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x41RTIFACT_KIND_TORRENT_FILE\x10\x01\x12%\n!ARTIFACT_KIND_LIBTORRENT_PARTFILE\x10\x02\x12!\n\x1d\x41RTIFACT_KIND_OTHER_AUXILIARY\x10\x03\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'archive_control.v1.transfer_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_MATERIALIZATIONMETHOD']._serialized_start=1920
_globals['_MATERIALIZATIONMETHOD']._serialized_end=2137
_globals['_ARTIFACTKIND']._serialized_start=2140
_globals['_ARTIFACTKIND']._serialized_end=2291
_globals['_MANIFESTFILE']._serialized_start=126
_globals['_MANIFESTFILE']._serialized_end=597
_globals['_MANIFESTARTIFACT']._serialized_start=600
_globals['_MANIFESTARTIFACT']._serialized_end=881
_globals['_TRANSFERMANIFEST']._serialized_start=884
_globals['_TRANSFERMANIFEST']._serialized_end=1775
_globals['_READYMARKER']._serialized_start=1778
_globals['_READYMARKER']._serialized_end=1917
# @@protoc_insertion_point(module_scope)
+119
View File
@@ -0,0 +1,119 @@
# archive-control-proto commit: 4ec852014dad74606d4078b3ae1aa208c814b033
import datetime
from archive_control.v1 import resource_pb2 as _resource_pb2
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class MaterializationMethod(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
MATERIALIZATION_METHOD_UNSPECIFIED: _ClassVar[MaterializationMethod]
MATERIALIZATION_METHOD_HARD_LINK: _ClassVar[MaterializationMethod]
MATERIALIZATION_METHOD_REFLINK: _ClassVar[MaterializationMethod]
MATERIALIZATION_METHOD_COPY: _ClassVar[MaterializationMethod]
MATERIALIZATION_METHOD_PREEXISTING_REUSED: _ClassVar[MaterializationMethod]
class ArtifactKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
ARTIFACT_KIND_UNSPECIFIED: _ClassVar[ArtifactKind]
ARTIFACT_KIND_TORRENT_FILE: _ClassVar[ArtifactKind]
ARTIFACT_KIND_LIBTORRENT_PARTFILE: _ClassVar[ArtifactKind]
ARTIFACT_KIND_OTHER_AUXILIARY: _ClassVar[ArtifactKind]
MATERIALIZATION_METHOD_UNSPECIFIED: MaterializationMethod
MATERIALIZATION_METHOD_HARD_LINK: MaterializationMethod
MATERIALIZATION_METHOD_REFLINK: MaterializationMethod
MATERIALIZATION_METHOD_COPY: MaterializationMethod
MATERIALIZATION_METHOD_PREEXISTING_REUSED: MaterializationMethod
ARTIFACT_KIND_UNSPECIFIED: ArtifactKind
ARTIFACT_KIND_TORRENT_FILE: ArtifactKind
ARTIFACT_KIND_LIBTORRENT_PARTFILE: ArtifactKind
ARTIFACT_KIND_OTHER_AUXILIARY: ArtifactKind
class ManifestFile(_message.Message):
__slots__ = ("file_index", "payload_relative_path", "target_canonical_path", "logical_bytes", "allocated_bytes", "sparse", "source_staging_method", "target_method", "target_preexisted")
FILE_INDEX_FIELD_NUMBER: _ClassVar[int]
PAYLOAD_RELATIVE_PATH_FIELD_NUMBER: _ClassVar[int]
TARGET_CANONICAL_PATH_FIELD_NUMBER: _ClassVar[int]
LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
ALLOCATED_BYTES_FIELD_NUMBER: _ClassVar[int]
SPARSE_FIELD_NUMBER: _ClassVar[int]
SOURCE_STAGING_METHOD_FIELD_NUMBER: _ClassVar[int]
TARGET_METHOD_FIELD_NUMBER: _ClassVar[int]
TARGET_PREEXISTED_FIELD_NUMBER: _ClassVar[int]
file_index: int
payload_relative_path: str
target_canonical_path: str
logical_bytes: int
allocated_bytes: int
sparse: bool
source_staging_method: MaterializationMethod
target_method: MaterializationMethod
target_preexisted: bool
def __init__(self, file_index: _Optional[int] = ..., payload_relative_path: _Optional[str] = ..., target_canonical_path: _Optional[str] = ..., logical_bytes: _Optional[int] = ..., allocated_bytes: _Optional[int] = ..., sparse: _Optional[bool] = ..., source_staging_method: _Optional[_Union[MaterializationMethod, str]] = ..., target_method: _Optional[_Union[MaterializationMethod, str]] = ..., target_preexisted: _Optional[bool] = ...) -> None: ...
class ManifestArtifact(_message.Message):
__slots__ = ("kind", "payload_relative_path", "logical_bytes", "allocated_bytes", "sparse", "format", "sha256_hex")
KIND_FIELD_NUMBER: _ClassVar[int]
PAYLOAD_RELATIVE_PATH_FIELD_NUMBER: _ClassVar[int]
LOGICAL_BYTES_FIELD_NUMBER: _ClassVar[int]
ALLOCATED_BYTES_FIELD_NUMBER: _ClassVar[int]
SPARSE_FIELD_NUMBER: _ClassVar[int]
FORMAT_FIELD_NUMBER: _ClassVar[int]
SHA256_HEX_FIELD_NUMBER: _ClassVar[int]
kind: ArtifactKind
payload_relative_path: str
logical_bytes: int
allocated_bytes: int
sparse: bool
format: str
sha256_hex: str
def __init__(self, kind: _Optional[_Union[ArtifactKind, str]] = ..., payload_relative_path: _Optional[str] = ..., logical_bytes: _Optional[int] = ..., allocated_bytes: _Optional[int] = ..., sparse: _Optional[bool] = ..., format: _Optional[str] = ..., sha256_hex: _Optional[str] = ...) -> None: ...
class TransferManifest(_message.Message):
__slots__ = ("manifest_version", "job_id", "resource_id", "source_client_id", "target_client_id", "route_id", "requested_files", "target_baseline_files", "transfer_delta_files", "files", "artifacts", "source_fingerprint", "target_baseline_fingerprint", "created_at")
MANIFEST_VERSION_FIELD_NUMBER: _ClassVar[int]
JOB_ID_FIELD_NUMBER: _ClassVar[int]
RESOURCE_ID_FIELD_NUMBER: _ClassVar[int]
SOURCE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
TARGET_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
ROUTE_ID_FIELD_NUMBER: _ClassVar[int]
REQUESTED_FILES_FIELD_NUMBER: _ClassVar[int]
TARGET_BASELINE_FILES_FIELD_NUMBER: _ClassVar[int]
TRANSFER_DELTA_FILES_FIELD_NUMBER: _ClassVar[int]
FILES_FIELD_NUMBER: _ClassVar[int]
ARTIFACTS_FIELD_NUMBER: _ClassVar[int]
SOURCE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int]
TARGET_BASELINE_FINGERPRINT_FIELD_NUMBER: _ClassVar[int]
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
manifest_version: int
job_id: str
resource_id: _resource_pb2.ResourceId
source_client_id: str
target_client_id: str
route_id: str
requested_files: _resource_pb2.SelectionSet
target_baseline_files: _resource_pb2.SelectionSet
transfer_delta_files: _resource_pb2.SelectionSet
files: _containers.RepeatedCompositeFieldContainer[ManifestFile]
artifacts: _containers.RepeatedCompositeFieldContainer[ManifestArtifact]
source_fingerprint: _resource_pb2.ResourceStateFingerprint
target_baseline_fingerprint: _resource_pb2.ResourceStateFingerprint
created_at: _timestamp_pb2.Timestamp
def __init__(self, manifest_version: _Optional[int] = ..., job_id: _Optional[str] = ..., resource_id: _Optional[_Union[_resource_pb2.ResourceId, _Mapping]] = ..., source_client_id: _Optional[str] = ..., target_client_id: _Optional[str] = ..., route_id: _Optional[str] = ..., requested_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., target_baseline_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., transfer_delta_files: _Optional[_Union[_resource_pb2.SelectionSet, _Mapping]] = ..., files: _Optional[_Iterable[_Union[ManifestFile, _Mapping]]] = ..., artifacts: _Optional[_Iterable[_Union[ManifestArtifact, _Mapping]]] = ..., source_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., target_baseline_fingerprint: _Optional[_Union[_resource_pb2.ResourceStateFingerprint, _Mapping]] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class ReadyMarker(_message.Message):
__slots__ = ("job_id", "manifest_sha256_hex", "ready_at")
JOB_ID_FIELD_NUMBER: _ClassVar[int]
MANIFEST_SHA256_HEX_FIELD_NUMBER: _ClassVar[int]
READY_AT_FIELD_NUMBER: _ClassVar[int]
job_id: str
manifest_sha256_hex: str
ready_at: _timestamp_pb2.Timestamp
def __init__(self, job_id: _Optional[str] = ..., manifest_sha256_hex: _Optional[str] = ..., ready_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
+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()
+112
View File
@@ -0,0 +1,112 @@
import asyncio
import os
import tempfile
import unittest
from pathlib import Path, PurePosixPath
from uuid import uuid4
from websockets.asyncio.server import serve
from archive_clients.config import ClientConfig, ConnectionConfig, ServiceConfig
from archive_clients.daemon import ArchiveClientDaemon
from archive_clients.probes import FilesystemProbe
from archive_clients.protocol import decode, encode, new_envelope
from archive_control.v1 import client_pb2, common_pb2, control_pb2
class DaemonTransportTests(unittest.IsolatedAsyncioTestCase):
async def test_registration_heartbeat_and_duplicate_command(self):
observed = {}
async def control(websocket):
registration = decode(await websocket.recv())
observed["token"] = registration.register_request.shared_token
observed["root_names"] = [
item.root_name
for item in registration.register_request.capabilities.filesystems
]
observed["addresses"] = list(
registration.register_request.capabilities
.syncthing_advertised_addresses
)
response = new_envelope()
response.correlation_id = registration.message_id
response.register_response.status = client_pb2.REGISTRATION_STATUS_ACCEPTED
response.register_response.negotiated_version.major = 1
await websocket.send(encode(response))
heartbeat = new_envelope()
heartbeat.heartbeat.sequence = 7
await websocket.send(encode(heartbeat))
observed["heartbeat"] = decode(await websocket.recv()).heartbeat_ack.sequence
command_id = str(uuid4())
command = new_envelope()
command.command.command_id = command_id
command.command.created_at.CopyFrom(command.sent_at)
command.command.request_job_snapshot.job_ids.append(str(uuid4()))
await websocket.send(encode(command))
observed["first"] = decode(await websocket.recv()).command_ack.status
observed["snapshot"] = (
decode(await websocket.recv()).WhichOneof("payload")
)
duplicate = new_envelope()
duplicate.command.CopyFrom(command.command)
await websocket.send(encode(duplicate))
observed["second"] = decode(await websocket.recv()).command_ack.status
unsupported = new_envelope()
unsupported.command.command_id = str(uuid4())
unsupported.command.created_at.CopyFrom(unsupported.sent_at)
unsupported.command.assign_job.SetInParent()
await websocket.send(encode(unsupported))
rejected = decode(await websocket.recv()).command_ack
observed["rejected"] = (rejected.status, rejected.error.code)
unsupported_duplicate = new_envelope()
unsupported_duplicate.command.CopyFrom(unsupported.command)
await websocket.send(encode(unsupported_duplicate))
observed["rejected_duplicate"] = (
decode(await websocket.recv()).command_ack.status
)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
token = root / "token"
token.write_text("shared-secret", encoding="utf-8")
os.chmod(token, 0o600)
async with serve(control, "127.0.0.1", 0, ping_interval=None) as server:
port = server.sockets[0].getsockname()[1]
service = ServiceConfig(
"http://local", PurePosixPath("/api"), root,
advertised_addresses=("dynamic",),
)
config = ClientConfig(
"cache-1", "Cache 1", "cache",
f"ws://127.0.0.1:{port}", token,
root / "state.db", root / "backups", service, service,
ConnectionConfig(registration_timeout=2),
)
probe = FilesystemProbe(root, True, True, True, True)
daemon = ArchiveClientDaemon(config, [probe, probe])
await asyncio.to_thread(daemon.store.initialize)
await daemon._connection()
self.assertEqual(observed["token"], "shared-secret")
self.assertEqual(observed["root_names"], ["qbittorrent", "syncthing"])
self.assertEqual(observed["addresses"], ["dynamic"])
self.assertEqual(observed["heartbeat"], 7)
self.assertEqual(observed["first"], control_pb2.COMMAND_ACK_STATUS_ACCEPTED)
self.assertEqual(observed["second"], control_pb2.COMMAND_ACK_STATUS_DUPLICATE)
self.assertEqual(observed["snapshot"], "client_state_snapshot")
self.assertEqual(
observed["rejected"],
(
control_pb2.COMMAND_ACK_STATUS_REJECTED,
common_pb2.ERROR_CODE_UNSUPPORTED,
),
)
self.assertEqual(
observed["rejected_duplicate"],
control_pb2.COMMAND_ACK_STATUS_REJECTED,
)
if __name__ == "__main__":
unittest.main()
+17
View File
@@ -0,0 +1,17 @@
import unittest
from pathlib import Path
from archive_clients import PROTO_COMMIT
class GeneratedContractTests(unittest.TestCase):
def test_all_generated_files_record_proto_commit(self):
files = list(Path("src/archive_control/v1").glob("*_pb2.py"))
files += list(Path("src/archive_control/v1").glob("*_pb2.pyi"))
self.assertEqual(len(files), 18)
for path in files:
self.assertIn(PROTO_COMMIT, path.read_text(encoding="utf-8")[:256])
if __name__ == "__main__":
unittest.main()
+33
View File
@@ -0,0 +1,33 @@
import json
import tempfile
import unittest
from pathlib import Path
from archive_clients.probes import probe_root
from archive_clients.protocol import ProtocolError, decode, encode, new_envelope
class ProtocolAndProbeTests(unittest.TestCase):
def test_protocol_round_trip_and_duplicate_key_rejection(self):
envelope = new_envelope()
envelope.heartbeat.sequence = 1
self.assertEqual(decode(encode(envelope)).heartbeat.sequence, 1)
raw = json.loads(encode(envelope))
duplicate = json.dumps(raw).replace("{", '{"messageId":"duplicate",', 1)
with self.assertRaises(ProtocolError):
decode(duplicate)
raw["messageId"] = "not-a-uuid"
with self.assertRaisesRegex(ProtocolError, "canonical UUID"):
decode(json.dumps(raw))
def test_filesystem_probe_cleans_up(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
result = probe_root(root)
self.assertTrue(result.readable)
self.assertTrue(result.writable)
self.assertEqual(list(root.iterdir()), [])
if __name__ == "__main__":
unittest.main()
+28
View File
@@ -0,0 +1,28 @@
import tempfile
import unittest
from pathlib import Path
from uuid import uuid4
from archive_clients.state import ClientStore, CommandConflict
class ClientStoreTests(unittest.TestCase):
def test_command_acceptance_is_durable_and_content_addressed(self):
with tempfile.TemporaryDirectory() as directory:
database = Path(directory) / "state.db"
store = ClientStore(database)
store.initialize()
command_id = str(uuid4())
first = store.accept_command(command_id, '{"b":2,"a":1}', '{"ok":true}')
duplicate = ClientStore(database).accept_command(
command_id, '{"a":1,"b":2}', '{"ok":false}'
)
self.assertFalse(first.duplicate)
self.assertTrue(duplicate.duplicate)
self.assertEqual(duplicate.acknowledgement_json, '{"ok":true}')
with self.assertRaises(CommandConflict):
store.accept_command(command_id, '{"a":2}', '{"ok":true}')
if __name__ == "__main__":
unittest.main()