mirror of
https://github.com/Smile-QWQ/SubTracker.git
synced 2026-05-23 01:07:01 +08:00
465 lines
12 KiB
Bash
465 lines
12 KiB
Bash
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
REPO_OWNER="Smile-QWQ"
|
||
REPO_NAME="SubTracker"
|
||
DEFAULT_RELEASE_TAG="latest"
|
||
DEFAULT_API_IMAGE="ghcr.io/smile-qwq/subtracker-api:latest"
|
||
DEFAULT_API_PORT="3001"
|
||
DEFAULT_WEB_PORT="8080"
|
||
DEFAULT_LOG_LEVEL="warn"
|
||
|
||
MODE=""
|
||
INSTALL_DIR=""
|
||
RELEASE_TAG="${DEFAULT_RELEASE_TAG}"
|
||
API_IMAGE="${DEFAULT_API_IMAGE}"
|
||
API_PORT="${DEFAULT_API_PORT}"
|
||
WEB_PORT="${DEFAULT_WEB_PORT}"
|
||
WEB_ORIGIN=""
|
||
LOG_LEVEL="${DEFAULT_LOG_LEVEL}"
|
||
NON_INTERACTIVE="false"
|
||
FORCE="false"
|
||
|
||
info() {
|
||
printf '[INFO] %s\n' "$*"
|
||
}
|
||
|
||
warn() {
|
||
printf '[WARN] %s\n' "$*" >&2
|
||
}
|
||
|
||
fail() {
|
||
printf '[ERROR] %s\n' "$*" >&2
|
||
exit 1
|
||
}
|
||
|
||
print_help() {
|
||
cat <<'EOF'
|
||
SubTracker deployment installer
|
||
|
||
Usage:
|
||
curl -fsSL https://raw.githubusercontent.com/Smile-QWQ/SubTracker/main/scripts/install.sh | bash
|
||
curl -fsSL https://raw.githubusercontent.com/Smile-QWQ/SubTracker/main/scripts/install.sh | bash -s -- --mode full --dir /opt/subtracker
|
||
|
||
Options:
|
||
--mode <api|full> 部署方式:api=只部署后端;full=前端+后端一起部署
|
||
--dir <path> 部署目录,默认 ./subtracker-<mode>
|
||
--release <tag|latest> 使用哪个 Release,默认 latest
|
||
--api-image <image> API 镜像,默认 ghcr.io/smile-qwq/subtracker-api:latest
|
||
--api-port <port> API 对外端口,默认 3001
|
||
--web-port <port> Full 模式前端对外端口,默认 8080
|
||
--web-origin <origin> 前端最终访问地址(用于 CORS),例如 https://subtracker.example.com
|
||
--log-level <level> API 日志级别,默认 warn
|
||
--force 若目录已存在则覆盖
|
||
--yes 非交互模式,缺省值直接使用默认值
|
||
--help 显示帮助
|
||
EOF
|
||
}
|
||
|
||
need_cmd() {
|
||
command -v "$1" >/dev/null 2>&1 || fail "缺少依赖命令:$1"
|
||
}
|
||
|
||
parse_args() {
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--mode)
|
||
MODE="${2:-}"
|
||
shift 2
|
||
;;
|
||
--dir)
|
||
INSTALL_DIR="${2:-}"
|
||
shift 2
|
||
;;
|
||
--release)
|
||
RELEASE_TAG="${2:-}"
|
||
shift 2
|
||
;;
|
||
--api-image)
|
||
API_IMAGE="${2:-}"
|
||
shift 2
|
||
;;
|
||
--api-port)
|
||
API_PORT="${2:-}"
|
||
shift 2
|
||
;;
|
||
--web-port)
|
||
WEB_PORT="${2:-}"
|
||
shift 2
|
||
;;
|
||
--web-origin)
|
||
WEB_ORIGIN="${2:-}"
|
||
shift 2
|
||
;;
|
||
--log-level)
|
||
LOG_LEVEL="${2:-}"
|
||
shift 2
|
||
;;
|
||
--force)
|
||
FORCE="true"
|
||
shift
|
||
;;
|
||
--yes)
|
||
NON_INTERACTIVE="true"
|
||
shift
|
||
;;
|
||
--help|-h)
|
||
print_help
|
||
exit 0
|
||
;;
|
||
*)
|
||
fail "未知参数:$1"
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
prompt_value() {
|
||
local label="$1"
|
||
local default_value="$2"
|
||
local current_value="${3:-}"
|
||
|
||
if [ -n "$current_value" ]; then
|
||
printf '%s' "$current_value"
|
||
return 0
|
||
fi
|
||
|
||
if [ "$NON_INTERACTIVE" = "true" ] || [ ! -r /dev/tty ]; then
|
||
printf '%s' "$default_value"
|
||
return 0
|
||
fi
|
||
|
||
local answer
|
||
printf '%s [%s]: ' "$label" "$default_value" > /dev/tty
|
||
IFS= read -r answer < /dev/tty || true
|
||
if [ -z "$answer" ]; then
|
||
printf '%s' "$default_value"
|
||
else
|
||
printf '%s' "$answer"
|
||
fi
|
||
}
|
||
|
||
select_mode() {
|
||
if [ -n "$MODE" ]; then
|
||
return 0
|
||
fi
|
||
|
||
if [ "$NON_INTERACTIVE" = "true" ] || [ ! -r /dev/tty ]; then
|
||
MODE="api"
|
||
return 0
|
||
fi
|
||
|
||
cat > /dev/tty <<'EOF'
|
||
|
||
请选择部署方式:
|
||
api = 只部署后端 API
|
||
前端静态文件需要你自己放到 Nginx / 宝塔 / 站点目录
|
||
|
||
full = 前端 + 后端一起部署
|
||
脚本会自动下载前端静态文件并准备 web-dist/
|
||
|
||
EOF
|
||
printf '请输入部署方式 [api/full](默认 api): ' > /dev/tty
|
||
local answer=""
|
||
IFS= read -r answer < /dev/tty || true
|
||
MODE="${answer:-api}"
|
||
}
|
||
|
||
normalize_inputs() {
|
||
select_mode
|
||
case "$MODE" in
|
||
api|full) ;;
|
||
*) fail "--mode 仅支持 api 或 full,当前是:$MODE" ;;
|
||
esac
|
||
|
||
if [ -z "$INSTALL_DIR" ]; then
|
||
INSTALL_DIR="$(prompt_value '部署目录(脚本会在这里生成 compose、.env、data)' "./subtracker-${MODE}" "$INSTALL_DIR")"
|
||
fi
|
||
|
||
API_PORT="$(prompt_value 'API 端口' "$DEFAULT_API_PORT" "$API_PORT")"
|
||
if [ "$MODE" = "full" ]; then
|
||
WEB_PORT="$(prompt_value '前端对外端口(Full 模式)' "$DEFAULT_WEB_PORT" "$WEB_PORT")"
|
||
fi
|
||
|
||
if [ -z "$WEB_ORIGIN" ]; then
|
||
WEB_ORIGIN="$(prompt_value '前端最终访问地址(用于浏览器跨域/CORS,例如 https://subtracker.example.com)' 'https://subtracker.example.com' "$WEB_ORIGIN")"
|
||
fi
|
||
}
|
||
|
||
http_get() {
|
||
local url="$1"
|
||
local output="$2"
|
||
if command -v curl >/dev/null 2>&1; then
|
||
curl -fsSL "$url" -o "$output"
|
||
elif command -v wget >/dev/null 2>&1; then
|
||
wget -qO "$output" "$url"
|
||
else
|
||
fail '需要 curl 或 wget 才能下载部署文件'
|
||
fi
|
||
}
|
||
|
||
extract_zip() {
|
||
local zip_file="$1"
|
||
local target_dir="$2"
|
||
|
||
mkdir -p "$target_dir"
|
||
|
||
if command -v unzip >/dev/null 2>&1; then
|
||
unzip -oq "$zip_file" -d "$target_dir"
|
||
return 0
|
||
fi
|
||
|
||
if command -v python3 >/dev/null 2>&1; then
|
||
python3 - <<PY
|
||
import zipfile
|
||
zipfile.ZipFile(r'''$zip_file''').extractall(r'''$target_dir''')
|
||
PY
|
||
return 0
|
||
fi
|
||
|
||
if command -v python >/dev/null 2>&1; then
|
||
python - <<PY
|
||
import zipfile
|
||
zipfile.ZipFile(r'''$zip_file''').extractall(r'''$target_dir''')
|
||
PY
|
||
return 0
|
||
fi
|
||
|
||
fail '无法解压 zip:缺少 unzip / python3 / python'
|
||
}
|
||
|
||
prepare_dir() {
|
||
if [ -e "$INSTALL_DIR" ]; then
|
||
if [ "$FORCE" = "true" ]; then
|
||
rm -rf "$INSTALL_DIR"
|
||
else
|
||
fail "目录已存在:$INSTALL_DIR;如需覆盖请加 --force"
|
||
fi
|
||
fi
|
||
|
||
mkdir -p "$INSTALL_DIR/data/logos"
|
||
}
|
||
|
||
release_asset_url() {
|
||
local asset_name="$1"
|
||
if [ "$RELEASE_TAG" = "latest" ]; then
|
||
printf 'https://github.com/%s/%s/releases/latest/download/%s' "$REPO_OWNER" "$REPO_NAME" "$asset_name"
|
||
else
|
||
printf 'https://github.com/%s/%s/releases/download/%s/%s' "$REPO_OWNER" "$REPO_NAME" "$RELEASE_TAG" "$asset_name"
|
||
fi
|
||
}
|
||
|
||
write_env_file() {
|
||
{
|
||
printf 'SUBTRACKER_API_IMAGE=%s\n' "$API_IMAGE"
|
||
printf 'PORT=%s\n' "$API_PORT"
|
||
printf 'HOST=0.0.0.0\n'
|
||
printf 'DATABASE_URL=file:/app/data/subtracker.db\n'
|
||
printf 'WEB_ORIGIN=%s\n' "$WEB_ORIGIN"
|
||
printf 'LOG_LEVEL=%s\n' "$LOG_LEVEL"
|
||
if [ "$MODE" = "full" ]; then
|
||
printf 'WEB_PORT=%s\n' "$WEB_PORT"
|
||
fi
|
||
} > "$INSTALL_DIR/.env"
|
||
}
|
||
|
||
write_readme() {
|
||
local compose_file="docker-compose.yml"
|
||
local pull_cmd='docker compose pull'
|
||
local up_cmd='docker compose up -d'
|
||
local logs_cmd='docker compose logs -f api'
|
||
|
||
if [ "$MODE" = "full" ]; then
|
||
compose_file='docker-compose.full.yml'
|
||
pull_cmd='docker compose -f docker-compose.full.yml pull'
|
||
up_cmd='docker compose -f docker-compose.full.yml up -d'
|
||
logs_cmd='docker compose -f docker-compose.full.yml logs -f api'
|
||
fi
|
||
|
||
cat > "$INSTALL_DIR/INSTALL-README.md" <<EOF
|
||
# SubTracker ${MODE} 部署目录
|
||
|
||
此目录由 install.sh 自动生成。
|
||
|
||
## 已准备好的内容
|
||
|
||
- ${compose_file}
|
||
- .env
|
||
- data/
|
||
- data/logos/
|
||
EOF
|
||
|
||
if [ "$MODE" = "full" ]; then
|
||
cat >> "$INSTALL_DIR/INSTALL-README.md" <<EOF
|
||
- docker/nginx.full.conf
|
||
- web-dist/
|
||
EOF
|
||
else
|
||
cat >> "$INSTALL_DIR/INSTALL-README.md" <<EOF
|
||
|
||
## 你还需要自行处理的内容
|
||
|
||
当前是 API-only 模式,脚本**不会**帮你托管前端静态文件。
|
||
请把 SubTracker 前端静态文件部署到你自己的 Nginx:
|
||
|
||
- Release 资产:subtracker-web-dist.zip
|
||
- 下载地址:$(release_asset_url 'subtracker-web-dist.zip')
|
||
|
||
你可以把它解压到你的 Nginx 网站根目录,然后按 DEPLOYMENT.md 里的反代配置把 /api/ 和 /static/logos/ 转给 API。
|
||
EOF
|
||
fi
|
||
|
||
cat >> "$INSTALL_DIR/INSTALL-README.md" <<EOF
|
||
|
||
## 反代 / SSL 说明
|
||
|
||
- 如果你外层还会再套一层 Nginx / 宝塔 / HTTPS 证书:
|
||
- `WEB_ORIGIN` 请填写用户最终访问地址
|
||
- 例如:`https://subtracker.example.com`
|
||
- 不要把 `WEB_ORIGIN` 填成:
|
||
- `http://127.0.0.1:${API_PORT}`
|
||
- `http://api:${API_PORT}`
|
||
- 容器内部地址
|
||
|
||
EOF
|
||
|
||
if [ "$MODE" = "api" ]; then
|
||
cat >> "$INSTALL_DIR/INSTALL-README.md" <<EOF
|
||
API-only 模式常见链路:
|
||
|
||
- 浏览器 -> https://你的域名
|
||
- 外层 Nginx -> http://127.0.0.1:${API_PORT} (API)
|
||
- 前端静态文件 -> 由你自己的 Nginx 托管
|
||
|
||
EOF
|
||
else
|
||
cat >> "$INSTALL_DIR/INSTALL-README.md" <<EOF
|
||
Full 模式常见链路:
|
||
|
||
- 浏览器 -> https://你的域名
|
||
- 外层 Nginx -> http://127.0.0.1:${WEB_PORT}
|
||
- Full 自带 Nginx -> API 容器
|
||
|
||
EOF
|
||
fi
|
||
|
||
cat >> "$INSTALL_DIR/INSTALL-README.md" <<EOF
|
||
|
||
## 启动
|
||
|
||
cd ${INSTALL_DIR}
|
||
${pull_cmd}
|
||
${up_cmd}
|
||
|
||
## 查看日志
|
||
|
||
cd ${INSTALL_DIR}
|
||
${logs_cmd}
|
||
EOF
|
||
}
|
||
|
||
download_bundle() {
|
||
local bundle_zip="$INSTALL_DIR/subtracker-deploy-bundle.zip"
|
||
local bundle_url
|
||
bundle_url="$(release_asset_url 'subtracker-deploy-bundle.zip')"
|
||
info "下载部署包:$bundle_url"
|
||
http_get "$bundle_url" "$bundle_zip"
|
||
|
||
local extract_dir="$INSTALL_DIR/.bundle-tmp"
|
||
extract_zip "$bundle_zip" "$extract_dir"
|
||
|
||
local root_dir="$extract_dir/subtracker-deploy"
|
||
[ -d "$root_dir" ] || fail '部署包结构不符合预期,缺少 subtracker-deploy/'
|
||
|
||
cp "$root_dir/DEPLOYMENT.md" "$INSTALL_DIR/"
|
||
cp "$root_dir/README.md" "$INSTALL_DIR/"
|
||
|
||
if [ -f "$root_dir/api.env.example" ]; then
|
||
cp "$root_dir/api.env.example" "$INSTALL_DIR/"
|
||
fi
|
||
|
||
if [ "$MODE" = "full" ]; then
|
||
cp "$root_dir/docker-compose.full.yml" "$INSTALL_DIR/"
|
||
mkdir -p "$INSTALL_DIR/docker"
|
||
cp "$root_dir/docker/nginx.full.conf" "$INSTALL_DIR/docker/"
|
||
else
|
||
cp "$root_dir/docker-compose.yml" "$INSTALL_DIR/"
|
||
fi
|
||
|
||
rm -rf "$extract_dir" "$bundle_zip"
|
||
}
|
||
|
||
download_web_dist_if_needed() {
|
||
[ "$MODE" = "full" ] || return 0
|
||
|
||
local web_zip="$INSTALL_DIR/subtracker-web-dist.zip"
|
||
local web_url
|
||
web_url="$(release_asset_url 'subtracker-web-dist.zip')"
|
||
info "下载前端静态包:$web_url"
|
||
http_get "$web_url" "$web_zip"
|
||
|
||
extract_zip "$web_zip" "$INSTALL_DIR/web-dist"
|
||
rm -f "$web_zip"
|
||
}
|
||
|
||
show_summary() {
|
||
local compose_cmd='docker compose'
|
||
|
||
if [ "$MODE" = "full" ]; then
|
||
compose_cmd='docker compose -f docker-compose.full.yml'
|
||
fi
|
||
|
||
printf '\n'
|
||
info "部署目录已生成:$INSTALL_DIR"
|
||
info "部署模式:$MODE"
|
||
info "Release 版本:$RELEASE_TAG"
|
||
info "安装脚本已执行完成,建议按下面步骤继续:"
|
||
|
||
printf '\n'
|
||
printf '1) 进入部署目录并检查环境变量\n'
|
||
printf ' cd %s\n' "$INSTALL_DIR"
|
||
printf ' 查看并按需修改 .env\n'
|
||
|
||
printf '\n'
|
||
printf '2) 拉取镜像并启动\n'
|
||
printf ' %s pull\n' "$compose_cmd"
|
||
printf ' %s up -d\n' "$compose_cmd"
|
||
|
||
printf '\n'
|
||
printf '3) 查看日志\n'
|
||
printf ' %s logs -f api\n' "$compose_cmd"
|
||
|
||
if [ "$MODE" = "api" ]; then
|
||
printf '\n'
|
||
warn '当前是 API-only 模式:前端静态文件需要你自己放到 Nginx。'
|
||
warn "可直接下载:$(release_asset_url 'subtracker-web-dist.zip')"
|
||
warn '你还需要做这两件事:'
|
||
warn ' 1. 把 subtracker-web-dist.zip 解压到你的 Nginx 网站根目录'
|
||
warn ' 2. 按 DEPLOYMENT.md 的示例,把 /api/ 和 /static/logos/ 反代到 API'
|
||
else
|
||
printf '\n'
|
||
info "Full 模式下前端静态文件已准备在:$INSTALL_DIR/web-dist"
|
||
info "如果你外层还会再套一层 Nginx / HTTPS,请把它反代到 http://127.0.0.1:$WEB_PORT"
|
||
fi
|
||
|
||
printf '\n'
|
||
info "如果使用反向代理 / SSL,WEB_ORIGIN 应填写用户最终访问地址,例如 https://subtracker.example.com"
|
||
info "更详细的说明见:$INSTALL_DIR/INSTALL-README.md"
|
||
}
|
||
|
||
main() {
|
||
parse_args "$@"
|
||
need_cmd mkdir
|
||
need_cmd cp
|
||
need_cmd rm
|
||
normalize_inputs
|
||
prepare_dir
|
||
download_bundle
|
||
download_web_dist_if_needed
|
||
write_env_file
|
||
write_readme
|
||
show_summary
|
||
}
|
||
|
||
main "$@"
|