Compare commits
9 Commits
0080b21bb0
...
7e11b2d531
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
HOST=0.0.0.0
|
||||||
PORT=8080
|
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
|
MAX_CONCURRENT=3
|
||||||
|
|
||||||
# Public uploader page (optional) — disabled by default
|
ADMIN_PASSWORD=test123
|
||||||
PUBLIC_UPLOAD_PAGE_ENABLED=TRUE
|
TIMEZONE=America/Edmonton
|
||||||
|
|
||||||
# Album (optional): auto-add uploads from public uploader to this album (creates if needed)
|
# Public uploader page (optional) — disabled by default
|
||||||
IMMICH_ALBUM_NAME=dead-drop
|
PUBLIC_UPLOAD_PAGE_ENABLED=false
|
||||||
|
|
||||||
# Local dedupe cache (SQLite)
|
# Local dedupe cache (SQLite)
|
||||||
STATE_DB=./data/state.db
|
STATE_DB=./data/state.db
|
||||||
@@ -19,13 +15,14 @@ STATE_DB=./data/state.db
|
|||||||
# e.g., PUBLIC_BASE_URL=https://photos.example.com
|
# e.g., PUBLIC_BASE_URL=https://photos.example.com
|
||||||
#PUBLIC_BASE_URL=
|
#PUBLIC_BASE_URL=
|
||||||
|
|
||||||
# Session and security
|
LOG_LEVEL=INFO
|
||||||
SESSION_SECRET=SET-A-STRONG-RANDOM-VALUE
|
|
||||||
LOG_LEVEL=DEBUG
|
|
||||||
|
|
||||||
# Chunked uploads (to work around 100MB proxy limits)
|
# 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.
|
# Recommended chunk size for Cloudflare Tunnel is <= 95MB.
|
||||||
CHUNKED_UPLOADS_ENABLED=false
|
CHUNKED_UPLOADS_ENABLED=false
|
||||||
CHUNK_SIZE_MB=95
|
CHUNK_SIZE_MB=95
|
||||||
|
|
||||||
|
# Custom session secrets
|
||||||
|
# By default, a random one is generated
|
||||||
|
#SESSION_SECRET=SET-A-STRONG-RANDOM-VALUE
|
||||||
|
|||||||
19
Dockerfile
19
Dockerfile
@@ -1,28 +1,27 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /immich_drop
|
WORKDIR /image_drop
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=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 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)
|
# Data dir for SQLite (state.db)
|
||||||
RUN mkdir -p /data
|
#RUN mkdir -p /data
|
||||||
VOLUME ["/data"]
|
#VOLUME ["/data"]
|
||||||
|
|
||||||
# Defaults (can be overridden via compose env)
|
# Defaults (can be overridden via compose env)
|
||||||
ENV HOST=0.0.0.0 \
|
ENV HOST=0.0.0.0 \
|
||||||
PORT=8080 \
|
PORT=8080 \
|
||||||
STATE_DB=/data/state.db
|
STATE_DB=/image_drop/data/state.db
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|||||||
23
app/app.py
23
app/app.py
@@ -17,6 +17,7 @@ import hashlib
|
|||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import binascii
|
import binascii
|
||||||
|
import pytz
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional
|
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."""
|
"""Get or create a directory for an album. Returns the path."""
|
||||||
if not album_name or not isinstance(album_name, str):
|
if not album_name or not isinstance(album_name, str):
|
||||||
album_name = "public"
|
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)
|
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)
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
return save_dir
|
return save_dir
|
||||||
|
|
||||||
@@ -926,13 +938,14 @@ async def api_albums(request: Request) -> JSONResponse:
|
|||||||
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
||||||
|
|
||||||
upload_root = "./data/uploads"
|
upload_root = "./data/uploads"
|
||||||
|
albums_root = os.path.join(upload_root, "albums")
|
||||||
try:
|
try:
|
||||||
os.makedirs(upload_root, exist_ok=True)
|
os.makedirs(albums_root, exist_ok=True)
|
||||||
# also make public dir
|
# also make public dir
|
||||||
os.makedirs(os.path.join(upload_root, "public"), exist_ok=True)
|
os.makedirs(os.path.join(upload_root, "public"), exist_ok=True)
|
||||||
albums = []
|
albums = [{"id": "public", "albumName": "public"}]
|
||||||
for name in os.listdir(upload_root):
|
for name in os.listdir(albums_root):
|
||||||
if os.path.isdir(os.path.join(upload_root, name)):
|
if os.path.isdir(os.path.join(albums_root, name)):
|
||||||
albums.append({"id": name, "albumName": name})
|
albums.append({"id": name, "albumName": name})
|
||||||
return JSONResponse(albums)
|
return JSONResponse(albums)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Settings:
|
|||||||
log_level: str = "INFO"
|
log_level: str = "INFO"
|
||||||
chunked_uploads_enabled: bool = False
|
chunked_uploads_enabled: bool = False
|
||||||
chunk_size_mb: int = 95
|
chunk_size_mb: int = 95
|
||||||
|
timezone: str = "UTC"
|
||||||
|
|
||||||
def _hash_password(pw: str) -> str:
|
def _hash_password(pw: str) -> str:
|
||||||
"""Return PBKDF2-SHA256 hash of a password."""
|
"""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"))
|
chunk_size_mb = int(os.getenv("CHUNK_SIZE_MB", "95"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
chunk_size_mb = 95
|
chunk_size_mb = 95
|
||||||
|
timezone = os.getenv("TIMEZONE", "UTC")
|
||||||
return Settings(
|
return Settings(
|
||||||
admin_password=admin_password,
|
admin_password=admin_password,
|
||||||
max_concurrent=maxc,
|
max_concurrent=maxc,
|
||||||
@@ -81,4 +83,5 @@ def load_settings() -> Settings:
|
|||||||
log_level=log_level,
|
log_level=log_level,
|
||||||
chunked_uploads_enabled=chunked_uploads_enabled,
|
chunked_uploads_enabled=chunked_uploads_enabled,
|
||||||
chunk_size_mb=chunk_size_mb,
|
chunk_size_mb=chunk_size_mb,
|
||||||
|
timezone=timezone,
|
||||||
)
|
)
|
||||||
|
|||||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
@@ -1,33 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
immich-drop:
|
image-drop:
|
||||||
build: .
|
build: .
|
||||||
container_name: immich-drop
|
container_name: image-drop
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
env_file:
|
||||||
environment:
|
- .env
|
||||||
#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
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- immich_drop_data:/data
|
- ./data:/image_drop/data
|
||||||
healthcheck:
|
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"]
|
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
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
volumes:
|
|
||||||
immich_drop_data:
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="mx-auto max-w-2xl p-6 space-y-6">
|
<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>
|
<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">
|
<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">
|
<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>
|
<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">
|
<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
|
pydantic_core==2.33.2
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
python-multipart==0.0.20
|
python-multipart==0.0.20
|
||||||
|
pytz==2025.2
|
||||||
PyYAML==6.0.2
|
PyYAML==6.0.2
|
||||||
qrcode==8.2
|
qrcode==8.2
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
|
|||||||
Reference in New Issue
Block a user