somratpro Claude Sonnet 4.6 commited on
Commit
a9c0f66
Β·
1 Parent(s): 83c8430

feat(sync): skip backup when no state changes detected

Browse files

Add 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>

Files changed (1) hide show
  1. 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}")