157 lines
7.7 KiB
Bash
Executable File
157 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
# Production entry point for first-admin setup. The installer keeps the
|
|
# EnvironmentFile root-readable only, so parse simple KEY=VALUE assignments
|
|
# without sourcing arbitrary shell code.
|
|
PATH=/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin
|
|
export PATH
|
|
umask 077
|
|
|
|
CONFIG_DIR=${TALLYNOTE_CONFIG_DIR:-/etc/tallynote}
|
|
CONFIG_FILE="$CONFIG_DIR/tallynote.env"
|
|
PREFIX=${TALLYNOTE_INSTALL_PREFIX:-/opt/tallynote}
|
|
SERVICE_NAME=${TALLYNOTE_SERVICE_NAME:-tallynote.service}
|
|
|
|
die() { printf 'tallynote admin-init: %s\n' "$*" >&2; exit 1; }
|
|
|
|
node_is_usable() {
|
|
local candidate=$1 major
|
|
[[ -n "$candidate" && -x "$candidate" ]] || return 1
|
|
major=$("$candidate" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)
|
|
[[ "$major" =~ ^[0-9]+$ && "$major" -ge 24 ]]
|
|
}
|
|
|
|
load_environment_file() {
|
|
[[ -e "$CONFIG_FILE" ]] || return 0
|
|
[[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] || die '环境文件不是安全的普通文件'
|
|
local uid mode_bits line key value
|
|
uid=$(stat -c '%u' "$CONFIG_FILE" 2>/dev/null || stat -f '%u' "$CONFIG_FILE")
|
|
[[ "$uid" == 0 ]] || die '环境文件必须由 root 拥有'
|
|
mode_bits=$(stat -c '%a' "$CONFIG_FILE" 2>/dev/null || stat -f '%Lp' "$CONFIG_FILE")
|
|
[[ "$mode_bits" =~ ^[0-7]+$ ]] || die '无法读取环境文件权限'
|
|
(( (8#$mode_bits & 18) == 0 )) || die '环境文件不能被组或其他用户写入'
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
[[ -z "$line" || "$line" == \#* ]] && continue
|
|
[[ "$line" =~ ^([A-Z][A-Z0-9_]*)=(.*)$ ]] || die '环境文件包含无法识别的配置行'
|
|
key=${BASH_REMATCH[1]}
|
|
value=${BASH_REMATCH[2]}
|
|
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "环境文件中的 $key 包含控制字符"
|
|
export "$key=$value"
|
|
done < "$CONFIG_FILE"
|
|
PREFIX=${TALLYNOTE_INSTALL_PREFIX:-$PREFIX}
|
|
}
|
|
|
|
resolve_release_root() {
|
|
local root prefix_root
|
|
[[ "$PREFIX" = /* && "$PREFIX" != *$'\n'* && "$PREFIX" != *$'\r'* ]] || die '安装目录无效'
|
|
[[ -L "$PREFIX/current" ]] || die '当前 release 链接不存在'
|
|
prefix_root=$(readlink -f -- "$PREFIX" 2>/dev/null || realpath "$PREFIX" 2>/dev/null || true)
|
|
[[ -n "$prefix_root" && -d "$prefix_root" && ! -L "$prefix_root" ]] || die '安装目录不安全'
|
|
root=$(readlink -f -- "$PREFIX/current" 2>/dev/null || realpath "$PREFIX/current" 2>/dev/null || true)
|
|
[[ -n "$root" && "$root" == "$prefix_root/releases/"* && -d "$root" && ! -L "$root" ]] || die '当前 release 链接不安全'
|
|
printf '%s' "$root"
|
|
}
|
|
|
|
run_as_service_user() {
|
|
local root=$1 node=$2 cli=$3
|
|
if [[ "${EUID:-$(id -u)}" == 0 ]]; then
|
|
local service_uid
|
|
service_uid=$(id -u tallynote 2>/dev/null) || die '找不到 tallynote 服务用户,拒绝以 root 身份执行管理员初始化'
|
|
[[ "$service_uid" =~ ^[1-9][0-9]*$ ]] || die 'tallynote 服务用户 UID 无效,拒绝以 root 身份执行管理员初始化'
|
|
command -v runuser >/dev/null 2>&1 || die '找不到 runuser,无法以 tallynote 用户初始化'
|
|
local -a environment=(
|
|
"NODE_ENV=production"
|
|
"TALLYNOTE_DATA_DIR=${TALLYNOTE_DATA_DIR:-/var/lib/tallynote}"
|
|
"TALLYNOTE_INSTALL_PREFIX=${TALLYNOTE_INSTALL_PREFIX:-$PREFIX}"
|
|
"TALLYNOTE_TRUST_PROXY=${TALLYNOTE_TRUST_PROXY:-false}"
|
|
"TALLYNOTE_UPDATE_STRATEGY=${TALLYNOTE_UPDATE_STRATEGY:-systemd}"
|
|
"TALLYNOTE_HOST=${TALLYNOTE_HOST:-127.0.0.1}"
|
|
"TALLYNOTE_PORT=${TALLYNOTE_PORT:-3000}"
|
|
"TALLYNOTE_PUBLIC_ORIGIN=${TALLYNOTE_PUBLIC_ORIGIN:-http://127.0.0.1:3000}"
|
|
"TALLYNOTE_COOKIE_SECURE=${TALLYNOTE_COOKIE_SECURE:-false}"
|
|
"TALLYNOTE_ALLOW_INSECURE_HTTP=${TALLYNOTE_ALLOW_INSECURE_HTTP:-false}"
|
|
"TALLYNOTE_TIMEZONE=${TALLYNOTE_TIMEZONE:-Asia/Shanghai}"
|
|
"TALLYNOTE_UPDATE_METADATA_URL=${TALLYNOTE_UPDATE_METADATA_URL:-https://git.awaioi.com/api/v1/repos/awaioi/TallyNote/releases/latest}"
|
|
"TALLYNOTE_UPDATE_ALLOWED_HOSTS=${TALLYNOTE_UPDATE_ALLOWED_HOSTS:-git.awaioi.com}"
|
|
"TALLYNOTE_UPDATE_REQUIRE_SIGNATURE=${TALLYNOTE_UPDATE_REQUIRE_SIGNATURE:-false}"
|
|
"TALLYNOTE_UPDATE_MAX_MB=${TALLYNOTE_UPDATE_MAX_MB:-512}"
|
|
"TALLYNOTE_UPDATE_CHECK_COOLDOWN_SECONDS=${TALLYNOTE_UPDATE_CHECK_COOLDOWN_SECONDS:-60}"
|
|
"TALLYNOTE_UPDATE_DOWNLOAD_COOLDOWN_SECONDS=${TALLYNOTE_UPDATE_DOWNLOAD_COOLDOWN_SECONDS:-15}"
|
|
"TALLYNOTE_UPDATE_APPLY_COOLDOWN_SECONDS=${TALLYNOTE_UPDATE_APPLY_COOLDOWN_SECONDS:-15}"
|
|
"TALLYNOTE_MAX_FILE_MB=${TALLYNOTE_MAX_FILE_MB:-20}"
|
|
"TALLYNOTE_MAX_FILES_PER_REQUEST=${TALLYNOTE_MAX_FILES_PER_REQUEST:-20}"
|
|
"TALLYNOTE_MAX_RECORD_MB=${TALLYNOTE_MAX_RECORD_MB:-100}"
|
|
"TALLYNOTE_MAX_TOTAL_MB=${TALLYNOTE_MAX_TOTAL_MB:-2048}"
|
|
"TALLYNOTE_MAX_CONCURRENT_EXPORTS=${TALLYNOTE_MAX_CONCURRENT_EXPORTS:-2}"
|
|
"TALLYNOTE_MAX_EXPORT_RECORDS=${TALLYNOTE_MAX_EXPORT_RECORDS:-5000}"
|
|
"TALLYNOTE_MAX_EXPORT_MB=${TALLYNOTE_MAX_EXPORT_MB:-1024}"
|
|
"TALLYNOTE_MAX_EXPORT_STORAGE_MB=${TALLYNOTE_MAX_EXPORT_STORAGE_MB:-2048}"
|
|
"TALLYNOTE_SESSION_IDLE_HOURS=${TALLYNOTE_SESSION_IDLE_HOURS:-24}"
|
|
"TALLYNOTE_SESSION_ABSOLUTE_HOURS=${TALLYNOTE_SESSION_ABSOLUTE_HOURS:-168}"
|
|
"TALLYNOTE_EXPORT_TTL_MINUTES=${TALLYNOTE_EXPORT_TTL_MINUTES:-15}"
|
|
)
|
|
[[ -n "${TALLYNOTE_UPDATE_PUBLIC_KEY:-}" ]] && environment+=("TALLYNOTE_UPDATE_PUBLIC_KEY=$TALLYNOTE_UPDATE_PUBLIC_KEY")
|
|
if [[ -n "${TALLYNOTE_UPDATE_PUBLIC_KEY_FILE:-}" ]]; then
|
|
environment+=("TALLYNOTE_UPDATE_PUBLIC_KEY_FILE=$TALLYNOTE_UPDATE_PUBLIC_KEY_FILE")
|
|
fi
|
|
if [[ -n "${TALLYNOTE_UPDATE_HELPER_PATH:-}" ]]; then
|
|
environment+=("TALLYNOTE_UPDATE_HELPER_PATH=$TALLYNOTE_UPDATE_HELPER_PATH")
|
|
fi
|
|
runuser -u tallynote -- env -i "${environment[@]}" PATH="$PATH" "$node" "$cli" "${@:4}"
|
|
else
|
|
"$node" "$cli" "${@:4}"
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
load_environment_file
|
|
local root node cli systemctl service_was_active=0 result check_only=0
|
|
for argument in "$@"; do
|
|
[[ "$argument" == "--check" ]] && check_only=1
|
|
done
|
|
root=$(resolve_release_root)
|
|
node=${TALLYNOTE_NODE:-}
|
|
node_is_usable "$node" || node=$(command -v node || true)
|
|
node_is_usable "$node" || die '找不到 Node.js 24+'
|
|
cli="$root/dist/server/cli/admin-init.js"
|
|
[[ -f "$cli" && ! -L "$cli" ]] || die '管理员初始化程序不存在'
|
|
|
|
# Validate the privilege boundary before stopping an active service. A
|
|
# damaged installation must fail closed without causing avoidable downtime.
|
|
if [[ "${EUID:-$(id -u)}" == 0 ]]; then
|
|
local service_uid
|
|
service_uid=$(id -u tallynote 2>/dev/null) || die '找不到 tallynote 服务用户,拒绝以 root 身份执行管理员初始化'
|
|
[[ "$service_uid" =~ ^[1-9][0-9]*$ ]] || die 'tallynote 服务用户 UID 无效,拒绝以 root 身份执行管理员初始化'
|
|
command -v runuser >/dev/null 2>&1 || die '找不到 runuser,无法以 tallynote 用户初始化'
|
|
fi
|
|
|
|
# admin-init uses the same instance lock as the web process. Pause an active
|
|
# service for the duration, then restore exactly its previous active state.
|
|
systemctl=$(command -v systemctl || true)
|
|
if (( ! check_only )) && [[ "${EUID:-$(id -u)}" == 0 && -n "$systemctl" && -x "$systemctl" ]] && "$systemctl" is-active --quiet "$SERVICE_NAME"; then
|
|
service_was_active=1
|
|
printf 'tallynote admin-init: 暂停服务以完成管理员初始化\n' >&2
|
|
"$systemctl" stop "$SERVICE_NAME" || die '无法暂停 TallyNote 服务'
|
|
fi
|
|
restore_service() {
|
|
local exit_code=$?
|
|
if (( service_was_active )); then
|
|
printf 'tallynote admin-init: 恢复 TallyNote 服务\n' >&2
|
|
"$systemctl" start "$SERVICE_NAME" || printf 'tallynote admin-init: 警告:服务恢复失败,请执行 systemctl start %s\n' "$SERVICE_NAME" >&2
|
|
fi
|
|
return "$exit_code"
|
|
}
|
|
trap restore_service EXIT
|
|
|
|
cd -- "$root"
|
|
set +e
|
|
run_as_service_user "$root" "$node" "$cli" "$@"
|
|
result=$?
|
|
set -e
|
|
exit "$result"
|
|
}
|
|
|
|
main "$@"
|