Merge remote-tracking branch 'origin/develop' into PostgreSQL

This commit is contained in:
Keith Martin 2025-08-29 11:35:28 +10:00
commit b86ddf0ec6
64 changed files with 1258 additions and 486 deletions

1
.gitignore vendored
View file

@ -79,4 +79,5 @@ Thumbs.db
.c9revisions
.settings
.swp
AGENTS.md
.vscode

View file

@ -119,6 +119,8 @@ devtools: install-go dep-npm
.SILENT: help;
logs:
$(DOCKER_COMPOSE) logs -f
down:
$(DOCKER_COMPOSE) --profile=all down --remove-orphans
help:
@echo "For build instructions, visit <https://docs.photoprism.app/developer-guide/>."
docs: swag
@ -797,6 +799,10 @@ docker-release-plucky:
docker pull --platform=arm64 photoprism/develop:plucky
docker pull --platform=arm64 photoprism/develop:plucky-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce /plucky
start-traefik:
$(DOCKER_COMPOSE) up -d --wait traefik
stop-traefik:
$(DOCKER_COMPOSE) down traefik
start-local:
$(DOCKER_COMPOSE) -f compose.local.yaml up -d --wait
stop-local:

View file

@ -58,6 +58,8 @@ services:
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # Don't use TensorFlow for image classification
PHOTOPRISM_DETECT_NSFW: "false" # Flag photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # Allows uploads that may be offensive
PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all)
PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import)
PHOTOPRISM_DARKTABLE_PRESETS: "false" # Enables Darktable presets and disables concurrent RAW conversion
PHOTOPRISM_THUMB_FILTER: "lanczos" # Resample filter, best to worst: blackman, lanczos, cubic, linear
PHOTOPRISM_THUMB_UNCACHED: "true" # Enables on-demand thumbnail rendering (high memory and cpu usage)

View file

@ -28,6 +28,7 @@ services:
- "traefik:dummy-webdav.localssl.dev"
labels:
- "traefik.enable=true"
- "traefik.docker.network=photoprism"
- "traefik.http.services.photoprism.loadbalancer.server.port=2342"
- "traefik.http.services.photoprism.loadbalancer.server.scheme=http"
- "traefik.http.routers.photoprism.entrypoints=websecure"
@ -114,6 +115,8 @@ services:
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW images (reduces performance)
PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that MAY be offensive (no effect without TensorFlow)
PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all)
PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import)
PHOTOPRISM_THUMB_LIBRARY: "auto" # image processing library to be used for generating thumbnails (auto, imaging, vips)
PHOTOPRISM_THUMB_FILTER: "auto" # downscaling filter (imaging best to worst: blackman, lanczos, cubic, linear, nearest)
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)

View file

@ -12,6 +12,7 @@ services:
- "2344:2342" # HTTP port (host:container)
labels:
- "traefik.enable=true"
- "traefik.docker.network=photoprism"
- "traefik.http.services.latest.loadbalancer.server.port=2342"
- "traefik.http.routers.latest.entrypoints=websecure"
- "traefik.http.routers.latest.rule=Host(`latest.localssl.dev`)"
@ -50,6 +51,8 @@ services:
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that MAY be offensive (no effect without TensorFlow)
PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all)
PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import)
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW images (reduces performance)
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
@ -63,7 +66,7 @@ services:
- "./storage:/photoprism/storage"
- "./storage/originals:/photoprism/originals"
## Join shared "photoprism-develop" network
## Join shared "photoprism" network
networks:
default:
name: photoprism

View file

@ -14,6 +14,7 @@ services:
- "2345:2342" # HTTP port (host:container)
labels:
- "traefik.enable=true"
- "traefik.docker.network=photoprism"
- "traefik.http.services.latest.loadbalancer.server.port=2342"
- "traefik.http.routers.latest.entrypoints=websecure"
- "traefik.http.routers.latest.rule=Host(`local.localssl.dev`)"
@ -51,6 +52,8 @@ services:
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that MAY be offensive (no effect without TensorFlow)
PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all)
PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import)
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW images (reduces performance)
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
@ -70,7 +73,7 @@ services:
volumes:
- "./storage:/photoprism/storage"
## Join shared "photoprism-develop" network
## Join shared "photoprism" network
networks:
default:
name: photoprism

View file

@ -145,7 +145,7 @@ services:
MYSQL_PASSWORD: "photoprism"
MYSQL_ROOT_PASSWORD: "photoprism"
## Join shared "photoprism-develop" network
## Join shared "photoprism" network
networks:
default:
name: photoprism

View file

@ -15,7 +15,7 @@ services:
MYSQL_PASSWORD: "photoprism"
MYSQL_ROOT_PASSWORD: "photoprism"
## Join shared "photoprism-develop" network
## Join shared "photoprism" network
networks:
default:
name: photoprism

View file

@ -31,6 +31,7 @@ services:
- "traefik:dummy-webdav.localssl.dev"
labels:
- "traefik.enable=true"
- "traefik.docker.network=photoprism"
- "traefik.http.services.photoprism.loadbalancer.server.port=2342"
- "traefik.http.services.photoprism.loadbalancer.server.scheme=http"
- "traefik.http.routers.photoprism.entrypoints=websecure"
@ -117,6 +118,8 @@ services:
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW images (reduces performance)
PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that MAY be offensive (no effect without TensorFlow)
PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all)
PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import)
PHOTOPRISM_THUMB_LIBRARY: "auto" # image processing library to be used for generating thumbnails (auto, imaging, vips)
PHOTOPRISM_THUMB_FILTER: "auto" # downscaling filter (imaging best to worst: blackman, lanczos, cubic, linear, nearest)
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)

View file

@ -122,6 +122,8 @@ services:
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW images (reduces performance)
PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that MAY be offensive (no effect without TensorFlow)
PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all)
PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import)
PHOTOPRISM_THUMB_LIBRARY: "auto" # image processing library to be used for generating thumbnails (auto, imaging, vips)
PHOTOPRISM_THUMB_FILTER: "auto" # downscaling filter (imaging best to worst: blackman, lanczos, cubic, linear, nearest)
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)

View file

@ -12,6 +12,7 @@ services:
- "2344:2342" # HTTP port (host:container)
labels:
- "traefik.enable=true"
- "traefik.docker.network=photoprism"
- "traefik.http.services.preview.loadbalancer.server.port=2342"
- "traefik.http.routers.preview.entrypoints=websecure"
- "traefik.http.routers.preview.rule=Host(`preview.localssl.dev`)"
@ -50,6 +51,8 @@ services:
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that MAY be offensive (no effect without TensorFlow)
PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all)
PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import)
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW images (reduces performance)
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
@ -63,7 +66,7 @@ services:
- "./storage:/photoprism/storage"
- "./storage/originals:/photoprism/originals"
## Join shared "photoprism-develop" network
## Join shared "photoprism" network
networks:
default:
name: photoprism

View file

@ -32,6 +32,7 @@ services:
- "traefik:dummy-webdav.localssl.dev"
labels:
- "traefik.enable=true"
- "traefik.docker.network=photoprism"
- "traefik.http.services.photoprism.loadbalancer.server.port=2342"
- "traefik.http.services.photoprism.loadbalancer.server.scheme=http"
- "traefik.http.routers.photoprism.entrypoints=websecure"
@ -122,6 +123,8 @@ services:
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW images (reduces performance)
PHOTOPRISM_DETECT_NSFW: "false" # automatically flags photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that MAY be offensive (no effect without TensorFlow)
PHOTOPRISM_UPLOAD_ALLOW: "" # restricts uploads to these file types (comma-separated list of EXTENSIONS; leave blank to allow all)
PHOTOPRISM_UPLOAD_ARCHIVES: "true" # allows upload of zip archives (will be extracted before import)
PHOTOPRISM_THUMB_LIBRARY: "auto" # image processing library to be used for generating thumbnails (auto, imaging, vips)
PHOTOPRISM_THUMB_FILTER: "auto" # downscaling filter (imaging best to worst: blackman, lanczos, cubic, linear, nearest)
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)

File diff suppressed because it is too large Load diff

View file

@ -44,7 +44,7 @@
"@mdi/font": "^7.4.47",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^5.0.1",
"@vitejs/plugin-react": "^5.0.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
@ -59,15 +59,15 @@
"babel-plugin-istanbul": "^7.0.0",
"babel-plugin-polyfill-corejs3": "^0.13.0",
"browserslist": "^4.25.3",
"chai": "^5.3.2",
"chai": "^5.3.3",
"cheerio": "1.0.0-rc.12",
"chrome-finder": "^1.0.7",
"core-js": "^3.45.1",
"cross-env": "^10.0.0",
"css-loader": "^7.1.2",
"cssnano": "^7.1.0",
"cssnano": "^7.1.1",
"easygettext": "^2.17.0",
"eslint": "^9.33.0",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-formatter-pretty": "^6.0.1",
"eslint-plugin-html": "^8.1.3",
@ -82,7 +82,7 @@
"file-saver": "^2.0.5",
"floating-vue": "^5.2.2",
"globals": "^16.3.0",
"hls.js": "^1.6.10",
"hls.js": "^1.6.11",
"i": "^0.3.7",
"jsdom": "^26.1.0",
"karma": "^6.4.4",
@ -93,7 +93,7 @@
"karma-verbose-reporter": "^0.0.8",
"karma-webpack": "^5.0.1",
"luxon": "^3.7.1",
"maplibre-gl": "^5.6.2",
"maplibre-gl": "^5.7.0",
"memoize-one": "^6.0.0",
"mini-css-extract-plugin": "^2.9.4",
"minimist": "^1.2.8",
@ -105,7 +105,7 @@
"postcss": "^8.5.6",
"postcss-import": "^16.1.1",
"postcss-loader": "^8.1.1",
"postcss-preset-env": "^10.2.4",
"postcss-preset-env": "^10.3.1",
"postcss-reporter": "^7.1.0",
"postcss-url": "^10.1.3",
"prettier": "^3.6.2",
@ -113,7 +113,7 @@
"regenerator-runtime": "^0.14.1",
"resolve-url-loader": "^5.0.0",
"sanitize-html": "^2.17.0",
"sass": "^1.90.0",
"sass": "^1.91.0",
"sass-loader": "^16.0.5",
"server": "^1.0.42",
"sockette": "^2.0.6",
@ -133,7 +133,7 @@
"vue-sanitize-directive": "^0.2.1",
"vue-style-loader": "^4.1.3",
"vue3-gettext": "^2.4.0",
"vuetify": "^3.9.5",
"vuetify": "^3.9.6",
"webpack": "^5.101.3",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^6.0.1",

6
go.mod
View file

@ -14,7 +14,7 @@ require (
github.com/esimov/pigo v1.4.6
github.com/gin-contrib/gzip v1.2.3
github.com/gin-gonic/gin v1.10.1
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31
github.com/google/open-location-code/go v0.0.0-20250620134813-83986da0156b
github.com/gorilla/websocket v1.5.3
github.com/gosimple/slug v1.15.0
@ -34,7 +34,7 @@ require (
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/sevlyar/go-daemon v0.1.6
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
@ -55,7 +55,7 @@ require github.com/google/uuid v1.6.0
require github.com/chzyer/readline v1.5.1 // indirect
require github.com/gabriel-vasile/mimetype v1.4.9
require github.com/gabriel-vasile/mimetype v1.4.10
require (
golang.org/x/sync v0.16.0

12
go.sum
View file

@ -113,8 +113,8 @@ github.com/esimov/pigo v1.4.6/go.mod h1:uqj9Y3+3IRYhFK071rxz1QYq0ePhA6+R9jrUZavi
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
@ -172,8 +172,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac h1:gBtvd74wZT02fkOyoG651Sx2imv5lKdVvDwpUjKPFnw=
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31 h1:226lwSa0uZO7sd7QU88n43nDIqGoc+bOh0vSO3Q/byU=
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -360,8 +360,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/sunfish-shogi/bufseekio v0.1.0 h1:zu38kFbv0KuuiwZQeuYeS02U9AM14j0pVA9xkHOCJ2A=
github.com/sunfish-shogi/bufseekio v0.1.0/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=

View file

@ -16,7 +16,7 @@ var (
DownloadUrl = ""
ServiceUri = ""
ServiceKey = ""
ServiceTimeout = 10 * time.Minute
ServiceTimeout = 5 * time.Minute
ServiceMethod = http.MethodPost
ServiceFileScheme = scheme.Data
ServiceRequestFormat = ApiFormatVision

View file

@ -39,7 +39,7 @@ func AlbumDownloadName(c *gin.Context) customize.DownloadName {
//
// @Summary streams the album contents as zip archive
// @Id DownloadAlbum
// @Tags Images, Albums
// @Tags Albums, Download
// @Produce application/zip
// @Failure 403,404,500 {object} i18n.Response
// @Success 200 {file} application/zip

View file

@ -12,6 +12,7 @@ import (
//
// @Summary returns the request and response headers as JSON if debug mode is enabled
// @Id Echo
// @Tags Debug
// @Success 200
// @Router /api/v1/echo [get]
func Echo(router *gin.RouterGroup) {

View file

@ -11,9 +11,15 @@ import (
"github.com/photoprism/photoprism/pkg/i18n"
)
// SendFeedback sends a feedback message.
// SendFeedback allows members to submit a feedback message to the PhotoPrism team.
//
// POST /api/v1/feedback
// @Summary allows members to submit a feedback message to the PhotoPrism team
// @Id SendFeedback
// @Tags Admin
// @Produce json
// @Success 200 {object} form.Feedback
// @Failure 400,401,403 {object} i18n.Response
// @Router /api/v1/feedback [post]
func SendFeedback(router *gin.RouterGroup) {
router.POST("/feedback", func(c *gin.Context) {
conf := get.Config()

View file

@ -9,6 +9,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promauto"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/photoprism/photoprism/internal/auth/acl"
@ -17,9 +18,15 @@ import (
"github.com/photoprism/photoprism/pkg/media/http/header"
)
// GetMetrics provides a prometheus-compatible metrics endpoint for monitoring.
// GetMetrics provides a Prometheus-compatible metrics stream for monitoring.
//
// GET /api/v1/metrics
// @Summary a prometheus-compatible metrics endpoint for monitoring this instance
// @Id GetMetrics
// @Tags Metrics
// @Produce event-stream
// @Success 200 {object} []dto.MetricFamily
// @Failure 401,403 {object} i18n.Response
// @Router /api/v1/metrics [get]
func GetMetrics(router *gin.RouterGroup) {
router.GET("/metrics", func(c *gin.Context) {
s := Auth(c, acl.ResourceMetrics, acl.AccessAll)
@ -43,14 +50,18 @@ func GetMetrics(router *gin.RouterGroup) {
registerCountMetrics(factory, counts)
registerBuildInfoMetric(factory, conf.ClientPublic())
metrics, err := reg.Gather()
var metrics []*dto.MetricFamily
var err error
metrics, err = reg.Gather()
if err != nil {
logErr("metrics", err)
return false
}
for _, metric := range metrics {
if _, err := expfmt.MetricFamilyToText(w, metric); err != nil {
if _, err = expfmt.MetricFamilyToText(w, metric); err != nil {
logErr("metrics", err)
return false
}

View file

@ -6,12 +6,14 @@ import (
"github.com/gin-gonic/gin"
)
// Options returns an empty response to handle CORS preflight requests.
// Options returns CORS headers with an empty response body.
//
// @Summary returns CORS headers with an empty response body
// @Id Options
// @Success 204
// @Router /api/v1/{any} [options]
// @Summary returns CORS headers with an empty response body
// @Description A preflight request is automatically issued by a browser and in normal cases, front-end developers don't need to craft such requests themselves. It appears when request is qualified as "to be preflighted" and omitted for simple requests.
// @Id Options
// @Tags CORS
// @Success 204
// @Router /api/v1/{any} [options]
func Options(router *gin.RouterGroup) {
router.OPTIONS("/*any", func(c *gin.Context) {
c.Status(http.StatusNoContent)

View file

@ -6,16 +6,24 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/internal/server/process"
)
// StopServer initiates a server restart if the user is authorized.
// StopServer allows authorized admins to restart the server.
//
// POST /api/v1/server/stop
// @Summary allows authorized admins to restart the server
// @Id StopServer
// @Tags Admin
// @Produce json
// @Success 200 {object} config.Options
// @Failure 401,403 {object} i18n.Response
// @Router /api/v1/server/stop [post]
func StopServer(router *gin.RouterGroup) {
router.POST("/server/stop", func(c *gin.Context) {
s := Auth(c, acl.ResourceConfig, acl.ActionManage)
conf := get.Config()
// Abort if permission is not granted.
@ -24,11 +32,14 @@ func StopServer(router *gin.RouterGroup) {
return
}
var options *config.Options
options = conf.Options()
// Trigger restart.
//
// Note that this requires an entrypoint script or other process to
// spawns a new instance when the server exists with status code 1.
c.JSON(http.StatusOK, conf.Options())
c.JSON(http.StatusOK, options)
process.Restart()
})
}

View file

@ -6,11 +6,11 @@ import (
"github.com/gin-gonic/gin"
)
// GetStatus reports if the server is operational.
// GetStatus responds with status code 200 if the server is operational.
//
// @Summary reports if the server is operational
// @Summary responds with status code 200 if the server is operational
// @Id GetStatus
// @Tags Server
// @Tags Debug
// @Produce json
// @Success 200 {object} gin.H
// @Router /api/v1/status [get]

View file

@ -402,8 +402,8 @@
"application/zip"
],
"tags": [
"Images",
"Albums"
"Albums",
"Download"
],
"summary": "streams the album contents as zip archive",
"operationId": "DownloadAlbum",
@ -1716,6 +1716,9 @@
},
"/api/v1/echo": {
"get": {
"tags": [
"Debug"
],
"summary": "returns the request and response headers as JSON if debug mode is enabled",
"operationId": "Echo",
"responses": {
@ -2064,6 +2067,44 @@
}
}
},
"/api/v1/feedback": {
"post": {
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "allows members to submit a feedback message to the PhotoPrism team",
"operationId": "SendFeedback",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/form.Feedback"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
}
}
}
},
"/api/v1/files/{hash}": {
"get": {
"produces": [
@ -2730,6 +2771,41 @@
"responses": {}
}
},
"/api/v1/metrics": {
"get": {
"produces": [
"text/event-stream"
],
"tags": [
"Metrics"
],
"summary": "a prometheus-compatible metrics endpoint for monitoring this instance",
"operationId": "GetMetrics",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.MetricFamily"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
}
}
}
},
"/api/v1/moments/time": {
"get": {
"produces": [
@ -3982,6 +4058,38 @@
}
}
},
"/api/v1/server/stop": {
"post": {
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "allows authorized admins to restart the server",
"operationId": "StopServer",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/config.Options"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
}
}
}
},
"/api/v1/services": {
"get": {
"produces": [
@ -4521,9 +4629,9 @@
"application/json"
],
"tags": [
"Server"
"Debug"
],
"summary": "reports if the server is operational",
"summary": "responds with status code 200 if the server is operational",
"operationId": "GetStatus",
"responses": {
"200": {
@ -5275,14 +5383,95 @@
},
"/api/v1/zip": {
"post": {
"produces": [
"application/json"
],
"tags": [
"Download"
],
"responses": {}
"summary": "creates a zip file archive for download",
"operationId": "ZipCreate",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
}
}
}
},
"/api/v1/zip/{filename}": {
"get": {
"produces": [
"application/zip"
],
"tags": [
"Download"
],
"summary": "returns a zip file archive after it has been created",
"operationId": "ZipDownload",
"parameters": [
{
"type": "string",
"description": "zip archive filename returned by the POST /api/v1/zip endpoint",
"name": "filename",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/i18n.Response"
}
}
}
}
},
"/api/v1/{any}": {
"options": {
"description": "A preflight request is automatically issued by a browser and in normal cases, front-end developers don't need to craft such requests themselves. It appears when request is qualified as \"to be preflighted\" and omitted for simple requests.",
"tags": [
"CORS"
],
"summary": "returns CORS headers with an empty response body",
"operationId": "Options",
"responses": {
@ -7193,6 +7382,29 @@
}
}
},
"form.Feedback": {
"type": "object",
"properties": {
"Category": {
"type": "string"
},
"Message": {
"type": "string"
},
"UserAgent": {
"type": "string"
},
"UserEmail": {
"type": "string"
},
"UserLocales": {
"type": "string"
},
"UserName": {
"type": "string"
}
}
},
"form.File": {
"type": "object",
"properties": {
@ -7578,6 +7790,295 @@
}
}
},
"io_prometheus_client.Bucket": {
"type": "object",
"properties": {
"cumulative_count": {
"description": "Cumulative in increasing order.",
"type": "integer"
},
"cumulative_count_float": {
"description": "Overrides cumulative_count if \u003e 0.",
"type": "number"
},
"exemplar": {
"$ref": "#/definitions/io_prometheus_client.Exemplar"
},
"upper_bound": {
"description": "Inclusive.",
"type": "number"
}
}
},
"io_prometheus_client.BucketSpan": {
"type": "object",
"properties": {
"length": {
"description": "Length of consecutive buckets.",
"type": "integer"
},
"offset": {
"description": "Gap to previous span, or starting point for 1st span (which can be negative).",
"type": "integer"
}
}
},
"io_prometheus_client.Counter": {
"type": "object",
"properties": {
"created_timestamp": {
"$ref": "#/definitions/timestamppb.Timestamp"
},
"exemplar": {
"$ref": "#/definitions/io_prometheus_client.Exemplar"
},
"value": {
"type": "number"
}
}
},
"io_prometheus_client.Exemplar": {
"type": "object",
"properties": {
"label": {
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.LabelPair"
}
},
"timestamp": {
"description": "OpenMetrics-style.",
"allOf": [
{
"$ref": "#/definitions/timestamppb.Timestamp"
}
]
},
"value": {
"type": "number"
}
}
},
"io_prometheus_client.Gauge": {
"type": "object",
"properties": {
"value": {
"type": "number"
}
}
},
"io_prometheus_client.Histogram": {
"type": "object",
"properties": {
"bucket": {
"description": "Buckets for the conventional histogram.",
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.Bucket"
}
},
"created_timestamp": {
"$ref": "#/definitions/timestamppb.Timestamp"
},
"exemplars": {
"description": "Only used for native histograms. These exemplars MUST have a timestamp.",
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.Exemplar"
}
},
"negative_count": {
"description": "Absolute count of each bucket.",
"type": "array",
"items": {
"type": "number"
}
},
"negative_delta": {
"description": "Use either \"negative_delta\" or \"negative_count\", the former for\nregular histograms with integer counts, the latter for float\nhistograms.",
"type": "array",
"items": {
"type": "integer"
}
},
"negative_span": {
"description": "Negative buckets for the native histogram.",
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.BucketSpan"
}
},
"positive_count": {
"description": "Absolute count of each bucket.",
"type": "array",
"items": {
"type": "number"
}
},
"positive_delta": {
"description": "Use either \"positive_delta\" or \"positive_count\", the former for\nregular histograms with integer counts, the latter for float\nhistograms.",
"type": "array",
"items": {
"type": "integer"
}
},
"positive_span": {
"description": "Positive buckets for the native histogram.\nUse a no-op span (offset 0, length 0) for a native histogram without any\nobservations yet and with a zero_threshold of 0. Otherwise, it would be\nindistinguishable from a classic histogram.",
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.BucketSpan"
}
},
"sample_count": {
"type": "integer"
},
"sample_count_float": {
"description": "Overrides sample_count if \u003e 0.",
"type": "number"
},
"sample_sum": {
"type": "number"
},
"schema": {
"description": "schema defines the bucket schema. Currently, valid numbers are -4 \u003c= n \u003c= 8.\nThey are all for base-2 bucket schemas, where 1 is a bucket boundary in each case, and\nthen each power of two is divided into 2^n logarithmic buckets.\nOr in other words, each bucket boundary is the previous boundary times 2^(2^-n).\nIn the future, more bucket schemas may be added using numbers \u003c -4 or \u003e 8.",
"type": "integer"
},
"zero_count": {
"description": "Count in zero bucket.",
"type": "integer"
},
"zero_count_float": {
"description": "Overrides sb_zero_count if \u003e 0.",
"type": "number"
},
"zero_threshold": {
"description": "Breadth of the zero bucket.",
"type": "number"
}
}
},
"io_prometheus_client.LabelPair": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
},
"io_prometheus_client.Metric": {
"type": "object",
"properties": {
"counter": {
"$ref": "#/definitions/io_prometheus_client.Counter"
},
"gauge": {
"$ref": "#/definitions/io_prometheus_client.Gauge"
},
"histogram": {
"$ref": "#/definitions/io_prometheus_client.Histogram"
},
"label": {
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.LabelPair"
}
},
"summary": {
"$ref": "#/definitions/io_prometheus_client.Summary"
},
"timestamp_ms": {
"type": "integer"
},
"untyped": {
"$ref": "#/definitions/io_prometheus_client.Untyped"
}
}
},
"io_prometheus_client.MetricFamily": {
"type": "object",
"properties": {
"help": {
"type": "string"
},
"metric": {
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.Metric"
}
},
"name": {
"type": "string"
},
"type": {
"$ref": "#/definitions/io_prometheus_client.MetricType"
},
"unit": {
"type": "string"
}
}
},
"io_prometheus_client.MetricType": {
"type": "integer",
"format": "int32",
"enum": [
0,
1,
2,
3,
4,
5
],
"x-enum-varnames": [
"MetricType_COUNTER",
"MetricType_GAUGE",
"MetricType_SUMMARY",
"MetricType_UNTYPED",
"MetricType_HISTOGRAM",
"MetricType_GAUGE_HISTOGRAM"
]
},
"io_prometheus_client.Quantile": {
"type": "object",
"properties": {
"quantile": {
"type": "number"
},
"value": {
"type": "number"
}
}
},
"io_prometheus_client.Summary": {
"type": "object",
"properties": {
"created_timestamp": {
"$ref": "#/definitions/timestamppb.Timestamp"
},
"quantile": {
"type": "array",
"items": {
"$ref": "#/definitions/io_prometheus_client.Quantile"
}
},
"sample_count": {
"type": "integer"
},
"sample_sum": {
"type": "number"
}
}
},
"io_prometheus_client.Untyped": {
"type": "object",
"properties": {
"value": {
"type": "number"
}
}
},
"nsfw.Result": {
"type": "object",
"properties": {
@ -8334,6 +8835,12 @@
"resizeOperation": {
"$ref": "#/definitions/tensorflow.ResizeOperation"
},
"shape": {
"type": "array",
"items": {
"$ref": "#/definitions/tensorflow.ShapeComponent"
}
},
"width": {
"type": "integer"
}
@ -8354,6 +8861,15 @@
"Padding"
]
},
"tensorflow.ShapeComponent": {
"type": "string",
"enum": [
"Batch"
],
"x-enum-varnames": [
"ShapeBatch"
]
},
"time.Duration": {
"type": "integer",
"format": "int64",
@ -8366,12 +8882,16 @@
1000000000,
60000000000,
3600000000000,
-9223372036854775808,
9223372036854775807,
1,
1000,
1000000,
1000000000,
60000000000,
3600000000000,
-9223372036854775808,
9223372036854775807,
1,
1000,
1000000,
@ -8388,12 +8908,16 @@
"Second",
"Minute",
"Hour",
"minDuration",
"maxDuration",
"Nanosecond",
"Microsecond",
"Millisecond",
"Second",
"Minute",
"Hour",
"minDuration",
"maxDuration",
"Nanosecond",
"Microsecond",
"Millisecond",
@ -8402,6 +8926,19 @@
"Hour"
]
},
"timestamppb.Timestamp": {
"type": "object",
"properties": {
"nanos": {
"description": "Non-negative fractions of a second at nanosecond resolution. Negative\nsecond values with fractions must still have non-negative nanos values\nthat count forward in time. Must be from 0 to 999,999,999\ninclusive.",
"type": "integer"
},
"seconds": {
"description": "Represents seconds of UTC time since Unix epoch\n1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n9999-12-31T23:59:59Z inclusive.",
"type": "integer"
}
}
},
"vision.ApiFormat": {
"type": "string",
"enum": [

View file

@ -25,8 +25,13 @@ import (
// ZipCreate creates a zip file archive for download.
//
// @Tags Download
// @Router /api/v1/zip [post]
// @Summary creates a zip file archive for download
// @Id ZipCreate
// @Tags Download
// @Produce json
// @Failure 400,403,404 {object} i18n.Response
// @Success 200 {file} application/zip
// @Router /api/v1/zip [post]
func ZipCreate(router *gin.RouterGroup) {
router.POST("/zip", func(c *gin.Context) {
s := Auth(c, acl.ResourcePhotos, acl.ActionDownload)
@ -148,9 +153,16 @@ func ZipCreate(router *gin.RouterGroup) {
})
}
// ZipDownload downloads a zip file archive.
// ZipDownload returns a zip file archive after it has been created.
//
// GET /api/v1/zip/:filename
// @Summary returns a zip file archive after it has been created
// @Id ZipDownload
// @Tags Download
// @Produce application/zip
// @Failure 403,404,500 {object} i18n.Response
// @Success 200 {file} application/zip
// @Param filename path string true "zip archive filename returned by the POST /api/v1/zip endpoint"
// @Router /api/v1/zip/{filename} [get]
func ZipDownload(router *gin.RouterGroup) {
router.GET("/zip/:filename", func(c *gin.Context) {
if InvalidDownloadToken(c) {

View file

@ -65,6 +65,7 @@ const (
ResourceWebhooks Resource = "webhooks"
ResourceMetrics Resource = "metrics"
ResourceVision Resource = "vision"
ResourcePortal Resource = "portal"
ResourceFeedback Resource = "feedback"
ResourceDefault Resource = "default"
)

View file

@ -125,6 +125,14 @@ var (
ActionView: true,
ActionUpdateOwn: true,
}
GrantSearchDownloadUpdateOwn = Grant{
AccessShared: true,
AccessOwn: true,
ActionSearch: true,
ActionView: true,
ActionDownload: true,
ActionUpdateOwn: true,
}
GrantSubscribeOwn = Grant{
AccessOwn: true,
ActionSubscribe: true,

View file

@ -27,6 +27,7 @@ var ResourceNames = []Resource{
ResourceWebhooks,
ResourceMetrics,
ResourceVision,
ResourcePortal,
ResourceFeedback,
ResourceDefault,
}

View file

@ -108,6 +108,10 @@ var Rules = ACL{
RoleAdmin: GrantFullAccess,
RoleClient: GrantUseOwn,
},
ResourcePortal: Roles{
RoleAdmin: GrantFullAccess,
RoleClient: GrantSearchDownloadUpdateOwn,
},
ResourceFeedback: Roles{
RoleAdmin: GrantFullAccess,
},

View file

@ -21,12 +21,12 @@ var AuthResetCommand = &cli.Command{
&cli.BoolFlag{
Name: "trace",
Aliases: []string{"t"},
Usage: "show trace logs for debugging",
Usage: "shows trace logs for debugging",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "assume \"yes\" and run non-interactively",
Usage: "runs the command non-interactively",
},
},
Action: authResetAction,

View file

@ -33,12 +33,12 @@ var backupFlags = []cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "replace the index database backup file, if it exists",
Usage: "replaces the index database backup file, if it exists",
},
&cli.BoolFlag{
Name: "albums",
Aliases: []string{"a"},
Usage: "create YAML files to back up album metadata (in the standard backup path if no other path is specified)",
Usage: "creates YAML files to back up album metadata (in the standard backup path if no other path is specified)",
},
&cli.PathFlag{
Name: "albums-path",
@ -48,7 +48,7 @@ var backupFlags = []cli.Flag{
&cli.BoolFlag{
Name: "database",
Aliases: []string{"index", "i"},
Usage: "create index database backup (in the backup path with the date as filename if no filename is passed, or sent to stdout if - is passed as filename)",
Usage: "creates an index database backup (in the backup path with the date as filename if no filename is passed, or sent to stdout if - is passed as filename)",
},
&cli.PathFlag{
Name: "database-path",

View file

@ -22,7 +22,7 @@ var CleanUpCommand = &cli.Command{
var cleanUpFlags = []cli.Flag{
&cli.BoolFlag{
Name: "dry",
Usage: "dry run, don't actually remove anything",
Usage: "performs a dry run that doesn't actually remove anything",
},
}

View file

@ -20,7 +20,7 @@ var ClientsRemoveCommand = &cli.Command{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "don't ask for confirmation",
Usage: "skips asking for confirmation",
},
},
Action: clientsRemoveAction,

View file

@ -17,12 +17,12 @@ var ClientsResetCommand = &cli.Command{
&cli.BoolFlag{
Name: "trace",
Aliases: []string{"t"},
Usage: "show trace logs for debugging",
Usage: "shows trace logs for debugging",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "assume \"yes\" and run non-interactively",
Usage: "runs the command non-interactively",
},
},
Action: clientsResetAction,

View file

@ -47,7 +47,7 @@ var FacesCommands = &cli.Command{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "remove all people and faces",
Usage: "removes all people and faces",
},
},
Action: facesResetAction,

View file

@ -38,7 +38,7 @@ var indexFlags = []cli.Flag{
&cli.BoolFlag{
Name: "cleanup",
Aliases: []string{"c"},
Usage: "remove orphan index entries and thumbnails",
Usage: "removes orphan index entries and thumbnails",
},
}

View file

@ -43,7 +43,7 @@ var MigrationsRunCommand = &cli.Command{
&cli.BoolFlag{
Name: "trace",
Aliases: []string{"t"},
Usage: "show trace logs for debugging",
Usage: "shows trace logs for debugging",
},
},
Action: migrationsRunAction,

View file

@ -27,12 +27,12 @@ var PasswdCommand = &cli.Command{
&cli.BoolFlag{
Name: "show",
Aliases: []string{"s"},
Usage: "show bcrypt hash of new password",
Usage: "shows bcrypt hash of new password",
},
&cli.BoolFlag{
Name: "remove",
Aliases: []string{"rm"},
Usage: "remove password to disable local authentication",
Usage: "removes password to disable local authentication",
},
},
Action: passwdAction,

View file

@ -30,7 +30,7 @@ var PlacesCommands = &cli.Command{
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "assume \"yes\" and run non-interactively",
Usage: "runs the command non-interactively",
},
},
Action: placesUpdateAction,

View file

@ -26,11 +26,11 @@ var PurgeCommand = &cli.Command{
var purgeFlags = []cli.Flag{
&cli.BoolFlag{
Name: "hard",
Usage: "permanently remove from index",
Usage: "permanently removes data from the index",
},
&cli.BoolFlag{
Name: "dry",
Usage: "dry run, don't actually remove anything",
Usage: "performs a dry run that doesn't actually remove anything",
},
}

View file

@ -25,17 +25,17 @@ var ResetCommand = &cli.Command{
&cli.BoolFlag{
Name: "index",
Aliases: []string{"i"},
Usage: "reset index database only",
Usage: "resets only the index database ",
},
&cli.BoolFlag{
Name: "trace",
Aliases: []string{"t"},
Usage: "show trace logs for debugging",
Usage: "shows trace logs for debugging",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "assume \"yes\" and run non-interactively",
Usage: "runs the command non-interactively",
},
},
Action: resetAction,

View file

@ -15,10 +15,10 @@ const (
UserRoleUsage = "user account `ROLE` (admin or guest)"
UserAuthUsage = "authentication `PROVIDER` (default, local, oidc or none)"
UserAuthIDUsage = "authentication `ID` e.g. Subject ID or Distinguished Name (DN)"
UserAdminUsage = "make user super admin with full access"
UserNoLoginUsage = "disable login on the web interface"
UserWebDAVUsage = "allow to sync files via WebDAV"
UserDisable2FA = "deactivate two-factor authentication"
UserAdminUsage = "makes user super admin with full access"
UserNoLoginUsage = "disables login on the web interface"
UserWebDAVUsage = "allows to sync files via WebDAV"
UserDisable2FA = "deactivates two-factor authentication"
)
// UsersCommands configures the user management subcommands.
@ -98,19 +98,19 @@ var UserTokensFlag = &cli.BoolFlag{
var UsersLoginFlag = &cli.BoolFlag{
Name: "login",
Aliases: []string{"l"},
Usage: "show date and time of last login",
Usage: "shows date and time of last login",
}
// UsersCreatedFlag is a CLI flag for showing the account creation timestamp in reports.
var UsersCreatedFlag = &cli.BoolFlag{
Name: "created",
Aliases: []string{"a"},
Usage: "show account creation timestamp",
Usage: "shows account creation timestamp",
}
// UsersDeletedFlag is a CLI flag for showing deleted user accounts in reports.
var UsersDeletedFlag = &cli.BoolFlag{
Name: "deleted",
Aliases: []string{"r"},
Usage: "show deleted user accounts",
Usage: "shows deleted user accounts",
}

View file

@ -21,7 +21,7 @@ var UsersRemoveCommand = &cli.Command{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "don't ask for confirmation",
Usage: "skips asking for confirmation",
},
},
Action: usersRemoveAction,

View file

@ -20,12 +20,12 @@ var UsersResetCommand = &cli.Command{
&cli.BoolFlag{
Name: "trace",
Aliases: []string{"t"},
Usage: "show trace logs for debugging",
Usage: "shows trace logs for debugging",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "assume \"yes\" and run non-interactively",
Usage: "runs the command non-interactively",
},
},
Action: usersResetAction,

View file

@ -30,14 +30,14 @@ func TestCliFlags_Replace(t *testing.T) {
Name: "public",
Aliases: []string{"p"},
Hidden: true,
Usage: "disable authentication, advanced settings, and WebDAV remote access",
Usage: "disables authentication, advanced settings, and WebDAV remote access",
EnvVars: EnvVars("PUBLIC"),
}}
newPublicFlag := CliFlag{Flag: &cli.BoolFlag{
Name: "public",
Hidden: false,
Usage: "disable authentication, advanced settings, and WebDAV remote access",
Usage: "disables authentication, advanced settings, and WebDAV remote access",
EnvVars: EnvVars("PUBLIC"),
}}
@ -114,7 +114,7 @@ func TestCliFlags_Insert(t *testing.T) {
Name: "public",
Aliases: []string{"p"},
Hidden: true,
Usage: "disable authentication, advanced settings, and WebDAV remote access",
Usage: "disables authentication, advanced settings, and WebDAV remote access",
EnvVars: EnvVars("PUBLIC"),
}}
@ -163,7 +163,7 @@ func TestCliFlags_InsertBefore(t *testing.T) {
Name: "public",
Aliases: []string{"p"},
Hidden: true,
Usage: "disable authentication, advanced settings, and WebDAV remote access",
Usage: "disables authentication, advanced settings, and WebDAV remote access",
EnvVars: EnvVars("PUBLIC"),
}}
@ -214,7 +214,7 @@ func TestCliFlags_Prepend(t *testing.T) {
Name: "public",
Aliases: []string{"p"},
Hidden: true,
Usage: "disable authentication, advanced settings, and WebDAV remote access",
Usage: "disables authentication, advanced settings, and WebDAV remote access",
EnvVars: EnvVars("PUBLIC"),
}}

View file

@ -291,6 +291,8 @@ func (c *Config) Propagate() {
thumb.SizeOnDemand = c.ThumbSizeUncached()
thumb.JpegQualityDefault = c.JpegQuality()
thumb.CachePublic = c.HttpCachePublic()
thumb.ExamplesPath = c.ExamplesPath()
thumb.IccProfilesPath = c.IccProfilesPath()
initThumbs()
// Configure video download package.

View file

@ -291,3 +291,8 @@ func (c *Config) ImgPath() string {
func (c *Config) ThemePath() string {
return filepath.Join(c.ConfigPath(), "theme")
}
// PortalPath returns the path to portal config files.
func (c *Config) PortalPath() string {
return filepath.Join(c.ConfigPath(), "portal")
}

View file

@ -600,6 +600,16 @@ func (c *Config) CustomAssetsPath() string {
return ""
}
// ProfilesPath returns the path where processing profile files are stored.
func (c *Config) ProfilesPath() string {
return filepath.Join(c.AssetsPath(), "profiles")
}
// IccProfilesPath returns the path where ICC color profile files are stored.
func (c *Config) IccProfilesPath() string {
return filepath.Join(c.AssetsPath(), "profiles/icc")
}
// CustomStaticPath returns the custom static assets' path.
func (c *Config) CustomStaticPath() string {
if dir := c.CustomAssetsPath(); dir == "" {

View file

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/functions"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -474,8 +475,10 @@ func TestConfig_ImportAllow(t *testing.T) {
assert.Equal(t, "", c.ImportAllow().String())
}
func TestConfig_AssetsPath2(t *testing.T) {
func TestConfig_AssetsPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.True(t, strings.HasSuffix(c.AssetsPath(), "/assets"))
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets", c.AssetsPath())
c.options.AssetsPath = ""
if s := c.AssetsPath(); s != "" && s != "/opt/photoprism/assets" {
@ -483,6 +486,27 @@ func TestConfig_AssetsPath2(t *testing.T) {
}
}
func TestConfig_ProfilesPath(t *testing.T) {
c := NewConfig(CliTestContext())
result := c.ProfilesPath()
assert.True(t, strings.HasSuffix(result, "/assets/profiles"))
assert.True(t, fs.PathExists(result))
}
func TestConfig_IccProfilesPath(t *testing.T) {
c := NewConfig(CliTestContext())
result := c.IccProfilesPath()
assert.True(t, strings.HasSuffix(result, "/assets/profiles/icc"))
}
func TestConfig_CustomAssetsPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.CustomAssetsPath())
}
func TestConfig_MariadbBin(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Contains(t, c.MariadbBin(), "mariadb")

View file

@ -201,18 +201,6 @@ func TestConfig_ThumbCachePath(t *testing.T) {
assert.True(t, strings.HasSuffix(c.ThumbCachePath(), "storage/testdata/"+functions.PhotoPrismTestToFolderName()+"/cache/thumbnails"))
}
func TestConfig_AssetsPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.True(t, strings.HasSuffix(c.AssetsPath(), "/assets"))
}
func TestConfig_CustomAssetsPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.CustomAssetsPath())
}
func TestConfig_AdminUser(t *testing.T) {
c := NewConfig(CliTestContext())
@ -287,6 +275,13 @@ func TestConfig_ThemePath(t *testing.T) {
assert.Equal(t, expected, path)
}
func TestConfig_PortalPath(t *testing.T) {
c := NewConfig(CliTestContext())
path := c.PortalPath()
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/storage/testdata/config/portal", path)
}
func TestConfig_IndexWorkers(t *testing.T) {
c := NewConfig(CliTestContext())

View file

@ -35,7 +35,7 @@ var Flags = CliFlags{
Name: "public",
Aliases: []string{"p"},
Hidden: true,
Usage: "disable authentication, advanced settings, and WebDAV remote access",
Usage: "disables authentication, advanced settings, and WebDAV remote access",
EnvVars: EnvVars("PUBLIC"),
}}, {
Flag: &cli.StringFlag{
@ -95,12 +95,12 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "oidc-redirect",
Usage: "automatically redirect unauthenticated users to the configured identity provider",
Usage: "automatically redirects unauthenticated users to the configured identity provider",
EnvVars: EnvVars("OIDC_REDIRECT"),
}}, {
Flag: &cli.BoolFlag{
Name: "oidc-register",
Usage: "allow new users to create an account when they sign in with OpenID Connect",
Usage: "allows new users to create an account when they sign in with OpenID Connect",
EnvVars: EnvVars("OIDC_REGISTER"),
}}, {
Flag: &cli.StringFlag{
@ -111,12 +111,12 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "oidc-webdav",
Usage: "allow new OpenID Connect users to use WebDAV when they have a role that allows it",
Usage: "allows new OpenID Connect users to use WebDAV when they have a role that allows it",
EnvVars: EnvVars("OIDC_WEBDAV"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-oidc",
Usage: "disable single sign-on via OpenID Connect, even if an identity provider has been configured",
Usage: "disables single sign-on via OpenID Connect, even if an identity provider has been configured",
EnvVars: EnvVars("DISABLE_OIDC"),
}}, {
Flag: &cli.Int64Flag{
@ -146,34 +146,34 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "prod",
Usage: "disable debug mode and log startup warnings and errors only",
Usage: "disables debug mode and only logs startup warnings and errors",
EnvVars: EnvVars("PROD"),
}}, {
Flag: &cli.BoolFlag{
Name: "debug",
Usage: "enable debug mode for development and troubleshooting",
Usage: "enables debug mode for development and troubleshooting",
EnvVars: EnvVars("DEBUG"),
}}, {
Flag: &cli.BoolFlag{
Name: "trace",
Usage: "enable trace mode to display all debug and trace logs",
Usage: "enables trace mode to display all debug and trace logs",
EnvVars: EnvVars("TRACE"),
}}, {
Flag: &cli.BoolFlag{
Name: "test",
Hidden: true,
Usage: "enable test mode",
Usage: "enables test mode",
}}, {
Flag: &cli.BoolFlag{
Name: "unsafe",
Hidden: true,
Usage: "disable safety checks",
Usage: "disables safety checks",
EnvVars: EnvVars("UNSAFE"),
}}, {
Flag: &cli.BoolFlag{
Name: "demo",
Hidden: true,
Usage: "enable demo mode",
Usage: "enables demo mode",
EnvVars: EnvVars("DEMO"),
}}, {
Flag: &cli.BoolFlag{
@ -198,7 +198,7 @@ var Flags = CliFlags{
Flag: &cli.StringFlag{
Name: "defaults-yaml",
Aliases: []string{"y"},
Usage: "load default config values from `FILENAME` if it exists, does not override CLI flags or environment variables",
Usage: "loads default config values from `FILENAME` if it exists, does not override CLI flags or environment variables",
Value: "/etc/photoprism/defaults.yml",
EnvVars: EnvVars("DEFAULTS_YAML"),
TakesFile: true,
@ -252,23 +252,23 @@ var Flags = CliFlags{
}}, {
Flag: &cli.StringFlag{
Name: "import-allow",
Usage: "restrict imports to these file types (comma-separated list of `EXTENSIONS`; leave blank to allow all)",
Usage: "restricts imports to these file types (comma-separated list of `EXTENSIONS`; leave blank to allow all)",
EnvVars: EnvVars("IMPORT_ALLOW"),
}}, {
Flag: &cli.BoolFlag{
Name: "upload-nsfw",
Aliases: []string{"n"},
Usage: "allow uploads that might be offensive (detecting unsafe content requires TensorFlow)",
Usage: "allows uploads that might be offensive (detecting unsafe content requires TensorFlow)",
EnvVars: EnvVars("UPLOAD_NSFW"),
}}, {
Flag: &cli.StringFlag{
Name: "upload-allow",
Usage: "restrict uploads to these file types (comma-separated list of `EXTENSIONS`; leave blank to allow all)",
Usage: "restricts uploads to these file types (comma-separated list of `EXTENSIONS`; leave blank to allow all)",
EnvVars: EnvVars("UPLOAD_ALLOW"),
}}, {
Flag: &cli.BoolFlag{
Name: "upload-archives",
Usage: "allow upload of zip archives (will be extracted before import)",
Usage: "allows upload of zip archives (will be extracted before import)",
EnvVars: EnvVars("UPLOAD_ARCHIVES"),
}}, {
Flag: &cli.IntFlag{
@ -312,12 +312,12 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "sidecar-yaml",
Usage: "create YAML sidecar files to back up picture metadata",
Usage: "creates YAML sidecar files to back up picture metadata",
EnvVars: EnvVars("SIDECAR_YAML"),
}, DocDefault: "true"}, {
Flag: &cli.BoolFlag{
Name: "usage-info",
Usage: "display usage information in the user interface",
Usage: "displays storage usage information in the user interface",
EnvVars: EnvVars("USAGE_INFO"),
}}, {
Flag: &cli.Uint64Flag{
@ -346,12 +346,12 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "backup-database",
Usage: "create regular backups based on the configured schedule",
Usage: "enables regular backups based on the configured schedule",
EnvVars: EnvVars("BACKUP_DATABASE"),
}, DocDefault: "true"}, {
Flag: &cli.BoolFlag{
Name: "backup-albums",
Usage: "create YAML files to back up album metadata",
Usage: "enables the use of YAML files for backing up album metadata",
EnvVars: EnvVars("BACKUP_ALBUMS"),
}, DocDefault: "true"}, {
Flag: &cli.IntFlag{
@ -389,118 +389,118 @@ var Flags = CliFlags{
Flag: &cli.BoolFlag{
Name: "read-only",
Aliases: []string{"r"},
Usage: "disable features that require write permission for the originals folder",
Usage: "disables features that require write permission for the originals folder",
EnvVars: EnvVars("READONLY"),
}}, {
Flag: &cli.BoolFlag{
Name: "experimental",
Aliases: []string{"e"},
Usage: "enable new features that may be incomplete or unstable",
Usage: "enables new features that may be incomplete or unstable",
EnvVars: EnvVars("EXPERIMENTAL"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-frontend",
Usage: "disable the web user interface so that only the service API endpoints are accessible",
Usage: "disables the web user interface so that only the service API endpoints are accessible",
EnvVars: EnvVars("DISABLE_FRONTEND"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-settings",
Usage: "disable the settings frontend and related API endpoints, e.g. in combination with public mode",
Usage: "disables the settings frontend and related API endpoints, e.g. in combination with public mode",
EnvVars: EnvVars("DISABLE_SETTINGS"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-backups",
Usage: "prevent database and album backups as well as YAML sidecar files from being created",
Usage: "prevents database and album backups as well as YAML sidecar files from being created",
EnvVars: EnvVars("DISABLE_BACKUPS"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-restart",
Usage: "prevent admins from restarting the server through the user interface",
Usage: "prevents admins from restarting the server through the user interface",
EnvVars: EnvVars("DISABLE_RESTART"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-webdav",
Usage: "prevent other apps from accessing PhotoPrism as a shared network drive",
Usage: "prevents other apps from accessing PhotoPrism as a shared network drive",
EnvVars: EnvVars("DISABLE_WEBDAV"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-places",
Usage: "disable interactive world maps and reverse geocoding",
Usage: "disables interactive world maps and reverse geocoding",
EnvVars: EnvVars("DISABLE_PLACES"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-tensorflow",
Usage: "disable features depending on TensorFlow, e.g. image classification and face recognition",
Usage: "disables features depending on TensorFlow, e.g. image classification and face recognition",
EnvVars: EnvVars("DISABLE_TENSORFLOW"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-faces",
Usage: "disable face detection and recognition (requires TensorFlow)",
Usage: "disables face detection and recognition (requires TensorFlow)",
EnvVars: EnvVars("DISABLE_FACES"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-classification",
Usage: "disable image classification (requires TensorFlow)",
Usage: "disables image classification (requires TensorFlow)",
EnvVars: EnvVars("DISABLE_CLASSIFICATION"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-ffmpeg",
Usage: "disable video transcoding and thumbnail extraction with FFmpeg",
Usage: "disables video transcoding and thumbnail extraction with FFmpeg",
EnvVars: EnvVars("DISABLE_FFMPEG"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-exiftool",
Usage: "disable metadata extraction with ExifTool (required for full Video, Live Photo, and XMP support)",
Usage: "disables metadata extraction with ExifTool (required for full Video, Live Photo, and XMP support)",
EnvVars: EnvVars("DISABLE_EXIFTOOL"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-vips",
Usage: "disable image processing and conversion with libvips",
Usage: "disables image processing and conversion with libvips",
EnvVars: EnvVars("DISABLE_VIPS"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-sips",
Usage: "disable file conversion using the sips command under macOS",
Usage: "disables file conversion using the sips command under macOS",
EnvVars: EnvVars("DISABLE_SIPS"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-darktable",
Usage: "disable conversion of RAW images with Darktable",
Usage: "disables conversion of RAW images with Darktable",
EnvVars: EnvVars("DISABLE_DARKTABLE"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-rawtherapee",
Usage: "disable conversion of RAW images with RawTherapee",
Usage: "disables conversion of RAW images with RawTherapee",
EnvVars: EnvVars("DISABLE_RAWTHERAPEE"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-imagemagick",
Usage: "disable conversion of image files with ImageMagick",
Usage: "disables conversion of image files with ImageMagick",
EnvVars: EnvVars("DISABLE_IMAGEMAGICK"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-heifconvert",
Usage: "disable conversion of HEIC images with libheif",
Usage: "disables conversion of HEIC images with libheif",
EnvVars: EnvVars("DISABLE_HEIFCONVERT"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-jpegxl",
Usage: "disable JPEG XL file format support",
Usage: "disables JPEG XL file format support",
EnvVars: EnvVars("DISABLE_JPEGXL"),
}}, {
Flag: &cli.BoolFlag{
Name: "disable-raw",
Usage: "disable indexing and conversion of RAW images",
Usage: "disables indexing and conversion of RAW images",
EnvVars: EnvVars("DISABLE_RAW"),
}}, {
Flag: &cli.BoolFlag{
Name: "raw-presets",
Usage: "enables applying user presets when converting RAW images (reduces performance)",
Usage: "enables custom user presets when converting RAW images (reduces performance)",
EnvVars: EnvVars("RAW_PRESETS"),
}}, {
Flag: &cli.BoolFlag{
Name: "exif-bruteforce",
Usage: "always perform a brute-force search if no Exif headers were found",
Usage: "performs a brute-force search if no Exif headers were found",
EnvVars: EnvVars("EXIF_BRUTEFORCE"),
}}, {
Flag: &cli.StringFlag{
@ -630,7 +630,7 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "cdn-video",
Usage: "stream videos over the specified CDN",
Usage: "streams videos over the specified CDN",
EnvVars: EnvVars("CDN_VIDEO"),
}}, {
Flag: &cli.StringFlag{
@ -651,6 +651,26 @@ var Flags = CliFlags{
EnvVars: EnvVars("CORS_METHODS"),
Value: header.DefaultAccessControlAllowMethods,
}}, {
Flag: &cli.StringFlag{
Name: "portal-url",
Usage: "PhotoPrism® Portal server `URL`",
EnvVars: EnvVars("PORTAL_URL"),
}}, {
Flag: &cli.StringFlag{
Name: "portal-client",
Usage: "PhotoPrism® Portal client `ID`",
EnvVars: EnvVars("PORTAL_CLIENT"),
}}, {
Flag: &cli.StringFlag{
Name: "portal-secret",
Usage: "PhotoPrism® Portal client `SECRET`",
EnvVars: EnvVars("PORTAL_SECRET"),
}}, {
Flag: &cli.StringFlag{
Name: "instance-secret",
Usage: "unique `SECRET` for authenticating this instance",
EnvVars: EnvVars("INSTANCE_SECRET"),
}}, {
Flag: &cli.StringFlag{
Name: "https-proxy",
Usage: "proxy server `URL` to be used for outgoing connections*optional*",
@ -658,7 +678,7 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "https-proxy-insecure",
Usage: "ignore invalid HTTPS certificates when using a proxy",
Usage: "ignores invalid HTTPS certificates when using a proxy",
EnvVars: EnvVars("HTTPS_PROXY_INSECURE"),
}}, {
Flag: &cli.StringFlag{
@ -693,12 +713,12 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "disable-tls",
Usage: "disable HTTPS/TLS even if the site URL starts with https:// and a certificate is available",
Usage: "disables HTTPS/TLS even if the site URL starts with https:// and a certificate is available",
EnvVars: EnvVars("DISABLE_TLS"),
}}, {
Flag: &cli.BoolFlag{
Name: "default-tls",
Usage: "default to a self-signed HTTPS/TLS certificate if no other certificate is available",
Usage: "uses a self-signed HTTPS/TLS certificate if no other certificate is available",
EnvVars: EnvVars("DEFAULT_TLS"),
}}, {
Flag: &cli.StringFlag{
@ -731,7 +751,7 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "http-cache-public",
Usage: "allow static content to be cached by a CDN or caching proxy",
Usage: "allows static content to be cached by a CDN or caching proxy",
EnvVars: EnvVars("HTTP_CACHE_PUBLIC"),
}}, {
Flag: &cli.IntFlag{
@ -995,7 +1015,7 @@ var Flags = CliFlags{
Flag: &cli.BoolFlag{
Name: "thumb-uncached",
Aliases: []string{"u"},
Usage: "generate missing thumbnails on demand (high memory and cpu usage)",
Usage: "generates missing thumbnails on demand (high memory and cpu usage)",
EnvVars: EnvVars("THUMB_UNCACHED"),
}}, {
Flag: &cli.StringFlag{
@ -1026,24 +1046,24 @@ var Flags = CliFlags{
}}, {
Flag: &cli.BoolFlag{
Name: "vision-api",
Usage: "enable computer vision service API endpoints under /api/v1/vision (requires authorized access token)",
Usage: "enables the computer vision API endpoints under /api/v1/vision (requires authorization)",
EnvVars: EnvVars("VISION_API"),
}}, {
Flag: &cli.StringFlag{
Name: "vision-uri",
Usage: "remote computer vision service `URI`, e.g. https://example.com/api/v1/vision (leave blank to disable)",
Usage: "vision service base `URI`, e.g. https://example.com/api/v1/vision (leave blank to disable)",
Value: "",
EnvVars: EnvVars("VISION_URI"),
}}, {
Flag: &cli.StringFlag{
Name: "vision-key",
Usage: "remote computer vision service access `TOKEN`*optional*",
Usage: "vision service access `TOKEN`*optional*",
Value: "",
EnvVars: EnvVars("VISION_KEY"),
}}, {
Flag: &cli.BoolFlag{
Name: "detect-nsfw",
Usage: "flag newly added pictures as private if they might be offensive (requires TensorFlow)",
Usage: "flags newly added pictures as private if they might be offensive (requires TensorFlow)",
EnvVars: EnvVars("DETECT_NSFW"),
}}, {
Flag: &cli.IntFlag{

View file

@ -140,6 +140,10 @@ type Options struct {
CORSOrigin string `yaml:"CORSOrigin" json:"-" flag:"cors-origin"`
CORSHeaders string `yaml:"CORSHeaders" json:"-" flag:"cors-headers"`
CORSMethods string `yaml:"CORSMethods" json:"-" flag:"cors-methods"`
PortalUrl string `yaml:"PortalUrl" json:"-" flag:"portal-url"`
PortalClient string `yaml:"PortalClient" json:"-" flag:"portal-client"`
PortalSecret string `yaml:"PortalSecret" json:"-" flag:"portal-secret"`
InstanceSecret string `yaml:"InstanceSecret" json:"-" flag:"instance-secret"`
HttpsProxy string `yaml:"HttpsProxy" json:"HttpsProxy" flag:"https-proxy"`
HttpsProxyInsecure bool `yaml:"HttpsProxyInsecure" json:"HttpsProxyInsecure" flag:"https-proxy-insecure"`
TrustedPlatform string `yaml:"TrustedPlatform" json:"-" flag:"trusted-platform"`

View file

@ -168,6 +168,12 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"cors-headers", c.CORSHeaders()},
{"cors-methods", c.CORSMethods()},
// Portal Server.
{"portal-url", fmt.Sprintf("%s", c.Options().PortalUrl)},
{"portal-client", fmt.Sprintf("%s", c.Options().PortalClient)},
{"portal-secret", fmt.Sprintf("%s", strings.Repeat("*", utf8.RuneCountInString(c.Options().PortalSecret)))},
{"instance-secret", fmt.Sprintf("%s", strings.Repeat("*", utf8.RuneCountInString(c.Options().InstanceSecret)))},
// URIs.
{"base-uri", c.BaseUri("/")},
{"api-uri", c.ApiUri()},

View file

@ -25,6 +25,7 @@ var OptionsReportSections = []ReportSection{
{Start: "PHOTOPRISM_READONLY", Title: "Feature Flags"},
{Start: "PHOTOPRISM_DEFAULT_LOCALE", Title: "Customization"},
{Start: "PHOTOPRISM_SITE_URL", Title: "Site Information"},
{Start: "PHOTOPRISM_PORTAL_URL", Title: "Portal Server"},
{Start: "PHOTOPRISM_HTTPS_PROXY", Title: "Proxy Server"},
{Start: "PHOTOPRISM_DISABLE_TLS", Title: "Web Server"},
{Start: "PHOTOPRISM_DATABASE_DRIVER", Title: "Database Connection"},
@ -51,6 +52,7 @@ var YamlReportSections = []ReportSection{
{Start: "ReadOnly", Title: "Feature Flags"},
{Start: "DefaultLocale", Title: "Customization"},
{Start: "SiteUrl", Title: "Site Information"},
{Start: "PortalUrl", Title: "Portal Server"},
{Start: "HttpsProxy", Title: "Proxy Server"},
{Start: "DisableTLS", Title: "Web Server"},
{Start: "DatabaseDriver", Title: "Database Connection"},

View file

@ -751,7 +751,7 @@ func TestMediaFile_MimeType(t *testing.T) {
assert.True(t, fs.SameType(header.ContentTypeXml, f.BaseType()))
assert.Equal(t, "text/xml", f.BaseType())
assert.Equal(t, "text/xml; charset=utf-8", f.MimeType())
assert.True(t, strings.EqualFold("text/xml; charset=utf-8", f.MimeType()))
assert.True(t, f.HasMimeType("text/xml"))
assert.False(t, f.IsMov())
})

View file

@ -1,5 +0,0 @@
package thumb
var (
CachePublic = false
)

View file

@ -12,9 +12,6 @@ const (
ColorPreserve ColorSpace = "preserve"
)
// Color sets the standard color profile for thumbnails.
var Color = ColorAuto
// ParseColor returns a ColorSpace based on the config value string and image library.
func ParseColor(name string, lib Lib) ColorSpace {
if lib == LibVips {

18
internal/thumb/config.go Normal file
View file

@ -0,0 +1,18 @@
package thumb
import (
"github.com/photoprism/photoprism/pkg/fs"
)
// Package configuration variables.
var (
Library = LibImaging // Image processing library to be used.
Color = ColorAuto // Color sets the standard color profile for thumbnails.
Filter = ResampleLanczos // Filter specifies the default downscaling filter.
SizeCached = SizeFit1920.Width // Pre-generated thumbnail size limit.
SizeOnDemand = SizeFit5120.Width // On-demand thumbnail size limit.
JpegQualityDefault = QualityMedium // JpegQualityDefault sets the compression level of newly created JPEGs.
CachePublic = false // Specifies if static content may be cached by a CDN or caching proxy.
ExamplesPath = fs.Abs("../../assets/examples")
IccProfilesPath = fs.Abs("../../assets/profiles/icc")
)

View file

@ -0,0 +1,22 @@
package thumb
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs"
)
func TestConfig(t *testing.T) {
t.Run("ExamplesPath", func(t *testing.T) {
t.Logf("examples-path: %s", ExamplesPath)
assert.Equal(t, fs.Abs("../../assets/examples"), ExamplesPath)
assert.True(t, fs.PathExists(ExamplesPath))
})
t.Run("IccProfilesPath", func(t *testing.T) {
t.Logf("icc-profiles-path: %s", ExamplesPath)
assert.Equal(t, fs.Abs("../../assets/profiles/icc"), IccProfilesPath)
// assert.True(t, fs.PathExists(IccProfilesPath))
})
}

View file

@ -20,9 +20,6 @@ const (
ResampleNearest ResampleFilter = "nearest"
)
// Filter specifies the default downscaling filter.
var Filter = ResampleLanczos
// String returns the downscaling filter name as string.
func (a ResampleFilter) String() string {
return string(a)

View file

@ -8,6 +8,3 @@ const (
LibVips Lib = "vips"
LibImaging Lib = "imaging"
)
// Library specifies the image library to be used.
var Library = LibImaging

View file

@ -19,9 +19,6 @@ const (
QualityMin Quality = 70
)
// JpegQualityDefault sets the compression level of newly created JPEGs.
var JpegQualityDefault = QualityMedium
// Quality represents a JPEG image quality.
type Quality int

View file

@ -4,12 +4,6 @@ import (
"slices"
)
// Default thumbnail size limits (cached and uncached).
var (
SizeCached = SizeFit1920.Width
SizeOnDemand = SizeFit5120.Width
)
// MaxSize returns the max supported size in pixels.
func MaxSize() int {
if SizeCached > SizeOnDemand {