diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e1d53ab --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.venv +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +.mypy_cache/ +var/ +dist/ +build/ +*.egg-info/ diff --git a/.gitignore b/.gitignore index c1f749a..28d4942 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ -__pycache__ -*egg-info +__pycache__/ +*.py[cod] +.venv/ +var/ +.pytest_cache/ +.mypy_cache/ +*.egg-info/ build/ +dist/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..33a1db5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends rsync openssh-client cron default-libmysqlclient-dev build-essential pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY pyproject.toml README.md ./ +COPY src ./src +COPY manage.py ./ +COPY scripts/docker-entrypoint ./scripts/docker-entrypoint + +RUN python -m pip install --upgrade pip \ + && python -m pip install -e ".[mariadb]" + +RUN mkdir -p /opt/pobsync/config/hosts /opt/pobsync/state/locks /opt/pobsync/logs /var/lib/pobsync +RUN chmod +x ./scripts/docker-entrypoint + +EXPOSE 8000 + +ENTRYPOINT ["./scripts/docker-entrypoint"] +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/README.md b/README.md index 6a3a5f6..7587ac1 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,11 @@ Cron output is redirected to: ## Development (optional) -For development purposes you can still use an editable install, this is why pyproject.toml still exists: +For development purposes you can still use an editable install, this is why pyproject.toml still exists. On systems with an externally managed Python installation, create a virtualenv first. ``` +python3 -m venv .venv +. .venv/bin/activate python3 -m pip install -e . pobsync --help ``` @@ -110,3 +112,64 @@ pobsync --help For production use, always use the canonical entrypoint: /opt/pobsync/bin/pobsync + +## Django backend (early refactor layer) + +The Django backend is a management layer around the existing pobsync engine. The current CLI remains the source of truth for executing backups; Django stores configs, schedules, backup runs, and snapshot metadata so the project can grow toward a web/admin/API surface without rewriting rsync behavior in one risky step. + +### Local SQLite development + +``` +python3 -m venv .venv +. .venv/bin/activate +python3 -m pip install -e . +mkdir -p var +python3 manage.py migrate +python3 manage.py createsuperuser +python3 manage.py runserver +``` + +The admin is available at: + +- http://127.0.0.1:8000/admin/ + +Import existing YAML configs into the database: + +``` +python3 manage.py import_pobsync_configs --prefix /opt/pobsync +``` + +Run a backup through Django while still using the existing pobsync engine: + +``` +python3 manage.py run_pobsync_backup --prefix /opt/pobsync --prune +``` + +### Docker with SQLite + +``` +docker compose up --build web +``` + +This starts Django on: + +- http://127.0.0.1:8000/admin/ + +The container persists `/opt/pobsync` and the SQLite database in Docker volumes. + +### Docker with MariaDB + +``` +docker compose --profile mariadb up --build web-mariadb +``` + +The MariaDB profile is optional. SQLite remains the default because it is enough for a single backup server and keeps deployment simple. + +### Refactor direction + +Recommended next steps: + +- Move config reading/writing behind a repository interface that can use YAML or Django models. +- Record `run-scheduled` results into `BackupRun`. +- Add a snapshot discovery command that syncs existing snapshot metadata into `SnapshotRecord`. +- Add tests around retention, scheduling, and config merge before deeper internal reshaping. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dd764af --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +services: + web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + environment: + POBSYNC_DJANGO_DEBUG: "1" + POBSYNC_DJANGO_SECRET_KEY: "dev-only-change-me" + POBSYNC_DJANGO_ALLOWED_HOSTS: "localhost,127.0.0.1,0.0.0.0" + POBSYNC_HOME: "/opt/pobsync" + POBSYNC_SQLITE_PATH: "/var/lib/pobsync/pobsync.sqlite3" + ports: + - "8000:8000" + volumes: + - pobsync_state:/opt/pobsync + - pobsync_db:/var/lib/pobsync + + web-mariadb: + profiles: ["mariadb"] + build: . + command: python manage.py runserver 0.0.0.0:8000 + environment: + POBSYNC_DJANGO_DEBUG: "1" + POBSYNC_DJANGO_SECRET_KEY: "dev-only-change-me" + POBSYNC_DJANGO_ALLOWED_HOSTS: "localhost,127.0.0.1,0.0.0.0" + POBSYNC_HOME: "/opt/pobsync" + POBSYNC_DB_ENGINE: "mariadb" + POBSYNC_DB_HOST: "db" + POBSYNC_DB_NAME: "pobsync" + POBSYNC_DB_USER: "pobsync" + POBSYNC_DB_PASSWORD: "pobsync" + depends_on: + db: + condition: service_healthy + ports: + - "8000:8000" + volumes: + - pobsync_state:/opt/pobsync + + db: + profiles: ["mariadb"] + image: mariadb:11 + environment: + MARIADB_DATABASE: "pobsync" + MARIADB_USER: "pobsync" + MARIADB_PASSWORD: "pobsync" + MARIADB_ROOT_PASSWORD: "pobsync-root" + volumes: + - mariadb_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 5s + retries: 20 + +volumes: + pobsync_state: + pobsync_db: + mariadb_data: diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..2bb1d20 --- /dev/null +++ b/manage.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +import sys + + +def main() -> None: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pobsync_server.settings") + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index fbbf18f..a6462a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,15 @@ version = "0.1.0" description = "Pull-based rsync backup tool with hardlinked snapshots" requires-python = ">=3.11" dependencies = [ + "Django>=5.2,<6.0", "PyYAML>=6.0" ] +[project.optional-dependencies] +mariadb = [ + "mysqlclient>=2.2" +] + [project.scripts] pobsync = "pobsync.cli:main" diff --git a/scripts/docker-entrypoint b/scripts/docker-entrypoint new file mode 100644 index 0000000..ed13e47 --- /dev/null +++ b/scripts/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/sh +set -eu + +mkdir -p "$(dirname "${POBSYNC_SQLITE_PATH:-/var/lib/pobsync/pobsync.sqlite3}")" + +python manage.py migrate --noinput + +exec "$@" diff --git a/src/pobsync_backend/__init__.py b/src/pobsync_backend/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pobsync_backend/__init__.py @@ -0,0 +1 @@ + diff --git a/src/pobsync_backend/admin.py b/src/pobsync_backend/admin.py new file mode 100644 index 0000000..ee4c7fc --- /dev/null +++ b/src/pobsync_backend/admin.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from django.contrib import admin + +from .models import BackupRun, GlobalConfig, HostConfig, ScheduleConfig, SnapshotRecord + + +@admin.register(GlobalConfig) +class GlobalConfigAdmin(admin.ModelAdmin): + list_display = ("name", "backup_root", "updated_at") + readonly_fields = ("created_at", "updated_at") + + +@admin.register(HostConfig) +class HostConfigAdmin(admin.ModelAdmin): + list_display = ("host", "address", "enabled", "updated_at") + list_filter = ("enabled",) + search_fields = ("host", "address") + readonly_fields = ("created_at", "updated_at") + + +@admin.register(BackupRun) +class BackupRunAdmin(admin.ModelAdmin): + list_display = ("host", "run_type", "status", "started_at", "ended_at", "snapshot_path") + list_filter = ("run_type", "status", "started_at") + search_fields = ("host__host", "snapshot_path") + + +@admin.register(SnapshotRecord) +class SnapshotRecordAdmin(admin.ModelAdmin): + list_display = ("host", "kind", "dirname", "status", "started_at") + list_filter = ("kind", "status", "started_at") + search_fields = ("host__host", "dirname", "path") + + +@admin.register(ScheduleConfig) +class ScheduleConfigAdmin(admin.ModelAdmin): + list_display = ("host", "cron_expr", "enabled", "prune", "updated_at") + list_filter = ("enabled", "prune") + search_fields = ("host__host", "cron_expr") diff --git a/src/pobsync_backend/apps.py b/src/pobsync_backend/apps.py new file mode 100644 index 0000000..c64fd53 --- /dev/null +++ b/src/pobsync_backend/apps.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from django.apps import AppConfig + + +class PobsyncBackendConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "pobsync_backend" + verbose_name = "Pobsync backend" diff --git a/src/pobsync_backend/management/__init__.py b/src/pobsync_backend/management/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pobsync_backend/management/__init__.py @@ -0,0 +1 @@ + diff --git a/src/pobsync_backend/management/commands/__init__.py b/src/pobsync_backend/management/commands/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pobsync_backend/management/commands/__init__.py @@ -0,0 +1 @@ + diff --git a/src/pobsync_backend/management/commands/import_pobsync_configs.py b/src/pobsync_backend/management/commands/import_pobsync_configs.py new file mode 100644 index 0000000..a1ad500 --- /dev/null +++ b/src/pobsync_backend/management/commands/import_pobsync_configs.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError + +from pobsync.config.load import load_global_config, load_host_config +from pobsync.paths import PobsyncPaths +from pobsync_backend.models import GlobalConfig, HostConfig + + +class Command(BaseCommand): + help = "Import pobsync YAML configs into the Django database." + + def add_arguments(self, parser) -> None: + parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory") + + def handle(self, *args: Any, **options: Any) -> None: + paths = PobsyncPaths(home=Path(options["prefix"])) + if not paths.global_config_path.exists(): + raise CommandError(f"Missing global config: {paths.global_config_path}") + + global_cfg = load_global_config(paths.global_config_path) + GlobalConfig.objects.update_or_create( + name="default", + defaults={ + "backup_root": global_cfg["backup_root"], + "pobsync_home": global_cfg.get("pobsync_home", str(paths.home)), + "data": global_cfg, + }, + ) + + count = 0 + for host_path in sorted(paths.hosts_dir.glob("*.yaml")): + host_cfg = load_host_config(host_path) + HostConfig.objects.update_or_create( + host=host_cfg["host"], + defaults={ + "address": host_cfg["address"], + "config": host_cfg, + "enabled": True, + }, + ) + count += 1 + + self.stdout.write(self.style.SUCCESS(f"Imported global config and {count} host config(s).")) diff --git a/src/pobsync_backend/management/commands/run_pobsync_backup.py b/src/pobsync_backend/management/commands/run_pobsync_backup.py new file mode 100644 index 0000000..4ce19d9 --- /dev/null +++ b/src/pobsync_backend/management/commands/run_pobsync_backup.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone + +from pobsync.commands.run_scheduled import run_scheduled +from pobsync.config.load import load_host_config +from pobsync.paths import PobsyncPaths +from pobsync_backend.models import BackupRun, HostConfig + + +class Command(BaseCommand): + help = "Run a scheduled pobsync backup and record the result in Django." + + def add_arguments(self, parser) -> None: + parser.add_argument("host", help="Host to back up") + parser.add_argument("--prefix", default=settings.POBSYNC_HOME, help="Pobsync home directory") + parser.add_argument("--dry-run", action="store_true", help="Run rsync --dry-run") + parser.add_argument("--prune", action="store_true", help="Apply retention after a successful run") + parser.add_argument("--prune-max-delete", type=int, default=10) + parser.add_argument("--prune-protect-bases", action="store_true") + + def handle(self, *args: Any, **options: Any) -> None: + host_name = options["host"] + paths = PobsyncPaths(home=Path(options["prefix"])) + host_path = paths.hosts_dir / f"{host_name}.yaml" + if not host_path.exists(): + raise CommandError(f"Missing host config: {host_path}") + + host_cfg = load_host_config(host_path) + host, _created = HostConfig.objects.update_or_create( + host=host_cfg["host"], + defaults={ + "address": host_cfg["address"], + "config": host_cfg, + "enabled": True, + }, + ) + + run = BackupRun.objects.create( + host=host, + run_type=BackupRun.RunType.SCHEDULED, + status=BackupRun.Status.RUNNING, + started_at=timezone.now(), + ) + + try: + result = run_scheduled( + prefix=paths.home, + host=host.host, + dry_run=bool(options["dry_run"]), + prune=bool(options["prune"]), + prune_max_delete=int(options["prune_max_delete"]), + prune_protect_bases=bool(options["prune_protect_bases"]), + ) + except Exception as exc: + run.status = BackupRun.Status.FAILED + run.ended_at = timezone.now() + run.result = {"ok": False, "error": str(exc), "type": type(exc).__name__} + run.save(update_fields=["status", "ended_at", "result"]) + raise + + run.status = BackupRun.Status.SUCCESS if result.get("ok") else BackupRun.Status.FAILED + run.ended_at = timezone.now() + run.snapshot_path = str(result.get("snapshot") or "") + run.base_path = str(result.get("base") or "") + rsync = result.get("rsync") if isinstance(result.get("rsync"), dict) else {} + run.rsync_exit_code = rsync.get("exit_code") + run.result = result + run.save( + update_fields=[ + "status", + "ended_at", + "snapshot_path", + "base_path", + "rsync_exit_code", + "result", + ], + ) + + if result.get("ok"): + self.stdout.write(self.style.SUCCESS(f"Backup completed for {host.host}.")) + return + + raise CommandError(f"Backup failed for {host.host}; run id={run.id}") diff --git a/src/pobsync_backend/migrations/0001_initial.py b/src/pobsync_backend/migrations/0001_initial.py new file mode 100644 index 0000000..9bc4734 --- /dev/null +++ b/src/pobsync_backend/migrations/0001_initial.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="GlobalConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(default="default", max_length=64, unique=True)), + ("backup_root", models.CharField(max_length=512)), + ("pobsync_home", models.CharField(default="/opt/pobsync", max_length=512)), + ("data", models.JSONField(blank=True, default=dict)), + ], + options={ + "verbose_name": "global config", + "verbose_name_plural": "global configs", + }, + ), + migrations.CreateModel( + name="HostConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("host", models.CharField(max_length=255, unique=True)), + ("address", models.CharField(max_length=255)), + ("enabled", models.BooleanField(default=True)), + ("config", models.JSONField(blank=True, default=dict)), + ], + options={ + "ordering": ["host"], + }, + ), + migrations.CreateModel( + name="BackupRun", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("run_type", models.CharField(choices=[("scheduled", "Scheduled"), ("manual", "Manual")], default="scheduled", max_length=16)), + ("status", models.CharField(choices=[("queued", "Queued"), ("running", "Running"), ("success", "Success"), ("failed", "Failed"), ("cancelled", "Cancelled")], default="queued", max_length=16)), + ("started_at", models.DateTimeField(blank=True, null=True)), + ("ended_at", models.DateTimeField(blank=True, null=True)), + ("snapshot_path", models.CharField(blank=True, max_length=1024)), + ("base_path", models.CharField(blank=True, max_length=1024)), + ("rsync_exit_code", models.IntegerField(blank=True, null=True)), + ("result", models.JSONField(blank=True, default=dict)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("host", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name="runs", to="pobsync_backend.hostconfig")), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="ScheduleConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("cron_expr", models.CharField(max_length=128)), + ("user", models.CharField(default="root", max_length=64)), + ("enabled", models.BooleanField(default=True)), + ("prune", models.BooleanField(default=False)), + ("prune_max_delete", models.PositiveIntegerField(default=10)), + ("prune_protect_bases", models.BooleanField(default=False)), + ("host", models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name="schedule", to="pobsync_backend.hostconfig")), + ], + options={ + "ordering": ["host__host"], + }, + ), + migrations.CreateModel( + name="SnapshotRecord", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("kind", models.CharField(choices=[("scheduled", "Scheduled"), ("manual", "Manual"), ("incomplete", "Incomplete")], max_length=16)), + ("dirname", models.CharField(max_length=255)), + ("path", models.CharField(max_length=1024)), + ("status", models.CharField(blank=True, max_length=32)), + ("started_at", models.DateTimeField(blank=True, null=True)), + ("ended_at", models.DateTimeField(blank=True, null=True)), + ("metadata", models.JSONField(blank=True, default=dict)), + ("discovered_at", models.DateTimeField(auto_now_add=True)), + ("host", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="snapshots", to="pobsync_backend.hostconfig")), + ], + options={ + "ordering": ["host__host", "-started_at", "dirname"], + }, + ), + migrations.AddConstraint( + model_name="snapshotrecord", + constraint=models.UniqueConstraint(fields=("host", "kind", "dirname"), name="unique_snapshot_per_host_kind"), + ), + ] diff --git a/src/pobsync_backend/migrations/__init__.py b/src/pobsync_backend/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pobsync_backend/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/src/pobsync_backend/models.py b/src/pobsync_backend/models.py new file mode 100644 index 0000000..a2a8308 --- /dev/null +++ b/src/pobsync_backend/models.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from django.db import models + + +class TimestampedModel(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class GlobalConfig(TimestampedModel): + name = models.CharField(max_length=64, default="default", unique=True) + backup_root = models.CharField(max_length=512) + pobsync_home = models.CharField(max_length=512, default="/opt/pobsync") + data = models.JSONField(default=dict, blank=True) + + class Meta: + verbose_name = "global config" + verbose_name_plural = "global configs" + + def __str__(self) -> str: + return self.name + + +class HostConfig(TimestampedModel): + host = models.CharField(max_length=255, unique=True) + address = models.CharField(max_length=255) + enabled = models.BooleanField(default=True) + config = models.JSONField(default=dict, blank=True) + + class Meta: + ordering = ["host"] + + def __str__(self) -> str: + return self.host + + +class BackupRun(models.Model): + class RunType(models.TextChoices): + SCHEDULED = "scheduled", "Scheduled" + MANUAL = "manual", "Manual" + + class Status(models.TextChoices): + QUEUED = "queued", "Queued" + RUNNING = "running", "Running" + SUCCESS = "success", "Success" + FAILED = "failed", "Failed" + CANCELLED = "cancelled", "Cancelled" + + host = models.ForeignKey(HostConfig, on_delete=models.PROTECT, related_name="runs") + run_type = models.CharField(max_length=16, choices=RunType.choices, default=RunType.SCHEDULED) + status = models.CharField(max_length=16, choices=Status.choices, default=Status.QUEUED) + started_at = models.DateTimeField(null=True, blank=True) + ended_at = models.DateTimeField(null=True, blank=True) + snapshot_path = models.CharField(max_length=1024, blank=True) + base_path = models.CharField(max_length=1024, blank=True) + rsync_exit_code = models.IntegerField(null=True, blank=True) + result = models.JSONField(default=dict, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self) -> str: + return f"{self.host} {self.run_type} {self.status}" + + +class SnapshotRecord(models.Model): + class Kind(models.TextChoices): + SCHEDULED = "scheduled", "Scheduled" + MANUAL = "manual", "Manual" + INCOMPLETE = "incomplete", "Incomplete" + + host = models.ForeignKey(HostConfig, on_delete=models.CASCADE, related_name="snapshots") + kind = models.CharField(max_length=16, choices=Kind.choices) + dirname = models.CharField(max_length=255) + path = models.CharField(max_length=1024) + status = models.CharField(max_length=32, blank=True) + started_at = models.DateTimeField(null=True, blank=True) + ended_at = models.DateTimeField(null=True, blank=True) + metadata = models.JSONField(default=dict, blank=True) + discovered_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["host", "kind", "dirname"], name="unique_snapshot_per_host_kind"), + ] + ordering = ["host__host", "-started_at", "dirname"] + + def __str__(self) -> str: + return f"{self.host}/{self.kind}/{self.dirname}" + + +class ScheduleConfig(TimestampedModel): + host = models.OneToOneField(HostConfig, on_delete=models.CASCADE, related_name="schedule") + cron_expr = models.CharField(max_length=128) + user = models.CharField(max_length=64, default="root") + enabled = models.BooleanField(default=True) + prune = models.BooleanField(default=False) + prune_max_delete = models.PositiveIntegerField(default=10) + prune_protect_bases = models.BooleanField(default=False) + + class Meta: + ordering = ["host__host"] + + def __str__(self) -> str: + return f"{self.host} {self.cron_expr}" diff --git a/src/pobsync_server/__init__.py b/src/pobsync_server/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pobsync_server/__init__.py @@ -0,0 +1 @@ + diff --git a/src/pobsync_server/asgi.py b/src/pobsync_server/asgi.py new file mode 100644 index 0000000..fc47d7b --- /dev/null +++ b/src/pobsync_server/asgi.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import os + +from django.core.asgi import get_asgi_application + + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pobsync_server.settings") + +application = get_asgi_application() diff --git a/src/pobsync_server/settings.py b/src/pobsync_server/settings.py new file mode 100644 index 0000000..784282d --- /dev/null +++ b/src/pobsync_server/settings.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import os +from pathlib import Path + + +BASE_DIR = Path(__file__).resolve().parents[2] + +SECRET_KEY = os.getenv("POBSYNC_DJANGO_SECRET_KEY", "dev-only-change-me") +DEBUG = os.getenv("POBSYNC_DJANGO_DEBUG", "0").lower() in {"1", "true", "yes", "on"} + +_allowed_hosts = os.getenv("POBSYNC_DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1") +ALLOWED_HOSTS = [host.strip() for host in _allowed_hosts.split(",") if host.strip()] + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "pobsync_backend.apps.PobsyncBackendConfig", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "pobsync_server.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "pobsync_server.wsgi.application" + + +def _database_config() -> dict[str, object]: + engine = os.getenv("POBSYNC_DB_ENGINE", "sqlite").strip().lower() + if engine in {"mariadb", "mysql"}: + return { + "ENGINE": "django.db.backends.mysql", + "NAME": os.getenv("POBSYNC_DB_NAME", "pobsync"), + "USER": os.getenv("POBSYNC_DB_USER", "pobsync"), + "PASSWORD": os.getenv("POBSYNC_DB_PASSWORD", "pobsync"), + "HOST": os.getenv("POBSYNC_DB_HOST", "db"), + "PORT": os.getenv("POBSYNC_DB_PORT", "3306"), + "OPTIONS": { + "charset": "utf8mb4", + }, + } + + sqlite_path = os.getenv("POBSYNC_SQLITE_PATH", str(BASE_DIR / "var" / "pobsync.sqlite3")) + return { + "ENGINE": "django.db.backends.sqlite3", + "NAME": sqlite_path, + } + + +DATABASES = { + "default": _database_config(), +} + +LANGUAGE_CODE = "en-us" +TIME_ZONE = os.getenv("POBSYNC_TIME_ZONE", "UTC") +USE_I18N = True +USE_TZ = True + +STATIC_URL = "static/" +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") diff --git a/src/pobsync_server/urls.py b/src/pobsync_server/urls.py new file mode 100644 index 0000000..99a087f --- /dev/null +++ b/src/pobsync_server/urls.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from django.contrib import admin +from django.urls import path + + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/src/pobsync_server/wsgi.py b/src/pobsync_server/wsgi.py new file mode 100644 index 0000000..9b73fc9 --- /dev/null +++ b/src/pobsync_server/wsgi.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +import os + +from django.core.wsgi import get_wsgi_application + + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pobsync_server.settings") + +application = get_wsgi_application()