Files
SubTracker/scripts/install.sh

465 lines
12 KiB
Bash
Raw Permalink 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.
#!/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 "如果使用反向代理 / SSLWEB_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 "$@"