chore(appstore): add Kaneo Postgres digest version

This commit is contained in:
okxlin
2026-10-19 10:26:50 +08:00
parent 289d5d0691
commit f144bfe83d
6 changed files with 594 additions and 0 deletions
@@ -0,0 +1,10 @@
CONTAINER_NAME=kaneo
PANEL_APP_BIND_ADDRESS=127.0.0.1
PANEL_APP_PORT_HTTP=5173
KANEO_CLIENT_URL=http://localhost:5173
DISABLE_REGISTRATION=false
POSTGRES_DB=kaneo
POSTGRES_USER=kaneo
POSTGRES_PASSWORD=generate
AUTH_SECRET=generate
APP_DATA_DIR=./data
@@ -0,0 +1,156 @@
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: 5173
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: http://localhost:5173
edit: true
envKey: KANEO_CLIENT_URL
labelEn: Public URL
labelZh: 公开访问地址
label:
en: Public URL
zh: 公开访问地址
zh-Hant: 公開存取網址
ja: 公開 URL
ko: 공개 URL
ru: Публичный URL
ms: URL awam
pt-br: URL publica
required: true
type: text
- default: "false"
edit: true
envKey: DISABLE_REGISTRATION
labelEn: Disable Registration
labelZh: 禁止新用户注册
label:
en: Disable Registration
zh: 禁止新用户注册
zh-Hant: 停用新使用者註冊
ja: 新規登録を無効化
ko: 신규 등록 비활성화
ru: Отключить регистрацию
ms: Nyahdayakan pendaftaran
pt-br: Desativar cadastro
required: true
type: select
values:
- label: "false"
value: "false"
- label: "true"
value: "true"
- default: kaneo
edit: true
envKey: POSTGRES_DB
labelEn: Database Name
labelZh: 数据库名称
label:
en: Database Name
zh: 数据库名称
zh-Hant: 資料庫名稱
ja: データベース名
ko: 데이터베이스 이름
ru: Имя базы данных
ms: Nama pangkalan data
pt-br: Nome do banco de dados
random: true
required: true
rule: paramCommon
type: text
- default: kaneo
edit: true
envKey: POSTGRES_USER
labelEn: Database User
labelZh: 数据库用户
label:
en: Database User
zh: 数据库用户
zh-Hant: 資料庫使用者
ja: データベースユーザー
ko: 데이터베이스 사용자
ru: Пользователь базы данных
ms: Pengguna pangkalan data
pt-br: Usuario do banco de dados
random: true
required: true
rule: paramCommon
type: text
- default: generate
edit: true
envKey: POSTGRES_PASSWORD
labelEn: Database Password
labelZh: 数据库密码
label:
en: Database Password
zh: 数据库密码
zh-Hant: 資料庫密碼
ja: データベースパスワード
ko: 데이터베이스 비밀번호
ru: Пароль базы данных
ms: Kata laluan pangkalan data
pt-br: Senha do banco de dados
required: true
type: password
- default: generate
edit: true
envKey: AUTH_SECRET
labelEn: Authentication Secret
labelZh: 认证密钥
label:
en: Authentication Secret
zh: 认证密钥
zh-Hant: 驗證密鑰
ja: 認証シークレット
ko: 인증 비밀 키
ru: Секрет аутентификации
ms: Rahsia pengesahan
pt-br: Segredo de autenticacao
required: true
type: password
- default: ./data
edit: true
envKey: APP_DATA_DIR
labelEn: Data Directory
labelZh: 数据目录
label:
en: Data Directory
zh: 数据目录
zh-Hant: 資料目錄
ja: データディレクトリ
ko: 데이터 디렉터리
ru: Каталог данных
ms: Direktori data
pt-br: Diretorio de dados
required: true
type: text
@@ -0,0 +1,73 @@
services:
kaneo-postgres:
image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
container_name: ${CONTAINER_NAME}-postgres
restart: unless-stopped
user: "70:70"
networks:
- kaneo-internal
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- "${APP_DATA_DIR}/postgres:/var/lib/postgresql/data"
read_only: true
tmpfs:
- /tmp:mode=1777,size=64m
- /var/run/postgresql:uid=70,gid=70,mode=0755,size=16m
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
labels:
createdBy: "Apps"
kaneo:
image: ghcr.io/usekaneo/kaneo:2.26.0@sha256:549af9f6faf74825171badeda63db124461125bf9151425519b580513d4e4f5b
container_name: ${CONTAINER_NAME}
restart: unless-stopped
user: "1001:1001"
depends_on:
kaneo-postgres:
condition: service_healthy
networks:
- kaneo-internal
- 1panel-network
ports:
- "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:5173"
environment:
KANEO_CLIENT_URL: ${KANEO_CLIENT_URL}
POSTGRES_HOST: kaneo-postgres
POSTGRES_PORT: "5432"
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
AUTH_SECRET: ${AUTH_SECRET}
DISABLE_REGISTRATION: ${DISABLE_REGISTRATION}
tmpfs:
- /tmp:mode=1777,size=64m
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5173/api/health"]
interval: 15s
timeout: 5s
retries: 8
start_period: 60s
labels:
createdBy: "Apps"
networks:
kaneo-internal:
internal: true
1panel-network:
external: true
+173
View File
@@ -0,0 +1,173 @@
#!/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"
local default_value="$2"
local value
value="${!key:-}"
if [[ -z "$value" ]]; then
value="$(read_env_value "$key")"
fi
printf '%s\n' "${value:-$default_value}"
}
resolve_app_path() {
local key="$1"
local raw="$2"
local clean candidate resolved current part
local -a parts=()
case "$raw" in
""|/*|.|..|../*|*/../*|*/..) echo "unsafe ${key} path" >&2; return 1 ;;
esac
if [[ "$raw" =~ [[:cntrl:]] ]]; then
echo "unsafe ${key} path" >&2
return 1
fi
clean="${raw#./}"
[[ -n "$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; }
command -v realpath >/dev/null 2>&1 || { echo "realpath is required" >&2; return 1; }
candidate="$ROOT_DIR/$clean"
resolved="$(realpath -m -- "$candidate")" || { echo "unsafe ${key} path" >&2; return 1; }
case "$resolved" in
"$ROOT_DIR"/*) ;;
*) echo "unsafe ${key} path" >&2; return 1 ;;
esac
current="$ROOT_DIR"
IFS='/' read -r -a parts <<< "$clean"
for part in "${parts[@]}"; do
[[ -z "$part" || "$part" == "." ]] && continue
current="$current/$part"
if [[ -L "$current" ]]; then
echo "unsafe ${key} path" >&2
return 1
fi
done
printf '%s\n' "$resolved"
}
set_env_value() {
local key="$1"
local value
local temp_file
case "$key" in
POSTGRES_PASSWORD|AUTH_SECRET) value="${!key}" ;;
*) fail "unsupported environment key: $key" ;;
esac
temp_file="$(mktemp "${ROOT_DIR}/.kaneo-env.tmp.XXXXXX")"
awk -v key="$key" -v value="$value" '
BEGIN { written = 0 }
$0 ~ "^" key "=" {
if (!written) {
print key "=" value
written = 1
}
next
}
{ print }
END { if (!written) print key "=" value }
' "$ENV_FILE" > "$temp_file"
chmod 600 "$temp_file"
mv -f -- "$temp_file" "$ENV_FILE"
}
validate_ipv4() {
local key="$1"
local value="$2"
local octet
local -a octets
[[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "$key must be an IPv4 address"
IFS=. read -r -a octets <<< "$value"
for octet in "${octets[@]}"; do
((10#$octet <= 255)) || fail "$key contains an invalid IPv4 octet"
done
}
validate_port() {
local key="$1"
local value="$2"
[[ "$value" =~ ^[0-9]+$ ]] || fail "$key must be an integer"
((10#$value >= 1 && 10#$value <= 65535)) || fail "$key must be between 1 and 65535"
}
generate_alphanumeric() {
local length="$1"
local material
material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)"
[[ ${#material} -ge $length ]] || fail "unable to generate sufficient random material"
printf '%s\n' "${material:0:length}"
}
[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found"
[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link"
[[ "$(id -u)" -eq 0 ]] || fail "Kaneo init must run as root"
command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials"
validate_ipv4 PANEL_APP_BIND_ADDRESS "$(read_env_value PANEL_APP_BIND_ADDRESS)"
validate_port PANEL_APP_PORT_HTTP "$(read_env_value PANEL_APP_PORT_HTTP)"
client_url="$(read_env_value KANEO_CLIENT_URL)"
[[ "$client_url" =~ ^https?://([A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?|localhost)(:([0-9]{1,5}))?$ ]] ||
fail "KANEO_CLIENT_URL must be an HTTP(S) origin without a path"
[[ "$client_url" != *..* ]] || fail "KANEO_CLIENT_URL contains an invalid hostname"
if [[ -n "${BASH_REMATCH[4]:-}" ]]; then
validate_port KANEO_CLIENT_URL_PORT "${BASH_REMATCH[4]}"
fi
disable_registration="$(read_env_value DISABLE_REGISTRATION)"
[[ "$disable_registration" == true || "$disable_registration" == false ]] ||
fail "DISABLE_REGISTRATION must be true or false"
postgres_db="$(read_env_value POSTGRES_DB)"
postgres_user="$(read_env_value POSTGRES_USER)"
[[ "$postgres_db" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_DB is invalid"
[[ "$postgres_user" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_USER is invalid"
POSTGRES_PASSWORD="$(read_env_value POSTGRES_PASSWORD)"
if [[ -z "$POSTGRES_PASSWORD" || "$POSTGRES_PASSWORD" == generate ]]; then
POSTGRES_PASSWORD="$(generate_alphanumeric 48)"
fi
[[ "$POSTGRES_PASSWORD" =~ ^[A-Za-z0-9]{32,128}$ ]] ||
fail "POSTGRES_PASSWORD must contain 32 to 128 alphanumeric characters"
AUTH_SECRET="$(read_env_value AUTH_SECRET)"
if [[ -z "$AUTH_SECRET" || "$AUTH_SECRET" == generate ]]; then
AUTH_SECRET="$(openssl rand -hex 32)"
fi
[[ "$AUTH_SECRET" =~ ^[A-Fa-f0-9]{64,128}$ ]] ||
fail "AUTH_SECRET must contain 64 to 128 hexadecimal characters"
data_raw="$(configured_value APP_DATA_DIR ./data)"
data_dir="$(resolve_app_path APP_DATA_DIR "$data_raw")"
install -d -m 0700 -- "$data_dir/postgres"
resolved_data="$(realpath -e -- "$data_dir")"
case "$resolved_data" in
"$ROOT_DIR"/*) ;;
*) fail "APP_DATA_DIR resolves outside the application version directory" ;;
esac
chown -R 70:70 -- "$resolved_data/postgres"
chmod 0700 -- "$resolved_data/postgres"
set_env_value POSTGRES_PASSWORD
set_env_value AUTH_SECRET
chmod 600 "$ENV_FILE"
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
if command -v docker-compose >/dev/null 2>&1; then
docker-compose down --remove-orphans
else
docker compose down --remove-orphans
fi
@@ -0,0 +1,174 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)"
ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
read_env_value() {
local file="$1"
local key="$2"
local value
value="$(sed -n "s/^${key}=//p" "$file" | tail -n 1)"
case "$value" in
\"*\") value="${value#\"}"; value="${value%\"}" ;;
\'*\') value="${value#\'}"; value="${value%\'}" ;;
esac
printf '%s\n' "$value"
}
resolve_app_path() {
local key="$1"
local raw="$2"
local clean candidate resolved current part
local -a parts=()
case "$raw" in
""|/*|.|..|../*|*/../*|*/..) echo "unsafe ${key} path" >&2; return 1 ;;
esac
if [[ "$raw" =~ [[:cntrl:]] ]]; then
echo "unsafe ${key} path" >&2
return 1
fi
clean="${raw#./}"
[[ -n "$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; }
command -v realpath >/dev/null 2>&1 || { echo "realpath is required" >&2; return 1; }
candidate="$ROOT_DIR/$clean"
resolved="$(realpath -m -- "$candidate")" || { echo "unsafe ${key} path" >&2; return 1; }
case "$resolved" in
"$ROOT_DIR"/*) ;;
*) echo "unsafe ${key} path" >&2; return 1 ;;
esac
current="$ROOT_DIR"
IFS='/' read -r -a parts <<< "$clean"
for part in "${parts[@]}"; do
[[ -z "$part" || "$part" == "." ]] && continue
current="$current/$part"
if [[ -L "$current" ]]; then
echo "unsafe ${key} path" >&2
return 1
fi
done
printf '%s\n' "$resolved"
}
set_env_value() {
local key="$1"
local value="$2"
local temp_file
temp_file="$(mktemp "${ENV_FILE}.kaneo-upgrade.XXXXXX")"
awk -v key="$key" -v value="$value" '
BEGIN { written = 0 }
$0 ~ "^" key "=" {
if (!written) {
print key "=" value
written = 1
}
next
}
{ print }
END { if (!written) print key "=" value }
' "$ENV_FILE" > "$temp_file"
chmod 600 "$temp_file"
mv -f -- "$temp_file" "$ENV_FILE"
}
restore_credentials_from_env() {
local candidate="$1"
local current_container current_db current_user current_password current_auth
local candidate_container candidate_db candidate_user
local candidate_password candidate_auth
[[ -f "$candidate" && ! -L "$candidate" ]] || return 1
current_password="$(read_env_value "$ENV_FILE" POSTGRES_PASSWORD)"
current_auth="$(read_env_value "$ENV_FILE" AUTH_SECRET)"
current_container="$(read_env_value "$ENV_FILE" CONTAINER_NAME)"
current_db="$(read_env_value "$ENV_FILE" POSTGRES_DB)"
current_user="$(read_env_value "$ENV_FILE" POSTGRES_USER)"
candidate_container="$(read_env_value "$candidate" CONTAINER_NAME)"
candidate_db="$(read_env_value "$candidate" POSTGRES_DB)"
candidate_user="$(read_env_value "$candidate" POSTGRES_USER)"
[[ "$candidate_container" == "$current_container" &&
"$candidate_db" == "$current_db" &&
"$candidate_user" == "$current_user" ]] || return 1
candidate_password="$(read_env_value "$candidate" POSTGRES_PASSWORD)"
candidate_auth="$(read_env_value "$candidate" AUTH_SECRET)"
[[ "$candidate_password" =~ ^[A-Za-z0-9]{32,128}$ ]] || return 1
[[ "$candidate_auth" =~ ^[A-Fa-f0-9]{64,128}$ ]] || return 1
if [[ "$current_password" == "" || "$current_password" == generate ]]; then
set_env_value POSTGRES_PASSWORD "$candidate_password"
fi
if [[ "$current_auth" == "" || "$current_auth" == generate ]]; then
set_env_value AUTH_SECRET "$candidate_auth"
fi
return 0
}
restore_credentials_from_backup() (
local panel_root app_key install_name backup_dir archive archive_entry
local temp_dir inner_archive env_entry latest_mtime archive_mtime
panel_root="${ROOT_DIR%%/apps/*}"
[[ "$panel_root" != "$ROOT_DIR" ]] || exit 1
app_key="$(basename "$(dirname "$ROOT_DIR")")"
install_name="$(basename "$ROOT_DIR")"
[[ "$app_key" =~ ^[A-Za-z0-9._-]+$ && "$install_name" =~ ^[A-Za-z0-9._-]+$ ]] || exit 1
backup_dir="$panel_root/backup/app/local${app_key}/${install_name}"
[[ -d "$backup_dir" && ! -L "$backup_dir" ]] || exit 1
archive=""
latest_mtime=0
while IFS= read -r -d '' candidate; do
archive_mtime="$(stat -c '%Y' "$candidate")"
if [[ -z "$archive" ]] || (( archive_mtime > latest_mtime )); then
archive="$candidate"
latest_mtime="$archive_mtime"
fi
done < <(find "$backup_dir" -maxdepth 1 -type f -name 'upgrade_backup_*.tar.gz' -print0)
[[ -n "$archive" ]] || exit 1
temp_dir="$(mktemp -d "$ROOT_DIR/.kaneo-upgrade-backup.XXXXXX")"
trap 'rm -rf -- "$temp_dir"' EXIT
inner_archive="$temp_dir/app.tar.gz"
archive_entry="$(tar -tzf "$archive" | awk '$0 ~ /\/app\.tar\.gz$/ { print; exit }')"
[[ -n "$archive_entry" ]] || exit 1
tar -xOf "$archive" "$archive_entry" > "$inner_archive"
env_entry="$install_name/.env"
tar -tzf "$inner_archive" | grep -Fqx "$env_entry" || exit 1
tar -xOf "$inner_archive" "$env_entry" > "$temp_dir/old.env"
restore_credentials_from_env "$temp_dir/old.env"
)
restore_previous_credentials() {
local current_password current_auth candidate
local -a snapshots
current_password="$(read_env_value "$ENV_FILE" POSTGRES_PASSWORD)"
current_auth="$(read_env_value "$ENV_FILE" AUTH_SECRET)"
if [[ "$current_password" != "" && "$current_password" != generate &&
"$current_auth" != "" && "$current_auth" != generate ]]; then
return 0
fi
shopt -s nullglob
snapshots=(/tmp/1panel-app-upgrade-*/.env)
shopt -u nullglob
for candidate in "${snapshots[@]}"; do
restore_credentials_from_env "$candidate" && return 0
done
restore_credentials_from_backup
}
[[ -f "$ENV_FILE" ]] || {
printf '%s\n' "$ENV_FILE not found" >&2
exit 1
}
if ! restore_previous_credentials; then
data_raw="$(read_env_value "$ENV_FILE" APP_DATA_DIR)"
data_dir="$(resolve_app_path APP_DATA_DIR "$data_raw")"
if [[ -f "$data_dir/postgres/PG_VERSION" ]]; then
printf '%s\n' 'Kaneo upgrade could not recover existing database credentials' >&2
exit 1
fi
fi
exec "$SCRIPT_DIR/init.sh"