feat: add Photofield app

This commit is contained in:
okxlin
2026-08-01 04:37:21 +08:00
parent f56a62270e
commit 8eb9435416
16 changed files with 520 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
CONTAINER_NAME=photofield-compose
PANEL_APP_BIND_ADDRESS=127.0.0.1
PANEL_APP_PORT_HTTP=8080
APP_DATA_DIR=./data
PHOTO_LIBRARY_DIR=./photos
TZ=Asia/Shanghai
+83
View File
@@ -0,0 +1,83 @@
additionalProperties:
formFields:
- default: 127.0.0.1
edit: true
envKey: PANEL_APP_BIND_ADDRESS
labelEn: Bind Address
labelZh: 绑定地址
label:
en: Bind Address
zh: 绑定地址
zh-Hant: 綁定位址
ja: バインドアドレス
ko: 바인드 주소
ru: Адрес привязки
ms: Alamat ikatan
pt-br: Endereco de vinculacao
required: true
type: text
- default: 8080
edit: true
envKey: PANEL_APP_PORT_HTTP
labelEn: HTTP Port
labelZh: HTTP 端口
label:
en: HTTP Port
zh: HTTP 端口
zh-Hant: HTTP 連接埠
ja: HTTP ポート
ko: HTTP 포트
ru: HTTP-порт
ms: Port HTTP
pt-br: Porta HTTP
required: true
rule: paramPort
type: number
- default: ./data
edit: true
envKey: APP_DATA_DIR
labelEn: Cache Data Directory
labelZh: 缓存数据目录
label:
en: Cache Data Directory
zh: 缓存数据目录
zh-Hant: 快取資料目錄
ja: キャッシュデータディレクトリ
ko: 캐시 데이터 디렉터리
ru: Каталог данных кэша
ms: Direktori data cache
pt-br: Diretorio de dados do cache
required: true
type: text
- default: ./photos
edit: true
envKey: PHOTO_LIBRARY_DIR
labelEn: Photo Library Directory
labelZh: 照片库目录
label:
en: Photo Library Directory
zh: 照片库目录
zh-Hant: 照片庫目錄
ja: 写真ライブラリディレクトリ
ko: 사진 라이브러리 디렉터리
ru: Каталог фототеки
ms: Direktori pustaka foto
pt-br: Diretorio da biblioteca de fotos
required: true
type: text
- default: Asia/Shanghai
edit: true
envKey: TZ
labelEn: Time Zone
labelZh: 时区
label:
en: Time Zone
zh: 时区
zh-Hant: 時區
ja: タイムゾーン
ko: 시간대
ru: Часовой пояс
ms: Zon waktu
pt-br: Fuso horario
required: true
type: text
+35
View File
@@ -0,0 +1,35 @@
services:
photofield:
image: "ghcr.io/smilyorg/photofield:v0.24.1@sha256:34de0c7b4680ffe3f23c35976c16e2c554f91c41902c37517a4a2c28f4433e79"
container_name: ${CONTAINER_NAME}
restart: unless-stopped
networks:
- 1panel-network
ports:
- "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8080"
environment:
- PHOTOFIELD_ADDRESS=:8080
- PHOTOFIELD_DATA_DIR=/app/data
- TZ=${TZ}
volumes:
- "${APP_DATA_DIR}:/app/data"
- "${PHOTO_LIBRARY_DIR}:/app/photos:ro"
read_only: true
tmpfs:
- /tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8080/ || exit 1"]
interval: 20s
timeout: 5s
retries: 6
start_period: 15s
labels:
createdBy: "Apps"
networks:
1panel-network:
external: true
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
fail() {
printf '%s\n' "$1" >&2
exit 1
}
read_env_value() {
local key="$1"
local value=""
value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)"
case "$value" in
\"*\") value="${value#\"}"; value="${value%\"}" ;;
\'*\') value="${value#\'}"; value="${value%\'}" ;;
esac
printf '%s\n' "$value"
}
configured_value() {
local key="$1"
if [[ -v "$key" ]]; then
printf '%s\n' "${!key}"
else
read_env_value "$key"
fi
}
resolve_safe_path() {
local key="$1"
local value="$2"
local resolved
[[ -n "$value" ]] || fail "$key must not be empty"
case "$value" in
*$'\n'* | *$'\r'* | *"\$"* | *"\`"*) fail "$key contains unsupported characters" ;;
esac
if [[ "$value" = /* ]]; then
resolved="$(realpath -m -- "$value")"
[[ "$resolved" != "/" ]] || fail "$key must not be the filesystem root"
else
resolved="$(realpath -m -- "${ROOT_DIR}/${value#./}")"
case "$resolved" in
"${ROOT_DIR}"/*) ;;
*) fail "$key must remain inside the application version directory when relative" ;;
esac
fi
[[ ! -L "$resolved" ]] || fail "$key must not be a symbolic link"
printf '%s\n' "$resolved"
}
[[ -f "$ENV_FILE" && ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must be a regular file"
port="$(configured_value PANEL_APP_PORT_HTTP)"
[[ "$port" =~ ^[0-9]+$ ]] || fail "PANEL_APP_PORT_HTTP must be an integer"
((10#$port >= 1 && 10#$port <= 65535)) || fail "PANEL_APP_PORT_HTTP must be between 1 and 65535"
bind_address="$(configured_value PANEL_APP_BIND_ADDRESS)"
[[ "$bind_address" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$bind_address" == "::1" ]] || \
fail "PANEL_APP_BIND_ADDRESS must be an IPv4 address or ::1"
timezone="$(configured_value TZ)"
[[ "$timezone" =~ ^[A-Za-z0-9_+./-]+$ && "$timezone" != *..* ]] || fail "TZ contains unsupported characters"
data_dir="$(resolve_safe_path APP_DATA_DIR "$(configured_value APP_DATA_DIR)")"
photo_dir="$(resolve_safe_path PHOTO_LIBRARY_DIR "$(configured_value PHOTO_LIBRARY_DIR)")"
[[ ! -e "$data_dir" || -d "$data_dir" ]] || fail "APP_DATA_DIR must be a directory"
[[ ! -e "$photo_dir" || -d "$photo_dir" ]] || fail "PHOTO_LIBRARY_DIR must be a directory"
install -d -m 0750 -- "$data_dir"
install -d -m 0750 -- "$photo_dir"
config_file="${data_dir}/configuration.yaml"
[[ ! -L "$config_file" ]] || fail "configuration.yaml must not be a symbolic link"
[[ ! -e "$config_file" || -f "$config_file" ]] || fail "configuration.yaml must be a regular file"
if [[ ! -e "$config_file" ]]; then
config_tmp="$(mktemp "${data_dir}/.photofield-config.XXXXXX")"
cat > "$config_tmp" <<'YAML'
collections:
- name: Photos
dirs:
- /app/photos
YAML
chmod 0640 "$config_tmp"
mv -- "$config_tmp" "$config_file"
fi
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Photo and cache data are intentionally left to 1Panel's uninstall options.
exit 0
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
exec "$SCRIPT_DIR/init.sh"
+34
View File
@@ -0,0 +1,34 @@
# Photofield
## 产品介绍
Photofield 是一款注重速度和简洁性的自托管照片图库。它从只读目录索引照片,使用 SQLite 保存缩略图与索引缓存,不会修改原始照片。
## 主要功能
- 可缩放的高密度照片浏览界面
- 快速文件索引、EXIF 元数据读取与多级缩略图缓存
- 支持相册、时间线、瀑布流、地图和高亮等布局
- 支持 JPEG、PNG、HEIF、视频等多种媒体格式
- 原始照片目录以只读方式挂载,缓存与配置独立保存
## 访问说明
应用没有内建账号、认证或授权。安装时默认仅绑定到 `127.0.0.1`,建议通过同机 HTTPS 反向代理访问并在代理层配置认证;只有在可信网络中才应改为 `0.0.0.0`。
照片目录以只读方式挂载到容器。缓存目录包含 `configuration.yaml`、SQLite 索引和缩略图缓存,升级前应备份该目录。卸载时脚本不会主动删除照片或缓存,最终是否移除缓存目录由 1Panel 的卸载选项决定。
媒体解析器处理操作员挂载的文件。当前上游镜像系列曾包含 libjxl 和 TIFF 解码器的高危缺陷;应只挂载可信媒体库、限制网络访问,并及时更新镜像。Photofield 不是面向不可信匿名上传的媒体处理服务。
## Introduction
Photofield is a fast, self-hosted photo gallery focused on simple, non-invasive browsing. It indexes a read-only photo library and keeps generated thumbnails and SQLite cache data in a separate writable data directory.
## Features
- Zoomable, high-density photo browsing
- Fast file indexing, metadata extraction, and thumbnail caching
- Album, timeline, wall, map, and highlight layouts
- Read-only source libraries with separate writable cache data
The application has no built-in authentication or authorization. It binds to `127.0.0.1` by default; publish it through a same-host HTTPS reverse proxy with access control. Mount only trusted media libraries, keep the photo mount read-only, back up the data directory before upgrades, and update promptly when media-decoder fixes are released.
+33
View File
@@ -0,0 +1,33 @@
name: Photofield
tags:
- Media
- Website
title: 高速只读自托管照片图库
description: 高速只读自托管照片图库
additionalProperties:
key: photofield
name: Photofield
tags:
- Media
- Website
shortDescZh: 高速只读自托管照片图库
shortDescEn: Fast read-only self-hosted photo gallery
description:
en: A fast self-hosted photo gallery that indexes read-only photo libraries.
zh: 一款可索引只读照片目录的高速自托管照片图库。
zh-Hant: 一款可索引唯讀照片目錄的高速自架照片圖庫。
ja: 読み取り専用の写真ライブラリを索引化する高速なセルフホスト写真ギャラリーです。
ko: 읽기 전용 사진 라이브러리를 색인하는 빠른 자체 호스팅 사진 갤러리입니다.
ru: Быстрая самостоятельно размещаемая фотогалерея для индексации каталогов только для чтения.
ms: Galeri foto hos kendiri yang pantas untuk mengindeks pustaka foto baca sahaja.
pt-br: Galeria de fotos auto-hospedada e rapida que indexa bibliotecas somente leitura.
type: website
crossVersionUpdate: true
limit: 0
recommend: 0
website: https://photofield.dev
github: https://github.com/SmilyOrg/photofield
document: https://photofield.dev/quick-start
architectures:
- amd64
- arm64
+6
View File
@@ -0,0 +1,6 @@
CONTAINER_NAME=photofield-compose
PANEL_APP_BIND_ADDRESS=127.0.0.1
PANEL_APP_PORT_HTTP=8080
APP_DATA_DIR=./data
PHOTO_LIBRARY_DIR=./photos
TZ=Asia/Shanghai
+83
View File
@@ -0,0 +1,83 @@
additionalProperties:
formFields:
- default: 127.0.0.1
edit: true
envKey: PANEL_APP_BIND_ADDRESS
labelEn: Bind Address
labelZh: 绑定地址
label:
en: Bind Address
zh: 绑定地址
zh-Hant: 綁定位址
ja: バインドアドレス
ko: 바인드 주소
ru: Адрес привязки
ms: Alamat ikatan
pt-br: Endereco de vinculacao
required: true
type: text
- default: 8080
edit: true
envKey: PANEL_APP_PORT_HTTP
labelEn: HTTP Port
labelZh: HTTP 端口
label:
en: HTTP Port
zh: HTTP 端口
zh-Hant: HTTP 連接埠
ja: HTTP ポート
ko: HTTP 포트
ru: HTTP-порт
ms: Port HTTP
pt-br: Porta HTTP
required: true
rule: paramPort
type: number
- default: ./data
edit: true
envKey: APP_DATA_DIR
labelEn: Cache Data Directory
labelZh: 缓存数据目录
label:
en: Cache Data Directory
zh: 缓存数据目录
zh-Hant: 快取資料目錄
ja: キャッシュデータディレクトリ
ko: 캐시 데이터 디렉터리
ru: Каталог данных кэша
ms: Direktori data cache
pt-br: Diretorio de dados do cache
required: true
type: text
- default: ./photos
edit: true
envKey: PHOTO_LIBRARY_DIR
labelEn: Photo Library Directory
labelZh: 照片库目录
label:
en: Photo Library Directory
zh: 照片库目录
zh-Hant: 照片庫目錄
ja: 写真ライブラリディレクトリ
ko: 사진 라이브러리 디렉터리
ru: Каталог фототеки
ms: Direktori pustaka foto
pt-br: Diretorio da biblioteca de fotos
required: true
type: text
- default: Asia/Shanghai
edit: true
envKey: TZ
labelEn: Time Zone
labelZh: 时区
label:
en: Time Zone
zh: 时区
zh-Hant: 時區
ja: タイムゾーン
ko: 시간대
ru: Часовой пояс
ms: Zon waktu
pt-br: Fuso horario
required: true
type: text
+35
View File
@@ -0,0 +1,35 @@
services:
photofield:
image: "ghcr.io/smilyorg/photofield:latest"
container_name: ${CONTAINER_NAME}
restart: unless-stopped
networks:
- 1panel-network
ports:
- "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:8080"
environment:
- PHOTOFIELD_ADDRESS=:8080
- PHOTOFIELD_DATA_DIR=/app/data
- TZ=${TZ}
volumes:
- "${APP_DATA_DIR}:/app/data"
- "${PHOTO_LIBRARY_DIR}:/app/photos:ro"
read_only: true
tmpfs:
- /tmp:rw,nosuid,nodev,noexec,size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8080/ || exit 1"]
interval: 20s
timeout: 5s
retries: 6
start_period: 15s
labels:
createdBy: "Apps"
networks:
1panel-network:
external: true
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
fail() {
printf '%s\n' "$1" >&2
exit 1
}
read_env_value() {
local key="$1"
local value=""
value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)"
case "$value" in
\"*\") value="${value#\"}"; value="${value%\"}" ;;
\'*\') value="${value#\'}"; value="${value%\'}" ;;
esac
printf '%s\n' "$value"
}
configured_value() {
local key="$1"
if [[ -v "$key" ]]; then
printf '%s\n' "${!key}"
else
read_env_value "$key"
fi
}
resolve_safe_path() {
local key="$1"
local value="$2"
local resolved
[[ -n "$value" ]] || fail "$key must not be empty"
case "$value" in
*$'\n'* | *$'\r'* | *"\$"* | *"\`"*) fail "$key contains unsupported characters" ;;
esac
if [[ "$value" = /* ]]; then
resolved="$(realpath -m -- "$value")"
[[ "$resolved" != "/" ]] || fail "$key must not be the filesystem root"
else
resolved="$(realpath -m -- "${ROOT_DIR}/${value#./}")"
case "$resolved" in
"${ROOT_DIR}"/*) ;;
*) fail "$key must remain inside the application version directory when relative" ;;
esac
fi
[[ ! -L "$resolved" ]] || fail "$key must not be a symbolic link"
printf '%s\n' "$resolved"
}
[[ -f "$ENV_FILE" && ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must be a regular file"
port="$(configured_value PANEL_APP_PORT_HTTP)"
[[ "$port" =~ ^[0-9]+$ ]] || fail "PANEL_APP_PORT_HTTP must be an integer"
((10#$port >= 1 && 10#$port <= 65535)) || fail "PANEL_APP_PORT_HTTP must be between 1 and 65535"
bind_address="$(configured_value PANEL_APP_BIND_ADDRESS)"
[[ "$bind_address" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$bind_address" == "::1" ]] || \
fail "PANEL_APP_BIND_ADDRESS must be an IPv4 address or ::1"
timezone="$(configured_value TZ)"
[[ "$timezone" =~ ^[A-Za-z0-9_+./-]+$ && "$timezone" != *..* ]] || fail "TZ contains unsupported characters"
data_dir="$(resolve_safe_path APP_DATA_DIR "$(configured_value APP_DATA_DIR)")"
photo_dir="$(resolve_safe_path PHOTO_LIBRARY_DIR "$(configured_value PHOTO_LIBRARY_DIR)")"
[[ ! -e "$data_dir" || -d "$data_dir" ]] || fail "APP_DATA_DIR must be a directory"
[[ ! -e "$photo_dir" || -d "$photo_dir" ]] || fail "PHOTO_LIBRARY_DIR must be a directory"
install -d -m 0750 -- "$data_dir"
install -d -m 0750 -- "$photo_dir"
config_file="${data_dir}/configuration.yaml"
[[ ! -L "$config_file" ]] || fail "configuration.yaml must not be a symbolic link"
[[ ! -e "$config_file" || -f "$config_file" ]] || fail "configuration.yaml must be a regular file"
if [[ ! -e "$config_file" ]]; then
config_tmp="$(mktemp "${data_dir}/.photofield-config.XXXXXX")"
cat > "$config_tmp" <<'YAML'
collections:
- name: Photos
dirs:
- /app/photos
YAML
chmod 0640 "$config_tmp"
mv -- "$config_tmp" "$config_file"
fi
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
# Photo and cache data are intentionally left to 1Panel's uninstall options.
exit 0
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
exec "$SCRIPT_DIR/init.sh"
+3
View File
@@ -0,0 +1,3 @@
Source: https://github.com/SmilyOrg/photofield/blob/v0.24.1/ui/public/android-chrome-192x192.png
License: MIT
Copyright: Photofield contributors
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB