Config: Refactor command flags, reports, and client options

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-04-15 15:42:03 +02:00
parent 2c47644ea8
commit 96dbb5ccbc
51 changed files with 427 additions and 104 deletions

View File

@@ -62,7 +62,7 @@ services:
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD # PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # On-demand rendering size limit (default 7680, min 720, max 7680) PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # On-demand rendering size limit (default 7680, min 720, max 7680)
PHOTOPRISM_JPEG_SIZE: 7680 # Size limit for converted image files in pixels (720-30000) PHOTOPRISM_JPEG_SIZE: 7680 # Size limit for converted image files in pixels (720-30000)
TF_CPP_MIN_LOG_LEVEL: 0 # Show TensorFlow log messages for development TF_CPP_MIN_LOG_LEVEL: 1 # Show TensorFlow log messages for development
## Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root): ## Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2" # PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
## Hardware video transcoding config (optional): ## Hardware video transcoding config (optional):

View File

@@ -109,7 +109,7 @@ services:
PHOTOPRISM_THUMB_LIBRARY: "auto" # image processing library to be used for generating thumbnails (auto, imaging, vips) 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_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) PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
## Intel Quick Sync Video (QSV) (https://docs.photoprism.app/getting-started/advanced/transcoding/#intel-quick-sync): ## Intel Quick Sync Video (QSV) (https://docs.photoprism.app/getting-started/advanced/transcoding/#intel-quick-sync):
PHOTOPRISM_FFMPEG_ENCODER: "intel" # H.264/AVC encoder (software, intel, nvidia, apple, raspberry, or vaapi) PHOTOPRISM_FFMPEG_ENCODER: "intel" # H.264/AVC encoder (software, intel, nvidia, apple, raspberry, or vaapi)
PHOTOPRISM_FFMPEG_SIZE: "1920" # video size limit in pixels (720-7680) (default: 3840) PHOTOPRISM_FFMPEG_SIZE: "1920" # video size limit in pixels (720-7680) (default: 3840)

View File

@@ -57,7 +57,7 @@ services:
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD # PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680) PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680)
PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000) PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
working_dir: "/photoprism" working_dir: "/photoprism"
volumes: volumes:
- "./storage:/photoprism/storage" - "./storage:/photoprism/storage"

View File

@@ -58,7 +58,7 @@ services:
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD # PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680) PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680)
PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000) PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
# PHOTOPRISM_INIT: "http gpu tensorflow" # Options: "update https gpu tensorflow davfs clitools clean" # PHOTOPRISM_INIT: "http gpu tensorflow" # Options: "update https gpu tensorflow davfs clitools clean"
PHOTOPRISM_FFMPEG_ENCODER: "nvidia" # Options: "software", "intel", "nvidia", "apple", "raspberry" PHOTOPRISM_FFMPEG_ENCODER: "nvidia" # Options: "software", "intel", "nvidia", "apple", "raspberry"
PHOTOPRISM_STORAGE_PATH: "/photoprism/storage" PHOTOPRISM_STORAGE_PATH: "/photoprism/storage"

View File

@@ -111,7 +111,7 @@ services:
PHOTOPRISM_THUMB_LIBRARY: "auto" # image processing library to be used for generating thumbnails (auto, imaging, vips) 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_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) PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
## Nvidia Video Transcoding (https://docs.photoprism.app/getting-started/advanced/transcoding/#nvidia-container-toolkit): ## Nvidia Video Transcoding (https://docs.photoprism.app/getting-started/advanced/transcoding/#nvidia-container-toolkit):
NVIDIA_VISIBLE_DEVICES: "all" NVIDIA_VISIBLE_DEVICES: "all"
NVIDIA_DRIVER_CAPABILITIES: "all" NVIDIA_DRIVER_CAPABILITIES: "all"

View File

@@ -65,7 +65,7 @@ services:
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD # PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680) PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680)
PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000) PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
## PostgreSQL Database Server ## PostgreSQL Database Server
## Docs: https://www.postgresql.org/docs/ ## Docs: https://www.postgresql.org/docs/

View File

@@ -57,7 +57,7 @@ services:
# PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD # PHOTOPRISM_THUMB_SIZE: 4096 # Retina 4K, DCI 4K (requires more storage); 7680 for 8K Ultra HD
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680) PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # on-demand rendering size limit (default 7680, min 720, max 7680)
PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000) PHOTOPRISM_JPEG_SIZE: 7680 # size limit for converted image files in pixels (720-30000)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
working_dir: "/photoprism" working_dir: "/photoprism"
volumes: volumes:
- "./storage:/photoprism/storage" - "./storage:/photoprism/storage"

View File

@@ -118,7 +118,7 @@ services:
PHOTOPRISM_THUMB_LIBRARY: "auto" # image processing library to be used for generating thumbnails (auto, imaging, vips) 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_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) PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development TF_CPP_MIN_LOG_LEVEL: 1 # show TensorFlow log messages for development
## Video Transcoding (https://docs.photoprism.app/getting-started/advanced/transcoding/): ## Video Transcoding (https://docs.photoprism.app/getting-started/advanced/transcoding/):
# PHOTOPRISM_FFMPEG_ENCODER: "software" # H.264/AVC encoder (software, intel, nvidia, apple, raspberry, or vaapi) # PHOTOPRISM_FFMPEG_ENCODER: "software" # H.264/AVC encoder (software, intel, nvidia, apple, raspberry, or vaapi)
# PHOTOPRISM_FFMPEG_SIZE: "1920" # video size limit in pixels (720-7680) (default: 3840) # PHOTOPRISM_FFMPEG_SIZE: "1920" # video size limit in pixels (720-7680) (default: 3840)

View File

@@ -31,7 +31,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_VERSION=1.15.2 \ TF_VERSION=1.15.2 \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \
GOBIN="/usr/local/bin" \ GOBIN="/usr/local/bin" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \ LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \ LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
NODE_ENV="production" \ NODE_ENV="production" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
NODE_ENV="production" \ NODE_ENV="production" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \ LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \ LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \ LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \ LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -30,7 +30,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \ LD_LIBRARY_PATH="/usr/local/lib:/usr/lib" \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
TMPDIR="/tmp" \ TMPDIR="/tmp" \
TF_CPP_MIN_LOG_LEVEL=0 \ TF_CPP_MIN_LOG_LEVEL=1 \
TF_ENABLE_ONEDNN_OPTS=1 \ TF_ENABLE_ONEDNN_OPTS=1 \
MALLOC_ARENA_MAX=4 \ MALLOC_ARENA_MAX=4 \
GOPATH="/go" \ GOPATH="/go" \

View File

@@ -3795,15 +3795,15 @@
} }
}, },
"node_modules/@pkgr/core": { "node_modules/@pkgr/core": {
"version": "0.2.2", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.2.tgz", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz",
"integrity": "sha512-25L86MyPvnlQoX2MTIV2OiUcb6vJ6aRbFa9pbwByn95INKD5mFH2smgjDhq+fwJoqAgvgbdJLj6Tz7V9X5CFAQ==", "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0" "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
}, },
"funding": { "funding": {
"url": "https://opencollective.com/unts" "url": "https://opencollective.com/pkgr"
} }
}, },
"node_modules/@polka/url": { "node_modules/@polka/url": {
@@ -6342,9 +6342,9 @@
} }
}, },
"node_modules/cssdb": { "node_modules/cssdb": {
"version": "8.2.4", "version": "8.2.5",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.2.4.tgz", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.2.5.tgz",
"integrity": "sha512-3KSCVkjZJe/QxicVXnbyYSY26WsFc1YoMY7jep1ZKWMEVc7jEm6V2Xq2r+MX8WKQIuB7ofGbnr5iVI+aZpoSzg==", "integrity": "sha512-leAt8/hdTCtzql9ZZi86uYAmCLzVKpJMMdjbvOGVnXFXz/BWFpBmM1MHEHU/RqtPyRYmabVmEW1DtX3YGLuuLA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -6995,9 +6995,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.136", "version": "1.5.137",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.136.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
"integrity": "sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==", "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/emmet": { "node_modules/emmet": {
@@ -7747,9 +7747,9 @@
} }
}, },
"node_modules/eslint-webpack-plugin": { "node_modules/eslint-webpack-plugin": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-5.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-5.0.1.tgz",
"integrity": "sha512-iDhXf2r55KO1UhMfpus8oGp93wdNF+934q5kEkwa7qn3BH9f51QEC11xQidt+8jfqRnEYYZa2/8lhac7U/vqWw==", "integrity": "sha512-Ur100Vi+z0uP7j4Z8Ccah0pXmNHhl3f7P2hCYZj3mZCOSc33G5c1R/vZ4KCapwWikPgRyD4dkangx6JW3KaVFQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
@@ -10709,9 +10709,9 @@
} }
}, },
"node_modules/maplibre-gl": { "node_modules/maplibre-gl": {
"version": "5.3.0", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.3.0.tgz", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.3.1.tgz",
"integrity": "sha512-qru6B6jHlDPR4Q9/P4W1zEPbPofR4wwYbrrjiHKWI7yLtyXmpJ1/G1KaIYDr5uNdFbPZ7uiZAWdqtfdNLmIhGg==", "integrity": "sha512-Ihx+oUUSsZkjMou1Cw5J6silE+5OtFFQSPslWF9+7v4yFC/XDHrpsORYO9lWE4KZI0djCEUpZQJpkpnMArAbeA==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@mapbox/geojson-rewind": "^0.5.2", "@mapbox/geojson-rewind": "^0.5.2",
@@ -15270,12 +15270,12 @@
} }
}, },
"node_modules/synckit": { "node_modules/synckit": {
"version": "0.11.3", "version": "0.11.4",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.3.tgz", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz",
"integrity": "sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==", "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@pkgr/core": "^0.2.1", "@pkgr/core": "^0.2.3",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
"engines": { "engines": {

View File

@@ -859,6 +859,10 @@ export default class Config {
} }
getIcon() { getIcon() {
if (this.theme?.variables?.icon) {
return this.theme.variables.icon;
}
switch (this.get("appIcon")) { switch (this.get("appIcon")) {
case "crisp": case "crisp":
case "mint": case "mint":
@@ -869,6 +873,15 @@ export default class Config {
} }
} }
getLoginIcon() {
const loginTheme = themes.Get("login");
if (loginTheme?.variables?.icon) {
return loginTheme?.variables?.icon;
}
return this.getIcon();
}
getVersion() { getVersion() {
return this.version; return this.version;
} }

View File

@@ -5,7 +5,11 @@
<v-col xs="12" class="pa-0 text-subtitle-2 text-selectable text-start hidden-xs"> <v-col xs="12" class="pa-0 text-subtitle-2 text-selectable text-start hidden-xs">
{{ about }} {{ about }}
</v-col> </v-col>
<v-col v-if="legalInfo" xs="12" class="pa-0 text-subtitle-2 text-center text-sm-end"> <v-col v-if="loginInfo" xs="12" class="pa-0 text-subtitle-2 text-center text-sm-end">
<a v-if="legalUrl" :href="legalUrl" target="_blank" class="text-link">{{ loginInfo }}</a>
<span v-else>{{ loginInfo }}</span>
</v-col>
<v-col v-else-if="legalInfo" xs="12" class="pa-0 text-subtitle-2 text-center text-sm-end">
<a v-if="legalUrl" :href="legalUrl" target="_blank" class="text-link">{{ legalInfo }}</a> <a v-if="legalUrl" :href="legalUrl" target="_blank" class="text-link">{{ legalInfo }}</a>
<span v-else>{{ legalInfo }}</span> <span v-else>{{ legalInfo }}</span>
</v-col> </v-col>
@@ -32,6 +36,7 @@ export default {
caption: config.values.siteCaption ? config.values.siteCaption : config.values.siteTitle, caption: config.values.siteCaption ? config.values.siteCaption : config.values.siteTitle,
legalUrl: config.values.legalUrl, legalUrl: config.values.legalUrl,
legalInfo: config.values.legalInfo, legalInfo: config.values.legalInfo,
loginInfo: config.values.loginInfo,
config: config.values, config: config.values,
rtl: this.$isRtl, rtl: this.$isRtl,
}; };

View File

@@ -934,6 +934,15 @@ export const Set = (name, theme) => {
themes[name] = theme; themes[name] = theme;
}; };
// Assign adds or replaces multiple themes at once.
export const Assign = (t) => {
for (const theme of t) {
if (theme?.name && theme?.colors) {
Set(theme.name, theme);
}
}
};
// Remove deletes a theme by name. // Remove deletes a theme by name.
export const Remove = (name) => { export const Remove = (name) => {
delete themes[name]; delete themes[name];

View File

@@ -131,7 +131,7 @@
</v-btn> </v-btn>
<v-btn <v-btn
:disabled="loginDisabled" :disabled="loginDisabled"
:block="$vuetify.display.xs" :block="$vuetify.display.xs || !(registerUri || enterCode)"
tabindex="4" tabindex="4"
color="highlight" color="highlight"
variant="flat" variant="flat"

View File

@@ -15,7 +15,7 @@ import (
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
_ = os.Setenv("TF_CPP_MIN_LOG_LEVEL", "2") _ = os.Setenv("TF_CPP_MIN_LOG_LEVEL", "3")
log = logrus.StandardLogger() log = logrus.StandardLogger()
log.SetLevel(logrus.TraceLevel) log.SetLevel(logrus.TraceLevel)

View File

@@ -66,6 +66,7 @@ type ClientConfig struct {
AuthMode string `json:"authMode"` AuthMode string `json:"authMode"`
UsersPath string `json:"usersPath"` UsersPath string `json:"usersPath"`
LoginUri string `json:"loginUri"` LoginUri string `json:"loginUri"`
LoginInfo string `json:"loginInfo"`
RegisterUri string `json:"registerUri"` RegisterUri string `json:"registerUri"`
PasswordLength int `json:"passwordLength"` PasswordLength int `json:"passwordLength"`
PasswordResetUri string `json:"passwordResetUri"` PasswordResetUri string `json:"passwordResetUri"`
@@ -308,6 +309,7 @@ func (c *Config) ClientPublic() *ClientConfig {
AuthMode: c.AuthMode(), AuthMode: c.AuthMode(),
UsersPath: c.UsersPath(), UsersPath: c.UsersPath(),
LoginUri: c.LoginUri(), LoginUri: c.LoginUri(),
LoginInfo: c.LoginInfo(),
RegisterUri: c.RegisterUri(), RegisterUri: c.RegisterUri(),
PasswordResetUri: c.PasswordResetUri(), PasswordResetUri: c.PasswordResetUri(),
Develop: c.Develop(), Develop: c.Develop(),
@@ -402,6 +404,7 @@ func (c *Config) ClientShare() *ClientConfig {
AuthMode: c.AuthMode(), AuthMode: c.AuthMode(),
UsersPath: "", UsersPath: "",
LoginUri: c.LoginUri(), LoginUri: c.LoginUri(),
LoginInfo: c.LoginInfo(),
RegisterUri: c.RegisterUri(), RegisterUri: c.RegisterUri(),
PasswordResetUri: c.PasswordResetUri(), PasswordResetUri: c.PasswordResetUri(),
Develop: c.Develop(), Develop: c.Develop(),
@@ -502,6 +505,7 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
AuthMode: c.AuthMode(), AuthMode: c.AuthMode(),
UsersPath: c.UsersPath(), UsersPath: c.UsersPath(),
LoginUri: c.LoginUri(), LoginUri: c.LoginUri(),
LoginInfo: c.LoginInfo(),
RegisterUri: c.RegisterUri(), RegisterUri: c.RegisterUri(),
PasswordLength: c.PasswordLength(), PasswordLength: c.PasswordLength(),
PasswordResetUri: c.PasswordResetUri(), PasswordResetUri: c.PasswordResetUri(),

View File

@@ -17,6 +17,7 @@ func TestConfig_ClientConfig(t *testing.T) {
assert.IsType(t, &ClientConfig{}, result) assert.IsType(t, &ClientConfig{}, result)
assert.Equal(t, AuthModePublic, result.AuthMode) assert.Equal(t, AuthModePublic, result.AuthMode)
assert.Equal(t, c.LibraryUri("/"), result.LoginUri) assert.Equal(t, c.LibraryUri("/"), result.LoginUri)
assert.Equal(t, "", result.LoginInfo)
assert.Equal(t, "", result.RegisterUri) assert.Equal(t, "", result.RegisterUri)
assert.Equal(t, 0, result.PasswordLength) assert.Equal(t, 0, result.PasswordLength)
assert.Equal(t, "", result.PasswordResetUri) assert.Equal(t, "", result.PasswordResetUri)

View File

@@ -50,7 +50,6 @@ import (
"github.com/photoprism/photoprism/internal/config/customize" "github.com/photoprism/photoprism/internal/config/customize"
"github.com/photoprism/photoprism/internal/config/ttl" "github.com/photoprism/photoprism/internal/config/ttl"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/service/hub" "github.com/photoprism/photoprism/internal/service/hub"
"github.com/photoprism/photoprism/internal/service/hub/places" "github.com/photoprism/photoprism/internal/service/hub/places"
@@ -62,8 +61,6 @@ import (
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )
// log points to the global logger.
var log = event.Log
var initThumbsMutex sync.Mutex var initThumbsMutex sync.Mutex
// Config holds database, cache and all parameters of photoprism // Config holds database, cache and all parameters of photoprism
@@ -123,13 +120,13 @@ func initLogger() {
}) })
if Env(EnvProd) { if Env(EnvProd) {
log.SetLevel(logrus.WarnLevel) SetLogLevel(logrus.WarnLevel)
} else if Env(EnvTrace) { } else if Env(EnvTrace) {
log.SetLevel(logrus.TraceLevel) SetLogLevel(logrus.TraceLevel)
} else if Env(EnvDebug) { } else if Env(EnvDebug) {
log.SetLevel(logrus.DebugLevel) SetLogLevel(logrus.DebugLevel)
} else { } else {
log.SetLevel(logrus.InfoLevel) SetLogLevel(logrus.InfoLevel)
} }
}) })
} }
@@ -583,9 +580,9 @@ func (c *Config) LogLevel() logrus.Level {
} }
} }
// SetLogLevel sets the Logrus log level. // SetLogLevel sets the application log level.
func (c *Config) SetLogLevel(level logrus.Level) { func (c *Config) SetLogLevel(level logrus.Level) {
log.SetLevel(level) SetLogLevel(level)
} }
// Shutdown shuts down the active processes and closes the database connection. // Shutdown shuts down the active processes and closes the database connection.

View File

@@ -1,6 +1,7 @@
package config package config
import ( import (
"path"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -44,13 +45,15 @@ func (c *Config) AppMode() string {
func (c *Config) AppIcon() string { func (c *Config) AppIcon() string {
defaultIcon := "logo" defaultIcon := "logo"
if c.options.AppIcon == "" || c.options.AppIcon == defaultIcon { if c.options.AppIcon != "" && c.options.AppIcon != defaultIcon {
// Default. if themeIcon := filepath.Join(c.ThemePath(), c.options.AppIcon); fs.FileExistsNotEmpty(themeIcon) {
return path.Join(ThemeUri, c.options.AppIcon)
} else if strings.Contains(c.options.AppIcon, "/") { } else if strings.Contains(c.options.AppIcon, "/") {
return c.options.AppIcon return c.options.AppIcon
} else if fs.FileExists(c.AppIconsPath(c.options.AppIcon, "16.png")) { } else if fs.FileExistsNotEmpty(c.AppIconsPath(c.options.AppIcon, "16.png")) {
return c.options.AppIcon return c.options.AppIcon
} }
}
return defaultIcon return defaultIcon
} }
@@ -88,6 +91,8 @@ func (c *Config) AppConfig() pwa.Config {
StaticUri: c.StaticUri(), StaticUri: c.StaticUri(),
SiteUrl: c.SiteUrl(), SiteUrl: c.SiteUrl(),
CdnUrl: c.CdnUrl("/"), CdnUrl: c.CdnUrl("/"),
ThemeUri: ThemeUri,
ThemePath: c.ThemePath(),
} }
} }

View File

@@ -151,6 +151,11 @@ func (c *Config) LoginUri() string {
return c.options.LoginUri return c.options.LoginUri
} }
// LoginInfo returns the login info text for the page footer.
func (c *Config) LoginInfo() string {
return c.options.LoginInfo
}
// SessionMaxAge returns the standard session expiration time in seconds. // SessionMaxAge returns the standard session expiration time in seconds.
func (c *Config) SessionMaxAge() int64 { func (c *Config) SessionMaxAge() int64 {
if c.options.SessionMaxAge < 0 { if c.options.SessionMaxAge < 0 {

View File

@@ -68,7 +68,7 @@ func TestConfig_AdminPassword(t *testing.T) {
assert.Equal(t, defaultPassword, c.AdminPassword()) assert.Equal(t, defaultPassword, c.AdminPassword())
} }
func TestPasswordLength(t *testing.T) { func TestConfig_PasswordLength(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Equal(t, 8, c.PasswordLength()) assert.Equal(t, 8, c.PasswordLength())
c.options.PasswordLength = 2 c.options.PasswordLength = 2
@@ -90,16 +90,25 @@ func TestPasswordResetUri(t *testing.T) {
assert.Equal(t, "", c.PasswordResetUri()) assert.Equal(t, "", c.PasswordResetUri())
} }
func TestRegisterUri(t *testing.T) { func TestConfig_RegisterUri(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Equal(t, "", c.RegisterUri()) assert.Equal(t, "", c.RegisterUri())
} }
func TestLoginUri(t *testing.T) { func TestConfig_LoginUri(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Equal(t, "/library/login", c.LoginUri()) assert.Equal(t, "/library/login", c.LoginUri())
} }
func TestConfig_LoginInfo(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.LoginInfo())
c.options.LoginInfo = "Foo Bar"
assert.Equal(t, "Foo Bar", c.LoginInfo())
c.options.LoginInfo = ""
assert.Equal(t, "", c.LoginInfo())
}
func TestSessionMaxAge(t *testing.T) { func TestSessionMaxAge(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Equal(t, DefaultSessionMaxAge, c.SessionMaxAge()) assert.Equal(t, DefaultSessionMaxAge, c.SessionMaxAge())

View File

@@ -21,6 +21,9 @@ const StaticUri = "/static"
// CustomStaticUri defines the standard path for serving custom static content. // CustomStaticUri defines the standard path for serving custom static content.
const CustomStaticUri = "/c/static" const CustomStaticUri = "/c/static"
// ThemeUri defines the optional theme URI path for serving theme assets.
const ThemeUri = "/_theme"
// DefaultIndexSchedule defines the default indexing schedule in cron format. // DefaultIndexSchedule defines the default indexing schedule in cron format.
const DefaultIndexSchedule = "" // e.g. "0 */3 * * *" for every 3 hours const DefaultIndexSchedule = "" // e.g. "0 */3 * * *" for every 3 hours
@@ -68,4 +71,5 @@ const (
Pro = "pro" Pro = "pro"
Plus = "plus" Plus = "plus"
Essentials = "essentials" Essentials = "essentials"
Community = "ce"
) )

View File

@@ -3,6 +3,7 @@ package config
import "math/bits" import "math/bits"
var Sponsor = Env(EnvDemo, EnvSponsor, EnvTest) var Sponsor = Env(EnvDemo, EnvSponsor, EnvTest)
var Features = Community
// DisableSettings checks if users should not be allowed to change settings. // DisableSettings checks if users should not be allowed to change settings.
func (c *Config) DisableSettings() bool { func (c *Config) DisableSettings() bool {

View File

@@ -4,12 +4,15 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
) )
const ( const (
@@ -90,13 +93,17 @@ func (c *Config) OIDCProvider() string {
// OIDCIcon returns the OIDC provider icon URI. // OIDCIcon returns the OIDC provider icon URI.
func (c *Config) OIDCIcon() string { func (c *Config) OIDCIcon() string {
if c.options.OIDCIcon == "" { if c.options.OIDCIcon != "" {
return c.StaticAssetUri(OidcDefaultProviderIcon) if themeIcon := filepath.Join(c.ThemePath(), c.options.OIDCIcon); fs.FileExistsNotEmpty(themeIcon) {
return path.Join(ThemeUri, c.options.OIDCIcon)
} }
return c.options.OIDCIcon return c.options.OIDCIcon
} }
return c.StaticAssetUri(OidcDefaultProviderIcon)
}
// OIDCRedirect checks if unauthenticated users should automatically be redirected to the OIDC login page. // OIDCRedirect checks if unauthenticated users should automatically be redirected to the OIDC login page.
func (c *Config) OIDCRedirect() bool { func (c *Config) OIDCRedirect() bool {
return c.options.OIDCRedirect return c.options.OIDCRedirect

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -135,17 +136,36 @@ func (c *Config) SiteDescription() string {
return c.options.SiteDescription return c.options.SiteDescription
} }
// SitePreview returns the site preview image URL for sharing. // SiteFavicon returns the site favicon image name.
func (c *Config) SitePreview() string { func (c *Config) SiteFavicon() string {
if c.options.SitePreview == "" { if c.options.SiteFavicon != "" {
return fmt.Sprintf("https://i.photoprism.app/prism?cover=64&style=centered%%20dark&caption=none&title=%s", url.QueryEscape(c.AppName())) if fs.FileExistsNotEmpty(c.options.SiteFavicon) {
return c.options.SiteFavicon
} else if fileName := filepath.Join(c.ThemePath(), strings.TrimPrefix(c.options.SiteFavicon, ThemeUri)); fs.FileExistsNotEmpty(fileName) {
return fileName
} else if fileName = filepath.Join(c.ImgPath(), c.options.SiteFavicon); fs.FileExistsNotEmpty(fileName) {
return fileName
}
}
return filepath.Join(c.ImgPath(), "favicon.ico")
}
// SitePreview returns the site preview image URL for sharing.
func (c *Config) SitePreview() string {
if c.options.SitePreview != "" {
if strings.HasPrefix(c.options.SitePreview, "http") {
return c.options.SitePreview
} else if fileName := filepath.Join(c.ThemePath(), c.options.SitePreview); fs.FileExistsNotEmpty(fileName) {
return strings.TrimRight(c.options.SiteUrl, "/") + path.Join(ThemeUri, c.options.SitePreview)
} }
if !strings.HasPrefix(c.options.SitePreview, "http") {
return c.SiteUrl() + strings.TrimPrefix(c.options.SitePreview, "/") return c.SiteUrl() + strings.TrimPrefix(c.options.SitePreview, "/")
} }
return c.options.SitePreview return fmt.Sprintf("https://i.photoprism.app/prism?cover=64&style=centered%%20dark&caption=none&title=%s", url.QueryEscape(c.AppName()))
} }
// LegalInfo returns the legal info text for the page footer. // LegalInfo returns the legal info text for the page footer.

View File

@@ -1,9 +1,12 @@
package config package config
import ( import (
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs"
) )
func TestConfig_BaseUri(t *testing.T) { func TestConfig_BaseUri(t *testing.T) {
@@ -115,6 +118,12 @@ func TestConfig_SiteHost(t *testing.T) {
assert.Equal(t, "localhost:2342", c.SiteHost()) assert.Equal(t, "localhost:2342", c.SiteHost())
} }
func TestConfig_SiteFavicon(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "favicon.ico", filepath.Base(c.SiteFavicon()))
assert.True(t, fs.FileExistsNotEmpty(c.SiteFavicon()))
}
func TestConfig_SitePreview(t *testing.T) { func TestConfig_SitePreview(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Equal(t, "https://i.photoprism.app/prism?cover=64&style=centered%20dark&caption=none&title=PhotoPrism", c.SitePreview()) assert.Equal(t, "https://i.photoprism.app/prism?cover=64&style=centered%20dark&caption=none&title=PhotoPrism", c.SitePreview())

View File

@@ -242,6 +242,12 @@ func (c *Config) ConfigPath() string {
} }
return filepath.Join(c.StoragePath(), "config") return filepath.Join(c.StoragePath(), "config")
} else if fs.FileExists(c.options.ConfigPath) {
if c.options.OptionsYaml == "" {
c.options.OptionsYaml = c.options.ConfigPath
}
c.options.ConfigPath = filepath.Dir(c.options.ConfigPath)
} }
return fs.Abs(c.options.ConfigPath) return fs.Abs(c.options.ConfigPath)
@@ -249,7 +255,13 @@ func (c *Config) ConfigPath() string {
// OptionsYaml returns the config options YAML filename. // OptionsYaml returns the config options YAML filename.
func (c *Config) OptionsYaml() string { func (c *Config) OptionsYaml() string {
return filepath.Join(c.ConfigPath(), "options.yml") configPath := c.ConfigPath()
if c.options.OptionsYaml == "" {
return filepath.Join(configPath, "options.yml")
}
return fs.Abs(c.options.OptionsYaml)
} }
// DefaultsYaml returns the default options YAML filename. // DefaultsYaml returns the default options YAML filename.

View File

@@ -4,8 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
tf "github.com/wamuir/graft/tensorflow"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
) )
@@ -45,11 +43,6 @@ func (c *Config) VisionKey() string {
} }
} }
// TensorFlowVersion returns the TenorFlow framework version.
func (c *Config) TensorFlowVersion() string {
return tf.Version()
}
// NasnetModelPath returns the TensorFlow model path. // NasnetModelPath returns the TensorFlow model path.
func (c *Config) NasnetModelPath() string { func (c *Config) NasnetModelPath() string {
return filepath.Join(c.AssetsPath(), "nasnet") return filepath.Join(c.AssetsPath(), "nasnet")

View File

@@ -34,13 +34,6 @@ func TestConfig_VisionKey(t *testing.T) {
assert.Equal(t, "", c.VisionKey()) assert.Equal(t, "", c.VisionKey())
} }
func TestConfig_TensorFlowVersion(t *testing.T) {
c := NewConfig(CliTestContext())
version := c.TensorFlowVersion()
assert.IsType(t, "2.18.0", version)
}
func TestConfig_TensorFlowModelPath(t *testing.T) { func TestConfig_TensorFlowModelPath(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())

View File

@@ -50,6 +50,12 @@ var Flags = CliFlags{
Usage: fmt.Sprintf("initial `PASSWORD` of the superadmin account (%d-%d characters)", entity.PasswordLength, txt.ClipPassword), Usage: fmt.Sprintf("initial `PASSWORD` of the superadmin account (%d-%d characters)", entity.PasswordLength, txt.ClipPassword),
EnvVars: EnvVars("ADMIN_PASSWORD"), EnvVars: EnvVars("ADMIN_PASSWORD"),
}}, { }}, {
Flag: &cli.IntFlag{
Name: "password-length",
Usage: "minimum password `LENGTH` in characters",
Value: 8,
EnvVars: EnvVars("PASSWORD_LENGTH"),
}}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
Name: "oidc-uri", Name: "oidc-uri",
Usage: "issuer `URI` for single sign-on via OpenID Connect, e.g. https://accounts.google.com", Usage: "issuer `URI` for single sign-on via OpenID Connect, e.g. https://accounts.google.com",
@@ -184,14 +190,14 @@ var Flags = CliFlags{
Flag: &cli.PathFlag{ Flag: &cli.PathFlag{
Name: "config-path", Name: "config-path",
Aliases: []string{"c"}, Aliases: []string{"c"},
Usage: "config storage `PATH`, values in options.yml override CLI flags and environment variables if present", Usage: "config storage `PATH` or options.yml filename, values in this file override CLI flags and environment variables if present",
EnvVars: EnvVars("CONFIG_PATH"), EnvVars: EnvVars("CONFIG_PATH"),
TakesFile: true, TakesFile: true,
}}, { }}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
Name: "defaults-yaml", Name: "defaults-yaml",
Aliases: []string{"y"}, Aliases: []string{"y"},
Usage: "load config defaults from `FILE` if exists, does not override CLI flags and environment variables", Usage: "load default config values from `FILENAME` if it exists, does not override CLI flags or environment variables",
Value: "/etc/photoprism/defaults.yml", Value: "/etc/photoprism/defaults.yml",
EnvVars: EnvVars("DEFAULTS_YAML"), EnvVars: EnvVars("DEFAULTS_YAML"),
TakesFile: true, TakesFile: true,
@@ -588,6 +594,12 @@ var Flags = CliFlags{
Usage: "site `DESCRIPTION`*optional*", Usage: "site `DESCRIPTION`*optional*",
EnvVars: EnvVars("SITE_DESCRIPTION"), EnvVars: EnvVars("SITE_DESCRIPTION"),
}}, { }}, {
Flag: &cli.StringFlag{
Name: "site-favicon",
Usage: "site favicon `FILENAME`*optional*",
EnvVars: EnvVars("SITE_FAVICON"),
TakesFile: true,
}}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
Name: "site-preview", Name: "site-preview",
Usage: "sharing preview image `URL`", Usage: "sharing preview image `URL`",
@@ -667,12 +679,12 @@ var Flags = CliFlags{
}}, { }}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
Name: "tls-cert", Name: "tls-cert",
Usage: "public HTTPS certificate `FILE` (.crt), ignored for Unix domain sockets", Usage: "public HTTPS certificate `FILENAME` (.crt), ignored for Unix domain sockets",
EnvVars: EnvVars("TLS_CERT"), EnvVars: EnvVars("TLS_CERT"),
}}, { }}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
Name: "tls-key", Name: "tls-key",
Usage: "private HTTPS key `FILE` (.key), ignored for Unix domain sockets", Usage: "private HTTPS key `FILENAME` (.key), ignored for Unix domain sockets",
EnvVars: EnvVars("TLS_KEY"), EnvVars: EnvVars("TLS_KEY"),
}}, { }}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
@@ -962,7 +974,7 @@ var Flags = CliFlags{
}}, { }}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
Name: "vision-yaml", Name: "vision-yaml",
Usage: "computer vision model configuration `FILE`*optional*", Usage: "computer vision model configuration `FILENAME`*optional*",
Value: "", Value: "",
EnvVars: EnvVars("VISION_YAML"), EnvVars: EnvVars("VISION_YAML"),
TakesFile: true, TakesFile: true,
@@ -1039,13 +1051,13 @@ var Flags = CliFlags{
}}, { }}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
Name: "pid-filename", Name: "pid-filename",
Usage: "process id `FILE`*daemon-mode only*", Usage: "process id `FILENAME`*daemon-mode only*",
EnvVars: EnvVars("PID_FILENAME"), EnvVars: EnvVars("PID_FILENAME"),
TakesFile: true, TakesFile: true,
}}, { }}, {
Flag: &cli.StringFlag{ Flag: &cli.StringFlag{
Name: "log-filename", Name: "log-filename",
Usage: "server log `FILE`*daemon-mode only*", Usage: "server log `FILENAME`*daemon-mode only*",
Value: "", Value: "",
EnvVars: EnvVars("LOG_FILENAME"), EnvVars: EnvVars("LOG_FILENAME"),
TakesFile: true, TakesFile: true,

36
internal/config/logs.go Normal file
View File

@@ -0,0 +1,36 @@
package config
import (
"os"
"github.com/sirupsen/logrus"
"github.com/photoprism/photoprism/internal/event"
)
// log points to the global logger.
var log = event.Log
// SetLogLevel sets the application log level.
func SetLogLevel(level logrus.Level) {
SetTensorFlowLogLevel(level)
log.SetLevel(level)
}
// SetTensorFlowLogLevel sets the TensorFlow log level.
func SetTensorFlowLogLevel(level logrus.Level) {
switch level {
case logrus.TraceLevel:
_ = os.Setenv("TF_CPP_MIN_LOG_LEVEL", "0")
case logrus.DebugLevel:
_ = os.Setenv("TF_CPP_MIN_LOG_LEVEL", "1")
case logrus.InfoLevel:
_ = os.Setenv("TF_CPP_MIN_LOG_LEVEL", "2")
case logrus.WarnLevel:
_ = os.Setenv("TF_CPP_MIN_LOG_LEVEL", "3")
case logrus.ErrorLevel:
_ = os.Setenv("TF_CPP_MIN_LOG_LEVEL", "4")
case logrus.FatalLevel, logrus.PanicLevel:
_ = os.Setenv("TF_CPP_MIN_LOG_LEVEL", "5")
}
}

View File

@@ -30,9 +30,10 @@ type Options struct {
AdminUser string `yaml:"AdminUser" json:"-" flag:"admin-user"` AdminUser string `yaml:"AdminUser" json:"-" flag:"admin-user"`
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"` AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
PasswordLength int `yaml:"PasswordLength" json:"-" flag:"password-length"` PasswordLength int `yaml:"PasswordLength" json:"-" flag:"password-length"`
PasswordResetUri string `yaml:"PasswordResetUri" json:"-" flag:"password-reset-uri"` PasswordResetUri string `yaml:"PasswordResetUri" json:"-" flag:"password-reset-uri" tags:"plus,pro"`
RegisterUri string `yaml:"-" json:"-" flag:"register-uri"` RegisterUri string `yaml:"RegisterUri" json:"-" flag:"register-uri" tags:"pro"`
LoginUri string `yaml:"-" json:"-" flag:"login-uri"` LoginUri string `yaml:"-" json:"-" flag:"login-uri"`
LoginInfo string `yaml:"LoginInfo" json:"-" flag:"login-info" tags:"plus,pro"`
OIDCUri string `yaml:"OIDCUri" json:"-" flag:"oidc-uri"` OIDCUri string `yaml:"OIDCUri" json:"-" flag:"oidc-uri"`
OIDCClient string `yaml:"OIDCClient" json:"-" flag:"oidc-client"` OIDCClient string `yaml:"OIDCClient" json:"-" flag:"oidc-client"`
OIDCSecret string `yaml:"OIDCSecret" json:"-" flag:"oidc-secret"` OIDCSecret string `yaml:"OIDCSecret" json:"-" flag:"oidc-secret"`
@@ -42,8 +43,8 @@ type Options struct {
OIDCRedirect bool `yaml:"OIDCRedirect" json:"OIDCRedirect" flag:"oidc-redirect"` OIDCRedirect bool `yaml:"OIDCRedirect" json:"OIDCRedirect" flag:"oidc-redirect"`
OIDCRegister bool `yaml:"OIDCRegister" json:"OIDCRegister" flag:"oidc-register"` OIDCRegister bool `yaml:"OIDCRegister" json:"OIDCRegister" flag:"oidc-register"`
OIDCUsername string `yaml:"OIDCUsername" json:"-" flag:"oidc-username"` OIDCUsername string `yaml:"OIDCUsername" json:"-" flag:"oidc-username"`
OIDCDomain string `yaml:"-" json:"-" flag:"oidc-domain"` OIDCDomain string `yaml:"-" json:"-" flag:"oidc-domain" tags:"pro"`
OIDCRole string `yaml:"-" json:"-" flag:"oidc-role"` OIDCRole string `yaml:"-" json:"-" flag:"oidc-role" tags:"pro"`
OIDCWebDAV bool `yaml:"OIDCWebDAV" json:"-" flag:"oidc-webdav"` OIDCWebDAV bool `yaml:"OIDCWebDAV" json:"-" flag:"oidc-webdav"`
DisableOIDC bool `yaml:"DisableOIDC" json:"DisableOIDC" flag:"disable-oidc"` DisableOIDC bool `yaml:"DisableOIDC" json:"DisableOIDC" flag:"disable-oidc"`
SessionMaxAge int64 `yaml:"SessionMaxAge" json:"-" flag:"session-maxage"` SessionMaxAge int64 `yaml:"SessionMaxAge" json:"-" flag:"session-maxage"`
@@ -58,6 +59,7 @@ type Options struct {
Demo bool `yaml:"-" json:"-" flag:"demo"` Demo bool `yaml:"-" json:"-" flag:"demo"`
Sponsor bool `yaml:"-" json:"-" flag:"sponsor"` Sponsor bool `yaml:"-" json:"-" flag:"sponsor"`
ConfigPath string `yaml:"ConfigPath" json:"-" flag:"config-path"` ConfigPath string `yaml:"ConfigPath" json:"-" flag:"config-path"`
OptionsYaml string `json:"-" yaml:"-" flag:"-"`
DefaultsYaml string `json:"-" yaml:"-" flag:"defaults-yaml"` DefaultsYaml string `json:"-" yaml:"-" flag:"defaults-yaml"`
OriginalsPath string `yaml:"OriginalsPath" json:"-" flag:"originals-path"` OriginalsPath string `yaml:"OriginalsPath" json:"-" flag:"originals-path"`
OriginalsLimit int `yaml:"OriginalsLimit" json:"OriginalsLimit" flag:"originals-limit"` OriginalsLimit int `yaml:"OriginalsLimit" json:"OriginalsLimit" flag:"originals-limit"`
@@ -74,12 +76,12 @@ type Options struct {
CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"` CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"`
TempPath string `yaml:"TempPath" json:"-" flag:"temp-path"` TempPath string `yaml:"TempPath" json:"-" flag:"temp-path"`
AssetsPath string `yaml:"AssetsPath" json:"-" flag:"assets-path"` AssetsPath string `yaml:"AssetsPath" json:"-" flag:"assets-path"`
CustomAssetsPath string `yaml:"-" json:"-" flag:"custom-assets-path"` CustomAssetsPath string `yaml:"-" json:"-" flag:"custom-assets-path" tags:"plus,pro"`
SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"` SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"`
SidecarYaml bool `yaml:"SidecarYaml" json:"SidecarYaml" flag:"sidecar-yaml" default:"true"` SidecarYaml bool `yaml:"SidecarYaml" json:"SidecarYaml" flag:"sidecar-yaml" default:"true"`
UsageInfo bool `yaml:"UsageInfo" json:"UsageInfo" flag:"usage-info"` UsageInfo bool `yaml:"UsageInfo" json:"UsageInfo" flag:"usage-info"`
FilesQuota uint64 `yaml:"FilesQuota" json:"-" flag:"files-quota"` FilesQuota uint64 `yaml:"FilesQuota" json:"-" flag:"files-quota"`
UsersQuota int `yaml:"UsersQuota" json:"-" flag:"users-quota"` UsersQuota int `yaml:"UsersQuota" json:"-" flag:"users-quota" tags:"pro"`
BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"` BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"`
BackupSchedule string `yaml:"BackupSchedule" json:"BackupSchedule" flag:"backup-schedule"` BackupSchedule string `yaml:"BackupSchedule" json:"BackupSchedule" flag:"backup-schedule"`
BackupRetain int `yaml:"BackupRetain" json:"BackupRetain" flag:"backup-retain"` BackupRetain int `yaml:"BackupRetain" json:"BackupRetain" flag:"backup-retain"`
@@ -128,6 +130,7 @@ type Options struct {
SiteTitle string `yaml:"SiteTitle" json:"SiteTitle" flag:"site-title"` SiteTitle string `yaml:"SiteTitle" json:"SiteTitle" flag:"site-title"`
SiteCaption string `yaml:"SiteCaption" json:"SiteCaption" flag:"site-caption"` SiteCaption string `yaml:"SiteCaption" json:"SiteCaption" flag:"site-caption"`
SiteDescription string `yaml:"SiteDescription" json:"SiteDescription" flag:"site-description"` SiteDescription string `yaml:"SiteDescription" json:"SiteDescription" flag:"site-description"`
SiteFavicon string `yaml:"SiteFavicon" json:"SiteFavicon" flag:"site-favicon"`
SitePreview string `yaml:"SitePreview" json:"SitePreview" flag:"site-preview"` SitePreview string `yaml:"SitePreview" json:"SitePreview" flag:"site-preview"`
CdnUrl string `yaml:"CdnUrl" json:"CdnUrl" flag:"cdn-url"` CdnUrl string `yaml:"CdnUrl" json:"CdnUrl" flag:"cdn-url"`
CdnVideo bool `yaml:"CdnVideo" json:"CdnVideo" flag:"cdn-video"` CdnVideo bool `yaml:"CdnVideo" json:"CdnVideo" flag:"cdn-video"`

View File

@@ -3,6 +3,8 @@ package config
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"slices"
"strings"
) )
// Report returns global config values as a table for reporting. // Report returns global config values as a table for reporting.
@@ -23,6 +25,14 @@ func (c Options) Report() (rows [][]string, cols []string) {
continue continue
} }
// Skip options by feature set if tags are set.
if tags := v.Type().Field(i).Tag.Get("tags"); tags == "" {
// Report.
} else if !slices.Contains(strings.Split(tags, ","), Features) {
// Skip.
continue
}
fieldType := fmt.Sprintf("%T", fieldValue.Interface()) fieldType := fmt.Sprintf("%T", fieldValue.Interface())
rows = append(rows, []string{yamlName, fieldType, "--" + flagName}) rows = append(rows, []string{yamlName, fieldType, "--" + flagName})

View File

@@ -12,4 +12,6 @@ type Config struct {
StaticUri string `json:"staticUri"` StaticUri string `json:"staticUri"`
SiteUrl string `json:"siteUrl"` SiteUrl string `json:"siteUrl"`
CdnUrl string `json:"cdnUrl"` CdnUrl string `json:"cdnUrl"`
ThemeUri string `json:"themeUri"`
ThemePath string `json:"themePath"`
} }

View File

@@ -2,7 +2,12 @@ package pwa
import ( import (
"fmt" "fmt"
"path/filepath"
"strings" "strings"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/media/http/header"
) )
// Icons represents a list of app icons. // Icons represents a list of app icons.
@@ -19,13 +24,51 @@ type Icon struct {
var IconSizes = []int{16, 32, 76, 114, 128, 144, 152, 160, 167, 180, 192, 196, 256, 400, 512} var IconSizes = []int{16, 32, 76, 114, 128, 144, 152, 160, 167, 180, 192, 196, 256, 400, 512}
// NewIcons creates new app icons in the default sizes based on the parameters provided. // NewIcons creates new app icons in the default sizes based on the parameters provided.
func NewIcons(staticUri, appIcon string) Icons { func NewIcons(c Config) Icons {
staticUri := c.StaticUri
appIcon := c.Icon
if appIcon == "" { if appIcon == "" {
appIcon = "logo" appIcon = "logo"
} else if strings.Contains(appIcon, "/") { } else if c.ThemePath != "" && strings.HasPrefix(appIcon, c.ThemeUri) {
var appIconSize string
var appIconType string
if fileName := strings.Replace(appIcon, c.ThemeUri, c.ThemePath, 1); !fs.FileExistsNotEmpty(fileName) {
appIconSize = "32x32"
appIconType = "image/png"
} else {
if info, err := thumb.FileInfo(fileName); err == nil {
appIconSize = fmt.Sprintf("%dx%d", info.Width, info.Height)
}
if mimeType := fs.MimeType(fileName); mimeType != "" {
appIconType = mimeType
}
}
return Icons{{ return Icons{{
Src: appIcon, Src: appIcon,
Type: "image/png", Sizes: appIconSize,
Type: appIconType,
}}
} else if strings.Contains(appIcon, "/") {
var appIconType string
switch fs.FileType(filepath.Base(appIcon)) {
case fs.ImageJpeg:
appIconType = header.ContentTypeJpeg
case fs.ImageWebp:
appIconType = header.ContentTypeWebp
case fs.ImageAvif:
appIconType = header.ContentTypeAvif
default:
appIconType = "image/png"
}
return Icons{{
Src: appIcon,
Type: appIconType,
}} }}
} }

View File

@@ -4,21 +4,33 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs"
) )
func TestNewIcons(t *testing.T) { func TestNewIcons(t *testing.T) {
t.Run("Standard", func(t *testing.T) { t.Run("Standard", func(t *testing.T) {
result := NewIcons("https://demo-cdn.photoprism.app/static", "test") c := Config{StaticUri: "https://demo-cdn.photoprism.app/static", Icon: "test"}
result := NewIcons(c)
assert.NotEmpty(t, result) assert.NotEmpty(t, result)
assert.Equal(t, "https://demo-cdn.photoprism.app/static/icons/test/16.png", result[0].Src) assert.Equal(t, "https://demo-cdn.photoprism.app/static/icons/test/16.png", result[0].Src)
assert.Equal(t, "image/png", result[0].Type) assert.Equal(t, "image/png", result[0].Type)
assert.Equal(t, "16x16", result[0].Sizes) assert.Equal(t, "16x16", result[0].Sizes)
}) })
t.Run("Custom", func(t *testing.T) { t.Run("Custom", func(t *testing.T) {
result := NewIcons("https://demo-cdn.photoprism.app/static", "/test.png") c := Config{StaticUri: "https://demo-cdn.photoprism.app/static", Icon: "/test.png"}
result := NewIcons(c)
assert.NotEmpty(t, result) assert.NotEmpty(t, result)
assert.Equal(t, "/test.png", result[0].Src) assert.Equal(t, "/test.png", result[0].Src)
assert.Equal(t, "image/png", result[0].Type) assert.Equal(t, "image/png", result[0].Type)
assert.Equal(t, "", result[0].Sizes) assert.Equal(t, "", result[0].Sizes)
}) })
t.Run("Theme", func(t *testing.T) {
c := Config{StaticUri: "https://demo-cdn.photoprism.app/static", Icon: "/_theme/example.png", ThemePath: fs.Abs("./testdata"), ThemeUri: "/_theme"}
result := NewIcons(c)
assert.NotEmpty(t, result)
assert.Equal(t, "/_theme/example.png", result[0].Src)
assert.Equal(t, "image/png", result[0].Type)
assert.Equal(t, "100x67", result[0].Sizes)
})
} }

View File

@@ -58,6 +58,6 @@ func NewManifest(c Config) (m *Manifest) {
Permissions: Permissions, Permissions: Permissions,
OptionalPermissions: OptionalPermissions, OptionalPermissions: OptionalPermissions,
HostPermissions: HostPermissions(c.SiteUrl, c.CdnUrl), HostPermissions: HostPermissions(c.SiteUrl, c.CdnUrl),
Icons: NewIcons(c.StaticUri, c.Icon), Icons: NewIcons(c),
} }
} }

BIN
internal/config/pwa/testdata/example.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -28,6 +28,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"password-reset-uri", c.PasswordResetUri()}, {"password-reset-uri", c.PasswordResetUri()},
{"register-uri", c.RegisterUri()}, {"register-uri", c.RegisterUri()},
{"login-uri", c.LoginUri()}, {"login-uri", c.LoginUri()},
{"login-info", c.LoginInfo()},
{"session-maxage", fmt.Sprintf("%d", c.SessionMaxAge())}, {"session-maxage", fmt.Sprintf("%d", c.SessionMaxAge())},
{"session-timeout", fmt.Sprintf("%d", c.SessionTimeout())}, {"session-timeout", fmt.Sprintf("%d", c.SessionTimeout())},
{"session-cache", fmt.Sprintf("%d", c.SessionCache())}, {"session-cache", fmt.Sprintf("%d", c.SessionCache())},
@@ -154,6 +155,7 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"site-title", c.SiteTitle()}, {"site-title", c.SiteTitle()},
{"site-caption", c.SiteCaption()}, {"site-caption", c.SiteCaption()},
{"site-description", c.SiteDescription()}, {"site-description", c.SiteDescription()},
{"site-favicon", c.SiteFavicon()},
{"site-preview", c.SitePreview()}, {"site-preview", c.SitePreview()},
// CDN and Cross-Origin Resource Sharing (CORS). // CDN and Cross-Origin Resource Sharing (CORS).
@@ -246,7 +248,6 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"vision-api", fmt.Sprintf("%t", c.VisionApi())}, {"vision-api", fmt.Sprintf("%t", c.VisionApi())},
{"vision-uri", c.VisionUri()}, {"vision-uri", c.VisionUri()},
{"vision-key", strings.Repeat("*", utf8.RuneCountInString(c.VisionKey()))}, {"vision-key", strings.Repeat("*", utf8.RuneCountInString(c.VisionKey()))},
{"tensorflow-version", c.TensorFlowVersion()},
{"nasnet-model-path", c.NasnetModelPath()}, {"nasnet-model-path", c.NasnetModelPath()},
{"facenet-model-path", c.FaceNetModelPath()}, {"facenet-model-path", c.FaceNetModelPath()},
{"nsfw-model-path", c.NSFWModelPath()}, {"nsfw-model-path", c.NSFWModelPath()},

View File

@@ -2,7 +2,6 @@ package server
import ( import (
"net/http" "net/http"
"path/filepath"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -42,7 +41,7 @@ func registerStaticRoutes(router *gin.Engine, conf *config.Config) {
router.NoRoute(api.AbortNotFound) router.NoRoute(api.AbortNotFound)
// Serves static favicon. // Serves static favicon.
router.StaticFile(conf.BaseUri("/favicon.ico"), filepath.Join(conf.ImgPath(), "favicon.ico")) router.StaticFile(conf.BaseUri("/favicon.ico"), conf.SiteFavicon())
// Serves static assets like js, css and font files. // Serves static assets like js, css and font files.
if dir := conf.StaticPath(); dir != "" { if dir := conf.StaticPath(); dir != "" {

View File

@@ -0,0 +1,56 @@
package thumb
import (
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"os"
"runtime/debug"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/webp"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
)
// FileInfo returns the image header info containing width and height.
func FileInfo(fileName string) (info image.Config, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic %s while decoding %s file info\nstack: %s", r, clean.Log(fileName), debug.Stack())
}
}()
// Resolve symlinks.
if fileName, err = fs.Resolve(fileName); err != nil {
return info, err
}
file, err := os.Open(fileName)
if err != nil || file == nil {
return info, err
}
defer file.Close()
// Reset file offset.
// see https://github.com/golang/go/issues/45902#issuecomment-1007953723
_, err = file.Seek(0, 0)
if err != nil {
return info, fmt.Errorf("%s on seek", err)
}
// Decode image config (dimensions).
info, _, err = image.DecodeConfig(file)
if err != nil {
return info, fmt.Errorf("%s while decoding file info", err)
}
return info, err
}

View File

@@ -0,0 +1,62 @@
package thumb
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs"
)
func TestFileInfo(t *testing.T) {
t.Run("Jpeg", func(t *testing.T) {
fileName := fs.Abs("./testdata/example.jpg")
if fileInfo, err := FileInfo(fileName); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 750, fileInfo.Width)
assert.Equal(t, 500, fileInfo.Height)
}
})
t.Run("BrokenJpeg", func(t *testing.T) {
fileName := fs.Abs("./testdata/broken.jpg")
if fileInfo, err := FileInfo(fileName); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 705, fileInfo.Width)
assert.Equal(t, 725, fileInfo.Height)
}
})
t.Run("Png", func(t *testing.T) {
fileName := fs.Abs("./testdata/example.png")
if fileInfo, err := FileInfo(fileName); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 100, fileInfo.Width)
assert.Equal(t, 67, fileInfo.Height)
}
})
t.Run("Bmp", func(t *testing.T) {
fileName := fs.Abs("./testdata/example.bmp")
if fileInfo, err := FileInfo(fileName); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 100, fileInfo.Width)
assert.Equal(t, 67, fileInfo.Height)
}
})
t.Run("Gif", func(t *testing.T) {
fileName := fs.Abs("./testdata/example.bmp")
if fileInfo, err := FileInfo(fileName); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, 100, fileInfo.Width)
assert.Equal(t, 67, fileInfo.Height)
}
})
}