mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-28 11:28:24 +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)}
|
||||
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>
|
||||
|
||||
@@ -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) ──────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user