updates to compose file .env file final one_time_linke release

This commit is contained in:
MEGASOL\simon.adams
2025-09-02 09:56:43 +02:00
parent 60f1526f06
commit dedfd339a2
12 changed files with 143 additions and 105 deletions

View File

@@ -1,17 +1,25 @@
# Backend host/port # Server (dev only)
HOST=0.0.0.0 HOST=0.0.0.0
PORT=8080 PORT=8080
# Immich connection (include /api)
# Immich connection IMMICH_BASE_URL=http://REPLACE_ME:2283/api
IMMICH_BASE_URL=http://127.0.0.1:2283/api IMMICH_API_KEY=ADD-YOUR-API-KEY # needs: asset.upload; for albums also: album.create, album.read, albumAsset.create
IMMICH_API_KEY=ADD-YOUR-API-KEY #key needs asset.upload (default functions)
MAX_CONCURRENT=3 MAX_CONCURRENT=3
# Optional: Public upload page # Public uploader page (optional) — disabled by default
PUBLIC_UPLOAD_PAGE_ENABLED=true PUBLIC_UPLOAD_PAGE_ENABLED=TRUE
# Optional: Album name for public upload page # Album (optional): auto-add uploads from public uploader to this album (creates if needed)
IMMICH_ALBUM_NAME=dead-drop #key needs album.create,album.read,albumAsset.create (extended functions) IMMICH_ALBUM_NAME=dead-drop
# Local dedupe cache (SQLite)
STATE_DB=./data/state.db
# Base URL for generating absolute invite links (recommended for production)
# 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
# Data path inside the container
STATE_DB=/data/state.db

178
README.md
View File

@@ -1,20 +1,24 @@
# Immich Drop Uploader # Immich Drop Uploader
A tiny, zero-login web app for collecting photos/videos into your **Immich** server. A tiny web app for collecting photos/videos into your **Immich** server.
Admin users log in to create public invite links; invite links are always public-by-URL. A public uploader page is optional and disabled by default.
![Immich Drop Uploader Dark Mode UI](./screenshot.png) ![Immich Drop Uploader Dark Mode UI](./screenshot.png)
## Features ## Features
- **No accounts** — open the page, drop files, done - **Invite links (public)** — create upload links you can share with anyone
- **Onetime link claim** — first browser session claims a onetime link; it can upload multiple files, others are blocked
- **Optional public uploader** — disabled by default; can be enabled via `.env`
- **Queue with progress** via WebSocket (success / duplicate / error) - **Queue with progress** via WebSocket (success / duplicate / error)
- **Duplicate prevention** (local SHA1 cache + optional Immich bulkcheck) - **Duplicate prevention** (local SHA1 cache + optional Immich bulkcheck)
- **Original dates preserved** (EXIF → `fileCreatedAt` / `fileModifiedAt`) - **Original dates preserved** (EXIF → `fileCreatedAt` / `fileModifiedAt`)
- **Mobilefriendly** - **Mobilefriendly**
- **.envonly config** (clean deploys) + Docker/Compose - **.envonly config** (clean deploys) + Docker/Compose
- **Privacyfirst**: never lists server media; UI only shows the current session - **Privacyfirst**: never lists server media; UI only shows the current session
- **Dark mode support** — automatically detects system preference, with manual toggle - **Dark mode** — detects system preference; manual toggle persists across pages
- **Album integration** — auto-adds uploads to a configured album (creates if needed) - **Albums** — add uploads to a configured album (creates if needed)
- **Copy + QR** — copy invite link and display QR for easy sharing
--- ---
@@ -34,10 +38,10 @@ A tiny, zero-login web app for collecting photos/videos into your **Immich** ser
--- ---
## Quick start ## Quick start
Copy the docker-compose.yml and the .env file to a common folder, You can run without a `.env` file by putting all settings in `docker-compose.yml` (recommended for deploys).
update the .env file before executing the CLI commands to quick start the container. Use a `.env` file only for local development.
### docker-compose.yml ### docker-compose.yml (deploy without .env)
```yaml ```yaml
version: "3.9" version: "3.9"
@@ -48,15 +52,25 @@ services:
container_name: immich-drop container_name: immich-drop
restart: unless-stopped restart: unless-stopped
# Optional: Set album name for auto-adding uploads # Configure all settings here (no .env required)
environment: environment:
IMMICH_ALBUM_NAME: dead-drop # Optional: uploads will be added to this album # Server (container port is 8080 by default)
HOST: 0.0.0.0
PORT: 8080
# Load all variables from your repo's .env (PORT, IMMICH_BASE_URL, IMMICH_API_KEY, etc.) # Immich connection (must include /api)
env_file: IMMICH_BASE_URL: https://immich.example.com/api
- ./.env IMMICH_API_KEY: ${IMMICH_API_KEY}
# Expose the app on the same port as configured in .env (defaults to 8080) # Optional behavior
IMMICH_ALBUM_NAME: dead-drop
PUBLIC_UPLOAD_PAGE_ENABLED: "false" # keep disabled by default
PUBLIC_BASE_URL: https://drop.example.com
# App internals
SESSION_SECRET: ${SESSION_SECRET}
# Expose the app on the host
ports: ports:
- 8080:8080 - 8080:8080
@@ -76,18 +90,7 @@ volumes:
immich_drop_data: immich_drop_data:
``` ```
### .env
``` ```
HOST=0.0.0.0
PORT=8080
IMMICH_BASE_URL=http://REPLACE_ME:2283/api
IMMICH_API_KEY=REPLACE_ME
MAX_CONCURRENT=3
IMMICH_ALBUM_NAME=dead-drop # Optional: auto-add uploads to this album
STATE_DB=/data/state.db
```
### CLI ### CLI
```bash ```bash
docker compose pull docker compose pull
@@ -97,6 +100,17 @@ docker compose up -d
## New Features ## New Features
### 🔐 Login + Menu
- Login with your Immich credentials to access the menu.
- The menu lets you list/create albums and create invite links.
- The menu is always behind login; logout clears the session.
### 🔗 Invite Links
- Links are always public by URL (no login required to use).
- You can make links onetime (claimed by the first browser session) or indefinite / limited uses.
- Set link expiry (e.g., 1, 2, 7 days). Expired links are inactive.
- Copy link and view a QR code for easy sharing.
### 🌙 Dark Mode ### 🌙 Dark Mode
- Automatically detects system dark/light preference on first visit - Automatically detects system dark/light preference on first visit
- Manual toggle button in the header (sun/moon icon) - Manual toggle button in the header (sun/moon icon)
@@ -134,18 +148,24 @@ docker compose up -d
``` ```
immich_drop/ immich_drop/
├─ app/ # FastAPI application (Python package) ├─ app/ # FastAPI application (Python package)
│ ├─ __init__.py │ ├─ app.py # ASGI app (uvicorn entry: app.app:app)
app.py # uvicorn app:app config.py # Settings loader (reads .env/env)
│ └─ config.py # loads .env from repo root ├─ frontend/ # Static UI (served at /static)
├─ frontend/ # static UI served at /static │ ├─ index.html # Public uploader (optional)
│ ├─ index.html │ ├─ login.html # Login page (admin)
app.js menu.html # Admin menu (create invites)
├─ main.py # thin entrypoint (python main.py) │ ├─ invite.html # Public invite upload page
├─ requirements.txt # Python deps │ ├─ app.js # Uploader logic (drop/queue/upload/ws)
├─ .env # single config file (see below) │ ├─ header.js # Shared header (theme + ping + banner)
│ └─ favicon.png # Tab icon (optional)
├─ data/ # Local dev data dir (bind to /data in Docker)
├─ main.py # Thin dev entrypoint (python main.py)
├─ requirements.txt # Python dependencies
├─ Dockerfile ├─ Dockerfile
├─ docker-compose.yml ├─ docker-compose.yml
README.md .env.example # Example dev environment (optional)
├─ README.md
└─ screenshot.png # UI screenshot for README
``` ```
--- ---
@@ -156,25 +176,49 @@ immich_drop/
- An **Immich** server + **API key** - An **Immich** server + **API key**
--- ---
## Configuration (.env) # Local dev quickstart
## Development
Run with live reload:
```bash
python main.py
```
The backend contains docstrings so you can generate docs later if desired.
---
## Dev Configuration (.env)
```ini ```ini
# Server # Server (dev only)
HOST=0.0.0.0 HOST=0.0.0.0
PORT=8080 PORT=8080
# Immich connection (include /api) # Immich connection (include /api)
IMMICH_BASE_URL=http://REPLACE_ME:2283/api IMMICH_BASE_URL=http://REPLACE_ME:2283/api
IMMICH_API_KEY=ADD-YOUR-API-KEY #key needs asset.upload (default functions) 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
# Optional: Album name for auto-adding uploads (creates if doesn't exist) # Public uploader page (optional) — disabled by default
IMMICH_ALBUM_NAME=dead-drop #key needs album.create,album.read,albumAsset.create (extended functions) PUBLIC_UPLOAD_PAGE_ENABLED=TRUE
# Album (optional): auto-add uploads from public uploader to this album (creates if needed)
IMMICH_ALBUM_NAME=dead-drop
# Local dedupe cache (SQLite)
STATE_DB=./data/state.db
# Base URL for generating absolute invite links (recommended for production)
# 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
# Local dedupe cache
STATE_DB=./data/state.db # local dev -> ./state.db (data folder is created in docker image)
# In Docker this is overridden to /data/state.db by docker-compose.yml
``` ```
@@ -197,52 +241,20 @@ You can keep a checkedin `/.env.example` with the keys above for onboarding.
--- ---
## Mobile notes
- Uses a **labelwrapped input** + short **ghostclick suppression** so the system picker does **not** reopen after tapping **Done** (fixes iOS/Android quirks).
- Draganddrop is desktoporiented; on touch, use **Choose files**.
---
## Troubleshooting
**Uploads don't start on phones / picker reopens**
Hardrefresh; current UI suppresses ghost clicks and resets the input.
If using a PWA/WebView, test in Safari/Chrome directly to rule out container quirks.
**WebSocket connects/disconnects in a loop**
Match schemes: `ws://` for `http://`, `wss://` for `https://`.
If behind a reverse proxy, ensure it forwards WebSockets.
**413 Request Entity Too Large**
If running behind nginx/Traefik/etc., bump body size limits (`client_max_body_size` for nginx).
**/assets returns 401**
Check `IMMICH_API_KEY` and ensure the base URL includes `/api` (e.g., `http://<host>:2283/api`).
**Duplicate detected but you expect an upload**
The proxy caches SHA1 in `state.db`. For a fresh run, delete that DB or point `STATE_DB` to a new file.
---
## Security notes ## Security notes
- The app is **unauthenticated** by design. Share the URL only with trusted people or keep it on a private network/VPN. - The menu and invite creation are behind login. Logout clears the session.
- Invite links are public by URL; share only with intended recipients.
- The default uploader page at `/` is disabled unless `PUBLIC_UPLOAD_PAGE_ENABLED=true`.
- The Immich API key remains **serverside**; the browser never sees it. - The Immich API key remains **serverside**; the browser never sees it.
- No browsing of uploaded media; only ephemeral session state is shown. - No browsing of uploaded media; only ephemeral session state is shown.
- Run behind HTTPS with a reverse proxy and restrict CORS to your domain(s).
--- ## Usage flow
## Development - Admin: Login → Menu → Create invite link (optionally onetime / expiry / album) → Share link or QR.
- Guest: Open invite link → Drop files → Upload progress and results shown.
Run with live reload: - Optional: Enable public uploader and set `IMMICH_ALBUM_NAME` for a default landing page.
```bash
python main.py
```
The backend contains docstrings so you can generate docs later if desired.
--- ---

View File

@@ -356,6 +356,15 @@ async def menu_page(request: Request) -> HTMLResponse:
return RedirectResponse(url="/login") return RedirectResponse(url="/login")
return FileResponse(os.path.join(FRONTEND_DIR, "menu.html")) return FileResponse(os.path.join(FRONTEND_DIR, "menu.html"))
@app.get("/favicon.ico")
async def favicon() -> Response:
"""Serve favicon from /static/favicon.png if present (avoids 404 noise)."""
path = os.path.join(FRONTEND_DIR, "favicon.png")
if os.path.exists(path):
with open(path, "rb") as f:
return Response(content=f.read(), media_type="image/png")
return Response(status_code=204)
@app.post("/api/ping") @app.post("/api/ping")
async def api_ping() -> dict: async def api_ping() -> dict:
"""Connectivity test endpoint used by the UI to display a temporary banner.""" """Connectivity test endpoint used by the UI to display a temporary banner."""

View File

@@ -15,11 +15,11 @@ class Settings:
"""App settings loaded from environment variables (.env).""" """App settings loaded from environment variables (.env)."""
immich_base_url: str immich_base_url: str
immich_api_key: str immich_api_key: str
max_concurrent: int = 3 max_concurrent: int
album_name: str = "" album_name: str = ""
public_upload_page_enabled: bool = False public_upload_page_enabled: bool = False
public_base_url: str = "" public_base_url: str = ""
state_db: str = "./state.db" state_db: str = ""
session_secret: str = "" session_secret: str = ""
log_level: str = "INFO" log_level: str = "INFO"
@@ -48,7 +48,7 @@ def load_settings() -> Settings:
maxc = int(os.getenv("MAX_CONCURRENT", "3")) maxc = int(os.getenv("MAX_CONCURRENT", "3"))
except ValueError: except ValueError:
maxc = 3 maxc = 3
state_db = os.getenv("STATE_DB", "./state.db") state_db = os.getenv("STATE_DB", "/data/state.db")
session_secret = os.getenv("SESSION_SECRET") or secrets.token_hex(32) session_secret = os.getenv("SESSION_SECRET") or secrets.token_hex(32)
log_level = os.getenv("LOG_LEVEL", "INFO").upper() log_level = os.getenv("LOG_LEVEL", "INFO").upper()
return Settings( return Settings(

View File

@@ -7,10 +7,15 @@ services:
- "8080:8080" - "8080:8080"
environment: 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 IMMICH_ALBUM_NAME: dead-drop
env_file:
- ./.env
volumes: volumes:
- immich_drop_data:/data - immich_drop_data:/data

BIN
frontend/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Immich Drop Uploader</title> <title>Immich Drop Uploader</title>
<link rel="icon" type="image/png" href="/static/favicon.png" />
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { tailwind.config = {

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Immich Drop Uploader (Invite)</title> <title>Immich Drop Uploader (Invite)</title>
<link rel="icon" type="image/png" href="/static/favicon.png" />
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { darkMode: 'class' }; tailwind.config = { darkMode: 'class' };

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Login Immich Drop</title> <title>Login Immich Drop</title>
<link rel="icon" type="image/png" href="/static/favicon.png" />
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { darkMode: 'class' }; tailwind.config = { darkMode: 'class' };

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Menu Immich Drop</title> <title>Menu Immich Drop</title>
<link rel="icon" type="image/png" href="/static/favicon.png" />
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
tailwind.config = { darkMode: 'class' }; tailwind.config = { darkMode: 'class' };

View File

@@ -11,6 +11,6 @@ if __name__ == "__main__":
load_dotenv() load_dotenv()
except Exception: except Exception:
pass pass
host = os.getenv("HOST", "127.0.0.1") host = os.getenv("HOST", "0.0.0.0")
port = int(os.getenv("PORT", "8080")) port = int(os.getenv("PORT", "8080"))
uvicorn.run("app.app:app", host=host, port=port, reload=True) uvicorn.run("app.app:app", host=host, port=port, reload=True)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 160 KiB