diff --git a/.env.example b/.env.example index 71f5ce1..f66f5f2 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,25 @@ -# Backend host/port +# Server (dev only) HOST=0.0.0.0 PORT=8080 - -# Immich connection -IMMICH_BASE_URL=http://127.0.0.1:2283/api -IMMICH_API_KEY=ADD-YOUR-API-KEY #key needs asset.upload (default functions) +# 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 -# Optional: Public upload page -PUBLIC_UPLOAD_PAGE_ENABLED=true +# Public uploader page (optional) — disabled by default +PUBLIC_UPLOAD_PAGE_ENABLED=TRUE -# Optional: Album name for public upload page -IMMICH_ALBUM_NAME=dead-drop #key needs album.create,album.read,albumAsset.create (extended functions) +# 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 -# Data path inside the container -STATE_DB=/data/state.db diff --git a/README.md b/README.md index 7a205f0..c80b7df 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,24 @@ # 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) ## Features -- **No accounts** — open the page, drop files, done +- **Invite links (public)** — create upload links you can share with anyone +- **One‑time link claim** — first browser session claims a one‑time 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) - **Duplicate prevention** (local SHA‑1 cache + optional Immich bulk‑check) - **Original dates preserved** (EXIF → `fileCreatedAt` / `fileModifiedAt`) -- **Mobile‑friendly** +- **Mobile‑friendly** - **.env‑only config** (clean deploys) + Docker/Compose -- **Privacy‑first**: never lists server media; UI only shows the current session -- **Dark mode support** — automatically detects system preference, with manual toggle -- **Album integration** — auto-adds uploads to a configured album (creates if needed) +- **Privacy‑first**: never lists server media; UI only shows the current session +- **Dark mode** — detects system preference; manual toggle persists across pages +- **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 -Copy the docker-compose.yml and the .env file to a common folder, -update the .env file before executing the CLI commands to quick start the container. +You can run without a `.env` file by putting all settings in `docker-compose.yml` (recommended for deploys). +Use a `.env` file only for local development. -### docker-compose.yml +### docker-compose.yml (deploy without .env) ```yaml version: "3.9" @@ -48,15 +52,25 @@ services: container_name: immich-drop restart: unless-stopped - # Optional: Set album name for auto-adding uploads + # Configure all settings here (no .env required) 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.) - env_file: - - ./.env + # Immich connection (must include /api) + IMMICH_BASE_URL: https://immich.example.com/api + 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: - 8080:8080 @@ -76,18 +90,7 @@ volumes: 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 ```bash docker compose pull @@ -97,6 +100,17 @@ docker compose up -d ## 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 one‑time (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 - Automatically detects system dark/light preference on first visit - Manual toggle button in the header (sun/moon icon) @@ -133,19 +147,25 @@ docker compose up -d ``` immich_drop/ -├─ app/ # FastAPI application (Python package) -│ ├─ __init__.py -│ ├─ app.py # uvicorn app:app -│ └─ config.py # loads .env from repo root -├─ frontend/ # static UI served at /static -│ ├─ index.html -│ └─ app.js -├─ main.py # thin entrypoint (python main.py) -├─ requirements.txt # Python deps -├─ .env # single config file (see below) +├─ app/ # FastAPI application (Python package) +│ ├─ app.py # ASGI app (uvicorn entry: app.app:app) +│ └─ config.py # Settings loader (reads .env/env) +├─ frontend/ # Static UI (served at /static) +│ ├─ index.html # Public uploader (optional) +│ ├─ login.html # Login page (admin) +│ ├─ menu.html # Admin menu (create invites) +│ ├─ invite.html # Public invite upload page +│ ├─ app.js # Uploader logic (drop/queue/upload/ws) +│ ├─ 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 ├─ 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** --- -## 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 -# Server -HOST=0.0.0.0 +# 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 #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 -# Optional: Album name for auto-adding uploads (creates if doesn't exist) -IMMICH_ALBUM_NAME=dead-drop #key needs album.create,album.read,albumAsset.create (extended functions) +# Public uploader page (optional) — disabled by default +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 checked‑in `/.env.example` with the keys above for onboarding. --- - -## Mobile notes - -- Uses a **label‑wrapped input** + short **ghost‑click suppression** so the system picker does **not** re‑open after tapping **Done** (fixes iOS/Android quirks). -- Drag‑and‑drop is desktop‑oriented; on touch, use **Choose files**. - ---- - -## Troubleshooting - -**Uploads don't start on phones / picker re‑opens** -– Hard‑refresh; 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://:2283/api`). - -**Duplicate detected but you expect an upload** -– The proxy caches SHA‑1 in `state.db`. For a fresh run, delete that DB or point `STATE_DB` to a new file. - ---- - ## 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 **server‑side**; 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 - -Run with live reload: - -```bash -python main.py -``` - -The backend contains docstrings so you can generate docs later if desired. +- Admin: Login → Menu → Create invite link (optionally one‑time / expiry / album) → Share link or QR. +- Guest: Open invite link → Drop files → Upload progress and results shown. +- Optional: Enable public uploader and set `IMMICH_ALBUM_NAME` for a default landing page. --- diff --git a/app/app.py b/app/app.py index 3f2522c..2261fea 100644 --- a/app/app.py +++ b/app/app.py @@ -356,6 +356,15 @@ async def menu_page(request: Request) -> HTMLResponse: return RedirectResponse(url="/login") 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") async def api_ping() -> dict: """Connectivity test endpoint used by the UI to display a temporary banner.""" diff --git a/app/config.py b/app/config.py index ff2b8c7..bcd4ffc 100644 --- a/app/config.py +++ b/app/config.py @@ -15,11 +15,11 @@ class Settings: """App settings loaded from environment variables (.env).""" immich_base_url: str immich_api_key: str - max_concurrent: int = 3 + max_concurrent: int album_name: str = "" public_upload_page_enabled: bool = False public_base_url: str = "" - state_db: str = "./state.db" + state_db: str = "" session_secret: str = "" log_level: str = "INFO" @@ -48,7 +48,7 @@ def load_settings() -> Settings: maxc = int(os.getenv("MAX_CONCURRENT", "3")) except ValueError: 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) log_level = os.getenv("LOG_LEVEL", "INFO").upper() return Settings( diff --git a/docker-compose.yml b/docker-compose.yml index a915c8e..7194a98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,10 +7,15 @@ services: - "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 - env_file: - - ./.env volumes: - immich_drop_data:/data diff --git a/frontend/favicon.png b/frontend/favicon.png new file mode 100644 index 0000000..de290f2 Binary files /dev/null and b/frontend/favicon.png differ diff --git a/frontend/index.html b/frontend/index.html index 6da372d..01facbf 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,7 @@ Immich Drop Uploader +