From 912446d9b70a383b754b0b0f379b9ddc76a78dbf Mon Sep 17 00:00:00 2001 From: Wally Hackenslacker Date: Sun, 22 Mar 2026 22:52:07 -0400 Subject: [PATCH] Refactor local settings to dotenv-based gallery paths --- .env.example | 2 + AGENTS.md | 22 ++++---- CLAUDE.md | 23 ++++----- NibasaViewer/settings.py | 109 +++++++++++++++++++++------------------ requirements.txt | 1 + 5 files changed, 83 insertions(+), 74 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b17e855 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +GALLERY_ROOT=/absolute/path/to/images +THUMBNAILS_ROOT=/absolute/path/to/thumb-cache diff --git a/AGENTS.md b/AGENTS.md index 860f1d5..88ca147 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,19 +28,21 @@ This document is intentionally explicit so tasks can be completed with minimal r - Apply migrations: `python manage.py migrate` - Run server: `python manage.py runserver` -Local configuration (`NibasaViewer/local_settings.py`) is required for real gallery usage: +Local configuration is loaded from `.env` via `python-dotenv`. -```python -from pathlib import Path +Use this minimum `.env` content for gallery paths: -GALLERY_ROOT = Path('/path/to/images') -THUMBNAILS_ROOT = Path('/path/to/thumb-cache') - -DEBUG = False -ALLOWED_HOSTS = ['yourdomain.example'] -SECRET_KEY = 'replace-me' +```env +GALLERY_ROOT=/path/to/images +THUMBNAILS_ROOT=/path/to/thumb-cache ``` +Notes: + +- `GALLERY_ROOT` and `THUMBNAILS_ROOT` are parsed as `pathlib.Path` in `settings.py`. +- Both are optional at import time and default to `None` when unset/empty. +- Paths do not need to be inside `BASE_DIR`. + ## 4) Build / Lint / Test Commands There is no dedicated linter or formatter config in-repo (no `ruff`, `flake8`, `black`, `mypy`, `pyright`, `tox`, or `pytest` config files detected). @@ -145,7 +147,7 @@ Notes: ## 9) Safety and Config Hygiene - Never commit secrets or production-specific local settings. -- Keep `local_settings.py` local; defaults in `settings.py` should remain safe placeholders. +- Keep `.env` local and out of version control; use `.env.example` as a template. - Avoid destructive git operations unless explicitly requested. - Do not revert unrelated working tree changes made by humans. diff --git a/CLAUDE.md b/CLAUDE.md index 51aeb1c..bf89427 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ The project has one Django app: **`viewer`**. All gallery functionality is conso ### Gallery Data Flow **Filesystem as Data Source:** -- Images served from `GALLERY_ROOT` path (configured in `local_settings.py`) +- Images served from `GALLERY_ROOT` path (configured in `.env`) - Thumbnails cached in `THUMBNAILS_ROOT` path - No database models for images or directories - Directory structure navigated using Python's `pathlib` @@ -108,22 +108,17 @@ Uses Django's built-in `django.contrib.auth` system: ## Configuration -### Required Local Settings +### Required Environment Variables -Create `NibasaViewer/local_settings.py` (not in git): +Create `.env` (not in git): -```python -from pathlib import Path - -GALLERY_ROOT = Path('/path/to/your/images') -THUMBNAILS_ROOT = Path('/path/to/thumbnail/cache') - -# Production settings -DEBUG = False -ALLOWED_HOSTS = ['yourdomain.com'] -SECRET_KEY = 'your-secret-key' +```env +GALLERY_ROOT=/path/to/your/images +THUMBNAILS_ROOT=/path/to/thumbnail/cache ``` +`settings.py` loads `.env` with `python-dotenv` and parses both values as `pathlib.Path`. Each variable is optional and resolves to `None` when unset or empty. + ### Pagination Constants Defined in viewer/views.py:23-25: @@ -143,7 +138,7 @@ Default production location: `/var/lib/NibasaViewer` - **Django 4.2.3** - Web framework - **Pillow 10.0.0** - Image processing (thumbnails) -- **filetype 1.2.0** - Legacy dependency (no longer actively used) +- **python-dotenv 1.1.1** - Loads local `.env` configuration - **gunicorn 21.2.0** - WSGI server (production) ## Code Patterns diff --git a/NibasaViewer/settings.py b/NibasaViewer/settings.py index 38b732d..d15b0f2 100644 --- a/NibasaViewer/settings.py +++ b/NibasaViewer/settings.py @@ -13,15 +13,31 @@ https://docs.djangoproject.com/en/4.2/ref/settings/ import os from pathlib import Path +# Third-party imports. +from dotenv import load_dotenv + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +load_dotenv(BASE_DIR / ".env") + + +def _path_from_env(var_name): + value = os.getenv(var_name) + if value is None: + return None + + value = value.strip() + if not value: + return None + + return Path(value) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-#_89g9-8to*_ogxz_e0jpnqlreo0hy10odxc_)99$cs66=#7(*' +SECRET_KEY = "django-insecure-#_89g9-8to*_ogxz_e0jpnqlreo0hy10odxc_)99$cs66=#7(*" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -33,55 +49,54 @@ ALLOWED_HOSTS = [] INSTALLED_APPS = [ # Django apps. - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", # Project apps. - 'viewer' + "viewer", ] 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', + "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 = 'NibasaViewer.urls' +ROOT_URLCONF = "NibasaViewer.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'NibasaViewer.wsgi.application' +WSGI_APPLICATION = "NibasaViewer.wsgi.application" # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -91,30 +106,30 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Authentication. -LOGIN_REDIRECT_URL = '/gallery/' -LOGOUT_REDIRECT_URL = 'login' -LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = "/gallery/" +LOGOUT_REDIRECT_URL = "login" +LOGIN_URL = "login" # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -124,20 +139,14 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = 'static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'static/') +STATIC_URL = "static/" +STATIC_ROOT = os.path.join(BASE_DIR, "static/") # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Gallery paths. -GALLERY_ROOT = None -THUMBNAILS_ROOT = None - -# Attempt to load local settings if any. -try: - from .local_settings import * -except ImportError: - pass +GALLERY_ROOT = _path_from_env("GALLERY_ROOT") +THUMBNAILS_ROOT = _path_from_env("THUMBNAILS_ROOT") diff --git a/requirements.txt b/requirements.txt index feeb6fc..4b7a36f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==4.2.3 gunicorn==21.2.0 Pillow==10.0.0 +python-dotenv==1.1.1