Add backup shrinking functionality to eMMC provisioning dashboard: implement API for shrinking raw .img backups using PiShrink, update UI to support shrink option after backup, and enhance documentation for backup image handling and storage options on Proxmox host.
This commit is contained in:
@@ -8,6 +8,7 @@ Supports USB boot mode (ask Backup/Deploy) and network-booted devices (register,
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -114,7 +115,7 @@ def _safe_backup_name(name):
|
||||
"""Reject path traversal and ensure it's a backup filename we manage."""
|
||||
if not name or ".." in name or "/" in name or "\\" in name:
|
||||
return False
|
||||
if not (name.endswith(".img") or name.endswith(".img.gz")):
|
||||
if not name.endswith((".img", ".img.gz", ".img.xz")):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -125,7 +126,7 @@ def list_backups():
|
||||
meta = _load_backups_meta()
|
||||
out = []
|
||||
for p in sorted(BACKUPS_DIR.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True):
|
||||
if p.is_file() and p.suffix in (".img", ".img.gz") and p.name != "backups_meta.json":
|
||||
if p.is_file() and p.name != "backups_meta.json" and p.name.endswith((".img", ".img.gz", ".img.xz")):
|
||||
try:
|
||||
st = p.stat()
|
||||
m = meta.get(p.name, {})
|
||||
@@ -195,6 +196,12 @@ def api_device_action():
|
||||
if source == "usb":
|
||||
try:
|
||||
os.makedirs(os.path.dirname(ACTION_REQUEST_FILE) or ".", exist_ok=True)
|
||||
# If user requested "shrink after backup", create flag so host runs PiShrink after dd
|
||||
if action == "backup" and body.get("shrink"):
|
||||
try:
|
||||
(BASE_DIR / "shrink_next_backup").write_text("1")
|
||||
except (PermissionError, OSError):
|
||||
pass # host may still have SHRINK_BACKUP=1
|
||||
with open(ACTION_REQUEST_FILE, "w") as f:
|
||||
f.write(action)
|
||||
return jsonify({"ok": True})
|
||||
@@ -314,6 +321,40 @@ def api_backup_set_as_golden(name):
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/backups/<path:name>/shrink", methods=["POST"])
|
||||
def api_backup_shrink(name):
|
||||
"""Run PiShrink on a raw .img backup (shrinks in place). Requires PiShrink in LXC/host."""
|
||||
if not _safe_backup_name(name):
|
||||
return jsonify({"ok": False, "error": "invalid backup name"}), 400
|
||||
if not name.endswith(".img") or name.endswith(".img.gz") or name.endswith(".img.xz"):
|
||||
return jsonify({"ok": False, "error": "only raw .img files can be shrunk (not .img.gz / .img.xz)"}), 400
|
||||
path = BACKUPS_DIR / name
|
||||
if not path.is_file():
|
||||
return jsonify({"ok": False, "error": "backup not found"}), 404
|
||||
pishrink = shutil.which("pishrink.sh") or "/usr/local/bin/pishrink.sh"
|
||||
if not pishrink or not os.path.isfile(pishrink):
|
||||
return jsonify({"ok": False, "error": "PiShrink not installed. Install on the host/LXC (e.g. scripts/install-pishrink-on-host.sh)"}), 503
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[pishrink, "-n", name],
|
||||
cwd=str(BACKUPS_DIR),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=3600,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return jsonify({
|
||||
"ok": False,
|
||||
"error": "PiShrink failed",
|
||||
"detail": (proc.stderr or proc.stdout or "").strip() or f"exit code {proc.returncode}",
|
||||
}), 500
|
||||
return jsonify({"ok": True, "message": f"Shrunk {name}"})
|
||||
except subprocess.TimeoutExpired:
|
||||
return jsonify({"ok": False, "error": "PiShrink timed out"}), 504
|
||||
except OSError as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/backups/<path:name>", methods=["PATCH"])
|
||||
def api_backup_update(name):
|
||||
"""Update backup metadata (display name, description) or rename the file."""
|
||||
|
||||
Reference in New Issue
Block a user