feat: add native fnOS PostgreSQL shared service

This commit is contained in:
2026-08-02 19:57:24 +08:00
commit 01976310e7
38 changed files with 4659 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
{
".url": {
"nxsir.postgresql.Application": {
"title": "PostgreSQL 共享服务",
"icon": "images/icon_{0}.png",
"type": "url",
"protocol": "",
"port": "15433",
"url": "/",
"allUsers": false
}
}
}
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -eu
ADMIN_PASSWORD="${wizard_postgres_admin_password:-}"
ADMIN_PASSWORD_CONFIRM="${wizard_postgres_admin_password_confirm:-}"
ENROLLMENT_TOKEN="${wizard_postgres_enrollment_token:-}"
ENROLLMENT_TOKEN_CONFIRM="${wizard_postgres_enrollment_token_confirm:-}"
unset wizard_postgres_admin_password wizard_postgres_admin_password_confirm
unset wizard_postgres_enrollment_token wizard_postgres_enrollment_token_confirm
fail() {
printf '%s\n' "$1" >&2
if [ -n "${TRIM_TEMP_LOGFILE:-}" ]; then
printf '%s\n' "$1" >>"$TRIM_TEMP_LOGFILE" 2>/dev/null || true
fi
exit 1
}
[ "$ADMIN_PASSWORD" = "$ADMIN_PASSWORD_CONFIRM" ] || fail "管理密码两次输入不一致。"
[ "${#ADMIN_PASSWORD}" -ge 12 ] || fail "管理密码至少需要 12 个字符。"
[ "${#ADMIN_PASSWORD}" -le 256 ] || fail "管理密码不能超过 256 个字符。"
[ "$ENROLLMENT_TOKEN" = "$ENROLLMENT_TOKEN_CONFIRM" ] || fail "接入令牌两次输入不一致。"
[ "${#ENROLLMENT_TOKEN}" -ge 20 ] || fail "接入令牌至少需要 20 个字符。"
[ "${#ENROLLMENT_TOKEN}" -le 256 ] || fail "接入令牌不能超过 256 个字符。"
case "$ADMIN_PASSWORD$ENROLLMENT_TOKEN" in
*$'\n'*|*$'\r'*) fail "密码和令牌不能包含换行符。" ;;
esac
mkdir -p "${TRIM_PKGVAR}/run" "${TRIM_PKGVAR}/log"
chmod 0700 "${TRIM_PKGVAR}" "${TRIM_PKGVAR}/run" 2>/dev/null || true
umask 077
printf '%s\n' "$ADMIN_PASSWORD" >"${TRIM_PKGVAR}/admin-password.seed"
printf '%s\n' "$ENROLLMENT_TOKEN" >"${TRIM_PKGVAR}/enrollment-token.seed"
unset ADMIN_PASSWORD ADMIN_PASSWORD_CONFIRM ENROLLMENT_TOKEN ENROLLMENT_TOKEN_CONFIRM
exit 0
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0
+175
View File
@@ -0,0 +1,175 @@
#!/bin/bash
set -u
PACKAGE_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
APP_ROOT="${TRIM_APPDEST:-$PACKAGE_ROOT/app}"
DATA_ROOT="${TRIM_PKGVAR:-$PACKAGE_ROOT/var}"
VOLUME_ROOT="${TRIM_APPDEST_VOL:-$DATA_ROOT/volume}"
BACKUP_ROOT="${POSTGRES_SERVICE_BACKUP_ROOT:-$VOLUME_ROOT/@appshare/postgresql/backups}"
RUNTIME_ROOT="$APP_ROOT/runtime"
SERVER="$APP_ROOT/server/PostgresService.WebApi"
PG_BIN="$RUNTIME_ROOT/usr/lib/postgresql/15/bin"
PG_SHARE="$RUNTIME_ROOT/usr/share/postgresql/15"
PG_LIB="$RUNTIME_ROOT/usr/lib/postgresql/15/lib"
PG_DATA="$DATA_ROOT/postgres"
RUN_ROOT="$DATA_ROOT/run"
LOG_ROOT="$DATA_ROOT/log"
APP_PID_FILE="$RUN_ROOT/postgres-service.pid"
APP_LOG="$LOG_ROOT/postgres-service.log"
PG_LOG="$LOG_ROOT/postgresql.log"
PG_PORT="${POSTGRES_SERVICE_PORT:-15432}"
SERVICE_PORT="${TRIM_SERVICE_PORT:-15433}"
RUNTIME_PATH="$PG_BIN:$RUNTIME_ROOT/usr/bin:${PATH:-/usr/local/bin:/usr/bin:/bin}"
RUNTIME_LIBRARY_PATH="$RUNTIME_ROOT/usr/lib/x86_64-linux-gnu:$RUNTIME_ROOT/lib/x86_64-linux-gnu:$PG_LIB"
log_message() {
mkdir -p "$LOG_ROOT"
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$APP_LOG"
}
run_pg() {
env LD_LIBRARY_PATH="$RUNTIME_LIBRARY_PATH" PATH="$RUNTIME_PATH" "$@"
}
app_pid() {
if [ -f "$APP_PID_FILE" ]; then
pid=$(sed -n '1p' "$APP_PID_FILE" | tr -d '[:space:]')
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
printf '%s' "$pid"
return 0
fi
fi
return 1
}
postgres_running() {
[ -x "$PG_BIN/pg_ctl" ] || return 1
[ -f "$PG_DATA/PG_VERSION" ] || return 1
run_pg "$PG_BIN/pg_ctl" -D "$PG_DATA" status >/dev/null 2>&1
}
initialize_postgres() {
[ ! -f "$PG_DATA/PG_VERSION" ] || return 0
mkdir -p "$PG_DATA" "$RUN_ROOT" "$LOG_ROOT"
chmod 0700 "$PG_DATA" "$RUN_ROOT"
run_pg "$PG_BIN/initdb" \
-D "$PG_DATA" \
-L "$PG_SHARE" \
--username=postgres_service \
--auth-local=trust \
--auth-host=scram-sha-256 \
--encoding=UTF8 \
--no-locale >>"$PG_LOG" 2>&1 || return 1
{
printf "listen_addresses = '127.0.0.1'\n"
printf "port = %s\n" "$PG_PORT"
printf "unix_socket_directories = '%s'\n" "$RUN_ROOT"
printf "password_encryption = 'scram-sha-256'\n"
printf "max_connections = 100\n"
printf "shared_buffers = '128MB'\n"
printf "timezone = 'UTC'\n"
printf "log_timezone = 'UTC'\n"
printf "log_min_duration_statement = 5000\n"
} >>"$PG_DATA/postgresql.conf"
{
printf 'local all postgres_service trust\n'
printf 'local all all reject\n'
printf 'host all all 127.0.0.1/32 scram-sha-256\n'
printf 'host all all ::1/128 scram-sha-256\n'
} >"$PG_DATA/pg_hba.conf"
}
start_postgres() {
postgres_running && return 0
initialize_postgres || { log_message "PostgreSQL 初始化失败。"; return 1; }
run_pg "$PG_BIN/pg_ctl" -D "$PG_DATA" -l "$PG_LOG" -w start || {
log_message "PostgreSQL 启动失败。"
return 1
}
}
stop_postgres() {
if postgres_running; then
run_pg "$PG_BIN/pg_ctl" -D "$PG_DATA" -m fast -t 180 -w stop >>"$PG_LOG" 2>&1 || {
log_message "PostgreSQL 未能在 180 秒内安全停止。"
return 1
}
fi
}
start_app() {
mkdir -p "$DATA_ROOT" "$RUN_ROOT" "$LOG_ROOT" "$BACKUP_ROOT" "$DATA_ROOT/tmp" "$DATA_ROOT/cache"
chmod 0700 "$DATA_ROOT" "$RUN_ROOT" "$DATA_ROOT/tmp" "$DATA_ROOT/cache" 2>/dev/null || true
[ -x "$SERVER" ] || { log_message "管理服务不存在或不可执行:$SERVER"; return 1; }
[ -x "$PG_BIN/postgres" ] || { log_message "PostgreSQL 原生运行环境不完整。"; return 1; }
if app_pid >/dev/null; then return 0; fi
start_postgres || return 1
(
export ASPNETCORE_ENVIRONMENT=Production
export ASPNETCORE_URLS="http://0.0.0.0:$SERVICE_PORT"
export POSTGRES_SERVICE_DATA_ROOT="$DATA_ROOT"
export POSTGRES_SERVICE_SOCKET_ROOT="$RUN_ROOT"
export POSTGRES_SERVICE_PORT="$PG_PORT"
export POSTGRES_SERVICE_ADMIN_USER=postgres_service
export POSTGRES_SERVICE_PG_BIN="$PG_BIN"
export POSTGRES_SERVICE_BACKUP_ROOT="$BACKUP_ROOT"
export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
export DOTNET_BUNDLE_EXTRACT_BASE_DIR="$DATA_ROOT/dotnet-bundle"
export XDG_CACHE_HOME="$DATA_ROOT/cache"
export TMPDIR="$DATA_ROOT/tmp"
export LD_LIBRARY_PATH="$RUNTIME_LIBRARY_PATH"
export PATH="$RUNTIME_PATH"
mkdir -p "$DOTNET_BUNDLE_EXTRACT_BASE_DIR"
cd "$APP_ROOT/server" || exit 1
exec "$SERVER"
) >>"$APP_LOG" 2>&1 &
pid=$!
printf '%s\n' "$pid" >"$APP_PID_FILE"
attempt=0
while [ "$attempt" -lt 90 ]; do
if ! kill -0 "$pid" 2>/dev/null; then
log_message "管理服务进程提前退出。"
stop_postgres
return 1
fi
if (exec 3<>"/dev/tcp/127.0.0.1/$SERVICE_PORT") 2>/dev/null; then
exec 3>&-
log_message "PostgreSQL 共享服务启动成功,管理端口 $SERVICE_PORT,数据库端口 $PG_PORT。"
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
log_message "管理服务未能在 90 秒内就绪。"
stop_app
return 1
}
stop_app() {
if pid=$(app_pid); then
kill "$pid" 2>/dev/null || true
attempt=0
while kill -0 "$pid" 2>/dev/null && [ "$attempt" -lt 30 ]; do
sleep 1
attempt=$((attempt + 1))
done
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$APP_PID_FILE"
stop_postgres
}
case "${1:-status}" in
start) start_app ;;
stop) stop_app ;;
restart) stop_app && start_app ;;
status)
if app_pid >/dev/null && postgres_running; then exit 0; fi
exit 3
;;
*) printf 'usage: %s {start|stop|restart|status}\n' "$0" >&2; exit 2 ;;
esac
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# 数据目录与手动备份默认保留,避免误删多个应用的共享数据。
exit 0
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -eu
"$(dirname -- "$0")/main" stop || true
exit 0
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -eu
# 当前服务固定在 PostgreSQL 15 主版本;数据目录与管理凭据位于
# TRIM_PKGVARfnOS 替换不可变应用文件时无需复制。
exit 0
+5
View File
@@ -0,0 +1,5 @@
{
"defaults": {
"run-as": "package"
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"data-share": {
"shares": [
{
"name": "postgresql",
"permission": {
"rw": ["nxsir.postgresql"]
}
},
{
"name": "postgresql/backups",
"permission": {
"rw": ["nxsir.postgresql"]
}
}
]
}
}
+12
View File
@@ -0,0 +1,12 @@
appname=nxsir.postgresql
version=15.1.0
display_name=PostgreSQL 共享服务
desc=面向 fnOS 应用的原生 PostgreSQL 15 共享数据库服务,包含 pgvector、安全凭据签发、管理面板和手动备份恢复
platform=x86
source=thirdparty
maintainer=Live Recorder Contributors
os_min_version=1.2.0
desktop_uidir=ui
desktop_applaunchname=nxsir.postgresql.Application
checkport=true
ctl_stop=true
+47
View File
@@ -0,0 +1,47 @@
[
{
"stepTitle": "设置 PostgreSQL 服务管理凭据",
"items": [
{
"type": "tips",
"helpText": "管理密码用于登录数据库面板;接入令牌用于其他 fnOS 应用首次申请独立数据库。两者均不会设置默认值。"
},
{
"type": "password",
"field": "wizard_postgres_admin_password",
"label": "管理面板密码",
"rules": [
{ "required": true, "message": "请输入管理面板密码" },
{ "min": 12, "message": "管理密码至少需要 12 个字符" },
{ "max": 256, "message": "管理密码不能超过 256 个字符" }
]
},
{
"type": "password",
"field": "wizard_postgres_admin_password_confirm",
"label": "再次输入管理密码",
"rules": [
{ "required": true, "message": "请再次输入管理密码" }
]
},
{
"type": "password",
"field": "wizard_postgres_enrollment_token",
"label": "应用接入令牌",
"rules": [
{ "required": true, "message": "请输入应用接入令牌" },
{ "min": 20, "message": "接入令牌至少需要 20 个字符" },
{ "max": 256, "message": "接入令牌不能超过 256 个字符" }
]
},
{
"type": "password",
"field": "wizard_postgres_enrollment_token_confirm",
"label": "再次输入接入令牌",
"rules": [
{ "required": true, "message": "请再次输入接入令牌" }
]
}
]
}
]
+11
View File
@@ -0,0 +1,11 @@
[
{
"stepTitle": "升级 PostgreSQL 共享服务",
"items": [
{
"type": "tips",
"helpText": "升级期间所有依赖应用会暂时失去数据库连接。当前版本仅执行 PostgreSQL 15 同主版本升级,并保留数据、凭据、审计记录和备份。"
}
]
}
]