#!/bin/bash # chmod +x 20-NewApi.sh && ./20-NewApi.sh # curl -sS -O https://gitea.tohub.top/Share/vps/raw/branch/main/install/20-NewApi.sh && chmod +x 20-NewApi.sh && ./20-NewApi.sh set -euo pipefail port80=8200 # 安装目录与更新脚本路径 install_dir=/root/data/docker_data/NewApi update_script="$install_dir/auto-update.sh" update_log="$install_dir/auto-update.log" if [ "$(id -u)" -ne 0 ]; then echo "请使用 root 用户运行本脚本。" >&2 exit 1 fi for required_cmd in openssl curl grep sed apt; do if ! command -v "$required_cmd" >/dev/null 2>&1; then echo "未检测到 $required_cmd。本脚本依赖 Debian/Ubuntu 系统环境,请先安装后再运行。" >&2 exit 1 fi done # 检测 compose 命令,结果存入全局数组 compose_cmd;检测失败返回 1 detect_compose() { if docker compose version >/dev/null 2>&1; then compose_cmd=(docker compose) elif command -v docker-compose >/dev/null 2>&1; then compose_cmd=(docker-compose) else return 1 fi return 0 } # 前置检查:docker 与 compose 必须先就绪,安装与更新分支都依赖它们 if ! command -v docker >/dev/null 2>&1; then echo "未检测到 docker,请先安装 Docker 后再运行本脚本。" >&2 exit 1 fi if ! detect_compose; then echo "未检测到 docker compose 或 docker-compose,请先安装 Docker Compose。" >&2 exit 1 fi # 生成 auto-update.sh 并注册 crontab 定时任务(幂等,可无条件重复执行)。 # 安装分支和"仅执行 Docker 更新"分支都调用本函数:每次调用都重建更新脚本 # 并刷新 cron 条目,无论脚本被删、内容过时还是 cron 丢失,重跑本脚本即可自愈。 setup_auto_update() { mkdir -p "$install_dir" cat <<'EOF' > "$update_script" #!/bin/bash set -euo pipefail install_dir=/root/data/docker_data/NewApi archive_dir=/root/data/docker_data/NewApi.archive image_name=calciumion/new-api:latest # 统一带时间戳输出,便于从 auto-update.log 区分每次运行、定位问题 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } # 备份数据:先写入临时目录,成功后再替换旧备份;任何一步失败都返回非零 backup_data() { local tmp_archive_dir="${archive_dir}.tmp" local rsync_rc=0 mkdir -p "$(dirname "$archive_dir")" || return 1 rm -rf "$tmp_archive_dir" || return 1 mkdir -p "$tmp_archive_dir" || return 1 if command -v rsync >/dev/null 2>&1; then rsync -a --delete --exclude 'auto-update.log' --exclude 'logs/' "$install_dir"/ "$tmp_archive_dir"/ || rsync_rc=$? # 退出码 24 表示复制期间部分源文件消失(正在运行的 sqlite 会产生转瞬即逝的 # WAL 临时文件),备份内容仍然可用,视为可接受 if [ "$rsync_rc" -ne 0 ] && [ "$rsync_rc" -ne 24 ]; then return "$rsync_rc" fi else cp -a "$install_dir"/. "$tmp_archive_dir"/ || return 1 rm -f "$tmp_archive_dir"/auto-update.log rm -rf "$tmp_archive_dir"/logs fi rm -rf "$archive_dir" || return 1 mv "$tmp_archive_dir" "$archive_dir" || return 1 } log "开始执行 NewApi 自动更新" # 备份尽力而为:失败只告警不阻断,避免备份环节的问题导致镜像更新被跳过 backup_rc=0 backup_data || backup_rc=$? if [ "$backup_rc" -eq 0 ]; then log "数据备份完成:$archive_dir" else log "警告:数据备份失败(退出码 $backup_rc),本次跳过备份,继续执行镜像更新" fi # 进入 docker-compose 所在的文件夹 cd "$install_dir" || { log "无法进入安装目录 $install_dir"; exit 1; } if docker compose version >/dev/null 2>&1; then compose_cmd=(docker compose) elif command -v docker-compose >/dev/null 2>&1; then compose_cmd=(docker-compose) else log "未检测到 docker compose 或 docker-compose,无法自动更新。" >&2 exit 1 fi # 记录更新前后的镜像 ID,便于从日志一眼区分"没跑 / 拉取失败 / 本来就没有新版本" old_image_id=$(docker image inspect --format '{{.Id}}' "$image_name" 2>/dev/null || echo "本地暂无该镜像") log "更新前镜像 ID:$old_image_id" "${compose_cmd[@]}" pull "${compose_cmd[@]}" up -d new_image_id=$(docker image inspect --format '{{.Id}}' "$image_name" 2>/dev/null || echo "本地暂无该镜像") log "更新后镜像 ID:$new_image_id" if [ "$new_image_id" != "$old_image_id" ]; then log "镜像已更新并重启" else log "镜像无更新" fi # 清理旧镜像,释放磁盘空间 docker image prune -f # 修剪日志避免无限增长(只保留最近 1000 行) # 用 cat 覆盖写回而非 mv,保留原 inode,避免 cron 持有的 append 文件描述符失效 log_file="$install_dir/auto-update.log" if [ -f "$log_file" ]; then tail -n 1000 "$log_file" > "$log_file.tmp" && cat "$log_file.tmp" > "$log_file" && rm -f "$log_file.tmp" fi EOF chmod +x "$update_script" # 写入每日 04:20 自动更新任务。 # 特意与 23-Sub2API.sh 的 04:00 任务错峰:两者都会执行全局的 docker image prune -f, # 同时运行有竞态风险,且拉取镜像的带宽 / 磁盘 IO 会叠加。 cron_line="20 4 * * * $update_script >> $update_log 2>&1" if command -v crontab >/dev/null 2>&1; then # 用 NewApi 更新脚本的完整路径过滤旧条目(含老机器遗留的 04:00 条目), # 幂等替换,且不会误删 crontab 中的其他任务(如 Sub2API 的 auto-update.sh) ( crontab -l 2>/dev/null | grep -v -F -- "$update_script" || true ; echo "$cron_line" ) | crontab - echo "已配置定时自动更新任务:每天 04:20 自动更新 NewApi。" else echo "未检测到 crontab,跳过定时更新任务配置。可手动执行 $update_script 更新。" fi } # 交互:已有安装时默认只更新,避免覆盖数据库路径、账号数据和密钥 existing_install=0 if [ -f "$install_dir/docker-compose.yml" ]; then existing_install=1 fi echo "请选择操作:" if [ "$existing_install" -eq 1 ]; then echo " 1) 已安装,仅执行 Docker 更新(默认,安全,不覆盖配置/数据库)" echo " 2) 强制重装 NewApi(危险,会重写 docker-compose.yml)" else echo " 1) 安装 NewApi(默认)" echo " 2) 不安装,立即执行 Docker 更新" fi read -r -p "请输入选项 [1/2](默认 1):" action_choice action_choice="${action_choice:-1}" # 严格校验输入,防止误输入落入安装/重装流程 if [ "$action_choice" != "1" ] && [ "$action_choice" != "2" ]; then echo "无效选项:$action_choice,已退出。未做任何修改。" >&2 exit 1 fi if { [ "$existing_install" -eq 1 ] && [ "$action_choice" = "1" ]; } || { [ "$existing_install" -eq 0 ] && [ "$action_choice" = "2" ]; }; then echo "已选择:立即执行 Docker 更新。" if [ ! -f "$install_dir/docker-compose.yml" ]; then echo "未检测到已安装的 NewApi(缺少 $install_dir/docker-compose.yml),请先安装。" >&2 exit 1 fi # 先无条件重建 auto-update.sh 并刷新 cron 条目,修复自动更新机制: # 脚本被删、内容过时、cron 丢失等任何损坏,重跑本分支即可自愈 setup_auto_update # 再用刚重建的脚本完成本次更新 "$update_script" echo "Docker 更新完成。" exit 0 fi if [ "$existing_install" -eq 1 ] && [ "$action_choice" = "2" ]; then echo "检测到已有 NewApi 安装:$install_dir/docker-compose.yml" echo "强制重装会覆盖 docker-compose.yml,可能导致服务连接到新的空数据库。" read -r -p "如确需重装,请输入 REINSTALL 确认:" reinstall_confirm if [ "$reinstall_confirm" != "REINSTALL" ]; then echo "未确认强制重装,已退出。原有配置未修改。" exit 0 fi fi # 生成 sed 安全的随机密钥(hex) redis_password=$(openssl rand -hex 16) session_secret=$(openssl rand -hex 24) ipv4_address=$(curl -fsS --max-time 10 ipv4.ip.sb || true) # 校验 IPv4 ipv4_octet='(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])' if ! printf '%s' "$ipv4_address" | grep -qE "^$ipv4_octet(\.$ipv4_octet){3}$"; then ipv4_address="服务器IP" fi # 1、更新包(更新失败不阻断后续安装) export DEBIAN_FRONTEND=noninteractive apt update -y || true apt upgrade -y || true # 2、创建安装目录 mkdir -p "$install_dir" cd "$install_dir" || { echo "无法进入安装目录 $install_dir" >&2; exit 1; } # 3、填写 docker-compose 配置 # 先写入占位符,再替换为实际配置 cat <<'EOF' > docker-compose.yml services: new-api: image: calciumion/new-api:latest container_name: new-api restart: always command: --log-dir /app/logs ports: - "__PORT80__:3000" volumes: - ./data:/data - ./logs:/app/logs - ./sqlite:/app/sqlite # SQLite数据库存储目录 environment: - SQL_DB_TYPE=sqlite - SQL_DB_PATH=/app/sqlite/new-api.db # SQLite数据库文件路径 - REDIS_CONN_STRING=redis://:__REDIS_PASSWORD__@redis - TZ=Asia/Shanghai - SESSION_SECRET=__SESSION_SECRET__ depends_on: - redis healthcheck: test: [ "CMD-SHELL", "(command -v wget >/dev/null 2>&1 && wget -q -O - http://localhost:3000/api/status || command -v curl >/dev/null 2>&1 && curl -fsS http://localhost:3000/api/status) | grep -q '\"success\"[[:space:]]*:[[:space:]]*true'" ] interval: 30s timeout: 10s retries: 3 redis: image: redis:alpine # 使用Alpine版本的Redis,更轻量 container_name: new-api-redis restart: always command: redis-server --requirepass __REDIS_PASSWORD__ volumes: - redis_data:/data # 持久化Redis数据 volumes: redis_data: EOF # 替换占位符为实际值 sed -i \ -e "s|__PORT80__|$port80|g" \ -e "s|__REDIS_PASSWORD__|$redis_password|g" \ -e "s|__SESSION_SECRET__|$session_secret|g" \ docker-compose.yml # 4、安装(docker 与 compose 已在脚本开头检查过) "${compose_cmd[@]}" up -d # 5、打开防火墙的端口 if command -v ufw >/dev/null 2>&1; then ufw allow "$port80" ufw status else echo "未检测到 ufw,跳过防火墙端口放行。" fi # 6、配置每日自动更新(生成 auto-update.sh 并写入 crontab) setup_auto_update # 打印访问链接 echo "------------------------" echo "访问链接:" echo "http://$ipv4_address:$port80" echo "User: root" echo "默认密码: 123456 —— 该端口已对公网开放,请【立即】登录并修改密码!" echo "------------------------" echo "已开启定时自动更新:每天 04:20 自动备份并拉取最新镜像(与 Sub2API 的 04:00 任务错峰)。" echo "手动更新可执行:$update_script" echo "------------------------"