(bugfix) preserve saved global backup root in Django setup form

Fix the global config edit view so default initial values are only used
when creating a new config, preventing saved backup_root values from
being hidden by form defaults.

Keep pobsync_home as an internal runtime setting instead of exposing it
in the normal Django setup form.

Mount a host backup directory into Docker at /backups and document
POBSYNC_BACKUP_ROOT so backup_root behaves predictably in containers.
This commit is contained in:
2026-05-19 12:48:32 +02:00
parent 66e1f549b9
commit aea22597ba
6 changed files with 42 additions and 7 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ __pycache__/
*.py[cod] *.py[cod]
.venv/ .venv/
var/ var/
backups/
.pytest_cache/ .pytest_cache/
.mypy_cache/ .mypy_cache/
*.egg-info/ *.egg-info/

View File

@@ -129,6 +129,15 @@ docker compose up --build web scheduler
``` ```
The container persists `/opt/pobsync` and the SQLite database in Docker volumes. The container persists `/opt/pobsync` and the SQLite database in Docker volumes.
Backup data is mounted at `/backups` inside the containers. By default this uses `./backups` on the host.
Override it with `POBSYNC_BACKUP_ROOT`:
```
POBSYNC_BACKUP_ROOT=/mnt/backups/pobsync docker compose up --build web scheduler
```
In the Django global config, set the backup root to `/backups` when running in Docker. For local, non-Docker use,
set it directly to the host path, for example `/mnt/backups/pobsync`.
## Docker With MariaDB ## Docker With MariaDB

View File

@@ -13,6 +13,7 @@ services:
volumes: volumes:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- pobsync_db:/var/lib/pobsync - pobsync_db:/var/lib/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
scheduler: scheduler:
build: . build: .
@@ -26,6 +27,7 @@ services:
volumes: volumes:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- pobsync_db:/var/lib/pobsync - pobsync_db:/var/lib/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
web-mariadb: web-mariadb:
profiles: ["mariadb"] profiles: ["mariadb"]
@@ -48,6 +50,7 @@ services:
- "8010:8000" - "8010:8000"
volumes: volumes:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
scheduler-mariadb: scheduler-mariadb:
profiles: ["mariadb"] profiles: ["mariadb"]
@@ -68,6 +71,7 @@ services:
condition: service_healthy condition: service_healthy
volumes: volumes:
- pobsync_state:/opt/pobsync - pobsync_state:/opt/pobsync
- ${POBSYNC_BACKUP_ROOT:-./backups}:/backups
db: db:
profiles: ["mariadb"] profiles: ["mariadb"]

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from django import forms from django import forms
from django.conf import settings
from .models import GlobalConfig, HostConfig, ScheduleConfig from .models import GlobalConfig, HostConfig, ScheduleConfig
from .scheduler import parse_cron_expr from .scheduler import parse_cron_expr
@@ -84,7 +85,6 @@ class GlobalConfigForm(forms.ModelForm):
fields = ( fields = (
"name", "name",
"backup_root", "backup_root",
"pobsync_home",
"ssh_user", "ssh_user",
"ssh_port", "ssh_port",
"ssh_options", "ssh_options",
@@ -104,11 +104,18 @@ class GlobalConfigForm(forms.ModelForm):
help_texts = { help_texts = {
"name": "Usually 'default'. The backup engine currently reads the default config.", "name": "Usually 'default'. The backup engine currently reads the default config.",
"backup_root": "Directory that contains host backup folders.", "backup_root": "Directory that contains host backup folders.",
"pobsync_home": "Base directory for runtime state inside the container or host.",
"default_source_root": "Used by hosts without a custom source root.", "default_source_root": "Used by hosts without a custom source root.",
"default_destination_subdir": "Optional subdirectory below each snapshot.", "default_destination_subdir": "Optional subdirectory below each snapshot.",
} }
def save(self, commit: bool = True):
instance = super().save(commit=False)
instance.pobsync_home = settings.POBSYNC_HOME
if commit:
instance.save()
self.save_m2m()
return instance
class ScheduleConfigForm(forms.ModelForm): class ScheduleConfigForm(forms.ModelForm):
cron_expr = forms.CharField( cron_expr = forms.CharField(

View File

@@ -76,7 +76,6 @@ class ViewTests(TestCase):
{ {
"name": "default", "name": "default",
"backup_root": "/backups", "backup_root": "/backups",
"pobsync_home": "/opt/pobsync",
"ssh_user": "backup", "ssh_user": "backup",
"ssh_port": "2222", "ssh_port": "2222",
"ssh_options": "StrictHostKeyChecking=no\nBatchMode=yes", "ssh_options": "StrictHostKeyChecking=no\nBatchMode=yes",
@@ -100,6 +99,7 @@ class ViewTests(TestCase):
self.assertContains(response, "Global config saved for default.") self.assertContains(response, "Global config saved for default.")
config = GlobalConfig.objects.get(name="default") config = GlobalConfig.objects.get(name="default")
self.assertEqual(config.backup_root, "/backups") self.assertEqual(config.backup_root, "/backups")
self.assertEqual(config.pobsync_home, "/opt/pobsync")
self.assertEqual(config.ssh_user, "backup") self.assertEqual(config.ssh_user, "backup")
self.assertEqual(config.ssh_port, 2222) self.assertEqual(config.ssh_port, 2222)
self.assertEqual(config.ssh_options, ["StrictHostKeyChecking=no", "BatchMode=yes"]) self.assertEqual(config.ssh_options, ["StrictHostKeyChecking=no", "BatchMode=yes"])
@@ -109,6 +109,21 @@ class ViewTests(TestCase):
self.assertEqual(config.retention_daily, 7) self.assertEqual(config.retention_daily, 7)
self.assertEqual(config.retention_yearly, 1) self.assertEqual(config.retention_yearly, 1)
def test_global_config_form_renders_saved_backup_root_on_edit(self) -> None:
self.client.force_login(self.staff_user)
GlobalConfig.objects.create(
name="default",
backup_root="/mnt/pobsync/backups",
pobsync_home="/custom/legacy/home",
)
response = self.client.get(reverse("edit_global_config"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "/mnt/pobsync/backups")
self.assertNotContains(response, "/opt/pobsync/backups")
self.assertNotContains(response, "Pobsync home")
def test_create_host_config_form_creates_host(self) -> None: def test_create_host_config_form_creates_host(self) -> None:
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
@@ -196,7 +211,7 @@ class ViewTests(TestCase):
self.assertContains(response, "web-01") self.assertContains(response, "web-01")
self.assertContains(response, "success") self.assertContains(response, "success")
self.assertContains(response, "ABCDEFGH") self.assertContains(response, "ABCDEFGH")
self.assertContains(response, '"ok": true') self.assertContains(response, ""ok": true")
self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id])) self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id]))
def test_snapshot_detail_renders_metadata_runs_and_children(self) -> None: def test_snapshot_detail_renders_metadata_runs_and_children(self) -> None:

View File

@@ -51,7 +51,7 @@ def edit_global_config(request):
messages.success(request, f"Global config saved for {saved_config.name}.") messages.success(request, f"Global config saved for {saved_config.name}.")
return redirect("dashboard") return redirect("dashboard")
else: else:
form = GlobalConfigForm(instance=global_config, initial=_default_global_initial()) form = GlobalConfigForm(instance=global_config) if global_config else GlobalConfigForm(initial=_default_global_initial())
return render( return render(
request, request,
@@ -235,8 +235,7 @@ def _default_schedule_initial() -> dict[str, object]:
def _default_global_initial() -> dict[str, object]: def _default_global_initial() -> dict[str, object]:
return { return {
"name": "default", "name": "default",
"backup_root": "/opt/pobsync/backups", "backup_root": "/backups",
"pobsync_home": "/opt/pobsync",
"ssh_user": "root", "ssh_user": "root",
"ssh_port": 22, "ssh_port": 22,
"rsync_binary": "rsync", "rsync_binary": "rsync",