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"