Compare commits

...

9 Commits

Author SHA1 Message Date
7e11b2d531 Prepare for Docker deployment 2025-11-23 11:32:48 -07:00
7b34cb0340 Remove another Immich reference 2025-11-23 11:32:30 -07:00
17cd5b32da refactor: Load environment variables from .env file
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-11-23 11:21:51 -07:00
c4e677aa5c Freeze requirements 2025-11-23 11:18:46 -07:00
c89f66f550 Update example .env 2025-11-23 11:18:34 -07:00
7e83d0806c refactor: Move album storage into nested 'albums' directory
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-11-23 10:47:59 -07:00
53dcbf2f95 refactor: Simplify pytz import and remove conditional checks 2025-11-23 10:47:56 -07:00
be5c8dec26 feat: Organize public uploads into daily directories based on timezone
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2025-11-23 10:43:16 -07:00
979354f87b gitkeep data/ 2025-11-23 10:38:08 -07:00
8 changed files with 47 additions and 50 deletions

View File

@@ -1,16 +1,12 @@
# Server (dev only)
HOST=0.0.0.0
PORT=8080
# Immich connection (include /api)
IMMICH_BASE_URL=http://REPLACE_ME:2283/api
IMMICH_API_KEY=ADD-YOUR-API-KEY # needs: asset.upload; for albums also: album.create, album.read, albumAsset.create
MAX_CONCURRENT=3
# Public uploader page (optional) — disabled by default
PUBLIC_UPLOAD_PAGE_ENABLED=TRUE
ADMIN_PASSWORD=test123
TIMEZONE=America/Edmonton
# Album (optional): auto-add uploads from public uploader to this album (creates if needed)
IMMICH_ALBUM_NAME=dead-drop
# Public uploader page (optional) — disabled by default
PUBLIC_UPLOAD_PAGE_ENABLED=false
# Local dedupe cache (SQLite)
STATE_DB=./data/state.db
@@ -19,13 +15,14 @@ STATE_DB=./data/state.db
# e.g., PUBLIC_BASE_URL=https://photos.example.com
#PUBLIC_BASE_URL=
# Session and security
SESSION_SECRET=SET-A-STRONG-RANDOM-VALUE
LOG_LEVEL=DEBUG
LOG_LEVEL=INFO
# Chunked uploads (to work around 100MB proxy limits)
# Enable to send files in chunks from browser to this service; the service reassembles and forwards to Immich.
# Enable to send files in chunks from browser to this service
# Recommended chunk size for Cloudflare Tunnel is <= 95MB.
CHUNKED_UPLOADS_ENABLED=false
CHUNK_SIZE_MB=95
# Custom session secrets
# By default, a random one is generated
#SESSION_SECRET=SET-A-STRONG-RANDOM-VALUE

View File

@@ -1,28 +1,27 @@
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim
WORKDIR /immich_drop
WORKDIR /image_drop
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Install Python deps
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt \
&& pip install --no-cache-dir python-multipart
# Copy app code
COPY . /immich_drop
COPY . /image_drop
# Install Python deps
RUN pip install --no-cache-dir -r requirements.txt \
&& pip install --no-cache-dir python-multipart
# Data dir for SQLite (state.db)
RUN mkdir -p /data
VOLUME ["/data"]
#RUN mkdir -p /data
#VOLUME ["/data"]
# Defaults (can be overridden via compose env)
ENV HOST=0.0.0.0 \
PORT=8080 \
STATE_DB=/data/state.db
STATE_DB=/image_drop/data/state.db
EXPOSE 8080

View File

@@ -17,6 +17,7 @@ import hashlib
import os
import sqlite3
import binascii
import pytz
from datetime import datetime
from typing import Dict, List, Optional
@@ -256,8 +257,19 @@ def get_or_create_album_dir(album_name: str) -> str:
"""Get or create a directory for an album. Returns the path."""
if not album_name or not isinstance(album_name, str):
album_name = "public"
if album_name == "public":
try:
tz = pytz.timezone(SETTINGS.timezone)
today = datetime.now(tz).strftime('%Y-%m-%d')
save_dir = os.path.join("./data/uploads", "public", today)
except Exception as e:
logger.warning("Timezone logic failed, falling back to 'public' album. Timezone: %s. Error: %s", SETTINGS.timezone, e)
save_dir = os.path.join("./data/uploads", "public")
else:
safe_album_name = sanitize_filename(album_name)
save_dir = os.path.join("./data/uploads", safe_album_name)
save_dir = os.path.join("./data/uploads", "albums", safe_album_name)
os.makedirs(save_dir, exist_ok=True)
return save_dir
@@ -926,13 +938,14 @@ async def api_albums(request: Request) -> JSONResponse:
return JSONResponse({"error": "unauthorized"}, status_code=401)
upload_root = "./data/uploads"
albums_root = os.path.join(upload_root, "albums")
try:
os.makedirs(upload_root, exist_ok=True)
os.makedirs(albums_root, exist_ok=True)
# also make public dir
os.makedirs(os.path.join(upload_root, "public"), exist_ok=True)
albums = []
for name in os.listdir(upload_root):
if os.path.isdir(os.path.join(upload_root, name)):
albums = [{"id": "public", "albumName": "public"}]
for name in os.listdir(albums_root):
if os.path.isdir(os.path.join(albums_root, name)):
albums.append({"id": name, "albumName": name})
return JSONResponse(albums)
except Exception as e:

View File

@@ -24,6 +24,7 @@ class Settings:
log_level: str = "INFO"
chunked_uploads_enabled: bool = False
chunk_size_mb: int = 95
timezone: str = "UTC"
def _hash_password(pw: str) -> str:
"""Return PBKDF2-SHA256 hash of a password."""
@@ -71,6 +72,7 @@ def load_settings() -> Settings:
chunk_size_mb = int(os.getenv("CHUNK_SIZE_MB", "95"))
except ValueError:
chunk_size_mb = 95
timezone = os.getenv("TIMEZONE", "UTC")
return Settings(
admin_password=admin_password,
max_concurrent=maxc,
@@ -81,4 +83,5 @@ def load_settings() -> Settings:
log_level=log_level,
chunked_uploads_enabled=chunked_uploads_enabled,
chunk_size_mb=chunk_size_mb,
timezone=timezone,
)

0
data/.gitkeep Normal file
View File

View File

@@ -1,33 +1,17 @@
services:
immich-drop:
image-drop:
build: .
container_name: immich-drop
container_name: image-drop
restart: unless-stopped
ports:
- "8080:8080"
environment:
#immich drop server ip
PUBLIC_BASE_URL: http://192.168.8.60:8080
# Immich connection
IMMICH_BASE_URL: http://192.168.8.60:2283/api
IMMICH_API_KEY: n7lO2oRFVhMXqI10YL8nfelIC9lZ8ND8AxZqx1XHiA
#Enable/Disable Public upload page to folder
PUBLIC_UPLOAD_PAGE_ENABLED: false
IMMICH_ALBUM_NAME: dead-drop
# Chunked uploads to bypass 100MB limits (e.g., Cloudflare Tunnel)
CHUNKED_UPLOADS_ENABLED: true
CHUNK_SIZE_MB: 95
env_file:
- .env
volumes:
- immich_drop_data:/data
- ./data:/image_drop/data
healthcheck:
test: ["CMD-SHELL", "python - <<'PY'\nimport urllib.request,sys\ntry:\n urllib.request.urlopen('http://localhost:8080/').read(); sys.exit(0)\nexcept Exception:\n sys.exit(1)\nPY"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
volumes:
immich_drop_data:

View File

@@ -14,7 +14,7 @@
<div class="mx-auto max-w-2xl p-6 space-y-6">
<div id="topBanner" class="hidden rounded-2xl border border-green-200 bg-green-50 p-3 text-green-700 text-center dark:bg-green-900 dark:border-green-700 dark:text-green-300"></div>
<header class="flex items-center justify-between">
<h1 class="text-2xl font-semibold">Login to Immich</h1>
<h1 class="text-2xl font-semibold">Login to Image Drop</h1>
<div class="flex items-center gap-2">
<a id="linkPublicUploader" href="/" class="hidden rounded-xl border px-3 py-1 text-sm dark:border-gray-600">Public uploader</a>
<button id="btnTheme" class="rounded-xl border px-3 py-1 text-sm dark:border-gray-600" title="Toggle dark mode">

View File

@@ -16,6 +16,7 @@ pydantic==2.11.7
pydantic_core==2.33.2
python-dotenv==1.1.1
python-multipart==0.0.20
pytz==2025.2
PyYAML==6.0.2
qrcode==8.2
requests==2.32.5