diff --git a/src/manage_migration.py b/src/manage_migration.py new file mode 100755 index 000000000..3221c90e3 --- /dev/null +++ b/src/manage_migration.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", + "paperless_migration.settings", + ) + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/src/paperless/migration_asgi.py b/src/paperless/migration_asgi.py new file mode 100644 index 000000000..abdc5d567 --- /dev/null +++ b/src/paperless/migration_asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless_migration.settings") + +application = get_asgi_application() diff --git a/src/paperless_migration/__init__.py b/src/paperless_migration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/paperless_migration/apps.py b/src/paperless_migration/apps.py new file mode 100644 index 000000000..dd1635af7 --- /dev/null +++ b/src/paperless_migration/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PaperlessMigrationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "paperless_migration" diff --git a/src/paperless_migration/settings.py b/src/paperless_migration/settings.py new file mode 100644 index 000000000..6037bfc22 --- /dev/null +++ b/src/paperless_migration/settings.py @@ -0,0 +1,180 @@ +"""Settings for migration-mode Django instance.""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +BASE_DIR = Path(__file__).resolve().parent.parent + +DEBUG = False + +ALLOWED_HOSTS = ["*"] + + +def __get_path( + key: str, + default: str | Path, +) -> Path: + if key in os.environ: + return Path(os.environ[key]).resolve() + return Path(default).resolve() + + +DATA_DIR = __get_path("PAPERLESS_DATA_DIR", BASE_DIR.parent / "data") + + +def _parse_db_settings() -> dict[str, dict[str, Any]]: + databases: dict[str, dict[str, Any]] = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": DATA_DIR / "db.sqlite3", + "OPTIONS": {}, + }, + } + if os.getenv("PAPERLESS_DBHOST"): + databases["sqlite"] = databases["default"].copy() + databases["default"] = { + "HOST": os.getenv("PAPERLESS_DBHOST"), + "NAME": os.getenv("PAPERLESS_DBNAME", "paperless"), + "USER": os.getenv("PAPERLESS_DBUSER", "paperless"), + "PASSWORD": os.getenv("PAPERLESS_DBPASS", "paperless"), + "OPTIONS": {}, + } + if os.getenv("PAPERLESS_DBPORT"): + databases["default"]["PORT"] = os.getenv("PAPERLESS_DBPORT") + + if os.getenv("PAPERLESS_DBENGINE") == "mariadb": + engine = "django.db.backends.mysql" + options = { + "read_default_file": "/etc/mysql/my.cnf", + "charset": "utf8mb4", + "ssl_mode": os.getenv("PAPERLESS_DBSSLMODE", "PREFERRED"), + "ssl": { + "ca": os.getenv("PAPERLESS_DBSSLROOTCERT"), + "cert": os.getenv("PAPERLESS_DBSSLCERT"), + "key": os.getenv("PAPERLESS_DBSSLKEY"), + }, + } + else: + engine = "django.db.backends.postgresql" + options = { + "sslmode": os.getenv("PAPERLESS_DBSSLMODE", "prefer"), + "sslrootcert": os.getenv("PAPERLESS_DBSSLROOTCERT"), + "sslcert": os.getenv("PAPERLESS_DBSSLCERT"), + "sslkey": os.getenv("PAPERLESS_DBSSLKEY"), + } + + databases["default"]["ENGINE"] = engine + databases["default"]["OPTIONS"].update(options) + + if os.getenv("PAPERLESS_DB_TIMEOUT") is not None: + timeout = int(os.getenv("PAPERLESS_DB_TIMEOUT")) + if databases["default"]["ENGINE"] == "django.db.backends.sqlite3": + databases["default"]["OPTIONS"].update({"timeout": timeout}) + else: + databases["default"]["OPTIONS"].update({"connect_timeout": timeout}) + databases["sqlite"]["OPTIONS"].update({"timeout": timeout}) + return databases + + +DATABASES = _parse_db_settings() + +SECRET_KEY = os.getenv( + "PAPERLESS_SECRET_KEY", + "e11fl1oa-*ytql8p)(06fbj4ukrlo+n7k&q5+$1md7i+mge=ee", +) + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True +CSRF_TRUSTED_ORIGINS: list[str] = [] + +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.mfa", + "paperless_migration", +] + +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", + "allauth.account.middleware.AccountMiddleware", +] + +ROOT_URLCONF = "paperless_migration.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 = "paperless_migration.wsgi.application" + +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] + +STATIC_URL = "/static/" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +LOGIN_URL = "/accounts/login/" +LOGIN_REDIRECT_URL = "/migration/" +LOGOUT_REDIRECT_URL = "/accounts/login/?loggedout=1" + +ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter" +ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS = False +SOCIALACCOUNT_ADAPTER = "allauth.socialaccount.adapter.DefaultSocialAccountAdapter" +SOCIALACCOUNT_ENABLED = False + +SESSION_ENGINE = "django.contrib.sessions.backends.db" + +MIGRATION_EXPORT_PATH = os.getenv( + "PAPERLESS_MIGRATION_EXPORT_PATH", + "/data/export.json", +) +MIGRATION_TRANSFORMED_PATH = os.getenv( + "PAPERLESS_MIGRATION_TRANSFORMED_PATH", + "/data/export.v3.json", +) diff --git a/src/paperless_migration/templates/paperless_migration/migration_home.html b/src/paperless_migration/templates/paperless_migration/migration_home.html new file mode 100644 index 000000000..4f6726910 --- /dev/null +++ b/src/paperless_migration/templates/paperless_migration/migration_home.html @@ -0,0 +1,61 @@ + + + + + + Paperless-ngx Migration Mode + + +
+

Migration Mode

+

+ This instance is running in migration mode. Use this interface to run + the v2 → v3 migration. +

+ {% if messages %} + + {% endif %} +
+

Step 1 — Export (v2)

+

Expected export file:

+ +
+ {% csrf_token %} + +
+
+
+

Step 2 — Transform

+

Expected transformed file:

+ +
+ {% csrf_token %} + +
+
+
+

Step 3 — Import (v3)

+
+ {% csrf_token %} + +
+
+
+ + diff --git a/src/paperless_migration/urls.py b/src/paperless_migration/urls.py new file mode 100644 index 000000000..38698133b --- /dev/null +++ b/src/paperless_migration/urls.py @@ -0,0 +1,9 @@ +from django.urls import include +from django.urls import path + +from paperless_migration import views + +urlpatterns = [ + path("accounts/", include("allauth.urls")), + path("migration/", views.migration_home, name="migration_home"), +] diff --git a/src/paperless_migration/views.py b/src/paperless_migration/views.py new file mode 100644 index 000000000..000822e5f --- /dev/null +++ b/src/paperless_migration/views.py @@ -0,0 +1,46 @@ +from pathlib import Path + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseForbidden +from django.shortcuts import redirect +from django.shortcuts import render +from django.views.decorators.http import require_http_methods + +from paperless_migration import settings + + +@login_required +@require_http_methods(["GET", "POST"]) +def migration_home(request): + if not request.user.is_superuser: + return HttpResponseForbidden("Superuser access required") + + export_path = Path(settings.MIGRATION_EXPORT_PATH) + transformed_path = Path(settings.MIGRATION_TRANSFORMED_PATH) + + if request.method == "POST": + action = request.POST.get("action") + if action == "check": + messages.success(request, "Checked export paths.") + elif action == "transform": + messages.info( + request, + "Transform step is not implemented yet.", + ) + elif action == "import": + messages.info( + request, + "Import step is not implemented yet.", + ) + else: + messages.error(request, "Unknown action.") + return redirect("migration_home") + + context = { + "export_path": export_path, + "export_exists": export_path.exists(), + "transformed_path": transformed_path, + "transformed_exists": transformed_path.exists(), + } + return render(request, "paperless_migration/migration_home.html", context) diff --git a/src/paperless_migration/wsgi.py b/src/paperless_migration/wsgi.py new file mode 100644 index 000000000..d1dd62f5f --- /dev/null +++ b/src/paperless_migration/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless_migration.settings") + +application = get_wsgi_application()