mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Merge remote-tracking branch 'origin/develop' into PostgreSQL
This commit is contained in:
commit
40ce242e89
90 changed files with 1749 additions and 208 deletions
|
|
@ -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?
|
||||
._*
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
65
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
65
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -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.
|
||||
|
|
|
|||
45
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
45
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
|
@ -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 <role>, I want <goal>, so that <outcome>.**`
|
||||
Feature requests MUST begin with a one sentence user story.
|
||||
**As a <role>, I want <goal>, so that <outcome>.**
|
||||
placeholder: "**As a <role>, I want <goal>, so that <outcome>.**"
|
||||
value: "**As a <role>, I want <goal>, so that <outcome>.**"
|
||||
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: |
|
||||
- [ ] <question> — options A/B/C and implications.
|
||||
- [ ] <question> — 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: |
|
||||
- [ ] <component> MUST <expected behavior>
|
||||
- [ ] <component> SHOULD <expected behavior>
|
||||
- [ ] <component> MAY <expected behavior>
|
||||
render: markdown
|
||||
- [ ] <component> MUST <expected behavior>.
|
||||
- [ ] <component> SHOULD <expected behavior>.
|
||||
- [ ] <component> MAY <expected behavior>.
|
||||
value: |
|
||||
- [ ] <component> MUST <expected behavior>.
|
||||
- [ ] <component> SHOULD <expected behavior>.
|
||||
- [ ] <component> MAY <expected behavior>.
|
||||
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.
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
3
.rsyncignore
Normal file
3
.rsyncignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.*
|
||||
*.db
|
||||
__*
|
||||
11
Dockerfile
11
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=<n> 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
|
||||
|
|
|
|||
11
Makefile
11
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."; \
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
examples
|
||||
README.md
|
||||
Thumbs.db
|
||||
docs
|
||||
.*
|
||||
_*
|
||||
_*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
35
compose.yaml
35
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"
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.<field>.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"]) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()`).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <positional> [--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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
37
internal/commands/cli_args.go
Normal file
37
internal/commands/cli_args.go
Normal file
|
|
@ -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
|
||||
}
|
||||
56
internal/commands/cli_args_test.go
Normal file
56
internal/commands/cli_args_test.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
43
setup/charts/plus/Chart.yaml
Normal file
43
setup/charts/plus/Chart.yaml
Normal file
|
|
@ -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
|
||||
93
setup/charts/plus/README.md
Normal file
93
setup/charts/plus/README.md
Normal file
|
|
@ -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/<release>-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)
|
||||
413
setup/charts/plus/questions.yaml
Normal file
413
setup/charts/plus/questions.yaml
Normal file
|
|
@ -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
|
||||
36
setup/charts/plus/templates/NOTES.txt
Normal file
36
setup/charts/plus/templates/NOTES.txt
Normal file
|
|
@ -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
|
||||
63
setup/charts/plus/templates/_helpers.tpl
Normal file
63
setup/charts/plus/templates/_helpers.tpl
Normal file
|
|
@ -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 -}}
|
||||
54
setup/charts/plus/templates/ingress.yaml
Normal file
54
setup/charts/plus/templates/ingress.yaml
Normal file
|
|
@ -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 }}
|
||||
17
setup/charts/plus/templates/photoprism-originals.yaml
Normal file
17
setup/charts/plus/templates/photoprism-originals.yaml
Normal file
|
|
@ -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 }}
|
||||
17
setup/charts/plus/templates/photoprism-storage.yaml
Normal file
17
setup/charts/plus/templates/photoprism-storage.yaml
Normal file
|
|
@ -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 }}
|
||||
37
setup/charts/plus/templates/secret.yaml
Normal file
37
setup/charts/plus/templates/secret.yaml
Normal file
|
|
@ -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 }}
|
||||
20
setup/charts/plus/templates/service.yaml
Normal file
20
setup/charts/plus/templates/service.yaml
Normal file
|
|
@ -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 }}
|
||||
154
setup/charts/plus/templates/statefulset.yaml
Normal file
154
setup/charts/plus/templates/statefulset.yaml
Normal file
|
|
@ -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 }}
|
||||
160
setup/charts/plus/values.yaml
Normal file
160
setup/charts/plus/values.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue