mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
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:
@@ -23,6 +23,7 @@ export function BackupStorageRetention() {
|
|||||||
const [editing, setEditing] = useState<StorageRetention | null>(null)
|
const [editing, setEditing] = useState<StorageRetention | null>(null)
|
||||||
const [fields, setFields] = useState<Record<string, string>>({})
|
const [fields, setFields] = useState<Record<string, string>>({})
|
||||||
const [keepAll, setKeepAll] = useState(false)
|
const [keepAll, setKeepAll] = useState(false)
|
||||||
|
const [useDefault, setUseDefault] = useState(false)
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
const [message, setMessage] = useState("")
|
const [message, setMessage] = useState("")
|
||||||
const [saveError, setSaveError] = useState("")
|
const [saveError, setSaveError] = useState("")
|
||||||
@@ -30,7 +31,8 @@ export function BackupStorageRetention() {
|
|||||||
function edit(storage: StorageRetention) {
|
function edit(storage: StorageRetention) {
|
||||||
const policy = parseRetention(storage.prune_backups)
|
const policy = parseRetention(storage.prune_backups)
|
||||||
setFields(policy.fields)
|
setFields(policy.fields)
|
||||||
setKeepAll(policy.keepAll || !storage.prune_backups)
|
setKeepAll(policy.keepAll)
|
||||||
|
setUseDefault(!storage.prune_backups)
|
||||||
setSaveError("")
|
setSaveError("")
|
||||||
setMessage("")
|
setMessage("")
|
||||||
setEditing(storage)
|
setEditing(storage)
|
||||||
@@ -38,8 +40,8 @@ export function BackupStorageRetention() {
|
|||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (!editing || busy) return
|
if (!editing || busy) return
|
||||||
const prune = buildRetention(fields, keepAll)
|
const prune = useDefault ? "" : buildRetention(fields, keepAll)
|
||||||
if (!prune) return setSaveError(t("backup.storageRetention.choosePolicy"))
|
if (!useDefault && !prune) return setSaveError(t("backup.storageRetention.choosePolicy"))
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
setSaveError("")
|
setSaveError("")
|
||||||
try {
|
try {
|
||||||
@@ -79,14 +81,18 @@ export function BackupStorageRetention() {
|
|||||||
<DialogContent className="bg-accent [&_input]:bg-background">
|
<DialogContent className="bg-accent [&_input]:bg-background">
|
||||||
<DialogHeader><DialogTitle>{t("backup.storageRetention.editTitle", { storage: editing?.id ?? "" })}</DialogTitle>
|
<DialogHeader><DialogTitle>{t("backup.storageRetention.editTitle", { storage: editing?.id ?? "" })}</DialogTitle>
|
||||||
<DialogDescription>{t("backup.storageRetention.effect")}</DialogDescription></DialogHeader>
|
<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)
|
setKeepAll(checked === true)
|
||||||
if (checked) setFields({})
|
if (checked) setFields({})
|
||||||
}} />{t("backup.storageRetention.keepAll")}</Label>
|
}} />{t("backup.storageRetention.keepAll")}</Label>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{RETENTION.map(key => <div key={key} className="space-y-1">
|
{RETENTION.map(key => <div key={key} className="space-y-1">
|
||||||
<Label htmlFor={`storage-retention-${key}`}>{t(`backup.retention.${key}`)}</Label>
|
<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={keepAll || busy}
|
<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 }))} />
|
value={fields[key] ?? ""} onChange={event => setFields(previous => ({ ...previous, [key]: event.target.value }))} />
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3948,6 +3948,7 @@
|
|||||||
},
|
},
|
||||||
"backup": {
|
"backup": {
|
||||||
"storageRetention": {
|
"storageRetention": {
|
||||||
|
"useDefault": "Use the Proxmox default (no policy on this storage)",
|
||||||
"title": "Storage retention",
|
"title": "Storage retention",
|
||||||
"keepAll": "Keep all backups",
|
"keepAll": "Keep all backups",
|
||||||
"description": "Default backup retention stored in Proxmox. Backup jobs with an explicit policy override this default.",
|
"description": "Default backup retention stored in Proxmox. Backup jobs with an explicit policy override this default.",
|
||||||
|
|||||||
@@ -4764,19 +4764,20 @@
|
|||||||
"wednesday": "Miércoles"
|
"wednesday": "Miércoles"
|
||||||
},
|
},
|
||||||
"storageRetention": {
|
"storageRetention": {
|
||||||
|
"useDefault": "Usar el valor por defecto de Proxmox (sin política en este almacenamiento)",
|
||||||
"title": "Retención del almacenamiento",
|
"title": "Retención del almacenamiento",
|
||||||
"keepAll": "Conservar todas las copias de seguridad",
|
"keepAll": "Conservar todos los backups",
|
||||||
"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.",
|
"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",
|
"refresh": "Actualizar",
|
||||||
"saved": "Retención del almacenamiento guardada. No se ha ejecutado ninguna purga.",
|
"saved": "Retención guardada. No se ha purgado ningún backup.",
|
||||||
"choosePolicy": "Elige Conservar todas o al menos un número de retención mayor que cero.",
|
"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.",
|
"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.",
|
"loadFailed": "No se pudo cargar la retención del almacenamiento.",
|
||||||
"empty": "No se han encontrado almacenamientos de copias de seguridad.",
|
"empty": "No hay almacenamientos de backups.",
|
||||||
"default": "Valor predeterminado de Proxmox",
|
"default": "Valor por defecto de Proxmox",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"editTitle": "Retención de {storage}",
|
"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",
|
"cancel": "Cancelar",
|
||||||
"save": "Guardar retención"
|
"save": "Guardar retención"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,13 +259,14 @@ def update_backup_storage_retention(storage_id):
|
|||||||
digest, prune = data.get("digest"), data.get("prune_backups")
|
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):
|
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")
|
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")
|
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]
|
keys = [key for key, _ in pairs]
|
||||||
if (len(keys) != len(set(keys)) or
|
if (len(keys) != len(set(keys)) or
|
||||||
("keep-all" in keys and (len(keys) != 1 or pairs[0][1] != "1")) 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")
|
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
|
# Check that this is still a backup storage, then let Proxmox check the
|
||||||
# digest under its storage lock. Other storage settings are never passed.
|
# 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)
|
return _err("This storage does not accept backups", 409)
|
||||||
if storage.get("digest") != digest:
|
if storage.get("digest") != digest:
|
||||||
return _err("Storage configuration changed; refresh before saving", 409)
|
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:
|
if not ok:
|
||||||
return _err(out, 409)
|
return _err(out, 409)
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|||||||
@@ -130,9 +130,13 @@ class VmBackupJobsApiTests(unittest.TestCase):
|
|||||||
self.assertEqual(self.storage_policy("keep-all=1").status_code, 200)
|
self.assertEqual(self.storage_policy("keep-all=1").status_code, 200)
|
||||||
self.assertEqual(self.changes()[0][-1], "keep-all=1")
|
self.assertEqual(self.changes()[0][-1], "keep-all=1")
|
||||||
|
|
||||||
|
def test_an_empty_policy_returns_the_storage_to_the_proxmox_default(self):
|
||||||
|
self.assertEqual(self.storage_policy("").status_code, 200)
|
||||||
|
self.assertEqual(self.changes(), [["set", "/storage/local", "--digest", "a" * 40, "--delete", "prune-backups"]])
|
||||||
|
|
||||||
def test_invalid_storage_retention_is_rejected_before_commands(self):
|
def test_invalid_storage_retention_is_rejected_before_commands(self):
|
||||||
invalid = [None, [], {}, {"digest": "a" * 40, "prune_backups": []}]
|
invalid = [None, [], {}, {"digest": "a" * 40, "prune_backups": []}]
|
||||||
for prune in ("", "keep-last=-1", "keep-all=0", "keep-all=2", "keep-last=0", "keep-last=1,keep-last=2",
|
for prune in ("keep-last=3,keep-daily=0", "keep-last=-1", "keep-all=0", "keep-all=2", "keep-last=0", "keep-last=1,keep-last=2",
|
||||||
"keep-all=1,keep-last=2", "keep-last=2 --delete path", "keep-last=1\n", "keep-other=1"):
|
"keep-all=1,keep-last=2", "keep-last=2 --delete path", "keep-last=1\n", "keep-other=1"):
|
||||||
invalid.append({"digest": "a" * 40, "prune_backups": prune})
|
invalid.append({"digest": "a" * 40, "prune_backups": prune})
|
||||||
invalid += [{"digest": "invalid", "prune_backups": "keep-last=1"}]
|
invalid += [{"digest": "invalid", "prune_backups": "keep-last=1"}]
|
||||||
|
|||||||
Reference in New Issue
Block a user