Update 1.2.2.2 beta

This commit is contained in:
MacRimi
2026-06-22 00:55:34 +02:00
parent 62fd69b147
commit 9d099ba358
4 changed files with 140 additions and 34 deletions

View File

@@ -1 +1 @@
83702ea66ef42f7c472783e3f5d9deed77299722e02af0e874901fa5db2ba57d ProxMenux-1.2.2.2-beta.AppImage
3bd1434e5ed24ab8244a6288a841615f6a126b9689069a8f46797d2d1ddbe139 ProxMenux-1.2.2.2-beta.AppImage

View File

@@ -762,7 +762,10 @@ export function HostBackup() {
onClose={() => setInspectingArchive(null)}
onDeleted={() => {
setInspectingArchive(null)
// Refresh both lists — a deleted item might have been local
// or remote (PBS/Borg). Cheap enough to revalidate both.
mutateArchives()
mutateRemoteArchives()
}}
/>
@@ -1129,21 +1132,40 @@ function InspectModal({
return downloadLocalArchive()
}
// Local archive deletion. The backend purges the .tar.zst, the
// .proxmenux.json sidecar and the matching <stem>.log so the
// /var/log/proxmenux directory stays in sync with the archive list.
// Remote archives (PBS / Borg) are not deletable from here — those
// belong to their own datastore prune policy.
const deleteLocalArchive = async () => {
if (!localArc) return
// Archive deletion — works for local, PBS and Borg backends.
// • local: DELETE /archives/<id> → removes .tar.zst + sidecar + run log.
// • pbs: DELETE /remote-archives → `proxmox-backup-client snapshot forget`.
// • borg: DELETE /remote-archives → `borg delete <repo>::<archive>`.
// PBS-protected snapshots come back as 409 with the original message so
// the operator can decide whether to lift the protection first.
const deleteArchive = async () => {
if (!archive) return
setDeletingArchive(true)
setError(null)
try {
const resp = await fetchApi<{ status: string; removed: string[] }>(
`/api/host-backups/archives/${encodeURIComponent(localArc.id)}`,
{ method: "DELETE" },
)
setArchiveDeleteResult(resp.removed || [])
let removed: string[] = []
if (archive.source === "local" && localArc) {
const resp = await fetchApi<{ status: string; removed: string[] }>(
`/api/host-backups/archives/${encodeURIComponent(localArc.id)}`,
{ method: "DELETE" },
)
removed = resp.removed || []
} else if (remoteArc) {
const resp = await fetchApi<{ status: string; removed: string }>(
`/api/host-backups/remote-archives`,
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
backend: remoteArc.backend,
repo_name: remoteArc.repo_name,
snapshot: remoteArc.snapshot,
}),
},
)
removed = resp.removed ? [resp.removed] : []
}
setArchiveDeleteResult(removed)
setShowDeleteArchiveConfirm(false)
if (onDeleted) onDeleted(); else onClose()
} catch (e) {
@@ -1387,22 +1409,26 @@ function InspectModal({
<span className="hidden sm:inline">View contents</span>
</Button>
</div>
{archive?.source === "local" && (
<Button
onClick={() => setShowDeleteArchiveConfirm(true)}
disabled={deletingArchive}
className="bg-red-500/10 border border-red-500/40 text-red-400 hover:bg-red-500/20 hover:text-red-300"
variant="outline"
title="Permanently delete this archive (.tar.zst + sidecar + log)"
>
{deletingArchive ? (
<Loader2 className="h-4 w-4 sm:mr-2 animate-spin" />
) : (
<Trash2 className="h-4 w-4 sm:mr-2" />
)}
<span className="hidden sm:inline">Delete</span>
</Button>
)}
<Button
onClick={() => setShowDeleteArchiveConfirm(true)}
disabled={deletingArchive}
className="bg-red-500/10 border border-red-500/40 text-red-400 hover:bg-red-500/20 hover:text-red-300"
variant="outline"
title={
archive?.source === "local"
? "Permanently delete this archive (.tar.zst + sidecar + log)"
: archive?.source === "pbs"
? "Permanently forget this PBS snapshot (proxmox-backup-client snapshot forget)"
: "Permanently delete this Borg archive (borg delete)"
}
>
{deletingArchive ? (
<Loader2 className="h-4 w-4 sm:mr-2 animate-spin" />
) : (
<Trash2 className="h-4 w-4 sm:mr-2" />
)}
<span className="hidden sm:inline">Delete</span>
</Button>
</div>
</DialogContent>
</Dialog>
@@ -1521,26 +1547,30 @@ function InspectModal({
<DialogHeader>
<DialogTitle className="text-base flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-red-500" />
Delete archive
Delete {backendLabel} backup
</DialogTitle>
<DialogDescription className="text-xs">
Removes the archive, its sidecar JSON and the matching run log. The action is permanent restore needs an off-host copy.
{archive?.source === "local"
? "Removes the archive, its sidecar JSON and the matching run log. The action is permanent — restore needs an off-host copy."
: archive?.source === "pbs"
? `Forgets this snapshot from the PBS repository "${remoteArc?.repo_name ?? ""}". The action is permanent — PBS GC may reclaim the underlying chunks at the next garbage-collection run.`
: `Deletes this archive from the Borg repository "${remoteArc?.repo_name ?? ""}". The action is permanent — Borg compacts the freed space at the next prune.`}
</DialogDescription>
</DialogHeader>
<div className="text-sm font-mono px-3 py-2 rounded-md border border-border bg-background/40 break-all">
{localArc?.id}
{archive?.source === "local" ? localArc?.id : remoteArc?.snapshot}
</div>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={() => setShowDeleteArchiveConfirm(false)} disabled={deletingArchive}>
Cancel
</Button>
<Button variant="destructive" onClick={deleteLocalArchive} disabled={deletingArchive}>
<Button variant="destructive" onClick={deleteArchive} disabled={deletingArchive}>
{deletingArchive ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Trash2 className="h-4 w-4 mr-2" />
)}
Delete archive
Delete
</Button>
</div>
</DialogContent>

View File

@@ -15440,6 +15440,82 @@ def api_host_backups_remote_archives():
return jsonify({'snapshots': snapshots, 'errors': errors})
@app.route('/api/host-backups/remote-archives', methods=['DELETE'])
@require_auth
def api_host_backups_remote_archive_delete():
"""Forget/delete a single PBS snapshot or Borg archive.
Body: {"backend": "pbs"|"borg", "repo_name": "...", "snapshot": "..."}
- PBS snapshot: "host/<backup-id>/<iso-ts>" → invokes
`proxmox-backup-client snapshot forget`.
- Borg archive: "<archive-name>" → invokes
`borg delete <repo>::<archive>`.
Local archives have their own DELETE endpoint
(`/api/host-backups/archives/<id>`); this one only handles remotes.
Protected PBS snapshots and missing remotes surface a clear error.
"""
payload = request.get_json(silent=True) or {}
backend = (payload.get('backend') or '').strip().lower()
repo_nm = (payload.get('repo_name') or '').strip()
snapshot = (payload.get('snapshot') or '').strip()
if backend not in ('pbs', 'borg'):
return jsonify({'error': "backend must be 'pbs' or 'borg'"}), 400
if not repo_nm:
return jsonify({'error': 'repo_name is required'}), 400
if not snapshot:
return jsonify({'error': 'snapshot is required'}), 400
if backend == 'pbs':
repo = next((r for r in _list_pbs_destinations() if r['name'] == repo_nm), None)
if not repo:
return jsonify({'error': f"PBS destination '{repo_nm}' not found"}), 404
pwd, fp = _pbs_secret_for(repo_nm)
if not pwd:
return jsonify({'error': f"no password available for PBS repo '{repo_nm}'"}), 400
env = {**os.environ, 'PBS_PASSWORD': pwd}
if fp:
env['PBS_FINGERPRINT'] = fp
try:
r = subprocess.run(
['proxmox-backup-client', 'snapshot', 'forget',
snapshot, '--repository', repo['repository']],
capture_output=True, text=True, timeout=30, env=env,
)
except (subprocess.TimeoutExpired, OSError) as e:
return jsonify({'error': str(e)}), 500
if r.returncode != 0:
msg = (r.stderr.strip() or r.stdout.strip() or
f'proxmox-backup-client exited {r.returncode}')
# Surface PBS's own "protected" error as a 409 so the UI
# can show a clearer message than the generic 500.
status = 409 if 'protected' in msg.lower() else 500
return jsonify({'error': msg}), status
return jsonify({'status': 'ok', 'removed': snapshot})
# backend == 'borg'
target = next((t for t in _list_borg_destinations() if t['name'] == repo_nm), None)
if not target:
return jsonify({'error': f"Borg destination '{repo_nm}' not found"}), 404
repo = target.get('repository') or ''
if not repo:
return jsonify({'error': "target has no repository path"}), 400
env = _borg_env_for(target)
try:
r = subprocess.run(
['borg', 'delete', f'{repo}::{snapshot}'],
capture_output=True, text=True, timeout=120, env=env,
)
except (subprocess.TimeoutExpired, OSError) as e:
return jsonify({'error': str(e)}), 500
if r.returncode != 0:
msg = (r.stderr.strip() or r.stdout.strip() or
f'borg delete exited {r.returncode}')
return jsonify({'error': msg}), 500
return jsonify({'status': 'ok', 'removed': snapshot})
# ── Borg helpers (mirrors the PBS pair above) ──────────────────────