feat: execute durable syncthing route setup

This commit is contained in:
2026-07-23 02:27:17 +00:00
parent 4058b6d3c8
commit 10e803eaee
8 changed files with 1189 additions and 11 deletions
+169 -4
View File
@@ -11,6 +11,9 @@ from dataclasses import dataclass
from pathlib import Path
SCHEMA_VERSION = 2
class CommandConflict(RuntimeError):
pass
@@ -40,12 +43,17 @@ class ClientStore:
raise RuntimeError("client database file permissions are unsafe")
with self._connect() as connection:
version = connection.execute("PRAGMA user_version").fetchone()[0]
if version > 1:
if version > SCHEMA_VERSION:
raise RuntimeError(
f"client database schema {version} is newer than supported"
)
connection.executescript(_SCHEMA)
connection.execute("PRAGMA user_version = 1")
if version == 0:
connection.executescript(_SCHEMA_V1)
connection.execute("PRAGMA user_version = 1")
version = 1
if version == 1:
connection.executescript(_SCHEMA_V2)
connection.execute("PRAGMA user_version = 2")
if connection.execute("PRAGMA foreign_key_check").fetchall():
raise RuntimeError("client database foreign-key check failed")
if not existed:
@@ -93,6 +101,16 @@ class ClientStore:
).fetchall()
return [dict(row) for row in rows]
def list_accepted_commands(self) -> list[dict[str, object]]:
with self._connect() as connection:
rows = connection.execute(
"""
SELECT command_id, command_json, acknowledgement_json
FROM commands ORDER BY rowid
"""
).fetchall()
return [dict(row) for row in rows]
def job_snapshot_rows(
self, job_ids: list[str]
) -> list[dict[str, object]]:
@@ -165,6 +183,133 @@ class ClientStore:
),
)
def begin_route_attempt(
self,
command_id: str,
route_id: str,
spec_json: str,
nonce: str,
) -> dict[str, object]:
spec = _canonical(json.loads(spec_json))
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute(
"SELECT * FROM route_attempts WHERE command_id = ?",
(command_id,),
).fetchone()
if existing:
if (
existing["route_id"] != route_id
or existing["spec_json"] != spec
):
raise CommandConflict(
"route command ID was reused with different content"
)
return dict(existing)
connection.execute(
"""
INSERT INTO route_attempts (
command_id, route_id, spec_json, nonce, state,
last_sequence
) VALUES (?, ?, ?, ?, 'accepted', 0)
""",
(command_id, route_id, spec, nonce),
)
row = connection.execute(
"SELECT * FROM route_attempts WHERE command_id = ?",
(command_id,),
).fetchone()
return dict(row)
def record_route_update(
self,
command_id: str,
sequence: int,
state: str,
update_json: str,
) -> None:
update = _canonical(json.loads(update_json))
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute(
"""
SELECT update_json FROM route_attempt_updates
WHERE command_id = ? AND sequence = ?
""",
(command_id, sequence),
).fetchone()
if existing:
if existing["update_json"] != update:
raise CommandConflict(
"route update sequence has different content"
)
return
attempt = connection.execute(
"""
SELECT last_sequence FROM route_attempts
WHERE command_id = ?
""",
(command_id,),
).fetchone()
if attempt is None:
raise CommandConflict("route attempt does not exist")
expected = attempt["last_sequence"] + 1
if sequence != expected:
raise CommandConflict(
f"route update sequence must be {expected}, received {sequence}"
)
connection.execute(
"""
INSERT INTO route_attempt_updates (
command_id, sequence, update_json
) VALUES (?, ?, ?)
""",
(command_id, sequence, update),
)
connection.execute(
"""
UPDATE route_attempts
SET state = ?, last_sequence = ?
WHERE command_id = ?
""",
(state, sequence, command_id),
)
def record_route_ownership(
self, command_id: str, created_device: bool, created_folder: bool
) -> None:
with self._connect() as connection:
cursor = connection.execute(
"""
UPDATE route_attempts
SET created_device = MAX(created_device, ?),
created_folder = MAX(created_folder, ?)
WHERE command_id = ?
""",
(int(created_device), int(created_folder), command_id),
)
if cursor.rowcount != 1:
raise CommandConflict("route attempt does not exist")
def get_route_attempt(self, command_id: str) -> dict[str, object] | None:
with self._connect() as connection:
row = connection.execute(
"SELECT * FROM route_attempts WHERE command_id = ?",
(command_id,),
).fetchone()
return dict(row) if row else None
def route_update_rows(self, command_id: str) -> list[dict[str, object]]:
with self._connect() as connection:
rows = connection.execute(
"""
SELECT sequence, update_json FROM route_attempt_updates
WHERE command_id = ? ORDER BY sequence
""",
(command_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
@@ -179,7 +324,7 @@ def _canonical(value: object) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
_SCHEMA = """
_SCHEMA_V1 = """
CREATE TABLE IF NOT EXISTS commands (
command_id TEXT PRIMARY KEY,
payload_sha256 TEXT NOT NULL,
@@ -210,3 +355,23 @@ CREATE TABLE IF NOT EXISTS file_journal (
state TEXT NOT NULL
);
"""
_SCHEMA_V2 = """
CREATE TABLE route_attempts (
command_id TEXT PRIMARY KEY REFERENCES commands(command_id),
route_id TEXT NOT NULL,
spec_json TEXT NOT NULL,
nonce TEXT NOT NULL,
state TEXT NOT NULL,
last_sequence INTEGER NOT NULL DEFAULT 0,
created_device INTEGER NOT NULL DEFAULT 0 CHECK(created_device IN (0, 1)),
created_folder INTEGER NOT NULL DEFAULT 0 CHECK(created_folder IN (0, 1))
);
CREATE TABLE route_attempt_updates (
command_id TEXT NOT NULL REFERENCES route_attempts(command_id),
sequence INTEGER NOT NULL,
update_json TEXT NOT NULL,
PRIMARY KEY(command_id, sequence)
);
"""