import logging import threading from pathlib import Path import config import db import extractor import sftp as sftp_module from uploader import CalibreClient log = logging.getLogger(__name__) _lock = threading.Lock() _running = False def is_running() -> bool: return _running def run_sync() -> None: global _running if not _lock.acquire(blocking=False): log.warning("Sync already running, skipping") return _running = True run_id = db.start_sync_run() counters = dict(zips_found=0, zips_new=0, books_uploaded=0, books_skipped=0, books_errored=0) try: cfg = config.load() _validate_config(cfg) work_dir = Path(cfg.local_work_dir) work_dir.mkdir(parents=True, exist_ok=True) log.info("Listing remote zips at %s@%s:%s", cfg.sftp.user, cfg.sftp.host, cfg.sftp.remote_path) new_zips = sftp_module.list_new_zips(cfg.sftp) counters["zips_found"] = len(new_zips) counters["zips_new"] = len(new_zips) client = CalibreClient(cfg.calibre) for remote_zip in new_zips: zip_status = "success" zip_error = None local_zip = None try: local_zip = sftp_module.download(cfg.sftp, remote_zip, str(work_dir / "downloads")) books = extractor.extract(local_zip, work_dir / "extracted") for book in books: status = client.upload(book, zip_source=remote_zip.remote_path) if status == "uploaded": counters["books_uploaded"] += 1 elif status == "skipped_duplicate": counters["books_skipped"] += 1 else: counters["books_errored"] += 1 extractor.cleanup(work_dir / "extracted" / local_zip.stem) except Exception as e: log.error("Error processing %s: %s", remote_zip.remote_path, e) zip_status = "error" zip_error = str(e) finally: if local_zip and local_zip.exists(): extractor.cleanup(local_zip) db.mark_zip_processed(remote_zip.remote_path, remote_zip.file_size, zip_status, zip_error) db.finish_sync_run(run_id, status="success", **counters) log.info( "Sync done. Zips: %d, Uploaded: %d, Skipped: %d, Errors: %d", counters["zips_new"], counters["books_uploaded"], counters["books_skipped"], counters["books_errored"], ) except Exception as e: log.exception("Sync run failed: %s", e) db.finish_sync_run(run_id, status="error", error_msg=str(e), **counters) finally: _running = False _lock.release() def _validate_config(cfg) -> None: missing = [] if not cfg.sftp.host: missing.append("SFTP host") if not cfg.sftp.user: missing.append("SFTP user") if not cfg.sftp.remote_path: missing.append("SFTP remote path") if cfg.sftp.auth_method == "key" and not cfg.sftp.key: missing.append("SSH private key") if cfg.sftp.auth_method == "password" and not cfg.sftp.password: missing.append("SSH password") if not cfg.calibre.url: missing.append("Calibre-Web URL") if not cfg.calibre.user: missing.append("Calibre-Web username") if missing: raise ValueError(f"Missing configuration: {', '.join(missing)}")