From 573177e1184c2182623e59ce436aa0a12ba74342 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Tue, 19 May 2026 13:14:22 +0200 Subject: [PATCH] (refactor) make Docker backup root static in Django setup Remove backup_root from the normal Django global config form and display the fixed container path /backups instead. Always persist /backups from the setup form so Docker deployments do not mix host paths with container paths. Update tests and docs to clarify that the host backup directory is chosen through the Docker mount, while Django always uses /backups internally. --- README.md | 8 ++-- src/pobsync_backend/forms.py | 3 +- .../templates/pobsync_backend/base.html | 1 + .../pobsync_backend/global_form.html | 4 ++ src/pobsync_backend/tests/test_views.py | 43 +++++++++++++++++-- src/pobsync_backend/views.py | 3 +- src/pobsync_server/settings.py | 1 + 7 files changed, 53 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 65dc5f1..f9da799 100644 --- a/README.md +++ b/README.md @@ -129,15 +129,15 @@ docker compose up --build web scheduler worker ``` 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`: +Backup data is always available at `/backups` inside the containers. By default this uses `./backups` on the host. +Override the host-side mount with `POBSYNC_BACKUP_ROOT`: ``` POBSYNC_BACKUP_ROOT=/mnt/backups/pobsync docker compose up --build web scheduler worker ``` -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`. +The Django setup UI keeps the backup root fixed at `/backups`; only the Docker mount decides which host directory +that points to. ## Docker With MariaDB diff --git a/src/pobsync_backend/forms.py b/src/pobsync_backend/forms.py index ef92e61..c57379f 100644 --- a/src/pobsync_backend/forms.py +++ b/src/pobsync_backend/forms.py @@ -84,7 +84,6 @@ class GlobalConfigForm(forms.ModelForm): model = GlobalConfig fields = ( "name", - "backup_root", "ssh_user", "ssh_port", "ssh_options", @@ -103,13 +102,13 @@ class GlobalConfigForm(forms.ModelForm): ) help_texts = { "name": "Usually 'default'. The backup engine currently reads the default config.", - "backup_root": "Directory that contains host backup folders.", "default_source_root": "Used by hosts without a custom source root.", "default_destination_subdir": "Optional subdirectory below each snapshot.", } def save(self, commit: bool = True): instance = super().save(commit=False) + instance.backup_root = settings.POBSYNC_BACKUP_ROOT instance.pobsync_home = settings.POBSYNC_HOME if commit: instance.save() diff --git a/src/pobsync_backend/templates/pobsync_backend/base.html b/src/pobsync_backend/templates/pobsync_backend/base.html index 1881132..a35d8bd 100644 --- a/src/pobsync_backend/templates/pobsync_backend/base.html +++ b/src/pobsync_backend/templates/pobsync_backend/base.html @@ -78,6 +78,7 @@ .status.failed { color: var(--failed); border-color: #e8b4b4; background: #fff0f0; } .status.running { color: var(--running); border-color: #e7cf8a; background: #fff8df; } .stack { display: grid; gap: 4px; } + .stack.spaced { margin-bottom: 14px; } .two-col { display: grid; gap: 18px; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } .actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 18px; } .actions.inline { margin: 12px 0 0; } diff --git a/src/pobsync_backend/templates/pobsync_backend/global_form.html b/src/pobsync_backend/templates/pobsync_backend/global_form.html index 18a9e99..7cabf63 100644 --- a/src/pobsync_backend/templates/pobsync_backend/global_form.html +++ b/src/pobsync_backend/templates/pobsync_backend/global_form.html @@ -11,6 +11,10 @@

{% if global_config %}Edit Global Config{% else %}Create Global Config{% endif %}

+
+
Backup root: {{ backup_root }}
+
This is the fixed path inside the Docker containers. Change the host directory by changing the Docker mount.
+
{% csrf_token %} {{ form.non_field_errors }} diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index c7c434b..f32ab1d 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -75,7 +75,6 @@ class ViewTests(TestCase): reverse("edit_global_config"), { "name": "default", - "backup_root": "/backups", "ssh_user": "backup", "ssh_port": "2222", "ssh_options": "StrictHostKeyChecking=no\nBatchMode=yes", @@ -109,7 +108,7 @@ class ViewTests(TestCase): self.assertEqual(config.retention_daily, 7) self.assertEqual(config.retention_yearly, 1) - def test_global_config_form_renders_saved_backup_root_on_edit(self) -> None: + def test_global_config_form_renders_static_container_backup_root_on_edit(self) -> None: self.client.force_login(self.staff_user) GlobalConfig.objects.create( name="default", @@ -120,10 +119,48 @@ class ViewTests(TestCase): response = self.client.get(reverse("edit_global_config")) self.assertEqual(response.status_code, 200) - self.assertContains(response, "/mnt/pobsync/backups") + self.assertContains(response, "Backup root:") + self.assertContains(response, "/backups") + self.assertNotContains(response, "/mnt/pobsync/backups") self.assertNotContains(response, "/opt/pobsync/backups") self.assertNotContains(response, "Pobsync home") + def test_global_config_form_resets_backup_root_to_static_container_path(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.post( + reverse("edit_global_config"), + { + "name": "default", + "ssh_user": "root", + "ssh_port": "22", + "ssh_options": "", + "rsync_binary": "rsync", + "rsync_args": "", + "rsync_extra_args": "", + "rsync_timeout_seconds": "0", + "rsync_bwlimit_kbps": "0", + "default_source_root": "/", + "default_destination_subdir": "", + "excludes_default": "", + "retention_daily": "14", + "retention_weekly": "8", + "retention_monthly": "12", + "retention_yearly": "0", + }, + follow=True, + ) + + self.assertRedirects(response, reverse("dashboard")) + config = GlobalConfig.objects.get(name="default") + self.assertEqual(config.backup_root, "/backups") + self.assertEqual(config.pobsync_home, "/opt/pobsync") + def test_create_host_config_form_creates_host(self) -> None: self.client.force_login(self.staff_user) diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index a8956d5..b103ce2 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -4,6 +4,7 @@ import json from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required +from django.conf import settings from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django.views.decorators.http import require_POST @@ -60,6 +61,7 @@ def edit_global_config(request): { "global_config": global_config, "form": form, + "backup_root": settings.POBSYNC_BACKUP_ROOT, }, ) @@ -264,7 +266,6 @@ def _default_schedule_initial() -> dict[str, object]: def _default_global_initial() -> dict[str, object]: return { "name": "default", - "backup_root": "/backups", "ssh_user": "root", "ssh_port": 22, "rsync_binary": "rsync", diff --git a/src/pobsync_server/settings.py b/src/pobsync_server/settings.py index 784282d..2f94e2b 100644 --- a/src/pobsync_server/settings.py +++ b/src/pobsync_server/settings.py @@ -89,3 +89,4 @@ STATIC_ROOT = os.getenv("POBSYNC_STATIC_ROOT", str(BASE_DIR / "var" / "static")) DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" POBSYNC_HOME = os.getenv("POBSYNC_HOME", "/opt/pobsync") +POBSYNC_BACKUP_ROOT = "/backups"