feat: execute durable syncthing route setup
This commit is contained in:
@@ -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)
|
||||
);
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user