Art4Hotel Hub: código + documentación extensiva
ERP a la medida (Python stdlib + SQLite + vanilla JS SPA). Incluye server.py, index.html, utilidades y documentación: README, MODELO_DATOS, API, INSTALACION, CONTEXTO, NEGOCIO, WEB, ONBOARDING, VALOR_SISTEMA, CLAUDE. Secretos y datos (art4hotel.db, secret.key, ACCESOS.html, uploads/, backups/) excluidos vía .gitignore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
90
backup.py
Normal file
90
backup.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Art4Hotel Hub — Backup diario
|
||||
Crea snapshot del DB + uploads, conserva últimos N días.
|
||||
Corre cada noche via cron.
|
||||
"""
|
||||
import sqlite3, shutil, datetime, tarfile, sys
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path("/mnt/iclaude/art4hotel-hub")
|
||||
DB = BASE / "art4hotel.db"
|
||||
UPLOADS = BASE / "uploads"
|
||||
BACKUPS = BASE / "backups"
|
||||
KEEP_DAYS = 30
|
||||
|
||||
|
||||
def log(msg):
|
||||
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[{ts}] {msg}", flush=True)
|
||||
|
||||
|
||||
def backup_db(out):
|
||||
"""Use SQLite online backup (safe with WAL, doesn't lock writers)."""
|
||||
src = sqlite3.connect(str(DB))
|
||||
dst = sqlite3.connect(str(out))
|
||||
with dst:
|
||||
src.backup(dst)
|
||||
src.close()
|
||||
dst.close()
|
||||
|
||||
|
||||
def backup_uploads(out_tar):
|
||||
if not UPLOADS.exists():
|
||||
return 0
|
||||
n = 0
|
||||
with tarfile.open(str(out_tar), "w:gz") as tar:
|
||||
for p in UPLOADS.rglob("*"):
|
||||
if p.is_file():
|
||||
tar.add(str(p), arcname=str(p.relative_to(UPLOADS.parent)))
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def prune_old():
|
||||
cutoff = datetime.datetime.now() - datetime.timedelta(days=KEEP_DAYS)
|
||||
removed = 0
|
||||
for d in BACKUPS.iterdir():
|
||||
if not d.is_dir():
|
||||
continue
|
||||
try:
|
||||
date_part = d.name.split("_")[0]
|
||||
d_date = datetime.datetime.strptime(date_part, "%Y-%m-%d")
|
||||
if d_date < cutoff:
|
||||
shutil.rmtree(d)
|
||||
removed += 1
|
||||
except Exception:
|
||||
pass
|
||||
return removed
|
||||
|
||||
|
||||
def main():
|
||||
BACKUPS.mkdir(exist_ok=True)
|
||||
stamp = datetime.datetime.now().strftime("%Y-%m-%d_%H%M")
|
||||
out_dir = BACKUPS / stamp
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
|
||||
log(f"Backup → {out_dir}")
|
||||
try:
|
||||
backup_db(out_dir / "art4hotel.db")
|
||||
log(f" DB OK ({(out_dir/'art4hotel.db').stat().st_size//1024} KB)")
|
||||
except Exception as e:
|
||||
log(f" DB ERROR: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
n = backup_uploads(out_dir / "uploads.tar.gz")
|
||||
if (out_dir / "uploads.tar.gz").exists():
|
||||
log(f" uploads OK ({n} archivos, {(out_dir/'uploads.tar.gz').stat().st_size//1024} KB)")
|
||||
except Exception as e:
|
||||
log(f" uploads ERROR: {e}")
|
||||
|
||||
removed = prune_old()
|
||||
if removed:
|
||||
log(f" Eliminados {removed} backup(s) > {KEEP_DAYS} días")
|
||||
|
||||
log("Listo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user