diff --git a/install.sh b/install.sh index c2f9ffc..bcb8ed3 100644 --- a/install.sh +++ b/install.sh @@ -41,8 +41,8 @@ MAINTENANCE_ACTION= FUSE_PATH_INPUT= confirm_fuse_action() { local confirmation - confirmation=$(prompt_tty '该操作将停止并重新启动 app;输入 FUSE_CONFIRM 继续: ') - [[ $confirmation == FUSE_CONFIRM ]] || die '未确认 FUSE 生命周期操作' + confirmation=$(prompt_tty '该操作将停止并重新启动应用,输入 1 继续,其他内容取消: ') + [[ $confirmation == 1 ]] || die '已取消挂载操作' } require_public_app_installation() { local missing=() @@ -80,14 +80,14 @@ print_fuse_status() { if [[ -n $mount_id && $source == xymediavault ]] && { [[ $fstype == fuse.xymediavault ]] || [[ $fstype == fuse ]]; }; then printf '挂载身份:owned xymediavault/fuse.xymediavault(ID=%s)\n' "$mount_id" >>$TTY_OUT elif [[ -n $mount_id ]]; then - printf '挂载身份:foreign/unknown(ID=%s source=%s fstype=%s)\n' "$mount_id" "$source" "$fstype" >>$TTY_OUT + printf '挂载身份:外部或未知(ID=%s 来源=%s 类型=%s)\n' "$mount_id" "$source" "$fstype" >>$TTY_OUT else printf '挂载身份:无法确认\n' >>$TTY_OUT fi fi health=$(curl --silent --show-error --max-time 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:${API_PORT}/api/health" 2>/dev/null || true) [[ $health =~ ^[0-9]{3}$ ]] || health='无法连接' - printf 'app health:%s\n' "$health" >>$TTY_OUT + printf '应用健康状态:%s\n' "$health" >>$TTY_OUT } while (($#)); do case "$1" in @@ -100,7 +100,7 @@ while (($#)); do --skip-components) SKIP_COMPONENTS=1; shift;; --existing-db) EXISTING_DB=1; shift;; -h|--help) usage; exit 0;; - *) die "unknown option: $1";; + *) die "未知选项:$1";; esac done die() { printf '\n\033[31mxymediavault installer: %s\033[0m\n' "$*" >&2; [[ -n $INSTALL_LOG ]] && printf '详情日志:%s\n' "$INSTALL_LOG" >&2; exit 1; } @@ -109,11 +109,11 @@ cleanup_installer_tmp() { [[ -z ${INSTALL_TMP_DIR:-} ]] || rm -rf -- "$INSTALL_T trap cleanup_installer_tmp EXIT init_installer_log() { local tmp_parent=${TMPDIR:-/tmp} mode - [[ -d $tmp_parent && ! -L $tmp_parent ]] || die 'TMPDIR must be an existing non-symlink directory' + [[ -d $tmp_parent && ! -L $tmp_parent ]] || die '临时目录必须存在且不能是符号链接目录' if (( EUID == 0 )); then mode=$(stat -c '%a' "$tmp_parent") - [[ $(stat -c '%u' "$tmp_parent") -eq 0 ]] || die 'TMPDIR must be owned by root when running as root' - [[ $((mode & 022)) -eq 0 || $((mode & 1000)) -ne 0 ]] || die 'TMPDIR must not be group/world writable unless sticky' + [[ $(stat -c '%u' "$tmp_parent") -eq 0 ]] || die '以管理员权限运行时,临时目录必须由 root 所有' + [[ $((mode & 022)) -eq 0 || $((mode & 1000)) -ne 0 ]] || die '临时目录不能允许组用户或其他用户写入,除非启用了粘滞位' fi umask 077 INSTALL_TMP_DIR=$(mktemp -d "$tmp_parent/xymediavault-install.XXXXXX") || die '无法创建私有安装临时目录' @@ -154,8 +154,8 @@ run_visible_logged() { } usage() { sed -n '1,20p' "$0"; } read_tty() { if ((TTY_TEST_MODE)); then [[ $TTY_TEST_INDEX -lt ${#TTY_TEST_LINES[@]} ]] || return 1; printf -v "$1" '%s' "${TTY_TEST_LINES[$TTY_TEST_INDEX]}"; TTY_TEST_INDEX=$((TTY_TEST_INDEX + 1)); else IFS= read -r "$1" <"$TTY_IN"; fi; } -prompt_tty() { [[ -r $TTY_IN && -w $TTY_OUT ]] || die 'interactive menu requires a TTY; use menu option 1 from a terminal'; printf '%s' "$1" >>$TTY_OUT; read_tty value || die 'unable to read terminal input'; printf '%s' "$value"; } -prompt_tty_into() { [[ -r $TTY_IN && -w $TTY_OUT ]] || die 'interactive menu requires a TTY; use menu option 1 from a terminal'; printf '%s' "$2" >>$TTY_OUT; read_tty "$1" || die 'unable to read terminal input'; } +prompt_tty() { [[ -r $TTY_IN && -w $TTY_OUT ]] || die '交互菜单需要终端,请在终端中运行安装器'; printf '%s' "$1" >>$TTY_OUT; read_tty value || die '无法读取终端输入'; printf '%s' "$value"; } +prompt_tty_into() { [[ -r $TTY_IN && -w $TTY_OUT ]] || die '交互菜单需要终端,请在终端中运行安装器'; printf '%s' "$2" >>$TTY_OUT; read_tty "$1" || die '无法读取终端输入'; } controller_token_file() { printf '%s/secrets/xiaoya-controller-token' "$INSTALL_DIR"; } ensure_xiaoya_dir() { local required=${1:-0} @@ -165,10 +165,10 @@ ensure_xiaoya_dir() { [[ $XIAOYA_DIR = /* && $XIAOYA_DIR != / ]] || die '小雅目录必须是绝对路径且不能是根目录' if [[ -d $XIAOYA_DIR ]] && [[ -n $(find "$XIAOYA_DIR" -mindepth 1 -maxdepth 1 -print -quit) ]]; then printf '检测到已有非空小雅目录:%s\n' "$XIAOYA_DIR" >>$TTY_OUT - printf '请选择 attach / install-controller / separate / return:' >>$TTY_OUT - read_tty choice || die 'unable to read terminal input' - [[ $choice == attach || $choice == install-controller || $choice == separate || $choice == return ]] || die '无效选择,已停止以保护已有数据' - [[ $choice != return ]] || exit 0 + printf '请选择:1) 继续使用此目录 0) 返回:' >>$TTY_OUT + read_tty choice || die '无法读取终端输入' + [[ $choice == 1 || $choice == 0 ]] || die '无效选择,已停止以保护已有数据' + [[ $choice != 0 ]] || exit 0 fi mkdir -p "$XIAOYA_DIR" "$INSTALL_DIR/secrets" } @@ -178,10 +178,10 @@ write_controller_token() { } menu() { [[ $# -eq 0 ]] || return 0 - [[ -r $TTY_IN && -w $TTY_OUT ]] || die 'no TTY: interactive installer menu cannot run from a non-interactive pipe' + [[ -r $TTY_IN && -w $TTY_OUT ]] || die '没有可用终端:安装菜单不能通过非交互管道运行' while :; do printf '\nXyMediaVault 安装器\n1) 仅安装管理平台\n2) 安装管理平台和本机小雅\n3) 安装管理平台并连接远程小雅控制器\n4) 仅安装小雅控制器\n5) 状态/诊断/维护\n0) 退出\n选择:' >>$TTY_OUT - read_tty choice || die 'unable to read terminal input' + read_tty choice || die '无法读取终端输入' case "$choice" in 1) return 0;; 2) ensure_xiaoya_dir; printf '小雅目录已准备:%s;将安装小雅和小雅控制器。\n' "$XIAOYA_DIR" >>$TTY_OUT; write_controller_token; CONTROLLER_URL='http://xymedia-controller:19090'; MENU_PROFILES='xiaoya xiaoya-control'; return 0;; @@ -189,7 +189,7 @@ menu() { 4) ensure_xiaoya_dir 1; if [[ ${XYMEDIA_INSTALL_TEST_DISCOVERY_ONLY:-0} != 1 && -z $CONTROLLER_BIND_EXPLICIT ]]; then prompt_tty_into CONTROLLER_BIND_ADDRESS "控制器监听地址 [${CONTROLLER_BIND_ADDRESS}]: "; CONTROLLER_BIND_ADDRESS=${CONTROLLER_BIND_ADDRESS:-0.0.0.0}; fi; if [[ ${XYMEDIA_INSTALL_TEST_DISCOVERY_ONLY:-0} != 1 && -z $CONTROLLER_PORT_EXPLICIT ]]; then prompt_tty_into CONTROLLER_PORT "控制器端口 [${CONTROLLER_PORT}]: "; CONTROLLER_PORT=${CONTROLLER_PORT:-19090}; fi; write_controller_token; CONTROLLER_ONLY=1; printf '小雅控制器安装目录已准备。\n' >>$TTY_OUT; MENU_PROFILES='xiaoya-control'; return 0;; 5) while :; do printf '\n维护菜单\n1) 查看状态\n2) 一键挂载本地媒体库\n3) 启动/重启已配置挂载\n4) 停用本地媒体库挂载\n0) 返回\n选择:' >>$TTY_OUT - read_tty maintenance_choice || die 'unable to read terminal input' + read_tty maintenance_choice || die '无法读取终端输入' case "$maintenance_choice" in 1) require_public_app_installation || continue; print_fuse_status;; 2) require_public_app_installation || continue; prompt_tty_into FUSE_PATH_INPUT '宿主机媒体库目录(必须为空):'; confirm_fuse_action; MAINTENANCE_ACTION=configure; return 0;; @@ -228,15 +228,15 @@ validate_container_name() { [[ $XIAOYA_CONTAINER =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,254}$ ]] || die '小雅容器名称不合法,不能包含换行或 Compose 特殊字符' } progress '检查安装环境' -[[ $INSTALL_DIR = /* && $INSTALL_DIR != / ]] || die 'install directory must be an absolute non-root path' +[[ $INSTALL_DIR = /* && $INSTALL_DIR != / ]] || die '安装目录必须是绝对路径且不能是根目录' guard_existing_install_tree init_installer_log -command -v docker >/dev/null || die 'Docker is required' -if docker compose version >/dev/null 2>&1; then COMPOSE=(docker compose); elif command -v docker-compose >/dev/null; then COMPOSE=(docker-compose); else die 'Docker Compose is required'; fi -docker info >/dev/null 2>&1 || die 'cannot connect to Docker' -command -v curl >/dev/null || die 'curl is required' -command -v zstd >/dev/null || die 'zstd is required for release archives' -command -v python3 >/dev/null || die 'python3 is required for safe archive extraction' +command -v docker >/dev/null || die '需要安装 Docker' +if docker compose version >/dev/null 2>&1; then COMPOSE=(docker compose); elif command -v docker-compose >/dev/null; then COMPOSE=(docker-compose); else die '需要安装 Docker Compose'; fi +docker info >/dev/null 2>&1 || die '无法连接 Docker,请确认 Docker 已启动' +command -v curl >/dev/null || die '需要安装 curl' +command -v zstd >/dev/null || die '需要安装 zstd,用于解压发布包' +command -v python3 >/dev/null || die '需要安装 python3,用于安全解压发布包' if [[ -n $MAINTENANCE_ACTION ]]; then require_public_app_installation || die '维护操作仅支持已有完整 app 安装' case "$MAINTENANCE_ACTION" in