diff --git a/.dockerignore b/.dockerignore index 334afeda9..a16f641f8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,6 +14,7 @@ /assets/models/ /storage/ /build/ +/setup/Makefile /photoprism /photoprism-* /frontend/tests/acceptance/screenshots/ @@ -69,6 +70,7 @@ venv .nv .eslintcache .gocache +.gitconfig .DS_Store .DS_Store? ._* diff --git a/.env.example b/.env.example index 347dcaf1b..95d06f8b7 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,12 @@ # read by Docker Compose itself (so no --profile flag is required). # COMPOSE_PROFILES=postgres +# In-container working directory and bind mount target for the repo checkout. +# Override when the host clone lives outside the default GOPATH layout; the +# value is reused for the Dockerfile WORKDIR (build arg), working_dir, the +# source bind mount, and every PhotoPrism path env var so they stay in sync. +# WORKING_DIR=/go/src/github.com/photoprism/photoprism + # Network interfaces to which Traefik and other services bind on the host. # Defaults expose Traefik on every interface (so *.localssl.dev is reachable # from the LAN) and keep direct service ports on loopback only. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1217b9f02..01ac9afce 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,18 +12,10 @@ body: 2. Work through the [Troubleshooting Checklists](https://docs.photoprism.app/getting-started/troubleshooting/) we provide. 3. Do not report [known issues](https://docs.photoprism.app/known-issues/) or [missing features](https://github.com/photoprism/photoprism/issues) as bugs. 4. Use [GitHub Discussions](https://github.com/photoprism/photoprism/discussions) for community help, and visit our [Support Guide](https://www.photoprism.app/kb/getting-support) to learn more about support options. - - type: checkboxes - id: prerequisites - attributes: - label: Before You Continue - description: Confirm that you reviewed the guidance above. - options: - - label: This is a new, confirmed bug that has not yet been reported or documented - required: true - type: textarea id: documented-behavior attributes: - label: What Is Not Working as Documented? + label: What is not working as documented? description: Be specific and include links to the relevant documentation when possible. placeholder: Describe the incorrect behavior and link to the related documentation on https://docs.photoprism.app/. validations: @@ -31,62 +23,65 @@ body: - type: textarea id: reproduction-steps attributes: - label: How Can We Reproduce It? + label: How can we reproduce it? description: Provide numbered steps so we can reproduce the behavior reliably. placeholder: | 1. Go to ... 2. Click ... 3. Scroll to ... 4. Expected vs. actual result - render: markdown validations: required: true + - type: checkboxes + id: prerequisites + attributes: + label: Have you verified that no similar reports exist? + description: Confirm that you read the relevant documentation, this is not a support request, and that no similar reports already exist. + options: + - label: This is a new bug that has not yet been reported or documented + required: true - type: textarea id: expected-behavior attributes: - label: What Behavior Do You Expect? + label: What behavior do you expect? description: Share the correct outcome you expected to see. validations: required: true - type: textarea id: possible-cause attributes: - label: What Could Be the Cause? + label: What could be the cause? description: Summarize any investigation you have already completed and the potential root cause. - - type: textarea - id: supporting-material - attributes: - label: Logs, Sample Files, or Screenshots - description: Attach or link to files that help us reproduce the problem. Import and indexing issues require sample files and logs. - placeholder: Provide links to archives, logs, or screenshots. See https://www.photoprism.app/contact for secure sharing options. - type: textarea id: software-versions attributes: - label: Which Software Versions Do You Use? + label: Which software versions do you use? description: Include all relevant software versions so we can reproduce the environment. - placeholder: | - - PhotoPrism Architecture & Build Number (AMD64, ARM64, ARMv7, ...): - - Database Type & Version (MariaDB, MySQL, SQLite, ...): - - Operating System Types & Versions (Linux, Windows, Android, ...): - - Browser Types & Versions (Firefox, Chrome, Safari on iPhone, ...): - - Ad Blockers, Browser Plugins, or Firewall Software: - render: markdown + value: | + - PhotoPrism Edition & Version (Build): + - Database Type & Version(s): + - Operating System(s): + - Browser Type & Version(s): + - Ad Blockers, Antivirus, and Plugins: validations: required: true - type: textarea id: installation-device attributes: - label: On What Device Is PhotoPrism Installed? + label: On what device is PhotoPrism installed? description: Provide hardware details, especially for performance, import, or indexing issues. - placeholder: | - - Device / Processor Type (Raspberry Pi 4, Intel Core i7-3770, AMD Ryzen 7 3800X, ...): - - Physical Memory & Swap Space (GB): - - Storage Type (HDD, SSD, RAID, USB, Network Storage, ...): - - Additional Context: - render: markdown + value: | + - CPU/Device Type(s): + - Physical Memory & Swap in GB: + - Storage (HDD/SSD/RAID, USB, Network): - type: textarea id: networking-setup attributes: - label: Do You Use a Reverse Proxy, Firewall, VPN, or CDN? + label: Do you use a reverse proxy, firewall, VPN, or CDN? description: Describe your network setup. If applicable, include details about your NGINX or other reverse proxy configuration. placeholder: Describe proxies, VPNs, CDNs, or firewall software involved. Include versions and configuration snippets when helpful. + - type: upload + id: supporting-material + attributes: + label: Logs, Sample Files, or Screenshots + description: Attach or link to files that help us reproduce the problem. Import and indexing issues require sample files and logs. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 5d9d6971c..336bb7b9a 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -10,21 +10,15 @@ body: Before submitting a new request, please check our GitHub roadmap and issues for similar requests: - https://link.photoprism.app/roadmap - https://github.com/photoprism/photoprism/issues - - type: checkboxes - id: request-prep - attributes: - label: Before You Continue - description: Make sure you completed these steps before opening a new request. - options: - - label: This is not a support request, and I verified that no similar issues exist - required: true - type: textarea id: user-story attributes: label: User Story description: | - Feature requests MUST begin with a one sentence user story in the format `**As a , I want , so that .**` + Feature requests MUST begin with a one sentence user story. + **As a , I want , so that .** placeholder: "**As a , I want , so that .**" + value: "**As a , I want , so that .**" validations: required: true - type: textarea @@ -49,36 +43,31 @@ body: label: Which alternatives or workarounds have you considered? description: Describe alternative solutions, workarounds, or related features you evaluated. placeholder: Mention any alternative solutions, workarounds, or related features you evaluated. + - type: checkboxes + id: prerequisites + attributes: + label: Have you verified that no similar issues exist? + options: + - label: This is not a support request, and I verified that no similar issues exist + required: true - type: textarea id: additional-context attributes: label: Additional Context description: Add any other context or screenshots about the feature request here. placeholder: Add any other context or screenshots that help explain the request. - - type: textarea - id: open-questions - attributes: - label: Open Questions - description: Use checklist items to capture ambiguities, tradeoffs, or decisions that still need clarification. - placeholder: | - - [ ] — options A/B/C and implications. - - [ ] — potential ambiguity or conflict to clarify. - render: markdown - type: textarea id: acceptance-criteria attributes: label: Acceptance Criteria description: Use checklist items and RFC 2119 keywords such as MUST, SHOULD, and MAY to define the expected behavior. placeholder: | - - [ ] MUST - - [ ] SHOULD - - [ ] MAY - render: markdown + - [ ] MUST . + - [ ] SHOULD . + - [ ] MAY . + value: | + - [ ] MUST . + - [ ] SHOULD . + - [ ] MAY . validations: required: true - - type: textarea - id: references - attributes: - label: References - description: Add links to external documentation, references, or examples. - placeholder: Add links to documentation, references, or examples that support the request. diff --git a/.gitignore b/.gitignore index 64df05b72..09c6d1075 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ venv .nv .eslintcache .gocache +.gitconfig .system /pro/ /portal/ @@ -58,6 +59,7 @@ frontend/coverage/ /photos/import/* /storage/ /build/ +/setup/Makefile /assets/docs/ /assets/facenet/ /assets/nasnet/ diff --git a/.rsyncignore b/.rsyncignore new file mode 100644 index 000000000..fcefac8ca --- /dev/null +++ b/.rsyncignore @@ -0,0 +1,3 @@ +.* +*.db +__* diff --git a/Dockerfile b/Dockerfile index 8f70eeb1b..ec6479d6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,16 +18,19 @@ ENV NPM_CONFIG_IGNORE_SCRIPTS=true # FROM photoprism/develop:bullseye # Debian 11 (Bullseye) # FROM photoprism/develop:buster # Debian 10 (Buster) -# Set default working directory. -WORKDIR "/go/src/github.com/photoprism/photoprism" +# Set default working directory. Override WORKING_DIR (e.g. via compose build +# args populated from .env) to match a custom host-side clone layout; the value +# must stay aligned with compose's working_dir and the source bind mount target. +ARG WORKING_DIR=/go/src/github.com/photoprism/photoprism +WORKDIR "${WORKING_DIR}" # Copy source to image. COPY . . -COPY --chown=root:root /scripts/dist/ /scripts/ +COPY --chown=root:root ./scripts/dist/ /scripts/ # Re-install the dev "mariadb" client config so a custom MARIADB_PORT in .env # is honored even when the base image was built before the port= line was # removed (no-op once the next dated base image picks up the new .my.cnf). -COPY --chown=root:root --chmod=644 .my.cnf /etc/my.cnf +COPY --chown=root:root --chmod=644 ./.my.cnf /etc/my.cnf RUN sudo /scripts/install-yt-dlp.sh diff --git a/Makefile b/Makefile index 006d239b2..600731242 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ @@ -39,6 +39,7 @@ BUILD_ARCH ?= $(shell ./scripts/dist/arch.sh) export BUILD_ARCH JS_BUILD_PATH ?= $(shell realpath "./assets/static/build") TF_VERSION ?= 2.18.0 +WORKING_DIR ?= /go/src/github.com/photoprism/photoprism # Install parameters. INSTALL_PATH ?= $(BUILD_PATH)/photoprism-ce_$(BUILD_TAG)-$(shell echo $(BUILD_OS) | tr '[:upper:]' '[:lower:]')-$(BUILD_ARCH) @@ -186,10 +187,10 @@ notice: fix-permissions: $(info Updating filesystem permissions...) @if [ $(UID) != 0 ]; then\ - echo "Running \"chown --preserve-root -Rcf $(UID):$(GID) /go /photoprism /opt/photoprism /tmp/photoprism\". Please wait."; \ - sudo chown --preserve-root -Rcf $(UID):$(GID) /go /photoprism /opt/photoprism /tmp/photoprism || true;\ - echo "Running \"chmod --preserve-root -Rcf u+rwX /go/src/github.com/photoprism/* /photoprism /opt/photoprism /tmp/photoprism\". Please wait.";\ - sudo chmod --preserve-root -Rcf u+rwX /go/src/github.com/photoprism/photoprism/* /photoprism /opt/photoprism /tmp/photoprism || true;\ + echo "Running \"chown --preserve-root -Rcf $(UID):$(GID) /go $(WORKING_DIR) /photoprism /opt/photoprism /tmp/photoprism\". Please wait."; \ + sudo chown --preserve-root -Rcf $(UID):$(GID) /go $(WORKING_DIR) /photoprism /opt/photoprism /tmp/photoprism || true;\ + echo "Running \"chmod --preserve-root -Rcf u+rwX $(WORKING_DIR)/* /photoprism /opt/photoprism /tmp/photoprism\". Please wait.";\ + sudo chmod --preserve-root -Rcf u+rwX $(WORKING_DIR)/* /photoprism /opt/photoprism /tmp/photoprism || true;\ echo "Done."; \ else\ echo "Running as root. Nothing to do."; \ diff --git a/assets/.buildignore b/assets/.buildignore index aa532e819..01955d790 100644 --- a/assets/.buildignore +++ b/assets/.buildignore @@ -1,5 +1,6 @@ examples README.md +Thumbs.db docs .* -_* \ No newline at end of file +_* diff --git a/compose.armv7.yaml b/compose.armv7.yaml index ac1e2d5ac..fe6378b08 100644 --- a/compose.armv7.yaml +++ b/compose.armv7.yaml @@ -46,10 +46,11 @@ services: PHOTOPRISM_TEST_DSN_SQLITE: "" PHOTOPRISM_TEST_DSN_SQLITEFILE: ".test.db?_foreign_keys=on&_busy_timeout=5000" PHOTOPRISM_TEST_DSN_POSTGRES: "postgresql://testdb:testdb@postgres:${POSTGRES_PORT:-4002}/testdb?TimeZone=UTC&connect_timeout=15&lock_timeout=5000&sslmode=disable" - PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets" - PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage" - PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals" - PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import" + WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" + PHOTOPRISM_ASSETS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/assets" + PHOTOPRISM_STORAGE_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage" + PHOTOPRISM_ORIGINALS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/originals" + PHOTOPRISM_IMPORT_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/import" PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server @@ -82,9 +83,9 @@ services: # - "/dev/video11:/dev/video11" # Video4Linux (h264_v4l2m2m) # - "/dev/dri/renderD128:/dev/dri/renderD128" # Intel GPU # - "/dev/dri/card0:/dev/dri/card0" - working_dir: "/go/src/github.com/photoprism/photoprism" + working_dir: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" volumes: - - ".:/go/src/github.com/photoprism/photoprism" + - ".:${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" - "go-mod:/go/pkg/mod" ## MariaDB Database Server diff --git a/compose.intel.yaml b/compose.intel.yaml index 35971b482..8f45ad0ad 100644 --- a/compose.intel.yaml +++ b/compose.intel.yaml @@ -1,7 +1,10 @@ services: ## PhotoPrism (Development Environment for Intel QSV hardware transcoding) photoprism: - build: . + build: + context: . + args: + WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" image: photoprism/photoprism:develop depends_on: - mariadb @@ -107,11 +110,12 @@ services: PHOTOPRISM_TEST_DSN_SQLITE: "" PHOTOPRISM_TEST_DSN_SQLITEFILE: "file:/go/src/github.com/photoprism/photoprism/storage/testdata/unit.test.db?_foreign_keys=on&_busy_timeout=5000" PHOTOPRISM_TEST_DSN_POSTGRES: "postgresql://testdb:testdb@postgres:${POSTGRES_PORT:-4002}/testdb?TimeZone=UTC&connect_timeout=15&lock_timeout=5000&sslmode=disable" - PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets" - PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage" - PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals" + WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" + PHOTOPRISM_ASSETS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/assets" + PHOTOPRISM_STORAGE_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage" + PHOTOPRISM_ORIGINALS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/originals" PHOTOPRISM_ORIGINALS_LIMIT: 128000 # sets originals file size limit to 128 GB - PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import" + PHOTOPRISM_IMPORT_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/import" PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server @@ -136,17 +140,17 @@ services: PHOTOPRISM_INIT: "https intel tensorflow" ## Additional dependencies and tools: TF_CPP_MIN_LOG_LEVEL: 1 - GOCACHE: "/go/src/github.com/photoprism/photoprism/.local/gocache" - CODEX_HOME: "/go/src/github.com/photoprism/photoprism/.local/codex" - CLAUDE_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/claude" + GOCACHE: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gocache" + CODEX_HOME: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/codex" + CLAUDE_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/claude" GH_REPO: "photoprism/photoprism" - GH_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/gh" + GH_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gh" ## Share hardware devices with FFmpeg for hardware video transcoding: devices: - "/dev/dri:/dev/dri" - working_dir: "/go/src/github.com/photoprism/photoprism" + working_dir: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" volumes: - - ".:/go/src/github.com/photoprism/photoprism" + - ".:${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" - "./storage:/photoprism" - "go-mod:/go/pkg/mod" diff --git a/compose.nvidia.yaml b/compose.nvidia.yaml index 58151e0d6..516c86625 100644 --- a/compose.nvidia.yaml +++ b/compose.nvidia.yaml @@ -1,7 +1,10 @@ services: ## PhotoPrism (Development Environment for Nvidia) photoprism: - build: . + build: + context: . + args: + WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" image: photoprism/photoprism:develop runtime: nvidia depends_on: @@ -110,11 +113,12 @@ services: PHOTOPRISM_TEST_DSN_SQLITE: "" PHOTOPRISM_TEST_DSN_SQLITEFILE: "file:/go/src/github.com/photoprism/photoprism/storage/testdata/unit.test.db?_foreign_keys=on&_busy_timeout=5000" PHOTOPRISM_TEST_DSN_POSTGRES: "postgresql://testdb:testdb@postgres:${POSTGRES_PORT:-4002}/testdb?TimeZone=UTC&connect_timeout=15&lock_timeout=5000&sslmode=disable" - PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets" - PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage" - PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals" + WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" + PHOTOPRISM_ASSETS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/assets" + PHOTOPRISM_STORAGE_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage" + PHOTOPRISM_ORIGINALS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/originals" PHOTOPRISM_ORIGINALS_LIMIT: 128000 # sets originals file size limit to 128 GB - PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import" + PHOTOPRISM_IMPORT_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/import" PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server @@ -145,18 +149,18 @@ services: PHOTOPRISM_FFMPEG_BITRATE: "64" # video bitrate limit in Mbps (default: 60) ## Additional dependencies and tools: TF_CPP_MIN_LOG_LEVEL: 1 - GOCACHE: "/go/src/github.com/photoprism/photoprism/.local/gocache" - CODEX_HOME: "/go/src/github.com/photoprism/photoprism/.local/codex" - CLAUDE_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/claude" + GOCACHE: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gocache" + CODEX_HOME: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/codex" + CLAUDE_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/claude" GH_REPO: "photoprism/photoprism" - GH_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/gh" + GH_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gh" ## Shared devices for video hardware transcoding (optional): # devices: # - "/dev/dri:/dev/dri" # Required Intel QSV or VAAPI hardware transcoding # - "/dev/video11:/dev/video11" # Video4Linux Video Encode Device (h264_v4l2m2m) - working_dir: "/go/src/github.com/photoprism/photoprism" + working_dir: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" volumes: - - ".:/go/src/github.com/photoprism/photoprism" + - ".:${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" - "./storage:/photoprism" - "go-mod:/go/pkg/mod" ## NVIDIA GPU Hardware Acceleration (see https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html): @@ -202,12 +206,26 @@ services: OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup OLLAMA_NOHISTORY: "true" # disables readline history - OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature - OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0) + # OLLAMA_FLASH_ATTENTION — small speedup on supported architectures + # (gemma3, gptoss, mistral3, qwen3*). Silently no-op on unsupported + # architectures and on CPU. Required to enable OLLAMA_KV_CACHE_TYPE + # quantization. Set to "false" for Qwen3-2507 builds (gh#12432). + OLLAMA_FLASH_ATTENTION: "true" + # OLLAMA_KV_CACHE_TYPE — cache precision (f16 is the default): + # f16 — native precision, works on every architecture + # q8_0 — halves cache VRAM; clean for qwen3*/gpt-oss/mistral3; + # 5x slowdown on gemma3 (gh#9683/#11949); no-op for + # gemma4/qwen2.5vl (not on FA allowlist — gh#13337) + # q4_0 — quarters cache VRAM; usable on qwen, hurts gemma; + # only reach for it when VRAM-constrained + OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0 OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs. - OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine # OLLAMA_DEBUG: "true" # shows additional debug information # OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection + ## Telemetry / privacy opt-outs (containers do not inherit /etc/environment): + DO_NOT_TRACK: "true" + HF_HUB_DISABLE_TELEMETRY: "1" + # OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features ## NVIDIA GPU Hardware Acceleration (see https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html): NVIDIA_VISIBLE_DEVICES: "all" NVIDIA_DRIVER_CAPABILITIES: "compute,utility" diff --git a/compose.yaml b/compose.yaml index 3e3999288..4d59e13fb 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,7 +4,10 @@ services: ## PhotoPrism Build & Test Environment photoprism: - build: . + build: + context: . + args: + WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" image: photoprism/photoprism:develop depends_on: - postgres @@ -115,11 +118,12 @@ services: PHOTOPRISM_TEST_DSN_SQLITE: "" PHOTOPRISM_TEST_DSN_SQLITEFILE: "file:/go/src/github.com/photoprism/photoprism/storage/testdata/unit.test.db?_foreign_keys=on&_busy_timeout=5000" PHOTOPRISM_TEST_DSN_POSTGRES: "postgresql://testdb:testdb@postgres:${POSTGRES_PORT:-4002}/testdb?TimeZone=UTC&connect_timeout=15&lock_timeout=5000&sslmode=disable" - PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets" - PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage" - PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals" + WORKING_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" + PHOTOPRISM_ASSETS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/assets" + PHOTOPRISM_STORAGE_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage" + PHOTOPRISM_ORIGINALS_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/originals" PHOTOPRISM_ORIGINALS_LIMIT: 128000 # sets originals file size limit to 128 GB - PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import" + PHOTOPRISM_IMPORT_PATH: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/storage/import" PHOTOPRISM_DISABLE_CHOWN: "false" # disables updating storage permissions via chmod and chown on startup PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server @@ -151,18 +155,18 @@ services: OLLAMA_API_KEY: "" # API key required to access Ollama (optional) ## Additional dependencies and tools: TF_CPP_MIN_LOG_LEVEL: 1 - GOCACHE: "/go/src/github.com/photoprism/photoprism/.local/gocache" - CODEX_HOME: "/go/src/github.com/photoprism/photoprism/.local/codex" - CLAUDE_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/claude" + GOCACHE: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gocache" + CODEX_HOME: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/codex" + CLAUDE_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/claude" GH_REPO: "photoprism/photoprism" - GH_CONFIG_DIR: "/go/src/github.com/photoprism/photoprism/.local/gh" + GH_CONFIG_DIR: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}/.local/gh" ## Shared devices for video hardware transcoding (optional): # devices: # - "/dev/dri:/dev/dri" # Required Intel QSV or VAAPI hardware transcoding # - "/dev/video11:/dev/video11" # Video4Linux Video Encode Device (h264_v4l2m2m) - working_dir: "/go/src/github.com/photoprism/photoprism" + working_dir: "${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" volumes: - - ".:/go/src/github.com/photoprism/photoprism" + - ".:${WORKING_DIR:-/go/src/github.com/photoprism/photoprism}" - "./storage:/photoprism" - "go-mod:/go/pkg/mod" @@ -309,12 +313,15 @@ services: OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup OLLAMA_NOHISTORY: "true" # disables readline history - OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature - OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0) + OLLAMA_FLASH_ATTENTION: "true" # required for OLLAMA_KV_CACHE_TYPE quantization + OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0 OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs. - OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine # OLLAMA_DEBUG: "true" # shows additional debug information # OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection + ## Telemetry / privacy opt-outs (containers do not inherit /etc/environment): + DO_NOT_TRACK: "true" + HF_HUB_DISABLE_TELEMETRY: "1" + # OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features ## NVIDIA GPU Hardware Acceleration (optional): # NVIDIA_VISIBLE_DEVICES: "all" # NVIDIA_DRIVER_CAPABILITIES: "compute,utility" diff --git a/docker/develop/armv7/Dockerfile b/docker/develop/armv7/Dockerfile index 23083f706..7a60b4dbd 100644 --- a/docker/develop/armv7/Dockerfile +++ b/docker/develop/armv7/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 24.04 LTS (Noble Numbat) FROM ubuntu:noble -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/bookworm-slim/Dockerfile b/docker/develop/bookworm-slim/Dockerfile index 4a48f4eba..6eebaffa0 100644 --- a/docker/develop/bookworm-slim/Dockerfile +++ b/docker/develop/bookworm-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Debian 12 (Bookworm) FROM debian:bookworm-slim -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/bookworm/Dockerfile b/docker/develop/bookworm/Dockerfile index 0b43def31..54657e597 100644 --- a/docker/develop/bookworm/Dockerfile +++ b/docker/develop/bookworm/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Debian 12 (Bookworm) FROM debian:bookworm-slim -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/bullseye-slim/Dockerfile b/docker/develop/bullseye-slim/Dockerfile index cca902336..65f4e1d7e 100644 --- a/docker/develop/bullseye-slim/Dockerfile +++ b/docker/develop/bullseye-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Debian 11 (Bullseye) FROM debian:bullseye-slim -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/bullseye/Dockerfile b/docker/develop/bullseye/Dockerfile index 1711901a6..c49965223 100644 --- a/docker/develop/bullseye/Dockerfile +++ b/docker/develop/bullseye/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Debian 11 (Bullseye) FROM golang:1-bullseye -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/buster/Dockerfile b/docker/develop/buster/Dockerfile index 3e39be4b7..a256319d9 100644 --- a/docker/develop/buster/Dockerfile +++ b/docker/develop/buster/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Debian 10 (Buster) FROM golang:1-buster -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/impish/Dockerfile b/docker/develop/impish/Dockerfile index 61942d2d7..7e2717d00 100644 --- a/docker/develop/impish/Dockerfile +++ b/docker/develop/impish/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 21.10 (Impish Indri) FROM ubuntu:impish -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/jammy-slim/Dockerfile b/docker/develop/jammy-slim/Dockerfile index 4cfe643d2..b6f04df51 100644 --- a/docker/develop/jammy-slim/Dockerfile +++ b/docker/develop/jammy-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 22.04 LTS (Jammy Jellyfish) FROM ubuntu:jammy -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/jammy/Dockerfile b/docker/develop/jammy/Dockerfile index e991e5aec..a97b3edc3 100644 --- a/docker/develop/jammy/Dockerfile +++ b/docker/develop/jammy/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 22.04 LTS (Jammy Jellyfish) FROM ubuntu:jammy -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/lunar-slim/Dockerfile b/docker/develop/lunar-slim/Dockerfile index fce34b9a7..59ea9ca7d 100644 --- a/docker/develop/lunar-slim/Dockerfile +++ b/docker/develop/lunar-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 23.04 (Lunar Lobster) FROM ubuntu:lunar -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/lunar/Dockerfile b/docker/develop/lunar/Dockerfile index 38caf58b3..c408522b8 100644 --- a/docker/develop/lunar/Dockerfile +++ b/docker/develop/lunar/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 23.04 (Lunar Lobster) FROM ubuntu:lunar -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/mantic-slim/Dockerfile b/docker/develop/mantic-slim/Dockerfile index 5a06edbbb..ea2823cac 100644 --- a/docker/develop/mantic-slim/Dockerfile +++ b/docker/develop/mantic-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 23.10 (Mantic Minotaur) FROM ubuntu:mantic -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/mantic/Dockerfile b/docker/develop/mantic/Dockerfile index 319cdbeb7..494f9e083 100644 --- a/docker/develop/mantic/Dockerfile +++ b/docker/develop/mantic/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 23.10 (Mantic Minotaur) FROM ubuntu:mantic -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/noble-slim/Dockerfile b/docker/develop/noble-slim/Dockerfile index 1f8757028..c845ef62f 100644 --- a/docker/develop/noble-slim/Dockerfile +++ b/docker/develop/noble-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 24.04 LTS (Noble Numbat) FROM ubuntu:noble -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/noble/Dockerfile b/docker/develop/noble/Dockerfile index bc262d93e..0c0ca38ba 100644 --- a/docker/develop/noble/Dockerfile +++ b/docker/develop/noble/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 24.04 LTS (Noble Numbat) FROM ubuntu:noble -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/oracular-slim/Dockerfile b/docker/develop/oracular-slim/Dockerfile index 6aa365825..134d12101 100644 --- a/docker/develop/oracular-slim/Dockerfile +++ b/docker/develop/oracular-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 24.10 (Oracular Oriole) FROM ubuntu:oracular -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/oracular/Dockerfile b/docker/develop/oracular/Dockerfile index c0adea3b5..f6a491d22 100644 --- a/docker/develop/oracular/Dockerfile +++ b/docker/develop/oracular/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 24.10 (Oracular Oriole) FROM ubuntu:oracular -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/plucky-slim/Dockerfile b/docker/develop/plucky-slim/Dockerfile index 9b4b114a1..93f47867e 100644 --- a/docker/develop/plucky-slim/Dockerfile +++ b/docker/develop/plucky-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 25.04 (Plucky Puffin) FROM ubuntu:plucky -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/plucky/Dockerfile b/docker/develop/plucky/Dockerfile index a0a9a621f..0e7fa2b52 100644 --- a/docker/develop/plucky/Dockerfile +++ b/docker/develop/plucky/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 25.04 (Plucky Puffin) FROM ubuntu:plucky -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/questing-slim/Dockerfile b/docker/develop/questing-slim/Dockerfile index 2732702b7..9491ba432 100644 --- a/docker/develop/questing-slim/Dockerfile +++ b/docker/develop/questing-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 25.10 (Questing Quokka) FROM ubuntu:questing -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/questing/Dockerfile b/docker/develop/questing/Dockerfile index 80091b48a..8fffd816a 100644 --- a/docker/develop/questing/Dockerfile +++ b/docker/develop/questing/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 25.10 (Questing Quokka) FROM ubuntu:questing -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/resolute-slim/Dockerfile b/docker/develop/resolute-slim/Dockerfile index 244176c60..e5659718e 100644 --- a/docker/develop/resolute-slim/Dockerfile +++ b/docker/develop/resolute-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 26.04 LTS (Resolute Raccoon) FROM ubuntu:resolute -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/resolute/Dockerfile b/docker/develop/resolute/Dockerfile index db6ced117..29af81671 100644 --- a/docker/develop/resolute/Dockerfile +++ b/docker/develop/resolute/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Ubuntu 26.04 LTS (Resolute Raccoon) FROM ubuntu:resolute -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/trixie-slim/Dockerfile b/docker/develop/trixie-slim/Dockerfile index b34b5682f..35edc1312 100644 --- a/docker/develop/trixie-slim/Dockerfile +++ b/docker/develop/trixie-slim/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Debian 13 (Trixie) FROM debian:trixie-slim -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/develop/trixie/Dockerfile b/docker/develop/trixie/Dockerfile index 6fea6fcb2..801b62f8d 100644 --- a/docker/develop/trixie/Dockerfile +++ b/docker/develop/trixie/Dockerfile @@ -1,7 +1,7 @@ #### Base Image: Debian 13 (Trixie) FROM debian:trixie-slim -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/armv7/Dockerfile b/docker/photoprism/armv7/Dockerfile index 9127e4c03..abb932b92 100644 --- a/docker/photoprism/armv7/Dockerfile +++ b/docker/photoprism/armv7/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:armv7 AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/bookworm/Dockerfile b/docker/photoprism/bookworm/Dockerfile index 605ca9594..048fbfebb 100644 --- a/docker/photoprism/bookworm/Dockerfile +++ b/docker/photoprism/bookworm/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:bookworm AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/bullseye/Dockerfile b/docker/photoprism/bullseye/Dockerfile index 52540ef2b..945d5a23f 100644 --- a/docker/photoprism/bullseye/Dockerfile +++ b/docker/photoprism/bullseye/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:bullseye AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/buster/Dockerfile b/docker/photoprism/buster/Dockerfile index 28981066e..e7f259f01 100644 --- a/docker/photoprism/buster/Dockerfile +++ b/docker/photoprism/buster/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:buster AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/impish/Dockerfile b/docker/photoprism/impish/Dockerfile index af3c2f991..8edde3a6c 100644 --- a/docker/photoprism/impish/Dockerfile +++ b/docker/photoprism/impish/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:impish AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/jammy/Dockerfile b/docker/photoprism/jammy/Dockerfile index 6546c0bc5..f5b478a79 100644 --- a/docker/photoprism/jammy/Dockerfile +++ b/docker/photoprism/jammy/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:jammy AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/lunar/Dockerfile b/docker/photoprism/lunar/Dockerfile index 4126894f5..bb550b8e7 100644 --- a/docker/photoprism/lunar/Dockerfile +++ b/docker/photoprism/lunar/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:lunar AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/mantic/Dockerfile b/docker/photoprism/mantic/Dockerfile index c23758019..6faf1227f 100644 --- a/docker/photoprism/mantic/Dockerfile +++ b/docker/photoprism/mantic/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:mantic AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/noble/Dockerfile b/docker/photoprism/noble/Dockerfile index 9d83b627f..d7dd41087 100644 --- a/docker/photoprism/noble/Dockerfile +++ b/docker/photoprism/noble/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:noble AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/oracular/Dockerfile b/docker/photoprism/oracular/Dockerfile index dbc45e1c7..8aafce144 100644 --- a/docker/photoprism/oracular/Dockerfile +++ b/docker/photoprism/oracular/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:oracular AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/plucky/Dockerfile b/docker/photoprism/plucky/Dockerfile index 7996c7391..10e4a39a3 100644 --- a/docker/photoprism/plucky/Dockerfile +++ b/docker/photoprism/plucky/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:plucky AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/questing/Dockerfile b/docker/photoprism/questing/Dockerfile index d6874184c..071dfdfbc 100644 --- a/docker/photoprism/questing/Dockerfile +++ b/docker/photoprism/questing/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:questing AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/docker/photoprism/resolute/Dockerfile b/docker/photoprism/resolute/Dockerfile index 1915f6331..524c32dad 100644 --- a/docker/photoprism/resolute/Dockerfile +++ b/docker/photoprism/resolute/Dockerfile @@ -1,7 +1,7 @@ ##################################################### BUILD STAGE ###################################################### FROM photoprism/develop:resolute AS build -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/frontend/Makefile b/frontend/Makefile index b88e79271..d8482f7e9 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -1,4 +1,4 @@ -# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. # # Questions? Email us at hello@photoprism.app or visit our website to learn # more about our team, products and services: https://www.photoprism.app/ diff --git a/frontend/tests/acceptance/acceptance-auth/settings/general.js b/frontend/tests/acceptance/acceptance-auth/settings/general.js index 6ede11355..1c32c5335 100644 --- a/frontend/tests/acceptance/acceptance-auth/settings/general.js +++ b/frontend/tests/acceptance/acceptance-auth/settings/general.js @@ -299,6 +299,16 @@ test.meta("testID", "settings-general-004").meta({ type: "short", mode: "auth" } await t.expect(photoviewer.markersEditToggle.exists).ok(); await photoviewer.triggerPhotoViewerAction("close-button"); + await photo.toggleSelectNthPhoto(0, "image"); + await photo.toggleSelectNthPhoto(1, "image"); + await contextmenu.triggerContextMenuAction("edit"); + + await t.expect(photoedit.batchDialog.visible).ok(); + await t.expect(photoedit.batchLabels.visible).ok(); + + await t.click(photoedit.dialogClose); + await contextmenu.clearSelection(); + await menu.checkMenuItemAvailability("people", true); await menu.checkMenuItemAvailability("labels", true); await menu.openPage("settings"); @@ -327,6 +337,16 @@ test.meta("testID", "settings-general-004").meta({ type: "short", mode: "auth" } await t.expect(photoviewer.markersEditToggle.exists).notOk(); await photoviewer.triggerPhotoViewerAction("close-button"); + await photo.toggleSelectNthPhoto(0, "image"); + await photo.toggleSelectNthPhoto(1, "image"); + await contextmenu.triggerContextMenuAction("edit"); + + await t.expect(photoedit.batchDialog.visible).ok(); + await t.expect(photoedit.batchLabels.exists).notOk(); + + await t.click(photoedit.dialogClose); + await contextmenu.clearSelection(); + await menu.checkMenuItemAvailability("people", false); await menu.checkMenuItemAvailability("labels", false); @@ -557,7 +577,9 @@ test.meta("testID", "settings-general-006").meta({ type: "short", mode: "auth" } await photoviewer.checkPhotoViewerActionAvailability("edit-button", false); await photoviewer.openSidebar(); - await photoviewer.assertSidebarIsReadOnly(); + // The first non-stacked photo has Title + Labels + Albums but no Caption or People; + // the Filmpreis-anchored block below covers the People positive case. + await photoviewer.assertSidebarIsReadOnly({ expectCaption: false, expectPeople: false }); await photoviewer.triggerPhotoViewerAction("close-button"); await t.expect(Selector("div.p-lightbox__pswp").visible).notOk(); diff --git a/frontend/tests/acceptance/acceptance-auth/sharing.js b/frontend/tests/acceptance/acceptance-auth/sharing.js index 1e2b69007..16e7cb87b 100644 --- a/frontend/tests/acceptance/acceptance-auth/sharing.js +++ b/frontend/tests/acceptance/acceptance-auth/sharing.js @@ -199,9 +199,6 @@ test.meta("testID", "sharing-002").meta({ type: "short", mode: "auth" })("Multi- }); test.meta("testID", "sharing-003").meta({ type: "short", mode: "auth" })("Common: Lightbox sidebar shows only restricted metadata on share links", async (t) => { - // Reuse the shared album exposed by sharing-002. The Vitest role x - // field matrix already covers the per-role logic against a mocked - // $session; this test pins the real anonymous path end-to-end. await t.useRole(Role.anonymous()); await t.navigateTo("http://localhost:2343/s/jxoux5ub1e/british-columbia-canada"); await t.expect(toolbar.toolbarSecondTitle.withText("British Columbia").visible).ok(); @@ -209,20 +206,13 @@ test.meta("testID", "sharing-003").meta({ type: "short", mode: "auth" })("Common await photoviewer.openPhotoViewer("nth", 0); await photoviewer.openSidebar(); - // Allow-list: file info and taken-at rows render for anonymous - // viewers so they can tell what they are looking at. await t.expect(photoviewer.sidebarRow("mdi-calendar").exists).ok(); - // Deny-list: edit affordances, face-marker controls, and every - // restricted section must be gone. - await photoviewer.assertSidebarIsReadOnly(); - await t.expect(photoviewer.markersVisibilityToggle.exists).notOk(); - await t.expect(photoviewer.markersEditToggle.exists).notOk(); + // The visitor share-link photo carries a Title but no Caption. + await photoviewer.assertSidebarIsReadOnly({ restricted: true, expectCaption: false }); // Merged file row renders for restricted sessions (type + size as // the title) but the filename subtitle must be suppressed. await t.expect(Selector(".p-lightbox-sidebar .meta-file .v-list-item-subtitle").exists).notOk(); - await t.expect(photoviewer.sidebarRow("mdi-camera").exists).notOk(); - await t.expect(photoviewer.sidebarRow("mdi-camera-iris").exists).notOk(); await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("People").exists).notOk(); await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("Labels").exists).notOk(); await t.expect(Selector(".p-lightbox-sidebar .text-subtitle-2").withText("Albums").exists).notOk(); diff --git a/frontend/tests/acceptance/acceptance-public/photos-sidebar-edit.js b/frontend/tests/acceptance/acceptance-public/photos-sidebar-edit.js index 4cf22c1b3..506d0e72d 100644 --- a/frontend/tests/acceptance/acceptance-public/photos-sidebar-edit.js +++ b/frontend/tests/acceptance/acceptance-public/photos-sidebar-edit.js @@ -67,6 +67,72 @@ test.meta("testID", "sidebar-edit-002").meta({ mode: "public" })("Common: Adds a await t.expect(Selector("div.is-photo").withAttribute("data-uid", uid).exists).ok(); }); +test.meta("testID", "sidebar-edit-004").meta({ mode: "public" })( + "Common: Removes a label and an album inline, undoes before save, and keeps them on the photo", + async (t) => { + const uid = await photoviewer.openSidebarOnFirstPhoto(); + + const stamp = Date.now(); + const labelTitle = `SidebarEditLabelUndo-${stamp}`; + const albumTitle = `SidebarEditAlbumUndo-${stamp}`; + + await photoviewer.typeAndConfirmInlineChip("Labels", labelTitle); + await photoviewer.typeAndConfirmInlineChip("Albums", albumTitle); + + const labelChip = photoviewer.chipByTitle("Labels", labelTitle); + const albumChip = photoviewer.chipByTitle("Albums", albumTitle); + + await photoviewer.removeInlineChip("Labels", labelTitle); + await t.expect(labelChip.exists).notOk(); + await photoviewer.removeInlineChip("Albums", albumTitle); + await t.expect(albumChip.exists).notOk(); + + await photoviewer.undoChipRemovals("Labels"); + await t.expect(labelChip.exists).ok(); + await photoviewer.undoChipRemovals("Albums"); + await t.expect(albumChip.exists).ok(); + + // Close + reopen rehydrates the sidebar from the API; both chips must + // still be there because Undo never reached the backend. + await photoviewer.triggerPhotoViewerAction("close-button"); + await photoviewer.openSidebarOnPhoto(uid); + + await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).ok(); + await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).ok(); + } +); + +test.meta("testID", "sidebar-edit-005").meta({ mode: "public" })( + "Common: Removes a label and an album inline, saves the removal, and detaches them from the photo", + async (t) => { + const uid = await photoviewer.openSidebarOnFirstPhoto(); + + const stamp = Date.now(); + const labelTitle = `SidebarEditLabelSave-${stamp}`; + const albumTitle = `SidebarEditAlbumSave-${stamp}`; + + await photoviewer.typeAndConfirmInlineChip("Labels", labelTitle); + await photoviewer.typeAndConfirmInlineChip("Albums", albumTitle); + + await photoviewer.removeInlineChip("Labels", labelTitle); + await photoviewer.removeInlineChip("Albums", albumTitle); + + await photoviewer.confirmChipRemovals("Labels"); + await photoviewer.confirmChipRemovals("Albums"); + + await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).notOk(); + await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).notOk(); + + // Close + reopen confirms the removal persisted past the round-trip; + // the saved photo no longer references either chip. + await photoviewer.triggerPhotoViewerAction("close-button"); + await photoviewer.openSidebarOnPhoto(uid); + + await t.expect(photoviewer.chipByTitle("Labels", labelTitle).exists).notOk(); + await t.expect(photoviewer.chipByTitle("Albums", albumTitle).exists).notOk(); + } +); + test.meta("testID", "sidebar-edit-003").meta({ mode: "public" })( "Common: Edits every taken-at, camera, and location field and confirms persistence", async (t) => { diff --git a/frontend/tests/acceptance/page-model/photo-edit.js b/frontend/tests/acceptance/page-model/photo-edit.js index 5fb3d00e7..ca7084d91 100644 --- a/frontend/tests/acceptance/page-model/photo-edit.js +++ b/frontend/tests/acceptance/page-model/photo-edit.js @@ -30,6 +30,7 @@ export default class Page { this.batchDialogToolbarCloseAction = Selector("div.v-dialog--batch-edit header.v-toolbar button.action-close"); this.batchToggleAllCheckbox = Selector("div.v-dialog--batch-edit .toggle-all div.v-selection-control__input"); this.batchToggleSelectCheckbox = Selector("div.v-dialog--batch-edit .toggle-select div.v-selection-control__input"); + this.batchLabels = Selector("div.v-dialog--batch-edit .input-labels", { timeout: 15000 }); this.detailsDone = Selector(".p-form-photo-details-meta button.action-done", { timeout: 15000, diff --git a/frontend/tests/acceptance/page-model/photoviewer.js b/frontend/tests/acceptance/page-model/photoviewer.js index 1cb778c07..6a9f5da85 100644 --- a/frontend/tests/acceptance/page-model/photoviewer.js +++ b/frontend/tests/acceptance/page-model/photoviewer.js @@ -107,10 +107,7 @@ export default class Page { // typeAndConfirmInlineChip adds a Labels or Albums chip via the always-rendered combobox. // The short wait lets Vuetify's combobox seat the typed value before Enter commits it. async typeAndConfirmInlineChip(sectionLabel, value) { - const sectionClass = sectionLabel === "Labels" ? "meta-labels" : sectionLabel === "Albums" ? "meta-albums" : null; - if (!sectionClass) { - throw new Error(`Unknown chip section: ${sectionLabel}`); - } + const sectionClass = this._chipSectionClass(sectionLabel); const input = Selector(`.p-lightbox-sidebar .${sectionClass} .meta-inline-edit input`, { timeout: 15000 }); await t.click(input).typeText(input, value); await t.wait(200); @@ -118,6 +115,42 @@ export default class Page { await t.expect(Selector(".meta-inline-menu").exists).notOk(); } + // chipByTitle returns the .meta-chip in the Labels or Albums section matching `title`. + chipByTitle(sectionLabel, title) { + const sectionClass = this._chipSectionClass(sectionLabel); + return Selector(`.p-lightbox-sidebar .${sectionClass}.metadata__chips .meta-chip`, { timeout: 15000 }).withText(title); + } + + // removeInlineChip clicks the × on a Labels or Albums chip matching `title`. + // Soft-removes locally via chipState..removals until confirm or undo. + async removeInlineChip(sectionLabel, title) { + await t.click(this.chipByTitle(sectionLabel, title).find(".meta-chip__remove")); + } + + // undoChipRemovals clicks Undo in the Labels or Albums chip toolbar, + // restoring all soft-removed chips for that section. + async undoChipRemovals(sectionLabel) { + const sectionClass = this._chipSectionClass(sectionLabel); + await t.click(Selector(`.p-lightbox-sidebar .${sectionClass} .meta-chip-undo`, { timeout: 15000 })); + } + + // confirmChipRemovals clicks Save in the Labels or Albums chip toolbar, + // persisting all pending removals. + async confirmChipRemovals(sectionLabel) { + const sectionClass = this._chipSectionClass(sectionLabel); + await t.click(Selector(`.p-lightbox-sidebar .${sectionClass} .meta-chip-confirm`, { timeout: 15000 })); + } + + _chipSectionClass(sectionLabel) { + if (sectionLabel === "Labels") { + return "meta-labels"; + } + if (sectionLabel === "Albums") { + return "meta-albums"; + } + throw new Error(`Unknown chip section: ${sectionLabel}`); + } + // startInlineEditOrAdd enters edit mode for Title or Caption by clicking the row, or // falls back to the add-prompt when the field is empty. async startInlineEditOrAdd(displayClass, promptLabel) { @@ -164,6 +197,16 @@ export default class Page { // editor or dialog. Paired with assertSidebarIsReadOnly() so every selector has both a // positive and a negative assertion. async assertSidebarIsEditable() { + // Title and Caption render unconditionally in editable mode; click each + // to open its inline editor and escape back out before continuing. + await this.startInlineEditOrAdd("meta-title", "Add a Title"); + await t.expect(Selector(".p-lightbox-sidebar .meta-inline-title").visible).ok(); + await t.pressKey("esc"); + + await this.startInlineEditOrAdd("meta-caption", "Add a Caption"); + await t.expect(Selector(".p-lightbox-sidebar .meta-inline-caption").visible).ok(); + await t.pressKey("esc"); + await t.click(this.sidebarRow("mdi-calendar")); await t.expect(this.dateTimeDialog.root.visible).ok(); await t.click(this.dateTimeDialog.cancel); @@ -180,6 +223,18 @@ export default class Page { await t.click(this.locationDialog.cancel); } + // People, Albums, and Labels: assert their editable-only controls render. + // The pencil toggle and chip-add inputs only exist when isEditable=true, + // so they discriminate edit mode from read-only mode (which uses the eye + // toggle and renders chips without an add input). + await t.expect(this.peopleHeader.exists).ok(); + await t.expect(this.markersEditToggle.exists).ok(); + await t.expect(this.markersVisibilityToggle.exists).notOk(); + await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-albums").exists).ok(); + await t.expect(Selector(".p-lightbox-sidebar .meta-albums .meta-inline-edit").exists).ok(); + await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-labels").exists).ok(); + await t.expect(Selector(".p-lightbox-sidebar .meta-labels .meta-inline-edit").exists).ok(); + // The next iteration's row click cancels the previous edit, so only the trailing // toolbar click is needed for cleanup. Gate on .visible so rows that v-show hides // for empty fields are skipped. @@ -194,19 +249,76 @@ export default class Page { await t.click(Selector(".p-lightbox-sidebar .v-toolbar-title")); } - // assertSidebarIsReadOnly is the inverse of assertSidebarIsEditable. - async assertSidebarIsReadOnly() { + // assertSidebarIsReadOnly verifies the sidebar surfaces no editors or dialogs. + // restricted=true asserts the visitor subset (Title, Caption, Date, Location). + // expect* flags toggle positive existence checks; defaults assert presence. + async assertSidebarIsReadOnly({ + restricted = false, + expectTitle = true, + expectCaption = true, + expectPeople = true, + expectLabels = true, + expectAlbums = true, + } = {}) { await t.click(this.sidebarRow("mdi-calendar")); await t.expect(this.dateTimeDialog.root.visible).notOk(); + if (await this.sidebarRow("mdi-map-marker").exists) { + await t.click(this.sidebarRow("mdi-map-marker")); + await t.expect(this.locationDialog.root.visible).notOk(); + } + + for (const [key, expected] of [ + ["title", expectTitle], + ["caption", expectCaption], + ]) { + const display = Selector(`.p-lightbox-sidebar .meta-${key}`); + if (expected) { + await t.expect(display.exists).ok(); + const row = display.parent(".v-list-item"); + await t.click(row); + await t.expect(row.find(".meta-inline-edit").exists).notOk(); + } else { + await t.expect(display.exists).notOk(); + } + } + // meta-add-prompt is an editing affordance and must never render here. + await t.expect(Selector(".p-lightbox-sidebar .meta-add-prompt").visible).notOk(); + + if (restricted) { + await t.expect(this.sidebarRow("mdi-camera").exists).notOk(); + await t.expect(this.sidebarRow("mdi-camera-iris").exists).notOk(); + await t.expect(this.peopleHeader.exists).notOk(); + await t.expect(Selector(".p-lightbox-sidebar .metadata__person-row").exists).notOk(); + await t.expect(this.markersVisibilityToggle.exists).notOk(); + await t.expect(this.markersEditToggle.exists).notOk(); + await t.expect(Selector(".p-lightbox-sidebar .meta-albums").exists).notOk(); + await t.expect(Selector(".p-lightbox-sidebar .meta-labels").exists).notOk(); + for (const key of ["subject", "artist", "copyright", "license", "keywords", "notes"]) { + await t.expect(Selector(`.p-lightbox-sidebar .v-list-item.meta-${key}`).exists).notOk(); + } + return; + } + if (await this.sidebarRow("mdi-camera").exists) { await t.click(this.sidebarRow("mdi-camera")); await t.expect(this.cameraDialog.root.visible).notOk(); } - if (await this.sidebarRow("mdi-map-marker").exists) { - await t.click(this.sidebarRow("mdi-map-marker")); - await t.expect(this.locationDialog.root.visible).notOk(); + // Mirror the editable assertions: section renders, but with read-only + // controls only — eye toggle for People, no chip-add input for Albums/Labels. + if (expectPeople) { + await t.expect(this.peopleHeader.exists).ok(); + await t.expect(this.markersVisibilityToggle.exists).ok(); + await t.expect(this.markersEditToggle.exists).notOk(); + } + if (expectAlbums) { + await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-albums").exists).ok(); + await t.expect(Selector(".p-lightbox-sidebar .meta-albums .meta-inline-edit").exists).notOk(); + } + if (expectLabels) { + await t.expect(Selector(".p-lightbox-sidebar .v-list-item.meta-labels").exists).ok(); + await t.expect(Selector(".p-lightbox-sidebar .meta-labels .meta-inline-edit").exists).notOk(); } for (const key of ["subject", "artist", "copyright", "license", "keywords", "notes"]) { diff --git a/internal/ai/nsfw/README.md b/internal/ai/nsfw/README.md index cb35e7cdf..7deb129ba 100644 --- a/internal/ai/nsfw/README.md +++ b/internal/ai/nsfw/README.md @@ -1,17 +1,42 @@ ## PhotoPrism — NSFW Package -**Last Updated:** December 23, 2025 +**Last Updated:** May 21, 2026 ### Overview -`internal/ai/nsfw` runs the built-in TensorFlow NSFW classifier to score images for drawing, hentai, neutral, porn, and sexy content. It is used during indexing and metadata workflows when the NSFW model is enabled. +`internal/ai/nsfw` runs the built-in TensorFlow NSFW classifier to score images for drawing, hentai, neutral, porn, and sexy content. It is the default backend that powers the `Type: nsfw` model entry in [`internal/ai/vision`](../vision/README.md) and is the only NSFW engine that ships with PhotoPrism out of the box; operators can override it through `vision.yml` with an Ollama or OpenAI endpoint when they prefer to run NSFW detection on a remote LLM. + +### Where It Gets Called + +The package itself only exposes the model loader and a thin scoring API (`Result.IsSafe`, `Result.IsNsfw(threshold)`). Two upstream callers wire it into the runtime: + +1. **Upload handler — [`internal/api/users_upload.go`](../../api/users_upload.go).** When `PHOTOPRISM_UPLOAD_NSFW=false` (default `true`), every accepted upload is screened by `vision.DetectNSFW` before indexing. Files that score above the threshold are deleted on the spot — they never reach `originals/`. When `UPLOAD_NSFW=true`, the upload path skips the check entirely. + +2. **Index + vision-worker pipelines — [`internal/photoprism/index_mediafile.go`](../../photoprism/index_mediafile.go), [`internal/workers/vision.go`](../../workers/vision.go), [`internal/workers/meta.go`](../../workers/meta.go).** When `PHOTOPRISM_DETECT_NSFW=true` (default `false`), the indexer marks new photos as `PhotoPrivate = true` if the NSFW model flags them. Both code paths short-circuit when `DetectNSFW()` is false — the model is then neither loaded nor invoked. + +Both flags are independent: you can reject uploads without flagging existing imports, flag existing imports without policing uploads, or both. The user-facing matrix lives at [docs.photoprism.app/user-guide/ai/nsfw/](https://docs.photoprism.app/user-guide/ai/nsfw/). + +### Detection Through the Labels Model + +When `Type: labels` is served by an Ollama or OpenAI engine and **both** `PHOTOPRISM_DETECT_NSFW=true` and `PHOTOPRISM_EXPERIMENTAL=true` are set, [`internal/config/config.go`](../../config/config.go) flips the package-level global `vision.DetectNSFWLabels` to `true`. The Ollama and OpenAI engine builders then swap their default label prompts for `LabelPromptNSFW` and the JSON schema generators add `nsfw` + `nsfw_confidence` fields, so NSFW classification piggybacks on the label-generation call instead of running as a separate inference pass. + +When the shortcut is active, the labels-path check in `index_mediafile.go` (`labels.IsNSFW(threshold)`) can promote a photo to private without this package being touched. The dedicated TensorFlow model in `internal/ai/nsfw` is still used as a fallback whenever the labels path either does not run or does not return NSFW signals, and whenever `vision run --models nsfw` is invoked directly. ### How It Works - **Model Loading** — Loads the NSFW SavedModel from `assets/models/` and resolves input/output ops (inferred if missing). -- **Input Preparation** — JPEG images are decoded and transformed to the configured input resolution. +- **Input Preparation** — JPEG thumbnails (default size `Fit720`, see `MediaFile.DetectNSFW`) are decoded and transformed to the configured input resolution. - **Inference & Output** — Produces five class probabilities mapped into a `Result` struct for downstream thresholds and UI badges. +### Threshold + +`vision.yml` carries a `Thresholds.NSFW` value (default `75`, range `0-100`) that controls how confident the model must be before a picture is flagged. Lower values are more aggressive; higher values more permissive. The threshold applies to both the dedicated NSFW model and the NSFW fields returned via the label-generation shortcut. + +```yaml +Thresholds: + NSFW: 75 +``` + ### Memory & Performance TensorFlow tensors allocate C memory and are freed by Go GC finalizers. To keep RSS bounded during long runs, PhotoPrism periodically triggers garbage collection to return freed tensor memory to the OS. Tune with: @@ -24,8 +49,13 @@ TensorFlow tensors allocate C memory and are freed by Go GC finalizers. To keep - **Model fails to load:** Verify `saved_model.pb` and `variables/` exist under the model path. - **Unexpected scores:** Confirm the input resolution matches the model and that logits are handled correctly. - **High memory usage:** Adjust `PHOTOPRISM_TF_GC_EVERY` or reduce concurrent indexing load. +- **NSFW detection appears to stop working after switching labels to an LLM:** Confirm both `PHOTOPRISM_DETECT_NSFW=true` and `PHOTOPRISM_EXPERIMENTAL=true` are set. Without both, the labels-path shortcut is disabled and only an explicit `vision run --models nsfw` (or another caller that goes through this package directly) will produce NSFW flags. ### Related Docs -- [`internal/ai/vision/README.md`](../vision/README.md) — model registry and run scheduling +- [`internal/ai/vision/README.md`](../vision/README.md) — model registry, run scheduling, and the `DetectNSFWLabels` global +- [`internal/ai/vision/ollama/README.md`](../vision/ollama/README.md) — Ollama engine: `LabelPromptNSFW` swap-in +- [`internal/ai/vision/openai/README.md`](../vision/openai/README.md) — OpenAI engine: NSFW-aware prompt and schema +- [`internal/ai/vision/schema/README.md`](../vision/schema/README.md) — JSON schema variants used when NSFW is enabled - [`internal/ai/tensorflow/README.md`](../tensorflow/README.md) — TensorFlow helpers, GC behavior, and model loading +- [docs.photoprism.app/user-guide/ai/nsfw/](https://docs.photoprism.app/user-guide/ai/nsfw/) — user-facing reference + flag matrix diff --git a/internal/ai/vision/README.md b/internal/ai/vision/README.md index 2cd862f62..cb3c323ba 100644 --- a/internal/ai/vision/README.md +++ b/internal/ai/vision/README.md @@ -1,6 +1,6 @@ ## PhotoPrism — Vision Package -**Last Updated:** March 3, 2026 +**Last Updated:** May 21, 2026 ### Overview @@ -201,6 +201,14 @@ Models: - **Ollama**: private, GPU/CPU-hosted multimodal LLMs; best for richer captions/labels without cloud traffic. - **OpenAI**: highest quality reasoning and multimodal support; requires API key and network access. +### NSFW Detection + +NSFW is wired through the same model registry as labels, captions, and faces — `Type: nsfw` resolves to the built-in TensorFlow classifier by default, and can be overridden in `vision.yml` to point at an Ollama or OpenAI endpoint. + +There is also a fast-path: when `Type: labels` is served by an LLM, PhotoPrism can ask the labels call to include `nsfw` + `nsfw_confidence` in the same response. This is gated by the package-level global `DetectNSFWLabels`, set from `config.go` as `DetectNSFW() && Experimental()` — both `PHOTOPRISM_DETECT_NSFW=true` **and** `PHOTOPRISM_EXPERIMENTAL=true` are required. When either flag is off, the labels prompt stays on `LabelPromptDefault` (no NSFW fields), and `labels.IsNSFW()` cannot trigger. + +The runtime guards in `internal/photoprism/index_mediafile.go` and `internal/workers/vision.go` additionally short-circuit any NSFW promotion on `conf.DetectNSFW()`. The dedicated `Type: nsfw` model is filtered out of scheduled runs by `VisionModelShouldRun` whenever `DetectNSFW()` is false. See [`internal/ai/nsfw/README.md`](../nsfw/README.md) for the full call-graph and the user-facing matrix at [docs.photoprism.app/user-guide/ai/nsfw/](https://docs.photoprism.app/user-guide/ai/nsfw/). + ### Model Unload on Idle PhotoPrism currently keeps TensorFlow models resident for the lifetime of the process to avoid repeated load costs. A future “model unload on idle” mode would track last-use timestamps and close the TensorFlow session/graph after a configurable idle period, releasing the model’s memory footprint back to the OS. The trade-off is higher latency and CPU overhead when a model is used again, plus extra I/O to reload weights. This may be attractive for low-frequency or memory-constrained deployments but would slow continuous indexing jobs, so it is not enabled today. diff --git a/internal/ai/vision/ollama/README.md b/internal/ai/vision/ollama/README.md index e9293e8d8..8514a6757 100644 --- a/internal/ai/vision/ollama/README.md +++ b/internal/ai/vision/ollama/README.md @@ -1,6 +1,6 @@ ## PhotoPrism — Ollama Engine Integration -**Last Updated:** February 23, 2026 +**Last Updated:** May 21, 2026 ### Overview @@ -41,7 +41,7 @@ This package provides PhotoPrism’s native adapter for Ollama-compatible multim - Captions: no system prompt by default; rely on user prompt or set one explicitly for stylistic needs. - **User Prompts** - Captions use `CaptionPrompt`, which requests one sentence in active voice. - - Labels default to `LabelPromptDefault`; when `DetectNSFWLabels` is true, the adapter swaps in `LabelPromptNSFW`. + - Labels default to `LabelPromptDefault`; when the package-level `DetectNSFWLabels` global is true, the adapter swaps in `LabelPromptNSFW`. The global is set by `config.go` to `DetectNSFW() && Experimental()`, so both `PHOTOPRISM_DETECT_NSFW=true` and `PHOTOPRISM_EXPERIMENTAL=true` are required to enable the NSFW-aware prompt. - For stricter noun enforcement, set `Prompt` to `LabelPromptStrict`. - **Schemas** - Labels rely on `schema.LabelsJson(nsfw)` (simple JSON template). Setting `Format: json` auto-attaches a reminder (`model.SchemaInstructions()`). diff --git a/internal/ai/vision/openai/README.md b/internal/ai/vision/openai/README.md index ff0dfdae5..8f2680e87 100644 --- a/internal/ai/vision/openai/README.md +++ b/internal/ai/vision/openai/README.md @@ -1,6 +1,6 @@ ## PhotoPrism — OpenAI API Integration -**Last Updated:** November 14, 2025 +**Last Updated:** May 21, 2026 ### Overview @@ -28,7 +28,7 @@ This package contains PhotoPrism’s adapter for the OpenAI Responses API. It en ### Prompt, Model, & Schema Guidance - **Models:** The adapter targets GPT‑5 vision tiers (e.g. `gpt-5-nano`, `gpt-5-mini`). These models support image inputs, structured outputs, and deterministic settings. Set `Name` to the exact provider identifier so defaults are applied correctly. Caption models share the same configuration surface and run through the same adapter. -- **Prompts:** Defaults live in `defaults.go`. Captions use a single-sentence instruction; labels use `LabelPromptDefault` (or `LabelPromptNSFW` when PhotoPrism requests NSFW metadata). Custom prompts should retain schema reminders so structured outputs stay valid. +- **Prompts:** Defaults live in `defaults.go`. Captions use a single-sentence instruction; labels use `LabelPromptDefault` (or `LabelPromptNSFW` when the package-level `vision.DetectNSFWLabels` global is true, which requires both `PHOTOPRISM_DETECT_NSFW=true` and `PHOTOPRISM_EXPERIMENTAL=true`). Custom prompts should retain schema reminders so structured outputs stay valid. - **Schemas:** Labels use the JSON schema returned by `schema.LabelsJsonSchema(nsfw)`; the response format name is derived via `schema.JsonSchemaName` (e.g. `photoprism_vision_labels_v1`). Captions omit schemas unless operators explicitly request a structured format. - **When to keep defaults:** For most deployments, leaving `System`, `Prompt`, `Schema`, and `Options` unset yields stable output with minimal configuration. Override them only when domain-specific language or custom scoring is necessary, and add regression tests alongside. diff --git a/internal/ai/vision/schema/README.md b/internal/ai/vision/schema/README.md index 4c8222cd6..8f6c2c92d 100644 --- a/internal/ai/vision/schema/README.md +++ b/internal/ai/vision/schema/README.md @@ -1,6 +1,6 @@ ## PhotoPrism — Vision Schema Reference -**Last Updated:** November 14, 2025 +**Last Updated:** May 21, 2026 ### Overview @@ -13,12 +13,12 @@ Both helpers build on the same field set (`name`, `confidence`, `topicality`, an ### Schema Types & Differences -| Helper | Target Engine | Format | Validation Style | When To Use | -|:--------------------------|:-------------------------|:-------------------------------------------------------|:------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------| -| `LabelsJsonSchema(false)` | OpenAI (standard labels) | JSON Schema Draft | Strong: OpenAI enforces field types/ranges server-side before returning a response. | When calling GPT‑vision models via `ApiFormatOpenAI` to ensure PhotoPrism receives well-formed label arrays. | -| `LabelsJsonSchema(true)` | OpenAI (labels + NSFW) | JSON Schema Draft with additional boolean/float fields | Strong: same enforcement plus required NSFW fields. | When `DetectNSFWLabels` or NSFW-specific prompts are active and the model must emit `nsfw` + `nsfw_confidence`. | -| `LabelsJson(false)` | Ollama (standard labels) | Plain JSON example | Soft: model is nudged to mimic the structure through prompt instructions. | When running self-hosted Ollama models that support “JSON mode” but do not consume JSON Schema definitions. | -| `LabelsJson(true)` | Ollama (labels + NSFW) | Plain JSON example with NSFW keys | Soft: prompts describe the required keys; the adapter validates after parsing. | When Ollama prompts mention NSFW scoring or PhotoPrism sets `DetectNSFWLabels=true`. | +| Helper | Target Engine | Format | Validation Style | When To Use | +|:--------------------------|:-------------------------|:-------------------------------------------------------|:------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `LabelsJsonSchema(false)` | OpenAI (standard labels) | JSON Schema Draft | Strong: OpenAI enforces field types/ranges server-side before returning a response. | When calling GPT‑vision models via `ApiFormatOpenAI` to ensure PhotoPrism receives well-formed label arrays. | +| `LabelsJsonSchema(true)` | OpenAI (labels + NSFW) | JSON Schema Draft with additional boolean/float fields | Strong: same enforcement plus required NSFW fields. | When `DetectNSFWLabels` (gated on `DETECT_NSFW=true && EXPERIMENTAL=true`) or NSFW-specific prompts are active and the model must emit `nsfw` + `nsfw_confidence`. | +| `LabelsJson(false)` | Ollama (standard labels) | Plain JSON example | Soft: model is nudged to mimic the structure through prompt instructions. | When running self-hosted Ollama models that support “JSON mode” but do not consume JSON Schema definitions. | +| `LabelsJson(true)` | Ollama (labels + NSFW) | Plain JSON example with NSFW keys | Soft: prompts describe the required keys; the adapter validates after parsing. | When Ollama prompts mention NSFW scoring or PhotoPrism sets `DetectNSFWLabels=true`. | **Key technical distinction:** OpenAI’s Responses API accepts a JSON Schema (see `LabelsJsonSchema*`) and guarantees compliance by rejecting invalid responses, while Ollama currently relies on prompt-directed output. For Ollama integrations we provide a representative JSON document (`LabelsJson*`) that models can imitate; PhotoPrism then normalizes and validates the results in Go. diff --git a/internal/api/README.md b/internal/api/README.md index 564fd9a17..535b1bdbe 100644 --- a/internal/api/README.md +++ b/internal/api/README.md @@ -30,6 +30,7 @@ The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each fil - Enforce rate limiting with the shared limiters (`limiter.Auth`, `limiter.Login`) and respond with `limiter.AbortJSON` to maintain consistent 429 JSON payloads. - Derive client IPs through `api.ClientIP` and extract bearer tokens with `header.BearerToken` or the helper setters. Use constant-time comparison for tokens and secrets. - For downloads or proxy endpoints, validate URLs against allowed schemes (`http`, `https`) and reject private or loopback addresses unless explicitly required. +- **Upload-time NSFW screening (`users_upload.go`)** — when `PHOTOPRISM_UPLOAD_NSFW=false`, the upload handler runs `vision.DetectNSFW` against every accepted file and deletes any file flagged above the NSFW threshold before it reaches `originals/`. The check is skipped entirely when `UPLOAD_NSFW=true` (default). See [`internal/ai/nsfw/README.md`](../ai/nsfw/README.md) for the full NSFW call-graph and flag matrix. ### Audit Logging diff --git a/internal/commands/README.md b/internal/commands/README.md index 2a290d9e7..ca3d43668 100644 --- a/internal/commands/README.md +++ b/internal/commands/README.md @@ -20,6 +20,24 @@ The `commands` package hosts the CLI implementation for the PhotoPrism binary. C - When integrating configuration options, call the accessors on `*config.Config` (for example, `conf.ClusterUUID()`) rather than mutating option structs directly. - For HTTP interactions, depend on the safe download helpers in `pkg/http/safe` or the specialized wrappers in `internal/thumb/avatar` to inherit timeout, size, and SSRF protection defaults. +### Positional Arguments & Flag Order + +`urfave/cli` v2 delegates flag parsing to the Go stdlib `flag` package, which **stops parsing at the first non-flag token**. For any subcommand that takes a positional argument (for example `photoprism users mod USERNAME --role guest`), flags placed **after** the positional are not parsed — they are returned as additional positionals and `ctx.IsSet(...)` reports `false` for each of them. + +Without guarding for this, an action that conditionally applies values via `if ctx.IsSet("role") { ... }` will silently no-op while still logging success. + +#### Mitigation Helper + +Call `commands.RejectTrailingFlags(ctx)` near the top of every leaf action whose CLI shape is `Action [--flags...]`. The helper returns a clear `flag "--name" must appear before positional arguments` error when it detects a flag-like token in `ctx.Args().Tail()`, so the user is told to re-order rather than seeing a silent no-op. Pair it with `commands.TrailingFlagToken(ctx)` when the action needs to inspect the offending token before deciding what to do. + +Helper behavior: + +- Single-dash `-` and the `--` terminator are treated as positionals, not flags, so commands like `photoprism backup -` (write to stdout) and `photoprism foo bar -- --literal` keep working. +- Unknown flags placed before the positional are not the helper's concern — `urfave/cli` raises its own "flag provided but not defined" error in that path. +- For commands that use a flag-based identifier instead of a positional, still call the helper after the existence check so trailing flags surface as a usage error rather than a silent ignore. + +The underlying parser limitation is tracked as a known issue for a broader fix; until a global arg-reorder pass lands, all new leaf actions that accept a positional MUST call `RejectTrailingFlags` before applying flag values. + ### Configuration & Flags Integration - Define new options in `internal/config/options.go` with the appropriate struct tags (`yaml`, `json`, `flag`) so they propagate to YAML, CLI, and API layers consistently. diff --git a/internal/commands/auth_add.go b/internal/commands/auth_add.go index da5b29246..ca09d2fa8 100644 --- a/internal/commands/auth_add.go +++ b/internal/commands/auth_add.go @@ -48,6 +48,12 @@ func authAddAction(ctx *cli.Context) error { // Get username from command flag. userName := clean.Username(ctx.Args().First()) + // Reject flags placed after the username; the stdlib flag parser + // would silently drop them and create the token with the defaults. + if err := RejectTrailingFlags(ctx); err != nil { + return err + } + // Find user account. user := entity.FindUserByName(userName) diff --git a/internal/commands/cli_args.go b/internal/commands/cli_args.go new file mode 100644 index 000000000..abe0d9adf --- /dev/null +++ b/internal/commands/cli_args.go @@ -0,0 +1,37 @@ +package commands + +import ( + "fmt" + "strings" + + "github.com/urfave/cli/v2" +) + +// TrailingFlagToken returns the first arg after the leading positional that +// looks like a CLI flag (e.g. "--name" or "-r"). +// +// urfave/cli v2 delegates flag parsing to the stdlib flag package, which +// stops at the first non-flag token; flags placed after a positional are +// silently ignored. Callers use this helper to reject such invocations +// instead of acting on a context with the supplied flag values dropped. +func TrailingFlagToken(ctx *cli.Context) string { + for _, a := range ctx.Args().Tail() { + if a == "--" { + return "" + } + if strings.HasPrefix(a, "-") && len(a) > 1 { + return a + } + } + return "" +} + +// RejectTrailingFlags returns a usage error when flag-like tokens appear after +// the leading positional argument, naming the first offending token so users +// can fix the invocation rather than seeing a silent no-op. +func RejectTrailingFlags(ctx *cli.Context) error { + if tok := TrailingFlagToken(ctx); tok != "" { + return fmt.Errorf("flag %q must appear before positional arguments", tok) + } + return nil +} diff --git a/internal/commands/cli_args_test.go b/internal/commands/cli_args_test.go new file mode 100644 index 000000000..881fa2f43 --- /dev/null +++ b/internal/commands/cli_args_test.go @@ -0,0 +1,56 @@ +package commands + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +func TestTrailingFlagToken(t *testing.T) { + cases := []struct { + name string + args []string + want string + }{ + {"NoTail", []string{"alice"}, ""}, + {"OnlyPositionals", []string{"alice", "bob"}, ""}, + {"LongFlagAfterPositional", []string{"alice", "--name", "Alicia"}, "--name"}, + {"EqualsFlagAfterPositional", []string{"alice", "--role=guest"}, "--role=guest"}, + {"ShortFlagAfterPositional", []string{"alice", "-r", "guest"}, "-r"}, + {"BareDashIsPositional", []string{"alice", "-"}, ""}, + {"DoubleDashStopsScan", []string{"alice", "--", "--name", "literal"}, ""}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + set := flag.NewFlagSet("test", flag.ContinueOnError) + if err := set.Parse(tc.args); err != nil { + t.Fatalf("flag parse: %v", err) + } + ctx := cli.NewContext(nil, set, nil) + assert.Equal(t, tc.want, TrailingFlagToken(ctx)) + }) + } +} + +func TestRejectTrailingFlags(t *testing.T) { + t.Run("NoTrailingFlag", func(t *testing.T) { + set := flag.NewFlagSet("test", flag.ContinueOnError) + _ = set.Parse([]string{"alice"}) + ctx := cli.NewContext(nil, set, nil) + assert.NoError(t, RejectTrailingFlags(ctx)) + }) + + t.Run("TrailingFlagReturnsError", func(t *testing.T) { + set := flag.NewFlagSet("test", flag.ContinueOnError) + _ = set.Parse([]string{"alice", "--name", "Alicia"}) + ctx := cli.NewContext(nil, set, nil) + err := RejectTrailingFlags(ctx) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "--name") + assert.Contains(t, err.Error(), "must appear before positional arguments") + } + }) +} diff --git a/internal/commands/clients_add.go b/internal/commands/clients_add.go index 7bebb8e54..20a49646f 100644 --- a/internal/commands/clients_add.go +++ b/internal/commands/clients_add.go @@ -31,6 +31,12 @@ func clientsAddAction(ctx *cli.Context) error { return CallWithDependencies(ctx, func(conf *config.Config) error { conf.MigrateDb(false, nil) + // Reject flags placed after the username; the stdlib flag parser + // would silently drop them and register the client with the defaults. + if err := RejectTrailingFlags(ctx); err != nil { + return err + } + frm := form.AddClientFromCli(ctx) interactive := true diff --git a/internal/commands/clients_mod.go b/internal/commands/clients_mod.go index ba9150ec0..746b75f35 100644 --- a/internal/commands/clients_mod.go +++ b/internal/commands/clients_mod.go @@ -34,6 +34,12 @@ func clientsModAction(ctx *cli.Context) error { return cli.ShowSubcommandHelp(ctx) } + // Reject flags placed after the client id; the stdlib flag parser + // would silently drop them and report "client updated" with no changes. + if err := RejectTrailingFlags(ctx); err != nil { + return err + } + // Find client record. client := entity.FindClientByUID(frm.ID()) diff --git a/internal/commands/users_add.go b/internal/commands/users_add.go index c73621cb9..2ce005602 100644 --- a/internal/commands/users_add.go +++ b/internal/commands/users_add.go @@ -28,6 +28,12 @@ func usersAddAction(ctx *cli.Context) error { return CallWithDependencies(ctx, func(conf *config.Config) error { conf.MigrateDb(false, nil) + // Reject flags placed after the username; the stdlib flag parser + // would silently drop them and create the account with the defaults. + if err := RejectTrailingFlags(ctx); err != nil { + return err + } + frm := form.NewUserFromCli(ctx) interactive := true diff --git a/internal/commands/users_mod.go b/internal/commands/users_mod.go index fe2ec8a46..05ec55211 100644 --- a/internal/commands/users_mod.go +++ b/internal/commands/users_mod.go @@ -37,6 +37,12 @@ func usersModAction(ctx *cli.Context) error { return cli.ShowSubcommandHelp(ctx) } + // Reject flags placed after the username; the stdlib flag parser + // would silently drop them and report "user updated" with no changes. + if err := RejectTrailingFlags(ctx); err != nil { + return err + } + // Find user record. var m *entity.User diff --git a/internal/commands/users_mod_test.go b/internal/commands/users_mod_test.go index 7415f9511..c44b0339c 100644 --- a/internal/commands/users_mod_test.go +++ b/internal/commands/users_mod_test.go @@ -4,6 +4,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/photoprism/photoprism/internal/entity" ) func TestUsersModCommand(t *testing.T) { @@ -25,4 +28,22 @@ func TestUsersModCommand(t *testing.T) { assert.Error(t, err) assert.Empty(t, output) }) + t.Run("RejectFlagsAfterPositional", func(t *testing.T) { + // Run with the broken arg order QA reported (positional first, then flags). + // The stdlib flag parser stops at "alice", so --name / --role would + // silently no-op without RejectTrailingFlags. + output, err := RunWithTestContext(UsersModCommand, []string{"mod", "alice", "--name", "Alicia", "--role", "guest"}) + + require.Error(t, err) + assert.Contains(t, err.Error(), "must appear before positional arguments") + assert.Empty(t, output) + + // Confirm the alice fixture is untouched when it still exists. Earlier + // tests in the suite may have deleted it, so skip the comparison in + // that case rather than coupling this test to suite ordering. + if alice := entity.FindUserByName("alice"); alice != nil { + assert.Equal(t, "Alice", alice.DisplayName) + assert.Equal(t, "admin", alice.UserRole) + } + }) } diff --git a/internal/config/flags.go b/internal/config/flags.go index 2c0ce9a4b..3d5bc5809 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -289,7 +289,7 @@ var Flags = CliFlags{ Flag: &cli.BoolFlag{ Name: "upload-nsfw", Aliases: []string{"n"}, - Usage: "allows uploads that might be offensive (detecting unsafe content requires TensorFlow)", + Usage: "allows uploads that might be offensive (when disabled, files flagged by the NSFW model are rejected before indexing)", EnvVars: EnvVars("UPLOAD_NSFW"), }}, { Flag: &cli.StringFlag{ @@ -1225,7 +1225,7 @@ var Flags = CliFlags{ }}, { Flag: &cli.BoolFlag{ Name: "detect-nsfw", - Usage: "flags newly added pictures as private if they might be offensive (requires TensorFlow)", + Usage: "flags newly added pictures as private if they might be offensive (uses the configured NSFW model; built-in TensorFlow by default)", EnvVars: EnvVars("DETECT_NSFW"), }}, { Flag: &cli.StringFlag{ diff --git a/internal/photoprism/README.md b/internal/photoprism/README.md index e276b16e1..127ef7155 100644 --- a/internal/photoprism/README.md +++ b/internal/photoprism/README.md @@ -1,6 +1,6 @@ ## PhotoPrism — Core Package -**Last Updated:** March 3, 2026 +**Last Updated:** May 21, 2026 ### Overview @@ -42,6 +42,7 @@ - Import: run via `ImportWorker` with `ImportOptions`; stacked handling is driven by metadata and document IDs. - Converters: use `Convert.ToImage` / `Convert.ToVideo` / `Convert.ToJson`; options come from `config.Config`. - Vision: thumbnails for vision models are selected in `mediafile_vision.go`; ensure models exist in `internal/ai/vision`. +- NSFW: `index_mediafile.go` flags new photos as `PhotoPrivate` when the labels-path NSFW shortcut (LLM with `DETECT_NSFW=true && EXPERIMENTAL=true`) hits or, as a fallback, when `m.DetectNSFW()` returns true and `PHOTOPRISM_DETECT_NSFW=true`. Both promotions short-circuit when `DetectNSFW()` is false. Full call-graph + flag matrix in [`internal/ai/nsfw/README.md`](../ai/nsfw/README.md). - Tests: targeted runs keep iteration fast, e.g. - `go test ./internal/photoprism -run TestMediaFile_ -count=1` - `go test ./internal/photoprism/index_mediafile_test.go -run TestIndexMediaFile` diff --git a/pkg/http/security/security.go b/pkg/http/security/security.go index f1705cf14..859d7e3f2 100644 --- a/pkg/http/security/security.go +++ b/pkg/http/security/security.go @@ -1,7 +1,7 @@ /* Package security provides shared HTTP security utilities. -Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved. +Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved. Questions? Email us at hello@photoprism.app or visit our website to learn more about our team, products and services: https://www.photoprism.app/ diff --git a/setup/charts/plus/Chart.yaml b/setup/charts/plus/Chart.yaml new file mode 100644 index 000000000..f19ae7a6d --- /dev/null +++ b/setup/charts/plus/Chart.yaml @@ -0,0 +1,43 @@ +apiVersion: v2 +name: photoprism-plus +type: application +description: >- + Deploys PhotoPrism® on Kubernetes with optional portal integration, + persistent storage, and configurable authentication backends. +version: "1.1.0" +appVersion: "260305" +home: https://www.photoprism.app/ +icon: https://dl.photoprism.app/icons/logo/256.png +maintainers: + - name: PhotoPrism UG + url: https://www.photoprism.app/contact +keywords: + - photos + - photoprism + - ai + - media + - dam + - webdav + - oidc +sources: + - https://docs.photoprism.app/ + - https://www.photoprism.app/kb + - https://www.photoprism.app/kb/getting-support + - https://github.com/photoprism/photoprism +annotations: + catalog.cattle.io/display-name: "PhotoPrism®" + catalog.cattle.io/type: "app" + catalog.cattle.io/category: "Digital Asset Management (DAM)" + catalog.cattle.io/requests-cpu: "1000m" + catalog.cattle.io/requests-memory: "1Gi" + catalog.cattle.io/os: "linux" + catalog.cattle.io/featured: "true" + artifacthub.io/license: "AGPL-3.0-or-later" + artifacthub.io/category: "storage" + artifacthub.io/links: | + - name: Documentation + url: https://docs.photoprism.app/ + - name: Knowledge Base + url: https://www.photoprism.app/kb + - name: Support + url: https://www.photoprism.app/kb/getting-support \ No newline at end of file diff --git a/setup/charts/plus/README.md b/setup/charts/plus/README.md new file mode 100644 index 000000000..04e772610 --- /dev/null +++ b/setup/charts/plus/README.md @@ -0,0 +1,93 @@ +## Introduction + +This chart provides an easy way to run [PhotoPrism®](https://www.photoprism.app/editions#compare) on a home lab or small business Kubernetes cluster while maintaining an approachable configuration. + +## Highlights + +- Runs the official `photoprism/photoprism` image with non-root defaults (UID/GID 1000). +- Ships with SQLite out of the box; switch to MariaDB/MySQL by filling in the database section. +- Creates lightweight PVCs for `/photoprism/storage` (5 GiB) and, optionally, `/photoprism/originals` (10 GiB) – the cluster’s default storage class is used unless you override it. +- Surfaces the most relevant customization settings (locale, themes, login footer, CORS/CDN, backup schedule) directly in Rancher’s UI or via `values.yaml`. + +## Quick Start + +```bash +helm repo add photoprism https://charts.photoprism.app/photoprism +helm repo update photoprism +helm upgrade --install photos photoprism/photoprism-plus \ + --namespace photos --create-namespace +``` + +This deploys PhotoPrism with SQLite storage. To use MariaDB (recommended for larger libraries): + +```bash +helm upgrade --install photos photoprism/photoprism-plus \ + --namespace photos \ + --set database.driver=mysql \ + --set database.server=mariadb.default.svc.cluster.local:3306 \ + --set database.name=photoprism \ + --set database.user=photoprism \ + --set database.password=changeme +``` + +## Storage & Backups + +- `persistence.storage` is always created and holds the application state. +- `persistence.originals` can be disabled or redirected to an NFS export when you manage originals elsewhere. +- Backup options (`PHOTOPRISM_BACKUP_*`) are exposed so you can point scheduled backups to another path or tweak retention. + +## Customization + +Key values you might want to adjust: + +- `config.PHOTOPRISM_SITE_TITLE`, `config.PHOTOPRISM_APP_NAME`, `config.PHOTOPRISM_DEFAULT_THEME` +- `config.PHOTOPRISM_AUTH_MODE`, `config.PHOTOPRISM_LOGIN_INFO`, `config.PHOTOPRISM_PASSWORD_LENGTH` +- `config.PHOTOPRISM_CDN_URL`, `config.PHOTOPRISM_CORS_ORIGIN` +- `config.PHOTOPRISM_FILES_QUOTA`, `config.PHOTOPRISM_UPLOAD_LIMIT` + +See [`values.yaml`](https://github.com/photoprism/photoprism/blob/develop/setup/charts/plus/values.yaml) for the full list. + +## Networking & TLS + +The chart exposes PhotoPrism on TCP 2342 through a ClusterIP service. You can override the service type or enable an Ingress resource when you terminate TLS in the cluster edge: + +```yaml +service: + type: ClusterIP + port: 2342 + +ingress: + enabled: true + className: traefik + hosts: + - host: photos.example.com + paths: + - path: / + pathType: Prefix + tls: + - hosts: + - photos.example.com + secretName: photos-tls +``` + +Because TLS typically terminates at the ingress or proxy layer, the chart keeps `PHOTOPRISM_DISABLE_TLS` set to `true`. Only enable PhotoPrism’s internal TLS if your cluster design requires end-to-end encryption and you manage the certificates yourself. + +## Security Tips + +- When `adminPassword` is left blank, a random password is generated and stored in `secret/-photoprism-secrets`. +- Prefer MariaDB/MySQL for multi-user setups or large libraries, and back up both the database and storage PVCs regularly. +- If you expose PhotoPrism on the public internet, pair it with HTTPS termination (Ingress/TLS or an external proxy) and keep the container image up to date. + +## Getting Support + +Commercial support is available with our Starter, Business, and Enterprise team plans: + +- https://www.photoprism.app/teams#compare +- https://www.photoprism.app/kb/getting-support + +## PhotoPrism® Documentation + +For more information on specific features, services and related resources, please refer to the other documentation available in our Knowledge Base and User Guide: + +- [PhotoPrism® User Guide](https://docs.photoprism.app/user-guide/) +- [PhotoPrism® Knowledge Base](https://www.photoprism.app/kb) diff --git a/setup/charts/plus/questions.yaml b/setup/charts/plus/questions.yaml new file mode 100644 index 000000000..3479e3c75 --- /dev/null +++ b/setup/charts/plus/questions.yaml @@ -0,0 +1,413 @@ +questions: + - variable: image.tag + group: "Customization" + label: "Image Tag" + description: "Override the container tag; leave empty to use the chart appVersion." + type: string + - variable: config.PHOTOPRISM_SITE_TITLE + group: "Customization" + label: "Site Title" + description: "Displayed title in the PhotoPrism UI. Example: Photo Library." + type: string + - variable: config.PHOTOPRISM_SITE_CAPTION + group: "Customization" + label: "Site Caption" + description: "Optional tagline shown beneath the title. Example: Explore Your Media." + type: string + - variable: config.PHOTOPRISM_SITE_DESCRIPTION + group: "Customization" + label: "Site Description" + description: "Short description used in metadata and link previews. Example: Private family archive." + type: string + - variable: config.PHOTOPRISM_SITE_AUTHOR + group: "Customization" + label: "Site Author" + description: "Owner or copyright notice displayed in metadata. Example: © 2025 The Smith Family." + type: string + - variable: config.PHOTOPRISM_SITE_FAVICON + group: "Customization" + label: "Site Favicon" + description: "Filename or URL of the favicon asset. Example: /photoprism/assets/favicon.ico." + type: string + - variable: config.PHOTOPRISM_SITE_PREVIEW + group: "Customization" + label: "Sharing Preview URL" + description: "Image URL used for social previews. Example: https://example.com/preview.jpg." + type: string + - variable: config.PHOTOPRISM_CDN_URL + group: "Customization" + label: "CDN URL" + description: "Base URL for serving static assets via CDN. Example: https://cdn.example.com." + type: string + - variable: config.PHOTOPRISM_CDN_VIDEO + group: "Customization" + label: "CDN Video URL" + description: "Optional CDN endpoint for streaming videos. Example: https://videos.example.com." + type: string + - variable: config.PHOTOPRISM_CORS_ORIGIN + group: "Customization" + label: "CORS Allowed Origin" + description: "Origin allowed to issue cross-origin requests. Example: https://app.example.com." + type: string + - variable: config.PHOTOPRISM_CORS_HEADERS + group: "Customization" + label: "CORS Headers" + description: "Comma-separated headers exposed for cross-origin requests." + type: string + default: "Accept, Accept-Ranges, Content-Disposition, Content-Encoding, Content-Range, Location" + - variable: config.PHOTOPRISM_CORS_METHODS + group: "Customization" + label: "CORS Methods" + description: "Allowed HTTP methods for cross-origin requests." + type: string + default: "GET, HEAD, OPTIONS" + - variable: config.PHOTOPRISM_APP_NAME + group: "Customization" + label: "App Name" + description: "Display name when PhotoPrism is installed as a PWA." + type: string + - variable: config.PHOTOPRISM_APP_ICON + group: "Customization" + label: "App Icon" + description: "Icon style for the PWA shortcut (logo, app, mint, square, etc.). Example: logo." + type: string + - variable: config.PHOTOPRISM_APP_COLOR + group: "Customization" + label: "App Color" + description: "Hex colour used for the PWA splash screen and tint. Example: #1d1d1e." + type: string + default: "#1d1d1e" + - variable: config.PHOTOPRISM_DEFAULT_TIMEZONE + group: "Customization" + label: "Default Timezone" + description: "Timezone name used for scheduling (e.g. Europe/Berlin)." + type: string + - variable: config.PHOTOPRISM_PLACES_LOCALE + group: "Customization" + label: "Places Locale" + description: "Language code for location details (en, de, local, etc.)." + type: string + default: "local" + - variable: config.PHOTOPRISM_DEFAULT_THEME + group: "Customization" + label: "Default Theme" + description: "Theme name applied to new users (leave blank for PhotoPrism default)." + type: string + - variable: config.PHOTOPRISM_DEFAULT_LOCALE + group: "Customization" + label: "Default Locale" + description: "Language code assigned to new users. Example: en or de." + type: string + default: "en" + - variable: config.PHOTOPRISM_AUTH_MODE + group: "Authentication" + label: "Authentication Mode" + description: "Select password-protected or public access." + type: enum + options: + - "password" + - "public" + default: "password" + - variable: config.PHOTOPRISM_LOG_LEVEL + group: "Customization" + label: "Log Level" + description: "Application log verbosity. Example: info." + type: enum + options: + - "trace" + - "debug" + - "info" + - "warning" + - "error" + default: "info" + - variable: adminUser + group: "Administration" + label: "Admin Username" + description: "Initial administrator account." + type: string + default: "admin" + required: true + - variable: adminPassword + group: "Administration" + label: "Admin Password" + description: "Leave empty to auto-generate and store the password in a secret." + type: string + - variable: config.PHOTOPRISM_PASSWORD_LENGTH + group: "Authentication" + label: "Minimum Password Length" + description: "Enforce a minimum password length for local users." + type: int + default: "8" + - variable: config.PHOTOPRISM_SESSION_MAXAGE + group: "Authentication" + label: "Session Max Age (Seconds)" + description: "Maximum session lifetime; doubled when 2FA is enabled." + type: int + default: "1209600" + - variable: config.PHOTOPRISM_SESSION_TIMEOUT + group: "Authentication" + label: "Session Idle Timeout (Seconds)" + description: "Maximum idle time before users are signed out." + type: int + default: "604800" + - variable: config.PHOTOPRISM_SESSION_CACHE + group: "Authentication" + label: "Session Cache TTL (Seconds)" + description: "Duration session data is cached in memory (60-3600)." + type: int + default: "900" + - variable: config.PHOTOPRISM_LOGIN_INFO + group: "Authentication" + label: "Login Footer Text" + description: "Optional message displayed below the login form." + type: string + - variable: config.PHOTOPRISM_LEGAL_INFO + group: "Customization" + label: "Legal Notice Text" + description: "Text displayed in the footer for legal notices." + type: string + - variable: config.PHOTOPRISM_LEGAL_URL + group: "Customization" + label: "Legal Notice URL" + description: "Link to full legal information." + type: string + - variable: config.PHOTOPRISM_WALLPAPER_URI + group: "Customization" + label: "Wallpaper URI" + description: "Background image URL for the login screen." + type: string + - variable: oidc.PHOTOPRISM_DISABLE_OIDC + group: "Authentication" + label: "Disable OpenID Connect" + description: "Set to false to enable OpenID Connect login with the settings below." + type: boolean + default: true + show_subquestion_if: false + subquestions: + - variable: oidc.PHOTOPRISM_OIDC_URI + label: "OIDC Issuer URI" + description: "Identity provider issuer URL." + type: string + - variable: oidc.PHOTOPRISM_OIDC_CLIENT + label: "OIDC Client ID" + description: "Client identifier issued by the identity provider. Example: photoprism-plus." + type: string + - variable: oidc.PHOTOPRISM_OIDC_SECRET + label: "OIDC Client Secret" + description: "Client secret stored in a Kubernetes Secret. Example: s3cr3t." + type: string + - variable: oidc.PHOTOPRISM_OIDC_SCOPES + label: "OIDC Scopes" + description: "Space-delimited scopes requested from the provider." + type: string + default: "openid email profile" + - variable: oidc.PHOTOPRISM_OIDC_REGISTER + label: "Auto Register Users" + description: "Automatically create PhotoPrism accounts after successful OIDC login." + type: boolean + default: true + - variable: oidc.PHOTOPRISM_OIDC_REDIRECT + label: "Force OIDC Redirect" + description: "Always redirect to the OIDC provider instead of showing the local login form." + type: boolean + default: false + - variable: oidc.PHOTOPRISM_OIDC_USERNAME + label: "OIDC Username Claim" + description: "Claim name used as the PhotoPrism username. Example: email." + type: string + default: "email" + - variable: oidc.PHOTOPRISM_OIDC_PROVIDER + label: "OIDC Provider Name" + description: "Label displayed on the login button." + type: string + - variable: oidc.PHOTOPRISM_OIDC_ICON + label: "OIDC Provider Icon" + description: "Optional icon URL for the login button." + type: string + - variable: oidc.PHOTOPRISM_OIDC_WEBDAV + label: "Enable OIDC WebDAV" + description: "Allow OIDC users to authenticate against WebDAV endpoints." + type: boolean + default: false + - variable: database.driver + group: "Database" + label: "Driver" + description: "Set to mysql when connecting to MariaDB/MySQL." + type: enum + options: + - "sqlite" + - "mysql" + default: "sqlite" + - variable: database.server + group: "Database" + label: "Server" + description: "Host:port or socket path for the database server." + type: string + - variable: database.name + group: "Database" + label: "Database Name" + description: "Schema name when using MariaDB/MySQL." + type: string + - variable: database.user + group: "Database" + label: "Database Username" + description: "User account for the schema." + type: string + - variable: database.password + group: "Database" + label: "Database Password" + description: "Optional password stored in the chart-managed secret." + type: string + - variable: database.timeout + group: "Database" + label: "Connection Timeout (Seconds)" + description: "Timeout when establishing a database connection." + type: int + default: "15" + - variable: database.conns + group: "Database" + label: "Max Open Connections" + description: "Maximum number of open connections (0 lets PhotoPrism decide)." + type: int + default: "0" + - variable: database.connsIdle + group: "Database" + label: "Max Idle Connections" + description: "Maximum number of idle connections (0 lets PhotoPrism decide)." + type: int + default: "0" + - variable: persistence.storage.size + group: "Storage" + label: "Storage Volume Size" + description: "PVC size for /photoprism/storage (can be expanded later)." + type: string + default: "5Gi" + - variable: config.PHOTOPRISM_FILES_QUOTA + group: "Storage" + label: "Total File Quota (GB)" + description: "Limit the total size of indexed originals (0 for unlimited)." + type: int + default: "0" + - variable: config.PHOTOPRISM_ORIGINALS_LIMIT + group: "Storage" + label: "Maximum Original Size (MB)" + description: "Largest allowed original file size." + type: int + default: "5000" + - variable: config.PHOTOPRISM_UPLOAD_LIMIT + group: "Storage" + label: "Maximum Upload Size (MB)" + description: "Largest allowed upload size." + type: int + default: "5000" + - variable: config.PHOTOPRISM_UPLOAD_ALLOW + group: "Storage" + label: "Allowed Upload Types" + description: "Comma-separated list of permitted upload extensions." + type: string + default: "zip,jpeg,jpg,png,gif,bmp,avif,webp,heic,heif,tiff,pdf,ai,svg,xmp" + - variable: config.PHOTOPRISM_IMPORT_ALLOW + group: "Storage" + label: "Allowed Import Types" + description: "Comma-separated list of permitted import extensions." + type: string + default: "zip,jpeg,jpg,png,gif,bmp,avif,webp,heic,heif,tiff,pdf,ai,svg,xmp" + - variable: persistence.originals.enabled + group: "Storage" + label: "Enable Originals PVC" + description: "Provision a PVC for originals; disable when mounting NFS manually." + type: boolean + default: true + show_subquestion_if: true + subquestions: + - variable: persistence.originals.size + label: "Originals Volume Size" + description: "PVC size for /photoprism/originals (can be expanded later)." + type: string + default: "10Gi" + - variable: persistence.originals.nfs.enabled + label: "Mount Originals via NFS" + description: "Use an existing NFS export instead of a dynamically provisioned PVC." + type: boolean + default: false + show_subquestion_if: true + subquestions: + - variable: persistence.originals.nfs.server + label: "NFS Server" + description: "Hostname or IP of the NFS server. Example: nfs.internal.local." + type: string + required: true + - variable: persistence.originals.nfs.path + label: "NFS Export Path" + description: "Export path on the NFS server. Example: /exports/photos." + type: string + required: true + - variable: config.PHOTOPRISM_BACKUP_SCHEDULE + group: "Backup" + label: "Backup Schedule" + description: "Cron schedule or keyword (daily/weekly) controlling automated backups. Example: 0 3 * * *." + type: string + default: "daily" + - variable: config.PHOTOPRISM_BACKUP_RETAIN + group: "Backup" + label: "Backups to Keep" + description: "Number of backup archives to retain (-1 keeps all). Example: 3." + type: int + default: "3" + - variable: config.PHOTOPRISM_BACKUP_PATH + group: "Backup" + label: "Backup Path" + description: "Custom base directory for backups. Example: /photoprism/backups." + type: string + - variable: config.PHOTOPRISM_BACKUP_DATABASE + group: "Backup" + label: "Backup Database" + description: "Enable scheduled database backups." + type: boolean + default: true + - variable: config.PHOTOPRISM_BACKUP_ALBUMS + group: "Backup" + label: "Backup Albums" + description: "Include YAML album metadata in backups." + type: boolean + default: true + - variable: cluster.integration.enabled + group: "Cluster Integration" + label: "Enable Portal Integration" + description: "Set to true when a PhotoPrism Portal is available." + type: boolean + default: false + show_subquestion_if: true + subquestions: + - variable: cluster.integration.secretName + label: "Portal Secret Name" + description: "Secret containing PHOTOPRISM_CLUSTER_* values (defaults to photoprism-cluster-secrets)." + type: string + - variable: cluster.integration.domain + label: "Cluster Domain Override" + description: "Optional domain suffix when not using the shared secret." + type: string + - variable: cluster.integration.portalURL + label: "Portal URL" + description: "Portal base URL if no secret is provided." + type: string + - variable: cluster.integration.joinToken + label: "Join Token" + description: "Portal-issued token used to register this node. Example: node_abc123." + type: string + - variable: cluster.integration.uuid + label: "Cluster UUID" + description: "Unique cluster identifier provided by the portal." + type: string + - variable: cluster.integration.cidr + label: "Cluster CIDR" + description: "Allowed IP range for trusted cluster traffic. Example: 10.42.0.0/16." + type: string + - variable: cluster.integration.trustedProxy + label: "Trusted Proxy" + description: "Reverse proxy address forwarded in X-Forwarded-For headers." + type: string + - variable: cluster.integration.jwtScope + label: "JWT Scope" + description: "Scope names allowed for issued cluster tokens." + type: string diff --git a/setup/charts/plus/templates/NOTES.txt b/setup/charts/plus/templates/NOTES.txt new file mode 100644 index 000000000..750385d19 --- /dev/null +++ b/setup/charts/plus/templates/NOTES.txt @@ -0,0 +1,36 @@ +PhotoPrism® has been deployed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}". + +1. Retrieve the admin password (auto-generated on first install unless adminPassword was set): + + kubectl --namespace {{ .Release.Namespace }} get secret \ + {{ include "photoprism-plus.fullname" . }}-secrets \ + -o jsonpath="{.data.PHOTOPRISM_ADMIN_PASSWORD}" | base64 -d ; echo + + Sign in as "{{ .Values.adminUser }}" with that password. + +2. Access PhotoPrism: + +{{- if .Values.ingress.enabled }} + An Ingress was created. Once your controller assigns an address, browse to one of the configured hosts: + {{- range .Values.ingress.hosts }} + - http(s)://{{ .host }}{{ (index .paths 0).path }} + {{- else }} + - http(s)://{{ include "photoprism-plus.name" $ }}/ + {{- end }} +{{- else if eq .Values.service.type "LoadBalancer" }} + The service is type LoadBalancer; once an external IP is assigned, browse to it on port {{ .Values.service.port }}: + + kubectl --namespace {{ .Release.Namespace }} get svc {{ include "photoprism-plus.fullname" . }} +{{- else }} + Forward the service port to your workstation: + + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "photoprism-plus.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} + + Then open http://localhost:{{ .Values.service.port }}/ in your browser. +{{- end }} + +3. Documentation: + + - User Guide: https://docs.photoprism.app/user-guide/ + - Knowledge Base: https://www.photoprism.app/kb + - Support: https://www.photoprism.app/kb/getting-support diff --git a/setup/charts/plus/templates/_helpers.tpl b/setup/charts/plus/templates/_helpers.tpl new file mode 100644 index 000000000..fc7ed29f3 --- /dev/null +++ b/setup/charts/plus/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +General helpers for the PhotoPrism Plus chart +*/}} +{{- define "photoprism-plus.name" -}} +{{- if .Values.nameOverride -}} +{{- .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- "photoprism" -}} +{{- end -}} +{{- end -}} + +{{- define "photoprism-plus.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-photoprism" .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "photoprism-plus.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version -}} +{{- end -}} + +{{- define "photoprism-plus.labels" -}} +helm.sh/chart: {{ include "photoprism-plus.chart" . }} +app.kubernetes.io/name: {{ include "photoprism-plus.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "photoprism-plus.clusterSecretName" -}} +{{- if .Values.cluster.integration.secretName -}} +{{- .Values.cluster.integration.secretName -}} +{{- else -}} +photoprism-cluster-secrets +{{- end -}} +{{- end -}} + +{{- define "photoprism-plus.clusterDomain" -}} +{{- $domain := default "" .Values.cluster.integration.domain | trim -}} +{{- if and (eq $domain "") .Values.cluster.integration.enabled -}} + {{- $secret := lookup "v1" "Secret" .Release.Namespace (include "photoprism-plus.clusterSecretName" .) -}} + {{- if $secret -}} + {{- $encoded := index $secret.data "PHOTOPRISM_CLUSTER_DOMAIN" -}} + {{- if $encoded -}} + {{- $domain = b64dec $encoded | trim -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{- $domain -}} +{{- end -}} + +{{- define "photoprism-plus.siteURL" -}} +{{- $site := default "" .Values.config.PHOTOPRISM_SITE_URL | trim -}} +{{- if and (eq $site "") .Values.cluster.integration.enabled -}} + {{- $domain := include "photoprism-plus.clusterDomain" . | trim -}} + {{- if ne $domain "" -}} + {{- printf "https://%s.%s/" .Release.Name $domain -}} + {{- end -}} +{{- else -}} + {{- $site -}} +{{- end -}} +{{- end -}} diff --git a/setup/charts/plus/templates/ingress.yaml b/setup/charts/plus/templates/ingress.yaml new file mode 100644 index 000000000..6e5a32d57 --- /dev/null +++ b/setup/charts/plus/templates/ingress.yaml @@ -0,0 +1,54 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "photoprism-plus.fullname" . }} + labels: + {{- include "photoprism-plus.labels" . | nindent 4 }} + {{- if .Values.ingress.annotations }} + annotations: + {{- toYaml .Values.ingress.annotations | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + rules: + {{- if not .Values.ingress.hosts }} + - host: {{ printf "%s" (include "photoprism-plus.name" .) | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "photoprism-plus.fullname" . }} + port: + number: {{ .Values.service.port }} + {{- else }} + {{- range .Values.ingress.hosts }} + - host: {{ tpl .host $ | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType | default "Prefix" }} + backend: + service: + name: {{ include "photoprism-plus.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} + {{- end }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ tpl . $ | quote }} + {{- end }} + {{- if .secretName }} + secretName: {{ tpl .secretName $ | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/setup/charts/plus/templates/photoprism-originals.yaml b/setup/charts/plus/templates/photoprism-originals.yaml new file mode 100644 index 000000000..033934f0b --- /dev/null +++ b/setup/charts/plus/templates/photoprism-originals.yaml @@ -0,0 +1,17 @@ +{{- $orig := .Values.persistence.originals -}} +{{- if and $orig.enabled (not $orig.nfs.enabled) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "photoprism-plus.fullname" . }}-originals + labels: + {{- include "photoprism-plus.labels" . | nindent 4 }} +spec: + accessModes: {{ toYaml $orig.accessModes | nindent 4 }} + resources: + requests: + storage: {{ $orig.size }} +{{- if .Values.persistence.storageClassName }} + storageClassName: {{ .Values.persistence.storageClassName }} +{{- end }} +{{- end }} diff --git a/setup/charts/plus/templates/photoprism-storage.yaml b/setup/charts/plus/templates/photoprism-storage.yaml new file mode 100644 index 000000000..e8e26a1ac --- /dev/null +++ b/setup/charts/plus/templates/photoprism-storage.yaml @@ -0,0 +1,17 @@ +{{- $store := .Values.persistence.storage -}} +{{- if $store.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "photoprism-plus.fullname" . }}-storage + labels: + {{- include "photoprism-plus.labels" . | nindent 4 }} +spec: + accessModes: {{ toYaml $store.accessModes | nindent 4 }} + resources: + requests: + storage: {{ $store.size }} +{{- if .Values.persistence.storageClassName }} + storageClassName: {{ .Values.persistence.storageClassName }} +{{- end }} +{{- end }} diff --git a/setup/charts/plus/templates/secret.yaml b/setup/charts/plus/templates/secret.yaml new file mode 100644 index 000000000..c8e52a7ee --- /dev/null +++ b/setup/charts/plus/templates/secret.yaml @@ -0,0 +1,37 @@ +{{- /* + Recover existing passwords via lookup so upgrades keep the same values. + On first install, a random admin password is generated. Note that lookup + returns nil under `helm template` and `helm install --dry-run`, so those + invocations always print a fresh random — cosmetic only; real installs + preserve the stored value. +*/ -}} +{{- $secretName := printf "%s-secrets" (include "photoprism-plus.fullname" .) -}} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName -}} +{{- $existingAdmin := "" -}} +{{- $existingDb := "" -}} +{{- if $existing -}} + {{- $existingAdmin = default "" (index $existing.data "PHOTOPRISM_ADMIN_PASSWORD") | b64dec | trim -}} + {{- $existingDb = default "" (index $existing.data "PHOTOPRISM_DATABASE_PASSWORD") | b64dec | trim -}} +{{- end -}} +{{- $adminPassword := default "" (.Values.adminPassword | toString | trim) -}} +{{- if eq $adminPassword "" -}} + {{- if ne $existingAdmin "" -}} + {{- $adminPassword = $existingAdmin -}} + {{- else -}} + {{- $adminPassword = randAlphaNum 16 -}} + {{- end -}} +{{- end -}} +{{- $dbPassword := default "" (.Values.database.password | toString | trim) -}} +{{- if and (eq $dbPassword "") (ne $existingDb "") -}} + {{- $dbPassword = $existingDb -}} +{{- end -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} +type: Opaque +stringData: + PHOTOPRISM_ADMIN_PASSWORD: {{ $adminPassword | quote }} + {{- if ne $dbPassword "" }} + PHOTOPRISM_DATABASE_PASSWORD: {{ $dbPassword | quote }} + {{- end }} diff --git a/setup/charts/plus/templates/service.yaml b/setup/charts/plus/templates/service.yaml new file mode 100644 index 000000000..2662e4dec --- /dev/null +++ b/setup/charts/plus/templates/service.yaml @@ -0,0 +1,20 @@ +{{- if hasKey .Values.service "enable" -}} +{{- fail "service.enable has been renamed to service.enabled — please update your values overrides." -}} +{{- end -}} +{{- if .Values.service.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "photoprism-plus.fullname" . }} + labels: + {{- include "photoprism-plus.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + app.kubernetes.io/name: {{ include "photoprism-plus.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: 2342 +{{- end }} diff --git a/setup/charts/plus/templates/statefulset.yaml b/setup/charts/plus/templates/statefulset.yaml new file mode 100644 index 000000000..9fd34f14a --- /dev/null +++ b/setup/charts/plus/templates/statefulset.yaml @@ -0,0 +1,154 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "photoprism-plus.fullname" . }} + labels: + {{- include "photoprism-plus.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "photoprism-plus.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + serviceName: {{ include "photoprism-plus.fullname" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "photoprism-plus.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.command }} + command: + {{- range .Values.command }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- if .Values.args }} + args: + {{- range .Values.args }} + - {{ . | quote }} + {{- end }} + {{- end }} + env: + {{- $siteURL := include "photoprism-plus.siteURL" . | trim -}} + {{- if ne $siteURL "" }} + - name: PHOTOPRISM_SITE_URL + value: {{ $siteURL | quote }} + {{- end }} + {{- if .Values.cluster.integration.enabled }} + {{- $integration := .Values.cluster.integration -}} + {{- $secretName := include "photoprism-plus.clusterSecretName" . | trim -}} + {{- $clusterFields := dict + "PHOTOPRISM_PORTAL_URL" $integration.portalURL + "PHOTOPRISM_CLUSTER_DOMAIN" $integration.domain + "PHOTOPRISM_CLUSTER_UUID" $integration.uuid + "PHOTOPRISM_CLUSTER_CIDR" $integration.cidr + "PHOTOPRISM_TRUSTED_PROXY" $integration.trustedProxy + "PHOTOPRISM_JOIN_TOKEN" $integration.joinToken + "PHOTOPRISM_JWT_SCOPE" $integration.jwtScope -}} + {{- range $key, $value := $clusterFields }} + {{- $inline := trim (default "" $value) -}} + {{- if ne $inline "" }} + - name: {{ $key }} + value: {{ $inline | quote }} + {{- else if ne $secretName "" }} + - name: {{ $key }} + valueFrom: + secretKeyRef: + name: {{ $secretName | quote }} + key: {{ $key }} + optional: true + {{- end }} + {{- end }} + {{- end }} + - name: PHOTOPRISM_ADMIN_USER + value: {{ .Values.adminUser | quote }} + - name: PHOTOPRISM_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ printf "%s-secrets" (include "photoprism-plus.fullname" .) }} + key: PHOTOPRISM_ADMIN_PASSWORD + - name: PHOTOPRISM_DATABASE_DRIVER + value: {{ .Values.database.driver | quote }} + {{- $dbServer := trim (default "" .Values.database.server) -}} + {{- if ne $dbServer "" }} + - name: PHOTOPRISM_DATABASE_SERVER + value: {{ $dbServer | quote }} + {{- end }} + {{- $dbName := trim (default "" .Values.database.name) -}} + {{- if ne $dbName "" }} + - name: PHOTOPRISM_DATABASE_NAME + value: {{ $dbName | quote }} + {{- end }} + {{- $dbUser := trim (default "" .Values.database.user) -}} + {{- if ne $dbUser "" }} + - name: PHOTOPRISM_DATABASE_USER + value: {{ $dbUser | quote }} + {{- end }} + {{- $dbPassword := trim (default "" .Values.database.password) -}} + {{- if ne $dbPassword "" }} + - name: PHOTOPRISM_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ printf "%s-secrets" (include "photoprism-plus.fullname" .) }} + key: PHOTOPRISM_DATABASE_PASSWORD + {{- end }} + - name: PHOTOPRISM_DATABASE_TIMEOUT + value: {{ printf "%v" .Values.database.timeout | quote }} + - name: PHOTOPRISM_DATABASE_CONNS + value: {{ printf "%v" .Values.database.conns | quote }} + - name: PHOTOPRISM_DATABASE_CONNS_IDLE + value: {{ printf "%v" .Values.database.connsIdle | quote }} + {{- range $key, $value := .Values.oidc }} + {{- if ne (printf "%v" $value) "" }} + - name: {{ $key }} + value: {{ printf "%v" $value | quote }} + {{- end }} + {{- end }} + {{- range $key, $value := .Values.config }} + {{- if ne (printf "%v" $value) "" }} + - name: {{ $key }} + value: {{ printf "%v" $value | quote }} + {{- end }} + {{- end }} + ports: + - containerPort: 2342 + name: http + volumeMounts: + {{- if .Values.persistence.originals.enabled }} + - name: originals + mountPath: {{ .Values.persistence.originals.mountPath }} + {{- end }} + {{- if .Values.persistence.storage.enabled }} + - name: storage + mountPath: {{ .Values.persistence.storage.mountPath }} + {{- end }} + volumes: + {{- if .Values.persistence.originals.enabled }} + - name: originals + {{- if and .Values.persistence.originals.enabled .Values.persistence.originals.nfs.enabled }} + nfs: + server: {{ required "persistence.originals.nfs.server is required when NFS mode is enabled" .Values.persistence.originals.nfs.server | quote }} + path: {{ required "persistence.originals.nfs.path is required when NFS mode is enabled" .Values.persistence.originals.nfs.path | quote }} + {{- else }} + persistentVolumeClaim: + claimName: {{ include "photoprism-plus.fullname" . }}-originals + {{- end }} + {{- end }} + {{- if .Values.persistence.storage.enabled }} + - name: storage + persistentVolumeClaim: + claimName: {{ include "photoprism-plus.fullname" . }}-storage + {{- end }} diff --git a/setup/charts/plus/values.yaml b/setup/charts/plus/values.yaml new file mode 100644 index 000000000..fe6a2201f --- /dev/null +++ b/setup/charts/plus/values.yaml @@ -0,0 +1,160 @@ +# Default values for the PhotoPrism® Helm chart. +# These defaults are tuned for home labs and small teams; enable the optional +# portal integration block only if a PhotoPrism Portal is available in your cluster. + +replicaCount: 1 + +image: + repository: docker.io/photoprism/photoprism + tag: "" + pullPolicy: IfNotPresent + +command: [] +args: [] + +securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + runAsUser: 1000 + runAsGroup: 1000 + +podSecurityContext: + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + +nameOverride: "" +fullnameOverride: "" + +adminUser: "admin" +adminPassword: "" + +cluster: + integration: + enabled: false + secretName: "" + domain: "" + portalURL: "" + uuid: "" + cidr: "" + trustedProxy: "" + joinToken: "" + jwtScope: "" + +persistence: + storageClassName: "" + originals: + enabled: true + size: 10Gi + accessModes: + - ReadWriteOnce + mountPath: /photoprism/originals + nfs: + enabled: false + server: "" + path: "" + storage: + enabled: true + size: 5Gi + accessModes: + - ReadWriteOnce + mountPath: /photoprism/storage + +service: + enabled: true + type: ClusterIP + port: 2342 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: [] + tls: [] + +resources: + limits: + cpu: 4000m + memory: 6Gi + requests: + cpu: 500m + memory: 1Gi + +nodeSelector: {} +tolerations: [] +affinity: {} + +oidc: + PHOTOPRISM_DISABLE_OIDC: true + PHOTOPRISM_OIDC_URI: "" + PHOTOPRISM_OIDC_CLIENT: "" + PHOTOPRISM_OIDC_SECRET: "" + PHOTOPRISM_OIDC_SCOPES: "openid email profile" + PHOTOPRISM_OIDC_REGISTER: true + PHOTOPRISM_OIDC_REDIRECT: false + PHOTOPRISM_OIDC_USERNAME: "email" + PHOTOPRISM_OIDC_PROVIDER: "" + PHOTOPRISM_OIDC_ICON: "" + PHOTOPRISM_OIDC_WEBDAV: false + +config: + PHOTOPRISM_SITE_URL: "" + PHOTOPRISM_SITE_TITLE: "PhotoPrism" + PHOTOPRISM_SITE_CAPTION: "" + PHOTOPRISM_SITE_DESCRIPTION: "" + PHOTOPRISM_SITE_AUTHOR: "" + PHOTOPRISM_SITE_FAVICON: "" + PHOTOPRISM_SITE_PREVIEW: "" + PHOTOPRISM_APP_NAME: "PhotoPrism" + PHOTOPRISM_APP_ICON: "" + PHOTOPRISM_APP_COLOR: "#1d1d1e" + PHOTOPRISM_DEFAULT_THEME: "" + PHOTOPRISM_DEFAULT_LOCALE: "en" + PHOTOPRISM_DEFAULT_TIMEZONE: "" + PHOTOPRISM_PLACES_LOCALE: "local" + PHOTOPRISM_AUTH_MODE: "password" + PHOTOPRISM_PASSWORD_LENGTH: 8 + PHOTOPRISM_SESSION_MAXAGE: 1209600 + PHOTOPRISM_SESSION_TIMEOUT: 604800 + PHOTOPRISM_SESSION_CACHE: 900 + PHOTOPRISM_LOGIN_INFO: "" + PHOTOPRISM_LOG_LEVEL: "info" + PHOTOPRISM_HTTP_COMPRESSION: "zstd,gzip" + PHOTOPRISM_CDN_URL: "" + PHOTOPRISM_CDN_VIDEO: "" + PHOTOPRISM_CORS_ORIGIN: "" + PHOTOPRISM_CORS_HEADERS: "Accept, Accept-Ranges, Content-Disposition, Content-Encoding, Content-Range, Location" + PHOTOPRISM_CORS_METHODS: "GET, HEAD, OPTIONS" + PHOTOPRISM_LEGAL_INFO: "" + PHOTOPRISM_LEGAL_URL: "" + PHOTOPRISM_BACKUP_SCHEDULE: "daily" + PHOTOPRISM_BACKUP_RETAIN: "3" + PHOTOPRISM_BACKUP_DATABASE: true + PHOTOPRISM_BACKUP_ALBUMS: true + PHOTOPRISM_BACKUP_PATH: "" + PHOTOPRISM_FILES_QUOTA: 0 + PHOTOPRISM_ORIGINALS_LIMIT: 5000 + PHOTOPRISM_UPLOAD_LIMIT: 5000 + PHOTOPRISM_UPLOAD_ALLOW: "zip,jpeg,jpg,png,gif,bmp,avif,webp,heic,heif,tiff,pdf,ai,svg,xmp" + PHOTOPRISM_IMPORT_ALLOW: "zip,jpeg,jpg,png,gif,bmp,avif,webp,heic,heif,tiff,pdf,ai,svg,xmp" + PHOTOPRISM_UPLOAD_ARCHIVES: true + PHOTOPRISM_UPLOAD_NSFW: true + PHOTOPRISM_DISABLE_TLS: true + PHOTOPRISM_DISABLE_STS: true + PHOTOPRISM_DISABLE_MCP: false + PHOTOPRISM_WALLPAPER_URI: "" + +# Database connection overrides (blank defaults keep PhotoPrism's internal sqlite) +database: + driver: "sqlite" + server: "" + name: "photoprism" + user: "photoprism" + password: "" + timeout: 15 + conns: 0 + connsIdle: 0 diff --git a/setup/docker/arm64/compose.yaml b/setup/docker/arm64/compose.yaml index cf6129ead..68a58896d 100644 --- a/setup/docker/arm64/compose.yaml +++ b/setup/docker/arm64/compose.yaml @@ -183,12 +183,15 @@ services: OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup OLLAMA_NOHISTORY: "true" # disables readline history - OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature - OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0) + OLLAMA_FLASH_ATTENTION: "true" # required for OLLAMA_KV_CACHE_TYPE quantization + OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0 OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs. - OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine # OLLAMA_DEBUG: "true" # shows additional debug information # OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection + ## Telemetry / privacy opt-outs (containers do not inherit /etc/environment): + DO_NOT_TRACK: "true" + HF_HUB_DISABLE_TELEMETRY: "1" + # OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features volumes: - "./ollama:/root/.ollama" diff --git a/setup/docker/compose.yaml b/setup/docker/compose.yaml index 31f9cd3e0..371a7e50c 100644 --- a/setup/docker/compose.yaml +++ b/setup/docker/compose.yaml @@ -188,12 +188,15 @@ services: OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup OLLAMA_NOHISTORY: "true" # disables readline history - OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature - OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0) + OLLAMA_FLASH_ATTENTION: "true" # required for OLLAMA_KV_CACHE_TYPE quantization + OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0 OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs. - OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine # OLLAMA_DEBUG: "true" # shows additional debug information # OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection + ## Telemetry / privacy opt-outs (containers do not inherit /etc/environment): + DO_NOT_TRACK: "true" + HF_HUB_DISABLE_TELEMETRY: "1" + # OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features ## NVIDIA GPU Hardware Acceleration (see https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html): # NVIDIA_VISIBLE_DEVICES: "all" # NVIDIA_DRIVER_CAPABILITIES: "compute,utility" diff --git a/setup/docker/nvidia/compose.yaml b/setup/docker/nvidia/compose.yaml index f8f725848..8caf68a17 100644 --- a/setup/docker/nvidia/compose.yaml +++ b/setup/docker/nvidia/compose.yaml @@ -188,12 +188,15 @@ services: OLLAMA_MULTIUSER_CACHE: "false" # optimize prompt caching for multi-user scenarios OLLAMA_NOPRUNE: "false" # disables pruning of model blobs at startup OLLAMA_NOHISTORY: "true" # disables readline history - OLLAMA_FLASH_ATTENTION: "false" # enables the experimental flash attention feature - OLLAMA_KV_CACHE_TYPE: "f16" # cache quantization (f16, q8_0, or q4_0) + OLLAMA_FLASH_ATTENTION: "true" # required for OLLAMA_KV_CACHE_TYPE quantization + OLLAMA_KV_CACHE_TYPE: "f16" # cache precision: f16 (default), q8_0, q4_0 OLLAMA_SCHED_SPREAD: "false" # allows scheduling models across all GPUs. - OLLAMA_NEW_ENGINE: "true" # enables the new Ollama engine # OLLAMA_DEBUG: "true" # shows additional debug information # OLLAMA_INTEL_GPU: "true" # enables experimental Intel GPU detection + ## Telemetry / privacy opt-outs (containers do not inherit /etc/environment): + DO_NOT_TRACK: "true" + HF_HUB_DISABLE_TELEMETRY: "1" + # OLLAMA_NO_CLOUD: "1" # uncomment to disable Ollama Cloud models/features ## NVIDIA GPU Hardware Acceleration (see https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html): NVIDIA_VISIBLE_DEVICES: "all" NVIDIA_DRIVER_CAPABILITIES: "compute,utility"