Add GPS-only backup/restore and database wipe
- Fix backup.py PRESERVED_FIELDS column names (lat/lng -> latitude/longitude) to match actual DB schema (pre-existing bug) - Add backup_gps() and restore_gps() dedicated GPS-only functions - Add /api/gps/backup and /api/gps/restore API endpoints - Add /api/gps/restore/upload for restoring from uploaded backup files - Add /api/db/wipe endpoint to clear all asset records - Update UI with GPS-only Backup button, Restore GPS from File picker, and Wipe All Assets with double-confirmation dialog - Fix backup listing to scan both seed-data/ and project root
This commit is contained in:
+103
-2
@@ -43,8 +43,16 @@ from backup import backup as do_backup, compare as compare_backup
|
||||
|
||||
|
||||
def _list_backup_files() -> list[dict]:
|
||||
"""List backup files in the seed-data directory."""
|
||||
files = sorted(glob.glob(str(PROJECT_ROOT / "seed-data" / "backup_*.json.gz")), reverse=True)
|
||||
"""List backup files in the seed-data directory OR project root."""
|
||||
patterns = [
|
||||
str(PROJECT_ROOT / "seed-data" / "backup_*.json.gz"),
|
||||
str(PROJECT_ROOT / "backup_*.json.gz"),
|
||||
]
|
||||
files = set()
|
||||
for pat in patterns:
|
||||
for f in glob.glob(pat):
|
||||
files.add(f)
|
||||
files = sorted(files, reverse=True)
|
||||
result = []
|
||||
for f in files:
|
||||
try:
|
||||
@@ -446,6 +454,99 @@ async def api_restore(
|
||||
raise HTTPException(500, f"Restore error: {e}")
|
||||
|
||||
|
||||
# ─── GPS-only endpoints ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/api/gps/backup")
|
||||
async def api_gps_backup(db_path: str = Query(DEFAULT_DB_PATH)):
|
||||
"""Backup GPS data only (latitude, longitude) — ideal before wiping."""
|
||||
if not os.path.exists(db_path):
|
||||
raise HTTPException(404, f"Database not found: {db_path}")
|
||||
try:
|
||||
from backup import backup_gps as do_backup_gps
|
||||
path = do_backup_gps(db_path)
|
||||
_session_state["last_backup"] = path
|
||||
return {"backup_path": path, "status": "ok", "type": "gps_only"}
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise HTTPException(500, f"GPS backup error: {e}")
|
||||
|
||||
|
||||
@app.post("/api/gps/restore")
|
||||
async def api_gps_restore(
|
||||
backup_path: str = Query(...),
|
||||
db_path: str = Query(DEFAULT_DB_PATH),
|
||||
):
|
||||
"""Restore GPS data only (latitude, longitude) from a GPS-only backup."""
|
||||
if not os.path.exists(backup_path):
|
||||
raise HTTPException(404, f"Backup not found: {backup_path}")
|
||||
try:
|
||||
from backup import restore_gps as do_restore_gps
|
||||
do_restore_gps(backup_path, db_path)
|
||||
return {"status": "ok", "restored_from": os.path.basename(backup_path), "db": db_path}
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise HTTPException(500, f"GPS restore error: {e}")
|
||||
|
||||
|
||||
@app.post("/api/gps/restore/upload")
|
||||
async def api_gps_restore_upload(
|
||||
backup_file: UploadFile = File(...),
|
||||
db_path: str = Query(DEFAULT_DB_PATH),
|
||||
):
|
||||
"""Upload a GPS backup file and restore it into the database."""
|
||||
if not backup_file.filename:
|
||||
raise HTTPException(400, "No file provided")
|
||||
suffix = Path(backup_file.filename).suffix.lower()
|
||||
if suffix not in (".gz", ".json"):
|
||||
raise HTTPException(400, f"Unsupported file type: {suffix}. Upload .json.gz or .json")
|
||||
try:
|
||||
# Save to temp
|
||||
import tempfile
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".json.gz")
|
||||
content = await backup_file.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
tmp.close()
|
||||
|
||||
from backup import restore_gps as do_restore_gps
|
||||
do_restore_gps(tmp_path, db_path)
|
||||
|
||||
os.unlink(tmp_path)
|
||||
|
||||
return {"status": "ok", "restored_from": backup_file.filename, "db": db_path}
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise HTTPException(500, f"GPS restore error: {e}")
|
||||
|
||||
|
||||
# ─── Database management ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.post("/api/db/wipe")
|
||||
async def api_db_wipe(db_path: str = Query(DEFAULT_DB_PATH)):
|
||||
"""Wipe all asset records from the database. Does NOT delete the file."""
|
||||
if not os.path.exists(db_path):
|
||||
raise HTTPException(404, f"Database not found: {db_path}")
|
||||
try:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(db_path)
|
||||
cur = conn.execute("SELECT COUNT(*) FROM assets")
|
||||
before = cur.fetchone()[0]
|
||||
conn.execute("DELETE FROM assets")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {
|
||||
"status": "ok",
|
||||
"records_deleted": before,
|
||||
"db_path": db_path,
|
||||
"note": "All asset records wiped. Database file preserved.",
|
||||
}
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise HTTPException(500, f"Wipe error: {e}")
|
||||
|
||||
|
||||
# ─── Static files ─────────────────────────────────────────────────────────────
|
||||
|
||||
STATIC_DIR = Path(__file__).parent / "static"
|
||||
|
||||
Reference in New Issue
Block a user