feat: add Django backend foundation and Docker runtime

Add a Django admin-backed management layer for pobsync configs, runs,
snapshots, and schedules. Keep the existing CLI engine as the execution
source of truth, add import/run management commands, and provide SQLite
default plus optional MariaDB Docker Compose support.
This commit is contained in:
2026-05-19 04:48:13 +02:00
parent 27acd790bd
commit 1a51c3e448
23 changed files with 722 additions and 3 deletions

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from pobsync.config.load import load_global_config, load_host_config
from pobsync.paths import PobsyncPaths
from pobsync_backend.models import GlobalConfig, HostConfig
class Command(BaseCommand):
help = "Import pobsync YAML configs into the Django database."
def add_arguments(self, parser) -> None:
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory")
def handle(self, *args: Any, **options: Any) -> None:
paths = PobsyncPaths(home=Path(options["prefix"]))
if not paths.global_config_path.exists():
raise CommandError(f"Missing global config: {paths.global_config_path}")
global_cfg = load_global_config(paths.global_config_path)
GlobalConfig.objects.update_or_create(
name="default",
defaults={
"backup_root": global_cfg["backup_root"],
"pobsync_home": global_cfg.get("pobsync_home", str(paths.home)),
"data": global_cfg,
},
)
count = 0
for host_path in sorted(paths.hosts_dir.glob("*.yaml")):
host_cfg = load_host_config(host_path)
HostConfig.objects.update_or_create(
host=host_cfg["host"],
defaults={
"address": host_cfg["address"],
"config": host_cfg,
"enabled": True,
},
)
count += 1
self.stdout.write(self.style.SUCCESS(f"Imported global config and {count} host config(s)."))

View File

@@ -0,0 +1,89 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from pobsync.commands.run_scheduled import run_scheduled
from pobsync.config.load import load_host_config
from pobsync.paths import PobsyncPaths
from pobsync_backend.models import BackupRun, HostConfig
class Command(BaseCommand):
help = "Run a scheduled pobsync backup and record the result in Django."
def add_arguments(self, parser) -> None:
parser.add_argument("host", help="Host to back up")
parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory")
parser.add_argument("--dry-run", action="store_true", help="Run rsync --dry-run")
parser.add_argument("--prune", action="store_true", help="Apply retention after a successful run")
parser.add_argument("--prune-max-delete", type=int, default=10)
parser.add_argument("--prune-protect-bases", action="store_true")
def handle(self, *args: Any, **options: Any) -> None:
host_name = options["host"]
paths = PobsyncPaths(home=Path(options["prefix"]))
host_path = paths.hosts_dir / f"{host_name}.yaml"
if not host_path.exists():
raise CommandError(f"Missing host config: {host_path}")
host_cfg = load_host_config(host_path)
host, _created = HostConfig.objects.update_or_create(
host=host_cfg["host"],
defaults={
"address": host_cfg["address"],
"config": host_cfg,
"enabled": True,
},
)
run = BackupRun.objects.create(
host=host,
run_type=BackupRun.RunType.SCHEDULED,
status=BackupRun.Status.RUNNING,
started_at=timezone.now(),
)
try:
result = run_scheduled(
prefix=paths.home,
host=host.host,
dry_run=bool(options["dry_run"]),
prune=bool(options["prune"]),
prune_max_delete=int(options["prune_max_delete"]),
prune_protect_bases=bool(options["prune_protect_bases"]),
)
except Exception as exc:
run.status = BackupRun.Status.FAILED
run.ended_at = timezone.now()
run.result = {"ok": False, "error": str(exc), "type": type(exc).__name__}
run.save(update_fields=["status", "ended_at", "result"])
raise
run.status = BackupRun.Status.SUCCESS if result.get("ok") else BackupRun.Status.FAILED
run.ended_at = timezone.now()
run.snapshot_path = str(result.get("snapshot") or "")
run.base_path = str(result.get("base") or "")
rsync = result.get("rsync") if isinstance(result.get("rsync"), dict) else {}
run.rsync_exit_code = rsync.get("exit_code")
run.result = result
run.save(
update_fields=[
"status",
"ended_at",
"snapshot_path",
"base_path",
"rsync_exit_code",
"result",
],
)
if result.get("ok"):
self.stdout.write(self.style.SUCCESS(f"Backup completed for {host.host}."))
return
raise CommandError(f"Backup failed for {host.host}; run id={run.id}")