mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-03 22:36:44 +00:00
Update 1.2.2.2 beta
This commit is contained in:
Binary file not shown.
@@ -1 +1 @@
|
|||||||
83702ea66ef42f7c472783e3f5d9deed77299722e02af0e874901fa5db2ba57d ProxMenux-1.2.2.2-beta.AppImage
|
3bd1434e5ed24ab8244a6288a841615f6a126b9689069a8f46797d2d1ddbe139 ProxMenux-1.2.2.2-beta.AppImage
|
||||||
|
|||||||
@@ -762,7 +762,10 @@ export function HostBackup() {
|
|||||||
onClose={() => setInspectingArchive(null)}
|
onClose={() => setInspectingArchive(null)}
|
||||||
onDeleted={() => {
|
onDeleted={() => {
|
||||||
setInspectingArchive(null)
|
setInspectingArchive(null)
|
||||||
|
// Refresh both lists — a deleted item might have been local
|
||||||
|
// or remote (PBS/Borg). Cheap enough to revalidate both.
|
||||||
mutateArchives()
|
mutateArchives()
|
||||||
|
mutateRemoteArchives()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1129,21 +1132,40 @@ function InspectModal({
|
|||||||
return downloadLocalArchive()
|
return downloadLocalArchive()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local archive deletion. The backend purges the .tar.zst, the
|
// Archive deletion — works for local, PBS and Borg backends.
|
||||||
// .proxmenux.json sidecar and the matching <stem>.log so the
|
// • local: DELETE /archives/<id> → removes .tar.zst + sidecar + run log.
|
||||||
// /var/log/proxmenux directory stays in sync with the archive list.
|
// • pbs: DELETE /remote-archives → `proxmox-backup-client snapshot forget`.
|
||||||
// Remote archives (PBS / Borg) are not deletable from here — those
|
// • borg: DELETE /remote-archives → `borg delete <repo>::<archive>`.
|
||||||
// belong to their own datastore prune policy.
|
// PBS-protected snapshots come back as 409 with the original message so
|
||||||
const deleteLocalArchive = async () => {
|
// the operator can decide whether to lift the protection first.
|
||||||
if (!localArc) return
|
const deleteArchive = async () => {
|
||||||
|
if (!archive) return
|
||||||
setDeletingArchive(true)
|
setDeletingArchive(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const resp = await fetchApi<{ status: string; removed: string[] }>(
|
let removed: string[] = []
|
||||||
`/api/host-backups/archives/${encodeURIComponent(localArc.id)}`,
|
if (archive.source === "local" && localArc) {
|
||||||
{ method: "DELETE" },
|
const resp = await fetchApi<{ status: string; removed: string[] }>(
|
||||||
)
|
`/api/host-backups/archives/${encodeURIComponent(localArc.id)}`,
|
||||||
setArchiveDeleteResult(resp.removed || [])
|
{ 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)
|
setShowDeleteArchiveConfirm(false)
|
||||||
if (onDeleted) onDeleted(); else onClose()
|
if (onDeleted) onDeleted(); else onClose()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1387,22 +1409,26 @@ function InspectModal({
|
|||||||
<span className="hidden sm:inline">View contents</span>
|
<span className="hidden sm:inline">View contents</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{archive?.source === "local" && (
|
<Button
|
||||||
<Button
|
onClick={() => setShowDeleteArchiveConfirm(true)}
|
||||||
onClick={() => setShowDeleteArchiveConfirm(true)}
|
disabled={deletingArchive}
|
||||||
disabled={deletingArchive}
|
className="bg-red-500/10 border border-red-500/40 text-red-400 hover:bg-red-500/20 hover:text-red-300"
|
||||||
className="bg-red-500/10 border border-red-500/40 text-red-400 hover:bg-red-500/20 hover:text-red-300"
|
variant="outline"
|
||||||
variant="outline"
|
title={
|
||||||
title="Permanently delete this archive (.tar.zst + sidecar + log)"
|
archive?.source === "local"
|
||||||
>
|
? "Permanently delete this archive (.tar.zst + sidecar + log)"
|
||||||
{deletingArchive ? (
|
: archive?.source === "pbs"
|
||||||
<Loader2 className="h-4 w-4 sm:mr-2 animate-spin" />
|
? "Permanently forget this PBS snapshot (proxmox-backup-client snapshot forget)"
|
||||||
) : (
|
: "Permanently delete this Borg archive (borg delete)"
|
||||||
<Trash2 className="h-4 w-4 sm:mr-2" />
|
}
|
||||||
)}
|
>
|
||||||
<span className="hidden sm:inline">Delete</span>
|
{deletingArchive ? (
|
||||||
</Button>
|
<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>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -1521,26 +1547,30 @@ function InspectModal({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-base flex items-center gap-2">
|
<DialogTitle className="text-base flex items-center gap-2">
|
||||||
<AlertTriangle className="h-5 w-5 text-red-500" />
|
<AlertTriangle className="h-5 w-5 text-red-500" />
|
||||||
Delete archive
|
Delete {backendLabel} backup
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs">
|
<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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="text-sm font-mono px-3 py-2 rounded-md border border-border bg-background/40 break-all">
|
<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>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button variant="ghost" onClick={() => setShowDeleteArchiveConfirm(false)} disabled={deletingArchive}>
|
<Button variant="ghost" onClick={() => setShowDeleteArchiveConfirm(false)} disabled={deletingArchive}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" onClick={deleteLocalArchive} disabled={deletingArchive}>
|
<Button variant="destructive" onClick={deleteArchive} disabled={deletingArchive}>
|
||||||
{deletingArchive ? (
|
{deletingArchive ? (
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
)}
|
)}
|
||||||
Delete archive
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -15440,6 +15440,82 @@ def api_host_backups_remote_archives():
|
|||||||
return jsonify({'snapshots': snapshots, 'errors': errors})
|
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) ──────────────────────
|
# ── Borg helpers (mirrors the PBS pair above) ──────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user