From 8dad22b55fb7b695ac5ff2b845547c18cad545c6 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sun, 22 Mar 2026 11:22:30 +0000 Subject: [PATCH] Initial upload: snippet-manager with CI/CD workflow --- snip/sync/protocol.py | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 snip/sync/protocol.py diff --git a/snip/sync/protocol.py b/snip/sync/protocol.py new file mode 100644 index 0000000..5f61cd2 --- /dev/null +++ b/snip/sync/protocol.py @@ -0,0 +1,110 @@ +"""HTTP-based P2P sync protocol for snippets.""" + +import http.server +import json +import socketserver +import threading +from datetime import datetime +from typing import Any +from urllib.request import urlopen + +from snip.db.database import Database + + +class SyncRequestHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path.startswith("/snippets"): + since = self.headers.get("X-Since", "1970-01-01T00:00:00") + snippets = self.server.db.list_snippets(limit=10000) + snippets = [s for s in snippets if s["updated_at"] > since] + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(snippets).encode()) + else: + self.send_response(404) + self.end_headers() + + def do_POST(self): + if self.path == "/snippets": + content_length = int(self.headers["Content-Length"]) + data = json.loads(self.rfile.read(content_length)) + for snippet in data: + self.server.db.import_snippet(snippet, strategy="duplicate") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"status": "ok"}).encode()) + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + pass + + +class SyncServer(socketserver.TCPServer): + allow_reuse_address = True + + def __init__(self, port: int, db: Database): + self.db = db + super().__init__(("", port), SyncRequestHandler) + + +class SyncProtocol: + def __init__(self, db: Database, port: int = 8765): + self.db = db + self.port = port + self.server = None + self.server_thread = None + + def start_server(self): + """Start the sync server in a background thread.""" + self.server = SyncServer(self.port, self.db) + self.server_thread = threading.Thread(target=self.server.serve_forever) + self.server_thread.daemon = True + self.server_thread.start() + + def stop_server(self): + """Stop the sync server.""" + if self.server: + self.server.shutdown() + self.server = None + + def sync_with_peer(self, host: str, port: int) -> int: + """Sync snippets with a peer.""" + snippets = [] + synced = 0 + + try: + with urlopen(f"http://{host}:{port}/snippets", timeout=30) as response: + snippets = json.loads(response.read()) + except Exception: + pass + + for snippet in snippets: + if "id" in snippet: + del snippet["id"] + self.db.import_snippet(snippet, strategy="skip") + synced += 1 + + return synced + + def push_to_peer(self, host: str, port: int) -> int: + """Push local snippets to a peer.""" + snippets = self.db.export_all() + pushed = 0 + + try: + req = urllib.request.Request( + f"http://{host}:{port}/snippets", + data=json.dumps(snippets).encode(), + headers={"Content-Type": "application/json"}, + ) + with urlopen(req, timeout=30) as response: + if response.status == 200: + pushed = len(snippets) + except Exception: + pass + + return pushed