mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-18 22:54:52 +08:00
Config: Refactor command flags, reports, and client options
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -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):
|
||||||
|
@@ -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)
|
||||||
|
@@ -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"
|
||||||
|
@@ -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"
|
||||||
|
@@ -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"
|
||||||
|
@@ -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/
|
||||||
|
@@ -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"
|
||||||
|
@@ -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)
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
@@ -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" \
|
||||||
|
40
frontend/package-lock.json
generated
40
frontend/package-lock.json
generated
@@ -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": {
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
@@ -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];
|
||||||
|
@@ -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"
|
||||||
|
@@ -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)
|
||||||
|
@@ -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(),
|
||||||
|
@@ -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)
|
||||||
|
@@ -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.
|
||||||
|
@@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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())
|
||||||
|
@@ -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"
|
||||||
)
|
)
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
@@ -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())
|
||||||
|
@@ -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.
|
||||||
|
@@ -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")
|
||||||
|
@@ -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())
|
||||||
|
|
||||||
|
@@ -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
36
internal/config/logs.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
@@ -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"`
|
||||||
|
@@ -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})
|
||||||
|
@@ -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"`
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@@ -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
BIN
internal/config/pwa/testdata/example.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@@ -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()},
|
||||||
|
@@ -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 != "" {
|
||||||
|
56
internal/thumb/fileinfo.go
Normal file
56
internal/thumb/fileinfo.go
Normal 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
|
||||||
|
}
|
62
internal/thumb/fileinfo_test.go
Normal file
62
internal/thumb/fileinfo_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user