Spaces:
Running
Running
feat(sync): skip backup when no state changes detected
Browse filesAdd pg_stat_database activity marker + filesystem markers for uploads,
secrets, and .next BUILD_ID. State persisted to /tmp; backup skipped
when markers match the last successful upload's snapshot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- postiz-sync.py +90 -0
postiz-sync.py
CHANGED
|
@@ -52,6 +52,7 @@ UPLOADS_DIR = Path(os.environ.get("UPLOAD_DIRECTORY", str(POSTIZ_HOME / "uploads
|
|
| 52 |
SECRETS_DIR = POSTIZ_HOME / ".secrets"
|
| 53 |
NEXT_DIR = Path("/app/apps/frontend/.next") # compiled frontend; backed up to skip rebuild
|
| 54 |
STATUS_FILE = Path("/tmp/sync-status.json")
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
# ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -355,9 +356,96 @@ def download_and_restore() -> bool | None:
|
|
| 355 |
return False
|
| 356 |
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
# ββ Public CLI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 359 |
def cmd_sync() -> bool:
|
| 360 |
logger.info("Syncing backup to HF Dataset...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
status = read_status()
|
| 362 |
try:
|
| 363 |
dump, ok = backup_database()
|
|
@@ -375,6 +463,8 @@ def cmd_sync() -> bool:
|
|
| 375 |
status["sync_count"] = status.get("sync_count", 0) + 1
|
| 376 |
write_status(status)
|
| 377 |
logger.info("Backup synced OK" if ok else "Backup sync failed")
|
|
|
|
|
|
|
| 378 |
return ok
|
| 379 |
except Exception as e:
|
| 380 |
logger.error(f"Backup operation failed: {e}")
|
|
|
|
| 52 |
SECRETS_DIR = POSTIZ_HOME / ".secrets"
|
| 53 |
NEXT_DIR = Path("/app/apps/frontend/.next") # compiled frontend; backed up to skip rebuild
|
| 54 |
STATUS_FILE = Path("/tmp/sync-status.json")
|
| 55 |
+
STATE_FILE = Path("/tmp/huggingpost-sync-state.json")
|
| 56 |
|
| 57 |
|
| 58 |
# ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 356 |
return False
|
| 357 |
|
| 358 |
|
| 359 |
+
# ββ Change detection helpers ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 360 |
+
def _get_db_marker() -> int:
|
| 361 |
+
"""Return cumulative DB activity count from pg_stat_database. -1 on error."""
|
| 362 |
+
db = parse_db_url(DATABASE_URL)
|
| 363 |
+
if not db:
|
| 364 |
+
return -1
|
| 365 |
+
try:
|
| 366 |
+
db_name = db["database"]
|
| 367 |
+
result = subprocess.run(
|
| 368 |
+
[
|
| 369 |
+
"psql",
|
| 370 |
+
f"--host={db['host']}",
|
| 371 |
+
f"--port={db['port']}",
|
| 372 |
+
f"--username={db['user']}",
|
| 373 |
+
"--no-password", "--tuples-only", "--no-align",
|
| 374 |
+
"-c",
|
| 375 |
+
f"SELECT xact_commit + xact_rollback + tup_inserted + tup_updated + tup_deleted "
|
| 376 |
+
f"FROM pg_stat_database WHERE datname = '{db_name}'",
|
| 377 |
+
],
|
| 378 |
+
env=env_with_password(db), capture_output=True, text=True, timeout=10,
|
| 379 |
+
)
|
| 380 |
+
if result.returncode == 0 and result.stdout.strip():
|
| 381 |
+
return int(result.stdout.strip())
|
| 382 |
+
except Exception:
|
| 383 |
+
pass
|
| 384 |
+
return -1
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def _fs_marker(root: Path) -> tuple[int, int, int]:
|
| 388 |
+
if not root.exists():
|
| 389 |
+
return (0, 0, 0)
|
| 390 |
+
fc = ts = nm = 0
|
| 391 |
+
for path in root.rglob("*"):
|
| 392 |
+
if not path.is_file():
|
| 393 |
+
continue
|
| 394 |
+
try:
|
| 395 |
+
st = path.stat()
|
| 396 |
+
fc += 1
|
| 397 |
+
ts += int(st.st_size)
|
| 398 |
+
nm = max(nm, int(st.st_mtime_ns))
|
| 399 |
+
except OSError:
|
| 400 |
+
continue
|
| 401 |
+
return (fc, ts, nm)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def _next_marker() -> int:
|
| 405 |
+
build_id = NEXT_DIR / "BUILD_ID"
|
| 406 |
+
try:
|
| 407 |
+
return int(build_id.stat().st_mtime_ns) if build_id.exists() else 0
|
| 408 |
+
except OSError:
|
| 409 |
+
return 0
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def _current_marker() -> tuple:
|
| 413 |
+
return (_get_db_marker(), *_fs_marker(UPLOADS_DIR), *_fs_marker(SECRETS_DIR), _next_marker())
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _load_sync_state():
|
| 417 |
+
try:
|
| 418 |
+
if STATE_FILE.exists():
|
| 419 |
+
d = json.loads(STATE_FILE.read_text())
|
| 420 |
+
m = d.get("marker")
|
| 421 |
+
if m and len(m) == 8:
|
| 422 |
+
return tuple(m)
|
| 423 |
+
except Exception:
|
| 424 |
+
pass
|
| 425 |
+
return None
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def _save_sync_state(marker: tuple) -> None:
|
| 429 |
+
try:
|
| 430 |
+
STATE_FILE.write_text(json.dumps({"marker": list(marker)}))
|
| 431 |
+
except Exception as e:
|
| 432 |
+
logger.debug(f"Could not save sync state: {e}")
|
| 433 |
+
|
| 434 |
+
|
| 435 |
# ββ Public CLI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 436 |
def cmd_sync() -> bool:
|
| 437 |
logger.info("Syncing backup to HF Dataset...")
|
| 438 |
+
|
| 439 |
+
last_marker = _load_sync_state()
|
| 440 |
+
current_marker = _current_marker()
|
| 441 |
+
if last_marker is not None and current_marker == last_marker:
|
| 442 |
+
status = read_status()
|
| 443 |
+
status["status"] = "synced"
|
| 444 |
+
status["message"] = "No state changes detected."
|
| 445 |
+
write_status(status)
|
| 446 |
+
logger.info("No state changes detected β skipping backup.")
|
| 447 |
+
return True
|
| 448 |
+
|
| 449 |
status = read_status()
|
| 450 |
try:
|
| 451 |
dump, ok = backup_database()
|
|
|
|
| 463 |
status["sync_count"] = status.get("sync_count", 0) + 1
|
| 464 |
write_status(status)
|
| 465 |
logger.info("Backup synced OK" if ok else "Backup sync failed")
|
| 466 |
+
if ok:
|
| 467 |
+
_save_sync_state(current_marker)
|
| 468 |
return ok
|
| 469 |
except Exception as e:
|
| 470 |
logger.error(f"Backup operation failed: {e}")
|