Files
2026-05-14 22:53:33 +02:00

193 lines
5.3 KiB
Python

import logging
from contextlib import asynccontextmanager
from pathlib import Path
from apscheduler.schedulers.background import BackgroundScheduler
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
import config
import db
import grimmory as grimmory_module
import sftp as sftp_module
import sync
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s%(message)s")
log = logging.getLogger(__name__)
_scheduler = BackgroundScheduler(timezone="UTC")
def _reschedule_auto_sync() -> None:
_scheduler.remove_all_jobs()
try:
interval = int(db.get_setting("scheduler_interval_minutes", "0") or "0")
except ValueError:
interval = 0
if interval > 0:
_scheduler.add_job(sync.run_sync, "interval", minutes=interval, id="auto_sync")
log.info("Auto-sync scheduled every %d minute(s)", interval)
else:
log.info("Auto-sync disabled")
def next_run_time() -> str | None:
job = _scheduler.get_job("auto_sync")
if job and job.next_run_time:
return job.next_run_time.strftime("%Y-%m-%d %H:%M UTC")
return None
@asynccontextmanager
async def lifespan(app: FastAPI):
db.init_db()
_scheduler.start()
_reschedule_auto_sync()
yield
_scheduler.shutdown(wait=False)
app = FastAPI(title="GrimmorySync", lifespan=lifespan)
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
# --- SPA root ---
@app.get("/", include_in_schema=False)
async def spa_root():
return FileResponse(Path(__file__).parent / "static" / "index.html")
# --- Dashboard data ---
@app.get("/api/dashboard")
async def api_dashboard():
interval = int(db.get_setting("scheduler_interval_minutes", "0") or "0")
return {
"stats": db.get_stats(),
"runs": [dict(r) for r in db.get_recent_runs(10)],
"zips": [dict(z) for z in db.get_recent_zips(20)],
"books": [dict(b) for b in db.get_recent_books(20)],
"sync_running": sync.is_running(),
"next_run": next_run_time(),
"cache_info": db.get_remote_cache_info(),
"interval": interval,
}
# --- Settings ---
@app.get("/api/settings")
async def api_get_settings():
s = db.get_all_settings()
key_pem = s.get("sftp_key", "")
return {
"settings": s,
"has_key": bool(key_pem.strip()),
"key_fingerprint": sftp_module.get_key_fingerprint(key_pem),
}
class SettingsPayload(BaseModel):
sftp_host: str = ""
sftp_port: str = "22"
sftp_user: str = ""
sftp_auth_method: str = "key"
sftp_key: str = ""
sftp_password: str = ""
sftp_remote_path: str = ""
grimmory_url: str = ""
grimmory_user: str = ""
grimmory_password: str = ""
grimmory_bookdrop_path: str = ""
work_dir: str = "/tmp/grimmorysync"
local_import_path: str = ""
scheduler_interval_minutes: str = "0"
sync_batch_size: str = "0"
@app.post("/api/settings")
async def api_save_settings(payload: SettingsPayload):
config.save(payload.model_dump())
_reschedule_auto_sync()
return {"ok": True}
# --- Sync triggers ---
@app.post("/api/sync")
async def api_trigger_sync(background_tasks: BackgroundTasks):
if sync.is_running():
return {"ok": False, "reason": "already_running"}
background_tasks.add_task(sync.run_sync)
return {"ok": True}
@app.post("/api/sync/test")
async def api_trigger_test_sync(background_tasks: BackgroundTasks):
if sync.is_running():
return {"ok": False, "reason": "already_running"}
background_tasks.add_task(sync.run_sync, 1)
return {"ok": True}
@app.post("/api/sync/rescan")
async def api_trigger_rescan(background_tasks: BackgroundTasks):
if sync.is_running():
return {"ok": False, "reason": "already_running"}
cfg = config.load()
background_tasks.add_task(sftp_module.refresh_remote_zip_cache, cfg.sftp)
return {"ok": True}
# --- Local folder import ---
@app.post("/api/import/local")
async def api_local_import(background_tasks: BackgroundTasks):
if sync.is_running():
return {"ok": False, "reason": "already_running"}
folder = db.get_setting("local_import_path", "")
if not folder:
return {"ok": False, "reason": "no_path_configured"}
background_tasks.add_task(sync.run_local_import, folder)
return {"ok": True}
# --- Connection tests ---
@app.get("/api/test/ssh")
async def test_ssh():
cfg = config.load()
ok, message = sftp_module.test_connection(cfg.sftp)
return {"ok": ok, "message": message}
@app.get("/api/test/grimmory")
async def test_grimmory():
cfg = config.load()
ok, message = grimmory_module.test_connection(cfg.grimmory.url, cfg.grimmory.user, cfg.grimmory.password)
return {"ok": ok, "message": message}
# --- Data reset ---
@app.post("/api/settings/reset-sync-data")
async def api_reset_sync_data():
counts = db.clear_sync_data()
log.info("Sync data cleared: %s", counts)
return {"ok": True, "counts": counts}
# --- JSON status API ---
@app.get("/api/status")
async def api_status():
return {
"sync_running": sync.is_running(),
"next_run": next_run_time(),
"stats": db.get_stats(),
"last_run": [dict(r) for r in db.get_recent_runs(1)],
}