Files
proxmox-snapshot-service/pvesnap/util.py
T
2026-07-30 16:52:58 +02:00

163 lines
5.4 KiB
Python

"""Kleine Hilfsfunktionen fuer pvesnap (Dauer-Parsing, Slugs, Listen)."""
from __future__ import annotations
import re
import unicodedata
_DURATION_UNITS = {
"s": 1, "sec": 1, "secs": 1, "second": 1, "seconds": 1,
"sek": 1, "sekunde": 1, "sekunden": 1,
"m": 60, "min": 60, "mins": 60, "minute": 60, "minutes": 60, "minuten": 60,
"h": 3600, "hour": 3600, "hours": 3600, "std": 3600, "stunde": 3600, "stunden": 3600,
"d": 86400, "day": 86400, "days": 86400, "t": 86400, "tag": 86400, "tage": 86400,
"w": 604800, "week": 604800, "weeks": 604800, "woche": 604800, "wochen": 604800,
"mo": 2592000, "month": 2592000, "months": 2592000, "monat": 2592000, "monate": 2592000,
"y": 31536000, "year": 31536000, "years": 31536000,
"j": 31536000, "jahr": 31536000, "jahre": 31536000,
}
_DURATION_TOKEN = re.compile(r"(\d+(?:[.,]\d+)?)\s*([a-zäöü]*)")
_NEVER = {"0", "never", "nie", "unbegrenzt", "unlimited", "off", "aus", "keine", "-"}
def parse_duration(text, default_unit="s"):
"""'2d12h' -> 216000. 0 / 'nie' / 'unbegrenzt' -> 0 (= keine Begrenzung)."""
if text is None:
raise ValueError("leere Zeitangabe")
s = str(text).strip().lower()
if not s:
raise ValueError("leere Zeitangabe")
if s in _NEVER:
return 0
total = 0.0
pos = 0
found = False
for match in _DURATION_TOKEN.finditer(s):
if match.start() != pos:
raise ValueError("ungueltige Zeitangabe: %r" % text)
pos = match.end()
value = float(match.group(1).replace(",", "."))
unit = match.group(2) or default_unit
if unit not in _DURATION_UNITS:
raise ValueError("unbekannte Zeiteinheit %r in %r" % (unit, text))
total += value * _DURATION_UNITS[unit]
found = True
if not found or pos != len(s):
raise ValueError("ungueltige Zeitangabe: %r" % text)
return int(round(total))
def format_duration(seconds, zero="unbegrenzt"):
"""216000 -> '2d12h'. Ergebnis ist wieder von parse_duration lesbar."""
seconds = int(seconds or 0)
if seconds <= 0:
return zero
parts = []
for unit, size in (("w", 604800), ("d", 86400), ("h", 3600), ("m", 60), ("s", 1)):
if seconds >= size:
count, seconds = divmod(seconds, size)
parts.append("%d%s" % (count, unit))
return "".join(parts)
def slugify(text, maxlen=20):
"""Gruppenname -> kurzer, PVE-tauglicher Bezeichner (nur a-z0-9, beginnt mit Buchstabe)."""
# Umlaute vor der Normalisierung ersetzen - NFKD wuerde sie sonst zerlegen.
lowered = str(text).lower()
for umlaut, replacement in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")):
lowered = lowered.replace(umlaut, replacement)
ascii_only = unicodedata.normalize("NFKD", lowered).encode("ascii", "ignore").decode("ascii")
slug = re.sub(r"[^a-z0-9]+", "", ascii_only)
if not slug:
slug = "grp"
if not slug[0].isalpha():
slug = "g" + slug
return slug[:maxlen]
_TRUE = {"1", "yes", "y", "true", "on", "ja", "j", "an", "ein", "enabled", "aktiv"}
_FALSE = {"0", "no", "n", "false", "off", "nein", "aus", "disabled", "inaktiv", ""}
def parse_bool(text, default=None):
if isinstance(text, bool):
return text
value = str(text).strip().lower()
if value in _TRUE:
return True
if value in _FALSE:
return default if (value == "" and default is not None) else False
raise ValueError("ungueltiger Ja/Nein-Wert: %r" % text)
def format_bool(value):
return "yes" if value else "no"
def parse_list(text):
"""Kommagetrennte (oder per Semikolon/Whitespace getrennte) Liste -> Liste von Strings."""
if not text:
return []
if isinstance(text, (list, tuple)):
items = list(text)
else:
items = re.split(r"[,;\s]+", str(text))
return [item.strip() for item in items if item and item.strip()]
def parse_vmid_list(text):
"""'100,101,105-108' -> [100, 101, 105, 106, 107, 108]."""
result = []
for token in parse_list(text):
match = re.match(r"^(\d+)\s*-\s*(\d+)$", token)
if match:
start, end = int(match.group(1)), int(match.group(2))
if end < start:
start, end = end, start
if end - start > 100000:
raise ValueError("VMID-Bereich zu gross: %r" % token)
result.extend(range(start, end + 1))
elif token.isdigit():
result.append(int(token))
else:
raise ValueError("ungueltige VMID: %r" % token)
return sorted(set(result))
def format_vmid_list(vmids):
"""[100,101,102,105] -> '100-102,105' (Umkehrung von parse_vmid_list)."""
ids = sorted(set(int(v) for v in vmids))
if not ids:
return ""
chunks = []
start = previous = ids[0]
for vmid in ids[1:]:
if vmid == previous + 1:
previous = vmid
continue
chunks.append((start, previous))
start = previous = vmid
chunks.append((start, previous))
out = []
for begin, end in chunks:
if end - begin >= 2:
out.append("%d-%d" % (begin, end))
else:
out.extend(str(v) for v in range(begin, end + 1))
return ",".join(out)
def truncate(text, width):
text = str(text)
if width <= 0:
return ""
if len(text) <= width:
return text
if width <= 1:
return text[:width]
return text[: width - 1] + "…"