pairling 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +5 -1
  2. package/payload/mac/SOURCE_BRANCH +1 -0
  3. package/payload/mac/SOURCE_DIRTY +1 -0
  4. package/payload/mac/SOURCE_REVISION +1 -0
  5. package/payload/mac/VERSION +1 -0
  6. package/payload/mac/companiond/integrations/__init__.py +1 -0
  7. package/payload/mac/companiond/integrations/aperture_cli/__init__.py +23 -0
  8. package/payload/mac/companiond/integrations/aperture_cli/launch.py +456 -0
  9. package/payload/mac/companiond/integrations/aperture_cli/status.py +393 -0
  10. package/payload/mac/companiond/live_activity_publisher.py +380 -0
  11. package/payload/mac/companiond/llm_route.py +108 -0
  12. package/payload/mac/companiond/local_mcp_bridge.py +156 -0
  13. package/payload/mac/companiond/model_status_contract.py +101 -0
  14. package/payload/mac/companiond/pairdrop_store.py +920 -0
  15. package/payload/mac/companiond/pairling_connectd_status.py +149 -0
  16. package/payload/mac/companiond/pairling_devices.py +459 -0
  17. package/payload/mac/companiond/pairling_pairing.py +404 -0
  18. package/payload/mac/companiond/pairling_relay_claims.py +232 -0
  19. package/payload/mac/companiond/pairling_tools.py +706 -0
  20. package/payload/mac/companiond/pairlingd.py +18438 -0
  21. package/payload/mac/companiond/providers/__init__.py +1 -0
  22. package/payload/mac/companiond/providers/base.py +255 -0
  23. package/payload/mac/companiond/providers/claude.py +127 -0
  24. package/payload/mac/companiond/providers/codex.py +124 -0
  25. package/payload/mac/companiond/providers/external.py +46 -0
  26. package/payload/mac/companiond/providers/registry.py +70 -0
  27. package/payload/mac/companiond/pty_broker.py +887 -0
  28. package/payload/mac/companiond/push_dispatcher.py +1990 -0
  29. package/payload/mac/companiond/push_event_catalog.py +566 -0
  30. package/payload/mac/companiond/request_proof.py +142 -0
  31. package/payload/mac/companiond/runtime_contract.py +47 -0
  32. package/payload/mac/companiond/runtime_manifest.py +197 -0
  33. package/payload/mac/companiond/runtime_paths.py +87 -0
  34. package/payload/mac/companiond/safety_monitor.py +542 -0
  35. package/payload/mac/companiond/sentinel_notifications.py +491 -0
  36. package/payload/mac/companiond/standard_push_publisher.py +516 -0
  37. package/payload/mac/companiond/substrate_status_contract.py +139 -0
  38. package/payload/mac/companiond/terminal_screen_backend.py +332 -0
  39. package/payload/mac/companiond/terminal_text_sanitizer.py +54 -0
  40. package/payload/mac/companiond/workstate_feed_contract.py +108 -0
  41. package/payload/mac/connectd/cmd/pairling-connectd/auth_open_test.go +116 -0
  42. package/payload/mac/connectd/cmd/pairling-connectd/main.go +345 -0
  43. package/payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go +33 -0
  44. package/payload/mac/connectd/go.mod +51 -0
  45. package/payload/mac/connectd/go.sum +229 -0
  46. package/payload/mac/connectd/internal/gateway/proxy.go +597 -0
  47. package/payload/mac/connectd/internal/gateway/proxy_test.go +531 -0
  48. package/payload/mac/connectd/internal/runtime/config.go +99 -0
  49. package/payload/mac/connectd/internal/runtime/config_test.go +29 -0
  50. package/payload/mac/connectd/internal/status/status.go +300 -0
  51. package/payload/mac/connectd/internal/status/status_test.go +263 -0
  52. package/payload/mac/guardian/companion-power-guardian.py +613 -0
  53. package/payload/mac/guardian/guardian_contract.py +67 -0
  54. package/payload/mac/install/bootstrap-first-run.sh +206 -0
  55. package/payload/mac/install/doctor.sh +660 -0
  56. package/payload/mac/install/install-runtime.sh +1241 -0
  57. package/payload/mac/install/render-launchd.py +119 -0
  58. package/payload/mac/install/uninstall-runtime.sh +136 -0
  59. package/payload/mac/mcp/phone_tools.py +210 -0
  60. package/payload/mac/packaging/bin/pairling +63 -0
  61. package/payload-manifest.json +255 -0
@@ -0,0 +1,920 @@
1
+ #!/usr/bin/env python3
2
+ """Mac-local PairDrop vault storage.
3
+
4
+ PairDrop stores user files under a Pairling-owned root and exposes files by
5
+ opaque ids, never by client-supplied paths. This module intentionally has no
6
+ HTTP dependency so daemon tests can exercise the storage contract directly.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import errno
13
+ import json
14
+ import os
15
+ import re
16
+ import secrets
17
+ import sqlite3
18
+ import stat
19
+ import time
20
+ from collections.abc import Iterator
21
+ from contextlib import contextmanager
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+
26
+ class PairDropStoreError(ValueError):
27
+ def __init__(self, code: str, message: str | None = None):
28
+ super().__init__(message or code)
29
+ self.code = code
30
+
31
+
32
+ def _now_iso() -> str:
33
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
34
+
35
+
36
+ def _safe_display_name(filename: str) -> str:
37
+ base = os.path.basename(str(filename or "").strip())
38
+ safe = re.sub(r"[^A-Za-z0-9_.-]", "_", base).strip("._")
39
+ if not safe:
40
+ return "upload.bin"
41
+ if len(safe) <= 120:
42
+ return safe
43
+ stem, dot, ext = safe.rpartition(".")
44
+ if dot and 1 <= len(ext) <= 12:
45
+ return stem[: 120 - len(ext) - 1] + "." + ext
46
+ return safe[:120]
47
+
48
+
49
+ def _json_list(value: Any) -> str:
50
+ return json.dumps(value if isinstance(value, list) else [])
51
+
52
+
53
+ class PairDropStore:
54
+ schema_version = 1
55
+
56
+ def __init__(self, root: Path):
57
+ self.root = Path(root).expanduser()
58
+ self.objects_dir = self.root / "objects"
59
+ self.partials_dir = self.root / "partials"
60
+ self.thumbnails_dir = self.root / "thumbnails"
61
+ self.exports_dir = self.root / "exports"
62
+ self.db_path = self.root / "index.sqlite"
63
+ self.audit_path = self.root / "audit.jsonl"
64
+ self._ensure_root()
65
+
66
+ def _ensure_root(self) -> None:
67
+ for path in [
68
+ self.root,
69
+ self.objects_dir,
70
+ self.partials_dir,
71
+ self.thumbnails_dir,
72
+ self.exports_dir,
73
+ ]:
74
+ path.mkdir(parents=True, exist_ok=True)
75
+ try:
76
+ # PairDrop stores private user files; the vault root must not be world-readable.
77
+ os.chmod(self.root, 0o700) # nosemgrep: python.lang.security.audit.insecure-file-permissions.insecure-file-permissions
78
+ except OSError:
79
+ pass
80
+ with self._connect() as conn:
81
+ self._ensure_schema(conn)
82
+
83
+ @contextmanager
84
+ def _connect(self) -> Iterator[sqlite3.Connection]:
85
+ conn = sqlite3.connect(str(self.db_path))
86
+ conn.row_factory = sqlite3.Row
87
+ try:
88
+ yield conn
89
+ conn.commit()
90
+ except Exception:
91
+ conn.rollback()
92
+ raise
93
+ finally:
94
+ conn.close()
95
+
96
+ def _ensure_schema(self, conn: sqlite3.Connection) -> None:
97
+ conn.execute(
98
+ """
99
+ CREATE TABLE IF NOT EXISTS files (
100
+ id TEXT PRIMARY KEY,
101
+ parent_id TEXT,
102
+ kind TEXT NOT NULL,
103
+ display_name TEXT NOT NULL,
104
+ original_name TEXT NOT NULL,
105
+ content_type TEXT NOT NULL,
106
+ byte_size INTEGER NOT NULL,
107
+ sha256 TEXT,
108
+ storage_relpath TEXT,
109
+ source_device_id TEXT,
110
+ source_install_id TEXT,
111
+ source_route TEXT,
112
+ created_at TEXT NOT NULL,
113
+ updated_at TEXT NOT NULL,
114
+ deleted_at TEXT,
115
+ last_opened_at TEXT,
116
+ session_hint TEXT,
117
+ tags_json TEXT NOT NULL DEFAULT '[]'
118
+ )
119
+ """
120
+ )
121
+ conn.execute(
122
+ """
123
+ CREATE TABLE IF NOT EXISTS events (
124
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
125
+ type TEXT NOT NULL,
126
+ file_id TEXT,
127
+ created_at TEXT NOT NULL,
128
+ summary_json TEXT NOT NULL
129
+ )
130
+ """
131
+ )
132
+ conn.execute(
133
+ """
134
+ CREATE TABLE IF NOT EXISTS upload_sessions (
135
+ upload_id TEXT PRIMARY KEY,
136
+ file_id TEXT,
137
+ display_name TEXT NOT NULL,
138
+ original_name TEXT NOT NULL,
139
+ content_type TEXT NOT NULL,
140
+ total_byte_count INTEGER NOT NULL,
141
+ expected_sha256 TEXT NOT NULL,
142
+ verified_offset INTEGER NOT NULL DEFAULT 0,
143
+ source_device_id TEXT,
144
+ source_install_id TEXT,
145
+ source_route TEXT,
146
+ state TEXT NOT NULL,
147
+ last_error TEXT,
148
+ created_at TEXT NOT NULL,
149
+ updated_at TEXT NOT NULL,
150
+ expires_at TEXT NOT NULL
151
+ )
152
+ """
153
+ )
154
+ conn.execute(
155
+ """
156
+ CREATE TABLE IF NOT EXISTS upload_chunks (
157
+ upload_id TEXT NOT NULL,
158
+ idempotency_key TEXT NOT NULL,
159
+ offset INTEGER NOT NULL,
160
+ byte_count INTEGER NOT NULL,
161
+ sha256 TEXT NOT NULL,
162
+ created_at TEXT NOT NULL,
163
+ PRIMARY KEY (upload_id, idempotency_key)
164
+ )
165
+ """
166
+ )
167
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_pairdrop_files_deleted ON files(deleted_at)")
168
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_pairdrop_files_created ON files(created_at)")
169
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_pairdrop_upload_sessions_state ON upload_sessions(state)")
170
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_pairdrop_upload_sessions_expires ON upload_sessions(expires_at)")
171
+
172
+ def upload_bytes(
173
+ self,
174
+ *,
175
+ filename: str,
176
+ content_type: str,
177
+ data: bytes,
178
+ source_device_id: str,
179
+ source_install_id: str,
180
+ source_route: str = "pairling-connectd",
181
+ session_hint: str = "",
182
+ expected_sha256: str | None = None,
183
+ parent_id: str | None = None,
184
+ ) -> dict[str, Any]:
185
+ if not data:
186
+ raise PairDropStoreError("empty_body")
187
+ display_name = _safe_display_name(filename)
188
+ digest = hashlib.sha256(data).hexdigest()
189
+ if expected_sha256 and expected_sha256.lower() != digest:
190
+ raise PairDropStoreError("sha256_mismatch")
191
+ if parent_id:
192
+ parent = self.get_file(parent_id)
193
+ if parent.get("kind") != "folder":
194
+ raise PairDropStoreError("bad_parent")
195
+
196
+ file_id = "pd_" + secrets.token_hex(16)
197
+ relpath = Path("objects") / digest[:2] / f"{file_id}.blob"
198
+ partial = self.partials_dir / f"{file_id}.partial"
199
+ target = self.root / relpath
200
+ target.parent.mkdir(parents=True, exist_ok=True)
201
+ partial.write_bytes(data)
202
+ os.replace(partial, target)
203
+ now = _now_iso()
204
+ with self._connect() as conn:
205
+ self._ensure_schema(conn)
206
+ conn.execute(
207
+ """
208
+ INSERT INTO files (
209
+ id, parent_id, kind, display_name, original_name, content_type,
210
+ byte_size, sha256, storage_relpath, source_device_id,
211
+ source_install_id, source_route, created_at, updated_at,
212
+ deleted_at, last_opened_at, session_hint, tags_json
213
+ ) VALUES (?, ?, 'file', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)
214
+ """,
215
+ (
216
+ file_id,
217
+ parent_id,
218
+ display_name,
219
+ str(filename or ""),
220
+ content_type or "application/octet-stream",
221
+ len(data),
222
+ digest,
223
+ str(relpath),
224
+ source_device_id,
225
+ source_install_id,
226
+ source_route,
227
+ now,
228
+ now,
229
+ session_hint,
230
+ _json_list([]),
231
+ ),
232
+ )
233
+ self._record_event(conn, "created", file_id, {
234
+ "byte_size": len(data),
235
+ "content_type": content_type or "application/octet-stream",
236
+ "sha256": digest,
237
+ })
238
+ conn.commit()
239
+ item = self.get_file(file_id)
240
+ self._audit("file.created", {
241
+ "file_id": file_id,
242
+ "byte_size": len(data),
243
+ "content_type": content_type or "application/octet-stream",
244
+ "sha256": digest,
245
+ })
246
+ return item
247
+
248
+ def create_upload_session(
249
+ self,
250
+ *,
251
+ filename: str,
252
+ content_type: str,
253
+ total_byte_count: int,
254
+ expected_sha256: str,
255
+ source_device_id: str,
256
+ source_install_id: str,
257
+ source_route: str = "pairling-connectd",
258
+ expires_in_seconds: int = 24 * 60 * 60,
259
+ ) -> dict[str, Any]:
260
+ total = int(total_byte_count)
261
+ digest = str(expected_sha256 or "").strip().lower()
262
+ if total <= 0:
263
+ raise PairDropStoreError("bad_total_byte_count")
264
+ if not re.fullmatch(r"[a-f0-9]{64}", digest):
265
+ raise PairDropStoreError("bad_expected_sha256")
266
+ upload_id = "pu_" + secrets.token_hex(16)
267
+ display_name = _safe_display_name(filename)
268
+ now = _now_iso()
269
+ expires_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() + max(60, int(expires_in_seconds))))
270
+ with self._connect() as conn:
271
+ self._ensure_schema(conn)
272
+ conn.execute(
273
+ """
274
+ INSERT INTO upload_sessions (
275
+ upload_id, file_id, display_name, original_name, content_type,
276
+ total_byte_count, expected_sha256, verified_offset,
277
+ source_device_id, source_install_id, source_route, state,
278
+ last_error, created_at, updated_at, expires_at
279
+ ) VALUES (?, NULL, ?, ?, ?, ?, ?, 0, ?, ?, ?, 'created', NULL, ?, ?, ?)
280
+ """,
281
+ (
282
+ upload_id,
283
+ display_name,
284
+ str(filename or ""),
285
+ content_type or "application/octet-stream",
286
+ total,
287
+ digest,
288
+ source_device_id,
289
+ source_install_id,
290
+ source_route,
291
+ now,
292
+ now,
293
+ expires_at,
294
+ ),
295
+ )
296
+ self._record_event(conn, "upload_session_created", None, {
297
+ "upload_id": upload_id,
298
+ "byte_size": total,
299
+ "content_type": content_type or "application/octet-stream",
300
+ })
301
+ conn.commit()
302
+ self._audit("upload_session.created", {
303
+ "upload_id": upload_id,
304
+ "byte_size": total,
305
+ "content_type": content_type or "application/octet-stream",
306
+ })
307
+ return self.get_upload_session(upload_id)
308
+
309
+ def get_upload_session(self, upload_id: str) -> dict[str, Any]:
310
+ if not self._valid_upload_id(upload_id):
311
+ raise PairDropStoreError("bad_upload_id")
312
+ with self._connect() as conn:
313
+ self._ensure_schema(conn)
314
+ row = conn.execute("SELECT * FROM upload_sessions WHERE upload_id = ?", (upload_id,)).fetchone()
315
+ if row is None:
316
+ raise PairDropStoreError("upload_not_found")
317
+ return self._public_upload_row(row)
318
+
319
+ def write_upload_chunk(
320
+ self,
321
+ upload_id: str,
322
+ *,
323
+ offset: int,
324
+ data: bytes,
325
+ chunk_sha256: str,
326
+ idempotency_key: str,
327
+ source_device_id: str,
328
+ source_install_id: str,
329
+ ) -> dict[str, Any]:
330
+ if not self._valid_upload_id(upload_id):
331
+ raise PairDropStoreError("bad_upload_id")
332
+ if not data:
333
+ raise PairDropStoreError("empty_chunk")
334
+ offset = int(offset)
335
+ if offset < 0:
336
+ raise PairDropStoreError("bad_offset")
337
+ chunk_hash = str(chunk_sha256 or "").strip().lower()
338
+ if not re.fullmatch(r"[a-f0-9]{64}", chunk_hash):
339
+ raise PairDropStoreError("bad_chunk_sha256")
340
+ actual_hash = hashlib.sha256(data).hexdigest()
341
+ if actual_hash != chunk_hash:
342
+ raise PairDropStoreError("chunk_hash_mismatch")
343
+ idem = str(idempotency_key or "").strip()
344
+ if not idem or len(idem) > 160:
345
+ raise PairDropStoreError("bad_idempotency_key")
346
+
347
+ with self._connect() as conn:
348
+ self._ensure_schema(conn)
349
+ row = conn.execute("SELECT * FROM upload_sessions WHERE upload_id = ?", (upload_id,)).fetchone()
350
+ if row is None:
351
+ raise PairDropStoreError("upload_not_found")
352
+ session = self._public_upload_row(row)
353
+ self._assert_upload_source(session, source_device_id, source_install_id)
354
+ if session["state"] in {"committed", "cancelled", "expired", "failed_terminal"}:
355
+ raise PairDropStoreError("upload_not_writable")
356
+
357
+ previous = conn.execute(
358
+ "SELECT * FROM upload_chunks WHERE upload_id = ? AND idempotency_key = ?",
359
+ (upload_id, idem),
360
+ ).fetchone()
361
+ if previous is not None:
362
+ if previous["offset"] != offset or previous["byte_count"] != len(data) or previous["sha256"] != chunk_hash:
363
+ raise PairDropStoreError("idempotency_conflict")
364
+ if self._partial_range_hash(upload_id, offset, len(data)) != chunk_hash:
365
+ raise PairDropStoreError("chunk_mismatch")
366
+ return {
367
+ **session,
368
+ "idempotent": True,
369
+ "verified_offset": max(session["verified_offset"], offset + len(data)),
370
+ }
371
+
372
+ verified_offset = int(session["verified_offset"] or 0)
373
+ if offset < verified_offset:
374
+ if self._partial_range_hash(upload_id, offset, len(data)) == chunk_hash:
375
+ return {**session, "idempotent": True}
376
+ raise PairDropStoreError("chunk_mismatch")
377
+ if offset != verified_offset:
378
+ raise PairDropStoreError("unexpected_offset")
379
+ if offset + len(data) > int(session["total_byte_count"]):
380
+ raise PairDropStoreError("chunk_exceeds_total")
381
+
382
+ self._write_partial_range(upload_id, offset, data)
383
+ new_offset = offset + len(data)
384
+ now = _now_iso()
385
+ conn.execute(
386
+ """
387
+ INSERT INTO upload_chunks (
388
+ upload_id, idempotency_key, offset, byte_count, sha256, created_at
389
+ ) VALUES (?, ?, ?, ?, ?, ?)
390
+ """,
391
+ (upload_id, idem, offset, len(data), chunk_hash, now),
392
+ )
393
+ conn.execute(
394
+ """
395
+ UPDATE upload_sessions
396
+ SET verified_offset = ?, state = 'receiving', updated_at = ?, last_error = NULL
397
+ WHERE upload_id = ?
398
+ """,
399
+ (new_offset, now, upload_id),
400
+ )
401
+ self._record_event(conn, "upload_session_progress", None, {
402
+ "upload_id": upload_id,
403
+ "verified_offset": new_offset,
404
+ })
405
+ conn.commit()
406
+
407
+ self._audit("upload_session.chunk", {
408
+ "upload_id": upload_id,
409
+ "offset": offset,
410
+ "byte_count": len(data),
411
+ })
412
+ updated = self.get_upload_session(upload_id)
413
+ return {**updated, "idempotent": False}
414
+
415
+ def complete_upload_session(
416
+ self,
417
+ upload_id: str,
418
+ *,
419
+ source_device_id: str,
420
+ source_install_id: str,
421
+ ) -> dict[str, Any]:
422
+ session = self.get_upload_session(upload_id)
423
+ self._assert_upload_source(session, source_device_id, source_install_id)
424
+ if session["state"] == "committed" and session.get("file_id"):
425
+ return {"ok": True, "state": "committed", "file": self.get_file(session["file_id"])}
426
+ if session["state"] in {"cancelled", "expired", "failed_terminal"}:
427
+ raise PairDropStoreError("upload_not_completable")
428
+
429
+ partial = self._partial_path(upload_id)
430
+ if partial.is_symlink():
431
+ self._mark_upload_error(upload_id, "failed_retryable", "missing_partial")
432
+ raise PairDropStoreError("missing_partial")
433
+ if not partial.is_file():
434
+ recovered = self._recover_completed_upload_session(session)
435
+ if recovered is not None:
436
+ return recovered
437
+ self._mark_upload_error(upload_id, "failed_retryable", "missing_partial")
438
+ raise PairDropStoreError("missing_partial")
439
+ byte_size = partial.stat().st_size
440
+ if byte_size != int(session["total_byte_count"]):
441
+ self._mark_upload_error(upload_id, "failed_retryable", "byte_count_mismatch")
442
+ raise PairDropStoreError("byte_count_mismatch")
443
+ digest = self._sha256_file(partial)
444
+ if digest != session["expected_sha256"]:
445
+ self._mark_upload_error(upload_id, "failed_terminal", "sha256_mismatch")
446
+ raise PairDropStoreError("sha256_mismatch")
447
+
448
+ file_id = session["file_id"] if self._valid_id(str(session.get("file_id") or "")) else "pd_" + secrets.token_hex(16)
449
+ relpath = Path("objects") / digest[:2] / f"{file_id}.blob"
450
+ target = self.root / relpath
451
+ target.parent.mkdir(parents=True, exist_ok=True)
452
+ now = _now_iso()
453
+ with self._connect() as conn:
454
+ self._ensure_schema(conn)
455
+ conn.execute(
456
+ """
457
+ UPDATE upload_sessions
458
+ SET file_id = ?, state = 'completing', updated_at = ?, last_error = NULL
459
+ WHERE upload_id = ?
460
+ """,
461
+ (file_id, now, upload_id),
462
+ )
463
+ conn.commit()
464
+ os.replace(partial, target)
465
+ with self._connect() as conn:
466
+ self._ensure_schema(conn)
467
+ conn.execute(
468
+ """
469
+ INSERT INTO files (
470
+ id, parent_id, kind, display_name, original_name, content_type,
471
+ byte_size, sha256, storage_relpath, source_device_id,
472
+ source_install_id, source_route, created_at, updated_at,
473
+ deleted_at, last_opened_at, session_hint, tags_json
474
+ ) VALUES (?, NULL, 'file', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, '', ?)
475
+ """,
476
+ (
477
+ file_id,
478
+ session["display_name"],
479
+ session["original_name"],
480
+ session["content_type"],
481
+ byte_size,
482
+ digest,
483
+ str(relpath),
484
+ session["source_device_id"],
485
+ session["source_install_id"],
486
+ session["source_route"],
487
+ now,
488
+ now,
489
+ _json_list([]),
490
+ ),
491
+ )
492
+ conn.execute(
493
+ """
494
+ UPDATE upload_sessions
495
+ SET file_id = ?, state = 'committed', verified_offset = ?,
496
+ updated_at = ?, last_error = NULL
497
+ WHERE upload_id = ?
498
+ """,
499
+ (file_id, byte_size, now, upload_id),
500
+ )
501
+ self._record_event(conn, "created", file_id, {
502
+ "byte_size": byte_size,
503
+ "content_type": session["content_type"],
504
+ "sha256": digest,
505
+ })
506
+ self._record_event(conn, "upload_session_committed", file_id, {"upload_id": upload_id})
507
+ conn.commit()
508
+ item = self.get_file(file_id)
509
+ self._audit("upload_session.committed", {
510
+ "upload_id": upload_id,
511
+ "file_id": file_id,
512
+ "byte_size": byte_size,
513
+ "content_type": session["content_type"],
514
+ "sha256": digest,
515
+ })
516
+ return {"ok": True, "state": "committed", "upload_id": upload_id, "file": item}
517
+
518
+ def _recover_completed_upload_session(self, session: dict[str, Any]) -> dict[str, Any] | None:
519
+ upload_id = str(session.get("upload_id") or "")
520
+ expected_sha256 = str(session.get("expected_sha256") or "")
521
+ total_byte_count = int(session.get("total_byte_count") or 0)
522
+ file_id = str(session.get("file_id") or "")
523
+ candidates: list[Path] = []
524
+ if self._valid_id(file_id):
525
+ candidates.append(self.objects_dir / expected_sha256[:2] / f"{file_id}.blob")
526
+ else:
527
+ candidates.extend((self.objects_dir / expected_sha256[:2]).glob("pd_*.blob"))
528
+
529
+ for candidate in candidates:
530
+ try:
531
+ if candidate.is_symlink() or not candidate.is_file():
532
+ continue
533
+ recovered_file_id = candidate.stem
534
+ if not self._valid_id(recovered_file_id):
535
+ continue
536
+ if candidate.stat().st_size != total_byte_count:
537
+ continue
538
+ if self._sha256_file(candidate) != expected_sha256:
539
+ continue
540
+ return self._commit_recovered_upload_session(session, recovered_file_id, candidate)
541
+ except FileNotFoundError:
542
+ continue
543
+ return None
544
+
545
+ def _commit_recovered_upload_session(self, session: dict[str, Any], file_id: str, object_path: Path) -> dict[str, Any]:
546
+ upload_id = str(session["upload_id"])
547
+ byte_size = int(session["total_byte_count"])
548
+ digest = str(session["expected_sha256"])
549
+ relpath = object_path.relative_to(self.root)
550
+ now = _now_iso()
551
+ with self._connect() as conn:
552
+ self._ensure_schema(conn)
553
+ existing = conn.execute("SELECT id FROM files WHERE id = ?", (file_id,)).fetchone()
554
+ if existing is None:
555
+ conn.execute(
556
+ """
557
+ INSERT INTO files (
558
+ id, parent_id, kind, display_name, original_name, content_type,
559
+ byte_size, sha256, storage_relpath, source_device_id,
560
+ source_install_id, source_route, created_at, updated_at,
561
+ deleted_at, last_opened_at, session_hint, tags_json
562
+ ) VALUES (?, NULL, 'file', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, '', ?)
563
+ """,
564
+ (
565
+ file_id,
566
+ session["display_name"],
567
+ session["original_name"],
568
+ session["content_type"],
569
+ byte_size,
570
+ digest,
571
+ str(relpath),
572
+ session["source_device_id"],
573
+ session["source_install_id"],
574
+ session["source_route"],
575
+ now,
576
+ now,
577
+ _json_list([]),
578
+ ),
579
+ )
580
+ self._record_event(conn, "created", file_id, {
581
+ "byte_size": byte_size,
582
+ "content_type": session["content_type"],
583
+ "sha256": digest,
584
+ })
585
+ conn.execute(
586
+ """
587
+ UPDATE upload_sessions
588
+ SET file_id = ?, state = 'committed', verified_offset = ?,
589
+ updated_at = ?, last_error = NULL
590
+ WHERE upload_id = ?
591
+ """,
592
+ (file_id, byte_size, now, upload_id),
593
+ )
594
+ self._record_event(conn, "upload_session_recovered", file_id, {"upload_id": upload_id})
595
+ conn.commit()
596
+ item = self.get_file(file_id)
597
+ self._audit("upload_session.recovered", {
598
+ "upload_id": upload_id,
599
+ "file_id": file_id,
600
+ "byte_size": byte_size,
601
+ "content_type": session["content_type"],
602
+ "sha256": digest,
603
+ })
604
+ return {"ok": True, "state": "committed", "upload_id": upload_id, "file": item}
605
+
606
+ def cancel_upload_session(
607
+ self,
608
+ upload_id: str,
609
+ *,
610
+ source_device_id: str,
611
+ source_install_id: str,
612
+ ) -> dict[str, Any]:
613
+ session = self.get_upload_session(upload_id)
614
+ self._assert_upload_source(session, source_device_id, source_install_id)
615
+ if session["state"] == "committed":
616
+ raise PairDropStoreError("upload_already_committed")
617
+ now = _now_iso()
618
+ with self._connect() as conn:
619
+ self._ensure_schema(conn)
620
+ conn.execute(
621
+ "UPDATE upload_sessions SET state = 'cancelled', updated_at = ? WHERE upload_id = ?",
622
+ (now, upload_id),
623
+ )
624
+ self._record_event(conn, "upload_session_cancelled", None, {"upload_id": upload_id})
625
+ conn.commit()
626
+ self._audit("upload_session.cancelled", {"upload_id": upload_id})
627
+ return self.get_upload_session(upload_id)
628
+
629
+ def list_files(self, *, include_deleted: bool = False) -> list[dict[str, Any]]:
630
+ query = (
631
+ "SELECT * FROM files ORDER BY created_at DESC, id DESC"
632
+ if include_deleted
633
+ else "SELECT * FROM files WHERE deleted_at IS NULL ORDER BY created_at DESC, id DESC"
634
+ )
635
+ with self._connect() as conn:
636
+ self._ensure_schema(conn)
637
+ rows = conn.execute(query).fetchall()
638
+ return [self._public_row(row) for row in rows]
639
+
640
+ def get_file(self, file_id: str, *, include_deleted: bool = False) -> dict[str, Any]:
641
+ if not self._valid_id(file_id):
642
+ raise PairDropStoreError("bad_file_id")
643
+ with self._connect() as conn:
644
+ self._ensure_schema(conn)
645
+ row = conn.execute("SELECT * FROM files WHERE id = ?", (file_id,)).fetchone()
646
+ if row is None:
647
+ raise PairDropStoreError("not_found")
648
+ if row["deleted_at"] and not include_deleted:
649
+ raise PairDropStoreError("deleted")
650
+ return self._public_row(row)
651
+
652
+ def delete_file(self, file_id: str) -> dict[str, Any]:
653
+ item = self.get_file(file_id)
654
+ now = _now_iso()
655
+ with self._connect() as conn:
656
+ self._ensure_schema(conn)
657
+ conn.execute(
658
+ "UPDATE files SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL",
659
+ (now, now, file_id),
660
+ )
661
+ self._record_event(conn, "deleted", file_id, {"byte_size": item.get("byte_size", 0)})
662
+ conn.commit()
663
+ self._audit("file.deleted", {"file_id": file_id, "byte_size": item.get("byte_size", 0)})
664
+ return {"ok": True, "id": file_id, "deleted_at": now}
665
+
666
+ def attach_descriptor(self, file_id: str, *, session_id: str = "") -> dict[str, Any]:
667
+ item = self.get_file(file_id)
668
+ path = self._object_path(item)
669
+ if not path.is_file():
670
+ raise PairDropStoreError("missing_object")
671
+ now = _now_iso()
672
+ with self._connect() as conn:
673
+ self._ensure_schema(conn)
674
+ conn.execute(
675
+ "UPDATE files SET last_opened_at = ?, updated_at = ? WHERE id = ?",
676
+ (now, now, file_id),
677
+ )
678
+ self._record_event(conn, "attached", file_id, {"session": bool(session_id)})
679
+ conn.commit()
680
+ self._audit("file.attached", {"file_id": file_id, "session": bool(session_id)})
681
+ return {
682
+ "ok": True,
683
+ "id": file_id,
684
+ "display_name": item["display_name"],
685
+ "content_type": item["content_type"],
686
+ "byte_size": item["byte_size"],
687
+ "sha256": item["sha256"],
688
+ "path": str(path),
689
+ }
690
+
691
+ def download_descriptor(self, file_id: str) -> dict[str, Any]:
692
+ item = self.get_file(file_id)
693
+ path = self._object_path(item)
694
+ if path.is_symlink() or not path.is_file():
695
+ raise PairDropStoreError("missing_object")
696
+ resolved_root = self.root.resolve()
697
+ resolved_path = path.resolve()
698
+ if not str(resolved_path).startswith(str(resolved_root) + os.sep):
699
+ raise PairDropStoreError("object_escape")
700
+ stat_result = path.stat()
701
+ if int(item.get("byte_size") or 0) != stat_result.st_size:
702
+ raise PairDropStoreError("byte_size_mismatch")
703
+ now = _now_iso()
704
+ with self._connect() as conn:
705
+ self._ensure_schema(conn)
706
+ conn.execute(
707
+ "UPDATE files SET last_opened_at = ?, updated_at = ? WHERE id = ?",
708
+ (now, now, file_id),
709
+ )
710
+ self._record_event(conn, "downloaded", file_id, {"byte_size": item.get("byte_size", 0)})
711
+ conn.commit()
712
+ self._audit("file.downloaded", {"file_id": file_id, "byte_size": item.get("byte_size", 0)})
713
+ updated = self.get_file(file_id)
714
+ return {"item": updated, "path": path}
715
+
716
+ def events_since(self, seq: int = 0, limit: int = 100) -> list[dict[str, Any]]:
717
+ seq = max(0, int(seq or 0))
718
+ limit = max(1, min(int(limit or 100), 500))
719
+ with self._connect() as conn:
720
+ self._ensure_schema(conn)
721
+ rows = conn.execute(
722
+ "SELECT * FROM events WHERE seq > ? ORDER BY seq ASC LIMIT ?",
723
+ (seq, limit),
724
+ ).fetchall()
725
+ return [
726
+ {
727
+ "seq": row["seq"],
728
+ "type": row["type"],
729
+ "file_id": row["file_id"],
730
+ "created_at": row["created_at"],
731
+ "summary": json.loads(row["summary_json"] or "{}"),
732
+ }
733
+ for row in rows
734
+ ]
735
+
736
+ def cleanup_partials(self, *, older_than_seconds: int = 3600) -> dict[str, Any]:
737
+ cutoff = time.time() - max(0, older_than_seconds)
738
+ removed = 0
739
+ skipped_symlinks = 0
740
+ for path in self.partials_dir.glob("*.partial"):
741
+ try:
742
+ stat = path.lstat()
743
+ if path.is_symlink():
744
+ skipped_symlinks += 1
745
+ continue
746
+ if stat.st_mtime < cutoff:
747
+ path.unlink()
748
+ removed += 1
749
+ except FileNotFoundError:
750
+ continue
751
+ expired = 0
752
+ now = _now_iso()
753
+ with self._connect() as conn:
754
+ self._ensure_schema(conn)
755
+ rows = conn.execute(
756
+ """
757
+ SELECT upload_id FROM upload_sessions
758
+ WHERE state NOT IN ('committed', 'cancelled', 'expired')
759
+ AND expires_at <= ?
760
+ """,
761
+ (now,),
762
+ ).fetchall()
763
+ for row in rows:
764
+ conn.execute(
765
+ "UPDATE upload_sessions SET state = 'expired', updated_at = ? WHERE upload_id = ?",
766
+ (now, row["upload_id"]),
767
+ )
768
+ expired += 1
769
+ conn.commit()
770
+ self._audit("partials.cleaned", {"removed": removed, "skipped_symlinks": skipped_symlinks, "expired_sessions": expired})
771
+ return {"ok": True, "removed": removed, "skipped_symlinks": skipped_symlinks, "expired_sessions": expired}
772
+
773
+ def _object_path(self, item: dict[str, Any]) -> Path:
774
+ relpath = str(item.get("storage_relpath") or "")
775
+ if relpath.startswith("/") or ".." in Path(relpath).parts:
776
+ raise PairDropStoreError("unsafe_object_path")
777
+ path = (self.root / relpath).resolve()
778
+ root = self.root.resolve()
779
+ if root not in path.parents and path != root:
780
+ raise PairDropStoreError("unsafe_object_path")
781
+ return path
782
+
783
+ def _partial_path(self, upload_id: str) -> Path:
784
+ if not self._valid_upload_id(upload_id):
785
+ raise PairDropStoreError("bad_upload_id")
786
+ path = self.partials_dir / f"{upload_id}.partial"
787
+ parent = path.parent.resolve()
788
+ if parent != self.partials_dir.resolve():
789
+ raise PairDropStoreError("unsafe_partial_path")
790
+ return path
791
+
792
+ def _write_partial_range(self, upload_id: str, offset: int, data: bytes) -> None:
793
+ path = self._partial_path(upload_id)
794
+ path.parent.mkdir(parents=True, exist_ok=True)
795
+ flags = os.O_RDWR | os.O_CREAT
796
+ no_follow = getattr(os, "O_NOFOLLOW", 0)
797
+ if no_follow:
798
+ flags |= no_follow
799
+ fd = -1
800
+ try:
801
+ fd = os.open(str(path), flags, 0o600)
802
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
803
+ raise PairDropStoreError("unsafe_partial_path")
804
+ with os.fdopen(fd, "r+b", closefd=True) as handle:
805
+ fd = -1
806
+ handle.seek(offset)
807
+ handle.write(data)
808
+ handle.flush()
809
+ os.fsync(handle.fileno())
810
+ except OSError as exc:
811
+ if exc.errno in {errno.ELOOP, errno.EISDIR, errno.ENOTDIR}:
812
+ raise PairDropStoreError("unsafe_partial_path") from exc
813
+ raise
814
+ finally:
815
+ if fd >= 0:
816
+ os.close(fd)
817
+
818
+ def _partial_range_hash(self, upload_id: str, offset: int, byte_count: int) -> str:
819
+ path = self._partial_path(upload_id)
820
+ if path.is_symlink() or not path.is_file():
821
+ raise PairDropStoreError("missing_partial")
822
+ with path.open("rb") as handle:
823
+ handle.seek(offset)
824
+ data = handle.read(byte_count)
825
+ if len(data) != byte_count:
826
+ raise PairDropStoreError("chunk_mismatch")
827
+ return hashlib.sha256(data).hexdigest()
828
+
829
+ def _sha256_file(self, path: Path) -> str:
830
+ hasher = hashlib.sha256()
831
+ with path.open("rb") as handle:
832
+ while True:
833
+ chunk = handle.read(1024 * 1024)
834
+ if not chunk:
835
+ break
836
+ hasher.update(chunk)
837
+ return hasher.hexdigest()
838
+
839
+ def _assert_upload_source(self, session: dict[str, Any], device_id: str, install_id: str) -> None:
840
+ if session.get("source_device_id") != device_id or session.get("source_install_id") != install_id:
841
+ raise PairDropStoreError("wrong_source")
842
+
843
+ def _mark_upload_error(self, upload_id: str, state: str, error: str) -> None:
844
+ now = _now_iso()
845
+ with self._connect() as conn:
846
+ self._ensure_schema(conn)
847
+ conn.execute(
848
+ "UPDATE upload_sessions SET state = ?, last_error = ?, updated_at = ? WHERE upload_id = ?",
849
+ (state, error, now, upload_id),
850
+ )
851
+ conn.commit()
852
+
853
+ def _public_row(self, row: sqlite3.Row) -> dict[str, Any]:
854
+ return {
855
+ "id": row["id"],
856
+ "parent_id": row["parent_id"],
857
+ "kind": row["kind"],
858
+ "display_name": row["display_name"],
859
+ "content_type": row["content_type"],
860
+ "byte_size": row["byte_size"],
861
+ "sha256": row["sha256"],
862
+ "source_device_id": row["source_device_id"],
863
+ "source_install_id": row["source_install_id"],
864
+ "source_route": row["source_route"],
865
+ "created_at": row["created_at"],
866
+ "updated_at": row["updated_at"],
867
+ "deleted_at": row["deleted_at"],
868
+ "last_opened_at": row["last_opened_at"],
869
+ "session_hint": row["session_hint"],
870
+ "storage_relpath": row["storage_relpath"],
871
+ "tags": json.loads(row["tags_json"] or "[]"),
872
+ }
873
+
874
+ def _public_upload_row(self, row: sqlite3.Row) -> dict[str, Any]:
875
+ return {
876
+ "upload_id": row["upload_id"],
877
+ "file_id": row["file_id"],
878
+ "display_name": row["display_name"],
879
+ "original_name": row["original_name"],
880
+ "content_type": row["content_type"],
881
+ "total_byte_count": row["total_byte_count"],
882
+ "expected_sha256": row["expected_sha256"],
883
+ "verified_offset": row["verified_offset"],
884
+ "source_device_id": row["source_device_id"],
885
+ "source_install_id": row["source_install_id"],
886
+ "source_route": row["source_route"],
887
+ "state": row["state"],
888
+ "last_error": row["last_error"],
889
+ "created_at": row["created_at"],
890
+ "updated_at": row["updated_at"],
891
+ "expires_at": row["expires_at"],
892
+ }
893
+
894
+ def _record_event(self, conn: sqlite3.Connection, event_type: str, file_id: str | None, summary: dict[str, Any]) -> None:
895
+ conn.execute(
896
+ "INSERT INTO events (type, file_id, created_at, summary_json) VALUES (?, ?, ?, ?)",
897
+ (event_type, file_id, _now_iso(), json.dumps(summary, sort_keys=True)),
898
+ )
899
+
900
+ def _audit(self, event: str, detail: dict[str, Any]) -> None:
901
+ self.audit_path.parent.mkdir(parents=True, exist_ok=True)
902
+ safe_detail = {
903
+ key: value for key, value in detail.items()
904
+ if key not in {"path", "body", "request_body", "contents"}
905
+ }
906
+ record = {
907
+ "ts": _now_iso(),
908
+ "event": event,
909
+ "detail": safe_detail,
910
+ }
911
+ with self.audit_path.open("a", encoding="utf-8") as handle:
912
+ handle.write(json.dumps(record, sort_keys=True) + "\n")
913
+
914
+ @staticmethod
915
+ def _valid_id(file_id: str) -> bool:
916
+ return bool(re.fullmatch(r"pd_[a-f0-9]{32}", str(file_id or "")))
917
+
918
+ @staticmethod
919
+ def _valid_upload_id(upload_id: str) -> bool:
920
+ return bool(re.fullmatch(r"pu_[a-f0-9]{32}", str(upload_id or "")))