mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-22 22:34:20 -06:00
Compare commits
3 Commits
feature-au
...
feature/mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a30fdc391f | ||
|
|
ef8323a8d5 | ||
|
|
3f467882bc |
@@ -8,6 +8,11 @@ echo "${log_prefix} Apply database migrations..."
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
|
if [[ "${PAPERLESS_MIGRATION_MODE:-0}" == "1" ]]; then
|
||||||
|
echo "${log_prefix} Migration mode enabled, skipping migrations."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# The whole migrate, with flock, needs to run as the right user
|
# The whole migrate, with flock, needs to run as the right user
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
|
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
|
||||||
|
|||||||
@@ -9,7 +9,15 @@ echo "${log_prefix} Running Django checks"
|
|||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
if [[ "${PAPERLESS_MIGRATION_MODE:-0}" == "1" ]]; then
|
||||||
|
python3 manage_migration.py check
|
||||||
|
else
|
||||||
python3 manage.py check
|
python3 manage.py check
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
|
if [[ "${PAPERLESS_MIGRATION_MODE:-0}" == "1" ]]; then
|
||||||
|
s6-setuidgid paperless python3 manage_migration.py check
|
||||||
|
else
|
||||||
s6-setuidgid paperless python3 manage.py check
|
s6-setuidgid paperless python3 manage.py check
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ if [[ -n "${PAPERLESS_FORCE_SCRIPT_NAME}" ]]; then
|
|||||||
export GRANIAN_URL_PATH_PREFIX=${PAPERLESS_FORCE_SCRIPT_NAME}
|
export GRANIAN_URL_PATH_PREFIX=${PAPERLESS_FORCE_SCRIPT_NAME}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ "${PAPERLESS_MIGRATION_MODE:-0}" == "1" ]]; then
|
||||||
exec granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
app_module="paperless.migration_asgi:application"
|
||||||
else
|
else
|
||||||
exec s6-setuidgid paperless granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
app_module="paperless.asgi:application"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
exec granian --interface asginl --ws --loop uvloop "${app_module}"
|
||||||
|
else
|
||||||
|
exec s6-setuidgid paperless granian --interface asginl --ws --loop uvloop "${app_module}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
13
src/manage_migration.py
Executable file
13
src/manage_migration.py
Executable file
@@ -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)
|
||||||
7
src/paperless/migration_asgi.py
Normal file
7
src/paperless/migration_asgi.py
Normal file
@@ -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()
|
||||||
0
src/paperless_migration/__init__.py
Normal file
0
src/paperless_migration/__init__.py
Normal file
6
src/paperless_migration/apps.py
Normal file
6
src/paperless_migration/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PaperlessMigrationConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "paperless_migration"
|
||||||
193
src/paperless_migration/settings.py
Normal file
193
src/paperless_migration/settings.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
"""Settings for migration-mode Django instance."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
# Tap paperless.conf if it's available
|
||||||
|
for path in [
|
||||||
|
os.getenv("PAPERLESS_CONFIGURATION_PATH"),
|
||||||
|
"../paperless.conf",
|
||||||
|
"/etc/paperless.conf",
|
||||||
|
"/usr/local/etc/paperless.conf",
|
||||||
|
]:
|
||||||
|
if path and Path(path).exists():
|
||||||
|
load_dotenv(path)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Paperless-ngx Migration Mode</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Migration Mode</h1>
|
||||||
|
<p>
|
||||||
|
This instance is running in migration mode. Use this interface to run
|
||||||
|
the v2 → v3 migration.
|
||||||
|
</p>
|
||||||
|
{% if messages %}
|
||||||
|
<ul>
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<section>
|
||||||
|
<h2>Step 1 — Export (v2)</h2>
|
||||||
|
<p>Expected export file:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Path:</strong> {{ export_path }}</li>
|
||||||
|
<li><strong>Status:</strong> {{ export_exists|yesno:"Found,Missing" }}</li>
|
||||||
|
</ul>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" name="action" value="check">
|
||||||
|
Re-check export
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Step 2 — Transform</h2>
|
||||||
|
<p>Expected transformed file:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Path:</strong> {{ transformed_path }}</li>
|
||||||
|
<li><strong>Status:</strong> {{ transformed_exists|yesno:"Found,Missing" }}</li>
|
||||||
|
</ul>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" name="action" value="transform">
|
||||||
|
Transform export
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Step 3 — Import (v3)</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" name="action" value="import">
|
||||||
|
Import transformed data
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
src/paperless_migration/urls.py
Normal file
9
src/paperless_migration/urls.py
Normal file
@@ -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"),
|
||||||
|
]
|
||||||
46
src/paperless_migration/views.py
Normal file
46
src/paperless_migration/views.py
Normal file
@@ -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)
|
||||||
7
src/paperless_migration/wsgi.py
Normal file
7
src/paperless_migration/wsgi.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user