Files
vps/install/20-NewApi.sh
T

302 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "------------------------"