Let a storage return to the Proxmox default retention and match the job editor labels

- An empty policy removes the storage setting with --delete prune-backups, and a storage without a policy opens with that choice
- Retention fields use the Keep last, Keep daily... labels of the backup job editor
- A retention count of zero is rejected before calling Proxmox
- Spanish uses backups for the retention messages
This commit is contained in:
MacRimi
2026-10-16 23:34:21 +02:00
parent f7220826a6
commit f336eb65f0
5 changed files with 32 additions and 18 deletions
@@ -23,6 +23,7 @@ export function BackupStorageRetention() {
const [editing, setEditing] = useState<StorageRetention | null>(null)
const [fields, setFields] = useState<Record<string, string>>({})
const [keepAll, setKeepAll] = useState(false)
const [useDefault, setUseDefault] = useState(false)
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState("")
const [saveError, setSaveError] = useState("")
@@ -30,7 +31,8 @@ export function BackupStorageRetention() {
function edit(storage: StorageRetention) {
const policy = parseRetention(storage.prune_backups)
setFields(policy.fields)
setKeepAll(policy.keepAll || !storage.prune_backups)
setKeepAll(policy.keepAll)
setUseDefault(!storage.prune_backups)
setSaveError("")
setMessage("")
setEditing(storage)
@@ -38,8 +40,8 @@ export function BackupStorageRetention() {
async function save() {
if (!editing || busy) return
const prune = buildRetention(fields, keepAll)
if (!prune) return setSaveError(t("backup.storageRetention.choosePolicy"))
const prune = useDefault ? "" : buildRetention(fields, keepAll)
if (!useDefault && !prune) return setSaveError(t("backup.storageRetention.choosePolicy"))
setBusy(true)
setSaveError("")
try {
@@ -79,14 +81,18 @@ export function BackupStorageRetention() {
<DialogContent className="bg-accent [&_input]:bg-background">
<DialogHeader><DialogTitle>{t("backup.storageRetention.editTitle", { storage: editing?.id ?? "" })}</DialogTitle>
<DialogDescription>{t("backup.storageRetention.effect")}</DialogDescription></DialogHeader>
<Label className="flex items-center gap-2"><Checkbox checked={keepAll} disabled={busy} onCheckedChange={checked => {
<Label className="flex items-center gap-2"><Checkbox checked={useDefault} disabled={busy} onCheckedChange={checked => {
setUseDefault(checked === true)
if (checked) { setKeepAll(false); setFields({}) }
}} />{t("backup.storageRetention.useDefault")}</Label>
<Label className="flex items-center gap-2"><Checkbox checked={keepAll} disabled={useDefault || busy} onCheckedChange={checked => {
setKeepAll(checked === true)
if (checked) setFields({})
}} />{t("backup.storageRetention.keepAll")}</Label>
<div className="grid grid-cols-2 gap-3">
{RETENTION.map(key => <div key={key} className="space-y-1">
<Label htmlFor={`storage-retention-${key}`}>{t(`backup.retention.${key}`)}</Label>
<Input id={`storage-retention-${key}`} type="number" min={1} max={99999} step={1} disabled={keepAll || busy}
<Label htmlFor={`storage-retention-${key}`}>{t(`backup.retention.keep${key[0].toUpperCase()}${key.slice(1)}`)}</Label>
<Input id={`storage-retention-${key}`} type="number" min={1} max={99999} step={1} disabled={useDefault || keepAll || busy}
value={fields[key] ?? ""} onChange={event => setFields(previous => ({ ...previous, [key]: event.target.value }))} />
</div>)}
</div>
+1
View File
@@ -3948,6 +3948,7 @@
},
"backup": {
"storageRetention": {
"useDefault": "Use the Proxmox default (no policy on this storage)",
"title": "Storage retention",
"keepAll": "Keep all backups",
"description": "Default backup retention stored in Proxmox. Backup jobs with an explicit policy override this default.",
+8 -7
View File
@@ -4764,19 +4764,20 @@
"wednesday": "Miércoles"
},
"storageRetention": {
"useDefault": "Usar el valor por defecto de Proxmox (sin política en este almacenamiento)",
"title": "Retención del almacenamiento",
"keepAll": "Conservar todas las copias de seguridad",
"description": "Retención de copias de seguridad predeterminada guardada en Proxmox. Las tareas de copia con una política explícita tienen prioridad sobre este valor.",
"keepAll": "Conservar todos los backups",
"description": "Retención por defecto de los backups, guardada en Proxmox. Las tareas de backup con su propia política tienen prioridad sobre ella.",
"refresh": "Actualizar",
"saved": "Retención del almacenamiento guardada. No se ha ejecutado ninguna purga.",
"choosePolicy": "Elige Conservar todas o al menos un número de retención mayor que cero.",
"saved": "Retención guardada. No se ha purgado ningún backup.",
"choosePolicy": "Elige Conservar todos o al menos un número de retención mayor que cero.",
"saveFailed": "No se pudo guardar la retención del almacenamiento. Actualiza la lista si ha cambiado la configuración.",
"loadFailed": "No se pudo cargar la retención del almacenamiento.",
"empty": "No se han encontrado almacenamientos de copias de seguridad.",
"default": "Valor predeterminado de Proxmox",
"empty": "No hay almacenamientos de backups.",
"default": "Valor por defecto de Proxmox",
"edit": "Editar",
"editTitle": "Retención de {storage}",
"effect": "Se aplica a las futuras copias de seguridad que hereden esta política de almacenamiento. Guardar no elimina las copias existentes. Las copias protegidas mantienen su protección.",
"effect": "Se aplica a los próximos backups que hereden la política de este almacenamiento. Guardar no elimina los backups existentes, y los protegidos siguen protegidos.",
"cancel": "Cancelar",
"save": "Guardar retención"
}
+6 -4
View File
@@ -259,13 +259,14 @@ def update_backup_storage_retention(storage_id):
digest, prune = data.get("digest"), data.get("prune_backups")
if not isinstance(digest, str) or not re.fullmatch(r"[a-fA-F0-9]{40}", digest):
return _err("Refresh the storage list before editing its retention")
if not isinstance(prune, str) or not PRUNE_RE.fullmatch(prune):
# An empty policy removes the storage setting, so the Proxmox default applies.
if not isinstance(prune, str) or (prune and not PRUNE_RE.fullmatch(prune)):
return _err("Invalid retention policy")
pairs = [part.split("=", 1) for part in prune.split(",")]
pairs = [part.split("=", 1) for part in prune.split(",")] if prune else []
keys = [key for key, _ in pairs]
if (len(keys) != len(set(keys)) or
("keep-all" in keys and (len(keys) != 1 or pairs[0][1] != "1")) or
not any(int(value) > 0 for _, value in pairs)):
any(int(value) < 1 for _, value in pairs)):
return _err("Choose keep-all=1 or one or more positive retention counts")
# Check that this is still a backup storage, then let Proxmox check the
# digest under its storage lock. Other storage settings are never passed.
@@ -276,7 +277,8 @@ def update_backup_storage_retention(storage_id):
return _err("This storage does not accept backups", 409)
if storage.get("digest") != digest:
return _err("Storage configuration changed; refresh before saving", 409)
ok, out = _pvesh(["set", f"/storage/{storage_id}", "--digest", digest, "--prune-backups", prune])
change = ["--prune-backups", prune] if prune else ["--delete", "prune-backups"]
ok, out = _pvesh(["set", f"/storage/{storage_id}", "--digest", digest, *change])
if not ok:
return _err(out, 409)
return jsonify({"success": True})