Ctrl K
ComunidadGuías…

start.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/sh
echo "=========================================="
echo "  NOWEN - Starting..."
echo "=========================================="
echo "Architecture: $(uname -m)"
echo ""

# ============================================================
# 🔑 防呆自动配置 — 自动检测 Docker 环境并设置关键环境变量
# ============================================================
# 绿联/飞牛等 NAS 的 Docker GUI 用户通常不会手动设置环境变量
# 所以我们在启动时自动检测并设置,确保即使什么都不配也能正常工作

# 自动设置 NODE_ENV=production(如果未设置且检测到在 Docker 容器内)
if [ -z "${NODE_ENV}" ]; then
  if [ -f "/.dockerenv" ] || [ -f "/app/start.sh" ] || grep -qsE 'docker|containerd|kubepods' /proc/1/cgroup 2>/dev/null; then
    export NODE_ENV=production
    echo "🔧 Auto-set NODE_ENV=production (Docker detected)"
  fi
fi

# 自动设置 SI_FILESYSTEM_DISK_PREFIX(如果未设置且 /host 挂载存在)
if [ -z "${SI_FILESYSTEM_DISK_PREFIX}" ] && [ -d "/host/proc" ]; then
  export SI_FILESYSTEM_DISK_PREFIX=/host
  echo "🔧 Auto-set SI_FILESYSTEM_DISK_PREFIX=/host"
fi

# 自动设置 PROC_PATH(如果未设置且 /host/proc 存在)
if [ -z "${PROC_PATH}" ] && [ -d "/host/proc" ]; then
  export PROC_PATH=/host/proc
  echo "🔧 Auto-set PROC_PATH=/host/proc"
fi

# 自动设置 SYS_PATH(如果未设置且 /host/sys 存在)
if [ -z "${SYS_PATH}" ] && [ -d "/host/sys" ]; then
  export SYS_PATH=/host/sys
  echo "🔧 Auto-set SYS_PATH=/host/sys"
fi

# 自动设置 FS_PATH(如果未设置且 /host 存在)
if [ -z "${FS_PATH}" ] && [ -d "/host" ]; then
  export FS_PATH=/host
  echo "🔧 Auto-set FS_PATH=/host"
fi

echo "   NODE_ENV=${NODE_ENV:-not set}"
echo ""

# ============================================================
# 🔑 数据持久化 — 终极防呆设计(兼容绿联/飞牛等 NAS)
# ============================================================
# 
# ⚠️ 问题根因:
#   绿联/飞牛等 NAS 的 Docker GUI 会为每个容器创建独立的存储目录,
#   路径格式为: 共享文件夹/docker/<容器名>/app_server_data
#   当更新镜像重建容器时,容器名从 xxx-1 变成 xxx-2,
#   新的存储目录是空的,导致数据丢失。
#   更关键的是:如果 Dockerfile 声明了 VOLUME,NAS 会为 VOLUME
#   路径也创建独立目录,安全备份卷也一起丢失。
#
# 解决策略:
#   1. Dockerfile 中不声明 VOLUME(避免 NAS 自动创建新目录)
#   2. 安全备份放在 /app/.nowen-safe(镜像层内路径,NAS 不挂载)
#   3. 启动时扫描宿主机旧容器目录,自动找回数据
#   4. 同时兼容用户手动配置的 bind mount / named volume
# ============================================================

DATA_DIR="/app/server/data"
DB_FILE="${DATA_DIR}/zen-garden.db"
BACKUP_DIR="${DATA_DIR}/backups"
# 🔑 安全备份使用镜像层内路径(不是 VOLUME,NAS 不会挂载它)
SAFE_BACKUP_DIR="/app/.nowen-safe"
SAFE_DB_FILE="${SAFE_BACKUP_DIR}/zen-garden.db.safe"

# 确保目录存在
mkdir -p "${BACKUP_DIR}"
mkdir -p "${SAFE_BACKUP_DIR}"

# ============================================================
# 第一步:挂载检测
# ============================================================
check_mount() {
  MOUNT_MARKER="${DATA_DIR}/.mount-marker"
  
  if mount | grep -q "/app/server/data"; then
    MOUNT_TYPE=$(mount | grep "/app/server/data" | head -1)
    echo "✅ Data volume detected: mounted"
    echo "   Mount info: ${MOUNT_TYPE}"
    return 0
  else
    if [ -f "${MOUNT_MARKER}" ]; then
      echo "✅ Data volume detected: persistent (marker found)"
      return 0
    else
      echo "⚠️  WARNING: Data directory may not be properly mounted!"
      echo ""
      echo "┌───────────────────────────────────────────────────┐"
      echo "│  ⚠️  数据持久化警告 / Data Persistence Warning   │"
      echo "├───────────────────────────────────────────────────┤"
      echo "│                                                   │"
      echo "│  未检测到数据卷挂载。                              │"
      echo "│  NOWEN 会自动在镜像内保留备份,但强烈建议         │"
      echo "│  在 NAS Docker 设置中手动指定宿主机存储路径:      │"
      echo "│                                                   │"
      echo "│  宿主机路径(固定) → /app/server/data               │"
      echo "│                                                   │"
      echo "└───────────────────────────────────────────────────┘"
      echo ""
      echo "$(date -Iseconds)" > "${MOUNT_MARKER}"
      return 1
    fi
  fi
}

# ============================================================
# 第二步:数据恢复(优先级从高到低)
# ============================================================
recover_database() {
  if [ -f "${DB_FILE}" ]; then
    DB_SIZE=$(ls -lh "${DB_FILE}" | awk '{print $5}')
    echo "✅ Database found: zen-garden.db (${DB_SIZE})"
    
    # 启动前备份到 data/backups/
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    cp "${DB_FILE}" "${BACKUP_DIR}/zen-garden_${TIMESTAMP}.db.bak"
    echo "   Auto backup: zen-garden_${TIMESTAMP}.db.bak"
    
    # 同步到镜像层内的安全备份
    cp "${DB_FILE}" "${SAFE_DB_FILE}"
    echo "   Safe backup synced to image layer"
    
    # 清理旧备份,只保留最近 5 个
    cd "${BACKUP_DIR}" && ls -1t *.db.bak 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null
    cd /app
    return 0
  fi

  echo "⚠️  Database not found in data directory!"
  echo ""

  # ============================================================
  # 恢复源 1: 镜像层内安全备份(/app/.nowen-safe/)
  # 这个路径在镜像层内,NAS 不会为它创建新目录
  # ============================================================
  if [ -f "${SAFE_DB_FILE}" ]; then
    SAFE_SIZE=$(ls -lh "${SAFE_DB_FILE}" | awk '{print $5}')
    echo "🔄 Found safe backup in image layer (${SAFE_SIZE}), restoring..."
    cp "${SAFE_DB_FILE}" "${DB_FILE}"
    if [ -f "${DB_FILE}" ]; then
      echo "✅ Database restored from image-layer safe backup!"
      return 0
    fi
  fi
  
  # ============================================================
  # 恢复源 1.5: 兼容旧版安全备份路径(/app/.data-backup/)
  # ============================================================
  OLD_SAFE_FILE="/app/.data-backup/zen-garden.db.safe"
  if [ -f "${OLD_SAFE_FILE}" ]; then
    OLD_SAFE_SIZE=$(ls -lh "${OLD_SAFE_FILE}" | awk '{print $5}')
    echo "🔄 Found legacy safe backup (${OLD_SAFE_SIZE}), restoring..."
    cp "${OLD_SAFE_FILE}" "${DB_FILE}"
    if [ -f "${DB_FILE}" ]; then
      echo "✅ Database restored from legacy safe backup!"
      return 0
    fi
  fi

  # ============================================================
  # 恢复源 2: data/backups/ 目录
  # ============================================================
  LATEST_BACKUP=$(ls -1t "${BACKUP_DIR}"/*.db.bak 2>/dev/null | head -1)
  if [ -n "${LATEST_BACKUP}" ]; then
    echo "🔄 Found backup: ${LATEST_BACKUP}"
    echo "   Restoring from backup..."
    cp "${LATEST_BACKUP}" "${DB_FILE}"
    if [ -f "${DB_FILE}" ]; then
      echo "✅ Database restored from backup!"
      return 0
    fi
  fi

  # ============================================================
  # 恢复源 3: 扫描宿主机常见路径 + 绿联/飞牛旧容器目录
  # 这是关键!绿联 NAS 的 Docker 数据存储在宿主机的
  # /docker/<容器名>/ 目录下,通过 /host 只读挂载可以访问
  # ============================================================
  echo "🔍 Scanning host filesystem for existing NOWEN data..."
  
  # 3a: 扫描绿联/飞牛等 NAS 的旧容器目录
  # 绿联挂载格式: 共享文件夹/docker/<容器名>/app_server_data/zen-garden.db
  # 在 /host 挂载下查找
  if [ -d "/host" ]; then
    echo "   Scanning NAS old container directories..."
    # 搜索所有可能的 docker 目录
    for DOCKER_BASE in \
      "/host/docker" \
      "/host/DATA/docker" \
      "/host/volume1/docker" \
      "/host/volume2/docker" \
      "/host/mnt/docker" \
      "/host/opt/docker" \
      "/host/home/docker" \
      "/host/mnt/user1/docker" \
      "/host/mnt/media_rw"; do
      if [ -d "${DOCKER_BASE}" ]; then
        # 搜索该 docker 目录下所有包含 nowen 的容器目录
        for CONTAINER_DIR in $(find "${DOCKER_BASE}" -maxdepth 2 -type d -iname "*nowen*" 2>/dev/null); do
          # 搜索该容器目录下的所有 zen-garden.db 文件
          for DB_FOUND in $(find "${CONTAINER_DIR}" -name "zen-garden.db" -type f 2>/dev/null); do
            FOUND_SIZE=$(ls -lh "${DB_FOUND}" 2>/dev/null | awk '{print $5}')
            # 验证文件大小(大于 100 字节才是有效数据库)
            FOUND_BYTES=$(stat -c%s "${DB_FOUND}" 2>/dev/null || stat -f%z "${DB_FOUND}" 2>/dev/null || echo "0")
            if [ "${FOUND_BYTES}" -gt 100 ] 2>/dev/null; then
              echo "🎉 Found database in old container: ${DB_FOUND} (${FOUND_SIZE})"
              cp "${DB_FOUND}" "${DB_FILE}" 2>/dev/null
              if [ -f "${DB_FILE}" ]; then
                echo "✅ Database restored from NAS old container directory!"
                # 同步到安全备份
                cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null
                return 0
              fi
            fi
          done
        done
        
        # 也搜索非 nowen 名称但包含 cropflre 的目录
        for CONTAINER_DIR in $(find "${DOCKER_BASE}" -maxdepth 2 -type d -iname "*cropflre*" 2>/dev/null); do
          for DB_FOUND in $(find "${CONTAINER_DIR}" -name "zen-garden.db" -type f 2>/dev/null); do
            FOUND_SIZE=$(ls -lh "${DB_FOUND}" 2>/dev/null | awk '{print $5}')
            FOUND_BYTES=$(stat -c%s "${DB_FOUND}" 2>/dev/null || stat -f%z "${DB_FOUND}" 2>/dev/null || echo "0")
            if [ "${FOUND_BYTES}" -gt 100 ] 2>/dev/null; then
              echo "🎉 Found database in old container: ${DB_FOUND} (${FOUND_SIZE})"
              cp "${DB_FOUND}" "${DB_FILE}" 2>/dev/null
              if [ -f "${DB_FILE}" ]; then
                echo "✅ Database restored from NAS old container directory!"
                cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null
                return 0
              fi
            fi
          done
        done
      fi
    done
    
    # 3b: 扫描传统绑定挂载路径
    for SCAN_PATH in \
      "/host/volume1/docker/nowen/data/zen-garden.db" \
      "/host/volume2/docker/nowen/data/zen-garden.db" \
      "/host/DATA/docker/nowen/data/zen-garden.db" \
      "/host/mnt/docker/nowen/data/zen-garden.db" \
      "/host/opt/docker/nowen/data/zen-garden.db" \
      "/host/home/docker/nowen/data/zen-garden.db" \
      "/host/root/nowen-data/zen-garden.db" \
      "/host/home/nowen-data/zen-garden.db"; do
      if [ -f "${SCAN_PATH}" ]; then
        FOUND_SIZE=$(ls -lh "${SCAN_PATH}" | awk '{print $5}')
        echo "🎉 Found existing database at: ${SCAN_PATH} (${FOUND_SIZE})"
        cp "${SCAN_PATH}" "${DB_FILE}"
        if [ -f "${DB_FILE}" ]; then
          echo "✅ Database restored from host filesystem!"
          cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null
          return 0
        fi
      fi
    done
    
    # 3c: 最后的搜索 — 全局搜索 /host 下所有 zen-garden.db(限时 30 秒)
    echo "   Deep scanning for zen-garden.db on host (timeout 30s)..."
    DEEP_FOUND=$(timeout 30 find /host -name "zen-garden.db" -type f -size +100c 2>/dev/null | head -5)
    for DB_FOUND in ${DEEP_FOUND}; do
      FOUND_SIZE=$(ls -lh "${DB_FOUND}" 2>/dev/null | awk '{print $5}')
      echo "🎉 Found database: ${DB_FOUND} (${FOUND_SIZE})"
      cp "${DB_FOUND}" "${DB_FILE}" 2>/dev/null
      if [ -f "${DB_FILE}" ]; then
        echo "✅ Database restored from deep host scan!"
        cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null
        return 0
      fi
    done
  fi

  # ============================================================
  # 恢复源 4: Docker 卷扫描
  # ============================================================
  if command -v docker > /dev/null 2>&1; then
    echo "🔍 Scanning Docker volumes for orphaned data..."
    for VOL in $(docker volume ls -q 2>/dev/null); do
      INSPECT_PATH=$(docker volume inspect "${VOL}" --format '{{ .Mountpoint }}' 2>/dev/null)
      if [ -n "${INSPECT_PATH}" ] && [ -f "${INSPECT_PATH}/zen-garden.db" ]; then
        FOUND_SIZE=$(ls -lh "${INSPECT_PATH}/zen-garden.db" 2>/dev/null | awk '{print $5}')
        echo "🎉 Found orphaned data in volume '${VOL}' (${FOUND_SIZE})"
        cp "${INSPECT_PATH}/zen-garden.db" "${DB_FILE}" 2>/dev/null
        if [ -f "${DB_FILE}" ]; then
          echo "✅ Database restored from orphaned Docker volume!"
          cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null
          return 0
        fi
      fi
    done
  fi

  echo "📝 No existing data found anywhere — creating fresh database"
  echo ""
  return 1
}

# ============================================================
# 第三步:运行时持续备份(后台进程)
# ============================================================
start_safe_backup_daemon() {
  # 每 3 分钟将 db 同步到镜像层内的安全备份
  while true; do
    sleep 180
    if [ -f "${DB_FILE}" ]; then
      cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null
      # 也尝试备份到旧路径(兼容已挂载 /app/.data-backup 的用户)
      if [ -d "/app/.data-backup" ]; then
        cp "${DB_FILE}" "/app/.data-backup/zen-garden.db.safe" 2>/dev/null
      fi
    fi
  done &
  BACKUP_DAEMON_PID=$!
  echo "   Safe backup daemon: PID ${BACKUP_DAEMON_PID} (every 3min)"
}

# ============================================================
# 第四步:优雅停止时保存
# ============================================================
graceful_shutdown() {
  echo ""
  echo "🛑 Shutting down NOWEN..."
  
  # 同步最新数据到安全备份(镜像层内)
  if [ -f "${DB_FILE}" ]; then
    cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null
    echo "✅ Final safe backup saved to image layer"
    # 也备份到旧路径
    if [ -d "/app/.data-backup" ]; then
      cp "${DB_FILE}" "/app/.data-backup/zen-garden.db.safe" 2>/dev/null
    fi
  fi
  
  # 停止后台进程
  kill "${BACKUP_DAEMON_PID}" 2>/dev/null
  kill "${BACKEND_PID}" 2>/dev/null
  
  # 停止 nginx
  nginx -s quit 2>/dev/null
  
  echo "👋 NOWEN stopped"
  exit 0
}

# 注册信号处理
trap graceful_shutdown SIGTERM SIGINT

# ============================================================
# 执行启动流程
# ============================================================

echo "--- Data Persistence Check ---"
check_mount
echo ""

echo "--- Database Recovery ---"
recover_database
RECOVERY_RESULT=$?
echo ""

# 显示数据目录内容
echo "📁 Data directory contents:"
ls -la "${DATA_DIR}/" 2>/dev/null
echo ""

# 启动安全备份守护进程
start_safe_backup_daemon

# 启动后端
echo "Starting backend on port 3001..."
cd /app/server && tsx src/index.ts &
BACKEND_PID=$!
sleep 2

# 启动后再次同步(initDatabase 可能创建了新库)
if [ -f "${DB_FILE}" ]; then
  cp "${DB_FILE}" "${SAFE_DB_FILE}" 2>/dev/null
fi

# 启动 nginx(前台运行)
echo "Starting nginx on port 3000..."
nginx -g "daemon off;" &
NGINX_PID=$!

# 等待任意子进程退出
wait ${NGINX_PID} ${BACKEND_PID}