Files
kennethreitz e29aefc065 Make S3 custom domain and addressing style env-driven
Defaults unchanged (Tigris virtual-host style); MinIO needs
path-style and a custom public host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:08:15 -04:00

269 lines
8.1 KiB
Python

"""
Django settings for exiftree project.
Generated by 'django-admin startproject' using Django 6.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
import os
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-x8w4k=pjv*+&gl02p#ucsdh7*5)k&m7av=ehc8_6v756pg@f=j")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", "1") == "1"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "https://exiftree.fly.dev,https://exiftree.org,https://photos.kennethreitz.org").split(",")
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sitemaps",
"django_bolt",
"storages",
"core",
"tree",
"gallery",
"ingest",
"search",
]
AUTH_USER_MODEL = "core.User"
MIDDLEWARE = [
"core.middleware.RequestLoggingMiddleware",
"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 = "exiftree.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"core.context_processors.site_context",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "exiftree.wsgi.application"
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
import dj_database_url
DATABASES = {
"default": dj_database_url.config(
default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}",
conn_max_age=10,
conn_health_checks=True,
)
}
if DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql":
# Fail fast if Postgres disappears — otherwise in-flight queries hang on the
# TCP socket forever, saturate the worker pool, and wedge the whole app.
DATABASES["default"].setdefault("OPTIONS", {}).update({
"connect_timeout": 5,
"keepalives": 1,
"keepalives_idle": 30,
"keepalives_interval": 10,
"keepalives_count": 3,
"options": "-c statement_timeout=30000",
})
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
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",
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]
# Media files
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# Object storage (S3-compatible — Tigris, R2, etc.)
# Set these via environment variables in production.
if os.environ.get("AWS_STORAGE_BUCKET_NAME") or os.environ.get("BUCKET_NAME"):
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME") or os.environ.get("BUCKET_NAME", "")
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL") or os.environ.get("AWS_ENDPOINT_URL_S3", "")
AWS_S3_REGION_NAME = os.environ.get("AWS_S3_REGION_NAME", "auto")
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
AWS_S3_ADDRESSING_STYLE = os.environ.get("AWS_S3_ADDRESSING_STYLE", "virtual")
# Public URL host for media (no scheme). Path-style providers like MinIO
# want "s3.example.org/<bucket>"; defaults to Tigris's virtual-host style.
AWS_S3_CUSTOM_DOMAIN = os.environ.get(
"AWS_S3_CUSTOM_DOMAIN", f"{AWS_STORAGE_BUCKET_NAME}.fly.storage.tigris.dev"
)
AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False
# Celery — use Postgres as broker in production, Redis locally
_db_url = os.environ.get("DATABASE_URL", "")
if _db_url and not os.environ.get("CELERY_BROKER_URL"):
# Convert postgres:// to sqla+postgresql:// for Celery's SQLAlchemy transport
_sqla_url = _db_url.replace("postgres://", "postgresql://", 1)
CELERY_BROKER_URL = f"sqla+{_sqla_url}"
CELERY_RESULT_BACKEND = "db+" + _sqla_url
else:
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
CELERY_BROKER_POOL_LIMIT = 1
CELERY_BROKER_TRANSPORT_OPTIONS = {'max_retries': 3}
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'standard',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
'loggers': {
'django.request': {
'handlers': ['console'],
'level': 'WARNING',
'propagate': False,
},
'django.db.backends': {
'handlers': ['console'],
'level': 'WARNING',
'propagate': False,
},
'urllib3.connectionpool': {
'handlers': ['console'],
'level': 'ERROR',
'propagate': False,
},
'botocore': {
'handlers': ['console'],
'level': 'WARNING',
'propagate': False,
},
'httpx': {
'handlers': ['console'],
'level': 'WARNING',
'propagate': False,
},
'ingest': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
'core': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
'core.requests': {
'handlers': ['console'],
'level': 'INFO',
'propagate': False,
},
},
}
# Email — configure a real backend in production
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEFAULT_FROM_EMAIL = "ExifTree <noreply@exiftree.org>"
# Upload limits
MAX_UPLOAD_SIZE = 50 * 1024 * 1024 # 50 MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 50 * 1024 * 1024 # 50 MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 50 * 1024 * 1024 # 50 MB