refactor: stop using legacy JSON for runtime config

Build runtime pobsync configuration exclusively from structured SQL
fields, leaving legacy JSON only for import and audit context. Add
SQL-first management commands for global and host configuration and
cover them with tests.
This commit is contained in:
2026-05-19 05:08:37 +02:00
parent a0eb5dcc8f
commit 6d9ddc4457
6 changed files with 231 additions and 45 deletions

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
from typing import Any
from django.core.management.base import BaseCommand, CommandError
from pobsync.cli import parse_retention
from pobsync.util import sanitize_host
from pobsync_backend.models import GlobalConfig, HostConfig
class Command(BaseCommand):
help = "Create or update a SQL-backed host pobsync configuration."
def add_arguments(self, parser) -> None:
parser.add_argument("host")
parser.add_argument("--address", required=True)
parser.add_argument("--ssh-user", default="")
parser.add_argument("--ssh-port", type=int, default=None)
parser.add_argument("--source-root", default="")
parser.add_argument("--include", action="append", default=[])
parser.add_argument("--exclude-add", action="append", default=[])
parser.add_argument("--exclude-replace", action="append", default=None)
parser.add_argument("--rsync-extra-arg", action="append", default=[])
parser.add_argument("--retention", default=None)
parser.add_argument("--disabled", action="store_true")
parser.add_argument("--force", action="store_true", help="Update existing host")
def handle(self, *args: Any, **options: Any) -> None:
host = sanitize_host(options["host"])
if HostConfig.objects.filter(host=host).exists() and not options["force"]:
raise CommandError(f"HostConfig {host!r} already exists; use --force to update")
retention = self._retention(options["retention"])
defaults = {
"address": options["address"],
"enabled": not options["disabled"],
"ssh_user": options["ssh_user"],
"ssh_port": options["ssh_port"],
"source_root": options["source_root"],
"includes": list(options["include"]),
"excludes_add": [] if options["exclude_replace"] is not None else list(options["exclude_add"]),
"excludes_replace": options["exclude_replace"],
"rsync_extra_args": list(options["rsync_extra_arg"]),
"retention_daily": retention["daily"],
"retention_weekly": retention["weekly"],
"retention_monthly": retention["monthly"],
"retention_yearly": retention["yearly"],
}
_obj, created = HostConfig.objects.update_or_create(host=host, defaults=defaults)
action = "Created" if created else "Updated"
self.stdout.write(self.style.SUCCESS(f"{action} HostConfig {host!r}."))
def _retention(self, value: str | None) -> dict[str, int]:
if value:
return parse_retention(value)
global_config = GlobalConfig.objects.filter(name="default").first()
if global_config is None:
return {"daily": 14, "weekly": 8, "monthly": 12, "yearly": 0}
return {
"daily": global_config.retention_daily,
"weekly": global_config.retention_weekly,
"monthly": global_config.retention_monthly,
"yearly": global_config.retention_yearly,
}