Files
TallyNote/bin/tallynote-admin-init
T
Qiufeng 344985f514
TallyNote release / linux-x64 (push) Successful in 6m26s
release: 1.1.12
2026-09-02 20:23:46 +08:00

150 lines
7.5 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/sbin:/usr/bin:/sbin:/bin:/usr/local/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; }
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="$root/runtime/bin/node"
[[ -x "$node" ]] || node=$(command -v node || true)
[[ -n "$node" && -x "$node" ]] || die '找不到 Node.js runtime'
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 "$@"