diff --git a/.gitignore b/.gitignore index 28d4942..c69f601 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] .venv/ var/ +backups/ .pytest_cache/ .mypy_cache/ *.egg-info/ diff --git a/README.md b/README.md index e63ec67..e25bc71 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,15 @@ docker compose up --build web scheduler ``` 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 diff --git a/docker-compose.yml b/docker-compose.yml index ea6b64c..f3cee44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: volumes: - pobsync_state:/opt/pobsync - pobsync_db:/var/lib/pobsync + - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups scheduler: build: . @@ -26,6 +27,7 @@ services: volumes: - pobsync_state:/opt/pobsync - pobsync_db:/var/lib/pobsync + - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups web-mariadb: profiles: ["mariadb"] @@ -48,6 +50,7 @@ services: - "8010:8000" volumes: - pobsync_state:/opt/pobsync + - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups scheduler-mariadb: profiles: ["mariadb"] @@ -68,6 +71,7 @@ services: condition: service_healthy volumes: - pobsync_state:/opt/pobsync + - ${POBSYNC_BACKUP_ROOT:-./backups}:/backups db: profiles: ["mariadb"] diff --git a/src/pobsync_backend/forms.py b/src/pobsync_backend/forms.py index 3254dd3..25d6574 100644 --- a/src/pobsync_backend/forms.py +++ b/src/pobsync_backend/forms.py @@ -1,6 +1,7 @@ from __future__ import annotations from django import forms +from django.conf import settings from .models import GlobalConfig, HostConfig, ScheduleConfig from .scheduler import parse_cron_expr @@ -84,7 +85,6 @@ class GlobalConfigForm(forms.ModelForm): fields = ( "name", "backup_root", - "pobsync_home", "ssh_user", "ssh_port", "ssh_options", @@ -104,11 +104,18 @@ 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.", - "pobsync_home": "Base directory for runtime state inside the container or host.", "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.pobsync_home = settings.POBSYNC_HOME + if commit: + instance.save() + self.save_m2m() + return instance + class ScheduleConfigForm(forms.ModelForm): cron_expr = forms.CharField( diff --git a/src/pobsync_backend/tests/test_views.py b/src/pobsync_backend/tests/test_views.py index ee2ed6c..9f4c279 100644 --- a/src/pobsync_backend/tests/test_views.py +++ b/src/pobsync_backend/tests/test_views.py @@ -76,7 +76,6 @@ class ViewTests(TestCase): { "name": "default", "backup_root": "/backups", - "pobsync_home": "/opt/pobsync", "ssh_user": "backup", "ssh_port": "2222", "ssh_options": "StrictHostKeyChecking=no\nBatchMode=yes", @@ -100,6 +99,7 @@ class ViewTests(TestCase): self.assertContains(response, "Global config saved for default.") config = GlobalConfig.objects.get(name="default") self.assertEqual(config.backup_root, "/backups") + self.assertEqual(config.pobsync_home, "/opt/pobsync") self.assertEqual(config.ssh_user, "backup") self.assertEqual(config.ssh_port, 2222) 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_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: self.client.force_login(self.staff_user) @@ -196,7 +211,7 @@ class ViewTests(TestCase): self.assertContains(response, "web-01") self.assertContains(response, "success") self.assertContains(response, "ABCDEFGH") - self.assertContains(response, '"ok": true') + self.assertContains(response, ""ok": true") self.assertContains(response, reverse("snapshot_detail", args=[snapshot.id])) def test_snapshot_detail_renders_metadata_runs_and_children(self) -> None: diff --git a/src/pobsync_backend/views.py b/src/pobsync_backend/views.py index da1a416..7892713 100644 --- a/src/pobsync_backend/views.py +++ b/src/pobsync_backend/views.py @@ -51,7 +51,7 @@ def edit_global_config(request): messages.success(request, f"Global config saved for {saved_config.name}.") return redirect("dashboard") 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( request, @@ -235,8 +235,7 @@ def _default_schedule_initial() -> dict[str, object]: def _default_global_initial() -> dict[str, object]: return { "name": "default", - "backup_root": "/opt/pobsync/backups", - "pobsync_home": "/opt/pobsync", + "backup_root": "/backups", "ssh_user": "root", "ssh_port": 22, "rsync_binary": "rsync",