#!/usr/bin/env bash # ============================================================ # 统一部署脚本:将本地产物同步到正式服务器(rsync over ssh) # # 覆盖两类目标(统一 ssh 别名 luxsin-api2,按顶层目录自动路由远程路径): # dashboard/ -> /data/project/dashboard/ (frontend / backend / compose) # www/ -> /data/project/dashboard/www/ (默认仅 .output/;nginx.conf / docker-compose.yml 需显式指定) # # 用法:./deploy/upload.sh [选项] [目标|路径...] # ============================================================ set -euo pipefail # ===== 连接(统一使用 ssh 别名) ===== SERVER="luxsin-api2" REMOTE_ROOT="/data/project/dashboard" # www 静态产物位于 ${REMOTE_ROOT}/www WWW_REMOTE_ROOT="${REMOTE_ROOT}/www" PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" FRONTEND_PATHS=( ./dashboard/frontend/dist/ ) BACKEND_PATHS=( ./dashboard/backend/dist/ ./dashboard/backend/package.json ./dashboard/backend/pnpm-lock.yaml ./dashboard/backend/pnpm-workspace.yaml ) COMPOSE_PATHS=( ./dashboard/docker-compose.yml ) WWW_PATHS=( ./www/.output/ ) DEFAULT_PATHS=( "${FRONTEND_PATHS[@]}" "${BACKEND_PATHS[@]}" "${COMPOSE_PATHS[@]}" ) GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $1"; } ok() { echo -e "${GREEN}[OK]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } show_help() { cat < ${SERVER}:${REMOTE_ROOT}/ www/ -> ${SERVER}:${WWW_REMOTE_ROOT}/ 选项: -n 虚拟执行,不实际传输或删除远程文件 -h, --help 显示此帮助信息 目标: frontend, f 同步 dashboard 前端 dist/(默认跳过 uploads/) backend, b 同步 dashboard 后端 dist/、package.json、pnpm-lock.yaml compose, c 同步 dashboard docker-compose.yml www, w 同步 www 构建产物 .output/(nginx.conf / docker-compose.yml 需显式指定路径) all, a 同步 frontend + backend + compose(不含 www) (路径) 同步指定文件或目录,可传多个 默认不同步 nginx.conf 与 Dockerfile(dashboard),需要时请显式指定,例如: ./dashboard/frontend/nginx.conf ./dashboard/backend/Dockerfile www 同步前需先本地构建: cd www && pnpm generate 示例: $(basename "$0") 同步 dashboard 默认内容 $(basename "$0") frontend 仅同步 dashboard 前端 $(basename "$0") www 仅同步 www .output/ $(basename "$0") www ./www/nginx.conf ./www/docker-compose.yml 同步 www 配置 $(basename "$0") backend www 同步后端与 www $(basename "$0") all www 同步全部(dashboard + www) $(basename "$0") -n www 虚拟执行,预览 www 同步 EOF } resolve_preset_paths() { local preset="$1" case "$preset" in frontend|f) printf '%s\n' "${FRONTEND_PATHS[@]}" ;; backend|b) printf '%s\n' "${BACKEND_PATHS[@]}" ;; compose|c) printf '%s\n' "${COMPOSE_PATHS[@]}" ;; www|w) printf '%s\n' "${WWW_PATHS[@]}" ;; all|a) printf '%s\n' "${DEFAULT_PATHS[@]}" ;; *) echo "$preset" ;; esac } # www 静态产物同步前校验本地已构建(nuxt generate) ensure_www_built() { local idx="${PROJECT_ROOT}/www/.output/public/index.html" if [[ ! -f "$idx" ]]; then echo "❌ 本地缺少 ${idx},请先执行:" echo " cd ${PROJECT_ROOT}/www && pnpm generate" exit 1 fi } rsync_item() { local item="$1" local rel local_full if [[ "$item" == /* ]]; then if [[ "$item" == "$PROJECT_ROOT"/* ]]; then rel="${item#"$PROJECT_ROOT"/}" local_full="$item" else warn "路径不在项目内,跳过: $item" return 1 fi else rel="${item#./}" rel="${rel%/}" local_full="${PROJECT_ROOT}/${rel}" fi # 依据顶层目录映射远程路径(dashboard/ 内容在远程根,www/ 内容在远程 www 子目录) local sub if [[ "$rel" == dashboard/* ]]; then sub="${rel#dashboard/}" elif [[ "$rel" == www/* ]]; then sub="$rel" else warn "路径不在 dashboard/ 或 www/ 下,跳过: $item" return 1 fi local remote_path="${REMOTE_ROOT}/${sub}" if [[ ! -e "$local_full" ]]; then warn "路径不存在: ${local_full}" return 1 fi local rsync_opts=(-avz --progress) if [[ "$DRY_RUN" = true ]]; then rsync_opts+=(-n) fi # dashboard 前端 dist 同步默认跳过 uploads/(服务器挂载的上传目录,避免 --delete 误删) if [[ "$sub" == "frontend/dist" ]]; then rsync_opts+=(--exclude '/uploads/') fi if [[ -d "$local_full" ]]; then if [[ "$DRY_RUN" = true ]]; then info "预览目录: ${rel}/ -> ${SERVER}:${remote_path}/" else info "同步目录: ${rel}/ -> ${SERVER}:${remote_path}/" fi rsync "${rsync_opts[@]}" --delete \ -e ssh \ "${local_full}/" "${SERVER}:${remote_path}/" else if [[ "$DRY_RUN" = true ]]; then info "预览文件: ${rel} -> ${SERVER}:${remote_path}" else info "同步文件: ${rel} -> ${SERVER}:${remote_path}" ssh "$SERVER" "mkdir -p $(dirname "$remote_path")" fi rsync "${rsync_opts[@]}" \ -e ssh \ "$local_full" "${SERVER}:${remote_path}" fi } sync_paths() { local paths=("$@") local item cd "$PROJECT_ROOT" for item in "${paths[@]}"; do rsync_item "$item" done } DRY_RUN=false ARGS=() for arg in "$@"; do case "$arg" in -n) DRY_RUN=true ;; -h|--help) show_help exit 0 ;; *) ARGS+=("$arg") ;; esac done PATHS=() if [[ ${#ARGS[@]} -eq 0 ]]; then PATHS=("${DEFAULT_PATHS[@]}") else for arg in "${ARGS[@]}"; do while IFS= read -r path; do PATHS+=("$path") done < <(resolve_preset_paths "$arg") done fi # 若包含 www .output 产物,先校验本地已构建 for p in "${PATHS[@]}"; do rel="${p#./}" if [[ "$rel" == www/.output || "$rel" == www/.output/* ]]; then ensure_www_built break fi done if [[ "$DRY_RUN" = true ]]; then info "虚拟同步(dashboard -> ${SERVER}:${REMOTE_ROOT}/ ;www -> ${SERVER}:${WWW_REMOTE_ROOT}/)" else info "同步(dashboard -> ${SERVER}:${REMOTE_ROOT}/ ;www -> ${SERVER}:${WWW_REMOTE_ROOT}/)" fi sync_paths "${PATHS[@]}" # www 部署后续提示 for p in "${PATHS[@]}"; do rel="${p#./}" if [[ "$rel" == www/* ]]; then echo "" ok "www 产物已上传,下一步(在服务器上执行):" echo " cd ${WWW_REMOTE_ROOT} && docker compose up -d" break fi done echo "" ok "完成!"