Compare commits
11 Commits
0080b21bb0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a5aa45759c | |||
| c5b161487b | |||
| 7e11b2d531 | |||
| 7b34cb0340 | |||
| 17cd5b32da | |||
| c4e677aa5c | |||
| c89f66f550 | |||
| 7e83d0806c | |||
| 53dcbf2f95 | |||
| be5c8dec26 | |||
| 979354f87b |
21
.env.example
21
.env.example
@@ -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
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,28 +1,28 @@
|
||||
# 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 \
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt \
|
||||
&& pip install --no-cache-dir python-multipart
|
||||
|
||||
# Copy app code
|
||||
COPY . /immich_drop
|
||||
COPY . /image_drop
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
32
app/app.py
32
app/app.py
@@ -17,6 +17,7 @@ import hashlib
|
||||
import os
|
||||
import sqlite3
|
||||
import binascii
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
@@ -233,7 +234,8 @@ def _hash_password(pw: str) -> str:
|
||||
salt = os.urandom(16)
|
||||
iterations = 200_000
|
||||
dk = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations)
|
||||
return f"pbkdf2_sha256${iterations}${binascii.hexlify(salt).decode()}${binascii.hexlify(dk).decode()}"
|
||||
# use - as the delimiter to avoid Docker env variable substitution
|
||||
return f"pbkdf2_sha256-{iterations}-{binascii.hexlify(salt).decode()}-{binascii.hexlify(dk).decode()}"
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@@ -242,7 +244,7 @@ def _verify_password(stored: str, pw: Optional[str]) -> bool:
|
||||
if not pw or not stored:
|
||||
return False
|
||||
try:
|
||||
algo, iter_s, salt_hex, hash_hex = stored.split("$")
|
||||
algo, iter_s, salt_hex, hash_hex = stored.split("-")
|
||||
if algo != 'pbkdf2_sha256':
|
||||
return False
|
||||
iterations = int(iter_s)
|
||||
@@ -256,8 +258,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"
|
||||
safe_album_name = sanitize_filename(album_name)
|
||||
save_dir = os.path.join("./data/uploads", safe_album_name)
|
||||
|
||||
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", "albums", safe_album_name)
|
||||
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
return save_dir
|
||||
|
||||
@@ -889,7 +902,7 @@ async def api_login(request: Request) -> JSONResponse:
|
||||
|
||||
stored_password = SETTINGS.admin_password
|
||||
password_ok = False
|
||||
if stored_password.startswith("pbkdf2_sha256$"):
|
||||
if stored_password.startswith("pbkdf2_sha256-"):
|
||||
password_ok = _verify_password(stored_password, password)
|
||||
else:
|
||||
password_ok = (password == stored_password)
|
||||
@@ -926,13 +939,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:
|
||||
|
||||
@@ -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."""
|
||||
@@ -33,7 +34,8 @@ def _hash_password(pw: str) -> str:
|
||||
salt = os.urandom(16)
|
||||
iterations = 200_000
|
||||
dk = hashlib.pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations)
|
||||
return f"pbkdf2_sha256${iterations}${binascii.hexlify(salt).decode()}${binascii.hexlify(dk).decode()}"
|
||||
# use - as the delimiter to avoid Docker env variable substitution
|
||||
return f"pbkdf2_sha256-{iterations}-{binascii.hexlify(salt).decode()}-{binascii.hexlify(dk).decode()}"
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@@ -45,7 +47,7 @@ def load_settings() -> Settings:
|
||||
except Exception:
|
||||
pass
|
||||
admin_password = os.getenv("ADMIN_PASSWORD", "admin") # Default for convenience, should be changed
|
||||
if not admin_password.startswith("pbkdf2_sha256$"):
|
||||
if not admin_password.startswith("pbkdf2_sha256-"):
|
||||
print("="*60)
|
||||
print("WARNING: ADMIN_PASSWORD is in plaintext.")
|
||||
print("For better security, use the hashed password below in your .env file:")
|
||||
@@ -71,6 +73,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 +84,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
0
data/.gitkeep
Normal 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:
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user