Files
app-backup/lib/usage.py
T

73 lines
2.7 KiB
Python

"""Best-effort, bounded disk usage summary for backup and restore mail reports."""
from pathlib import Path
import os
import subprocess
def human(value):
for unit in ('B', 'KiB', 'MiB', 'GiB', 'TiB'):
if value < 1024 or unit == 'TiB':
return f'{value:.1f} {unit}'
value /= 1024
def duration(seconds):
hours, remainder = divmod(max(0, int(seconds)), 3600)
minutes, seconds = divmod(remainder, 60)
return f'{hours}h {minutes:02d}m {seconds:02d}s'
def allocated(path):
"""Count allocated bytes without following symlinks or crossing filesystems."""
try:
result = subprocess.run(['du', '-sx', '-B1', '--', str(path)],
capture_output=True, text=True, timeout=30, check=True)
return int(result.stdout.split()[0])
except (OSError, ValueError, IndexError, subprocess.SubprocessError):
return None
def free_space(path):
try:
stat = os.statvfs(path)
return stat.f_bavail * stat.f_frsize
except OSError:
return None
def summary(seconds, workdir, archives, staging, restore, logs):
"""Measure the final local state, after temporary-file cleanup."""
lines = ['', 'Laufzeit und Speicher am Ende', '----------------------------',
f'Verarbeitung: {duration(seconds)} ({max(0, int(seconds))}s)']
paths = [('Arbeitsbereich gesamt', workdir), ('Backups (lokale Archive)', archives),
('Backup-Staging', staging), ('Restore-Arbeitsbereich', restore),
('Restore-Test', str(Path(workdir) / 'restore-test')), ('Protokolle und Berichte', logs)]
for label, name in paths:
if not name:
continue
path = Path(name)
amount = allocated(path) if path.exists() and not path.is_symlink() else None
lines.append(f'{label}: {human(amount) if amount is not None else "nicht verfügbar"} ({path})')
devices = set()
for label, name in [('Arbeitsbereich', workdir), ('Archive', archives),
('Staging', staging), ('Restore', restore), ('Restore-Test', str(Path(workdir) / 'restore-test')),
('Protokolle', logs)]:
if not name:
continue
try:
path = Path(name)
if path.is_symlink():
continue
device = path.stat().st_dev
except OSError:
continue
if device in devices:
continue
devices.add(device)
amount = free_space(path)
lines.append(f'Frei auf Datenträger {label}: '
f'{human(amount) if amount is not None else "nicht verfügbar"} ({path})')
lines.append('Teilbereiche sind im Arbeitsbereich enthalten; getrennte Datenträger werden separat gemessen.')
return lines