diff --git a/README.md b/README.md index bfaaa63..bcd381e 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,29 @@ ## 脚本清单 +### 部署与管理 + | 脚本 | 用途 | 目标环境 | |------|------|---------| | `sites-manage-local.sh` | 本地站点启停(npm run dev + Docker) | 本地 | | `sites-manage-cloud.sh` | 云服务器站点启停(SSH / 服务器本地运行) | 云端 | | `deploy-ai.sh` | 部署站点到 tlyq.ai(txjp 服务器) | 云端 | | `deploy-cc.sh` | 部署站点到 tlyq.cc(tgz 服务器) | 云端 | -| `backup-db.sh` | 备份数据库(assets / issue / ldap) | 云端 → 本地 | -| `restore-db.sh` | 恢复数据库到本地或云端 | 本地 / 云端 | | `edit-site-cc.sh` | www.tlyq.cc 官网内容管理 | 云端 | +### 备份与恢复(`backup/` 目录) + +| 脚本 | 用途 | 说明 | +|------|------|------| +| `backup/backup-sites.sh` | 站点独立备份 | **推荐**:按站点独立打包 | +| `backup/backup-db.sh` | 仅数据库备份 | 轻量快速 | +| `backup/backup-full.sh` | 完整灾难恢复备份 | 包含所有数据 | +| `backup/restore-sites.sh` | 站点恢复 | 交互式选择恢复 | +| `backup/restore-db.sh` | 数据库恢复 | 恢复单个数据库 | +| `backup/restore-full.sh` | 完整恢复 | 灾难恢复使用 | + +> 详细文档见 [backup/README.md](backup/README.md) + --- ## sites-manage-local.sh — 本地站点管理 @@ -69,36 +82,26 @@ bash scripts/deploy-ai.sh --restart # 仅重启容器 LDAP 部署特殊处理:上传 Dockerfile + docker-compose.yml,自动创建 .env,去除端口映射,`--build` 构建含 sqlite3 的镜像。 -## backup-db.sh — 数据库备份 +## backup/ — 备份与恢复 ```bash -bash scripts/backup-db.sh +# 站点独立备份(推荐) +bash scripts/backup/backup-sites.sh # 备份所有站点 +bash scripts/backup/backup-sites.sh assets issue # 备份指定站点 + +# 恢复 +bash scripts/backup/restore-sites.sh # 交互式恢复 +bash scripts/backup/restore-sites.sh 20260622_101858 # 指定时间戳 + +# 数据库单独备份 +bash scripts/backup/backup-db.sh ``` -一键备份三个数据库: +支持站点:`assets` / `issue` / `ldap` / `gitea` / `oa` -| 数据库 | 服务 | 内容 | -|--------|------|------| -| assets-{时间戳}.db | assets-ai | 设备资产 | -| issue-{时间戳}.db | issue-ai | 工单、配件 | -| ldap-{时间戳}.db | lldap | **全部用户账号** | +备份内容:数据库 + 上传文件 + 配置文件 + nginx + SSL 证书 -流程:WAL checkpoint → 云端打包 → 云端持久保留 → 下载到本地 → 完整性验证。 - -> 详细文档见 [BACKUP-RESTORE.md](../docs/BACKUP-RESTORE.md) - -## restore-db.sh — 数据库恢复 - -```bash -# 恢复云端数据库 -bash scripts/restore-db.sh cloud issue issue-20260518_1200.db -bash scripts/restore-db.sh cloud ldap ldap-20260518_1200.db - -# 恢复本地数据库 -bash scripts/restore-db.sh local assets assets-20260518_1200.db -``` - -支持服务:`assets` / `issue` / `ldap`。流程:检查 → 验证 → 确认 → 备份当前库 → 停止容器 → 替换 → 清理 WAL → 启动 → 验证。 +> 详细文档见 [backup/README.md](backup/README.md) ## 备份存储路径 @@ -106,7 +109,6 @@ bash scripts/restore-db.sh local assets assets-20260518_1200.db |------|------| | 云端持久备份 | `/root/docker/db-backups/` | | 本地备份 | `db-backups/` | -| 云端临时 | `/tmp/db-backup/`(完成后清理) | ## 依赖 diff --git a/backup/README.md b/backup/README.md new file mode 100644 index 0000000..28bde8c --- /dev/null +++ b/backup/README.md @@ -0,0 +1,121 @@ +# 备份与恢复脚本 + +云服务器站点数据备份和灾难恢复工具集。 + +## 目录结构 + +``` +scripts/backup/ +├── backup-db.sh # 数据库备份(轻量) +├── backup-full.sh # 完整灾难恢复备份 +├── backup-sites.sh # 站点独立备份(推荐) +├── restore-db.sh # 数据库恢复 +├── restore-full.sh # 完整恢复 +└── restore-sites.sh # 站点恢复 +``` + +## 备份策略对比 + +| 脚本 | 内容 | 大小 | 用途 | +|------|------|------|------| +| backup-db.sh | 仅数据库 | ~3M | 日常快速备份 | +| backup-sites.sh | 数据库+配置+文件 | ~135M | **推荐**:按站点独立备份 | +| backup-full.sh | 全部数据 | ~135M | 完整灾难恢复 | + +## 使用方法 + +### 站点独立备份(推荐) + +```bash +# 备份所有站点 +bash scripts/backup/backup-sites.sh + +# 仅备份指定站点 +bash scripts/backup/backup-sites.sh assets issue + +# 可用站点:assets, issue, ldap, gitea, oa +``` + +### 恢复站点 + +```bash +# 交互式恢复(自动检测备份,选择站点) +bash scripts/backup/restore-sites.sh + +# 指定时间戳恢复 +bash scripts/backup/restore-sites.sh 20260622_101858 +``` + +### 数据库单独备份 + +```bash +# 仅备份数据库(快速) +bash scripts/backup/backup-db.sh +``` + +## 备份内容 + +### 各站点备份内容 + +| 站点 | 包含内容 | +|------|---------| +| assets | 数据库 + 上传文件 + docker-compose + .env + Dockerfile | +| issue | 数据库 + 上传文件 + 报告 + docker-compose + .env + Dockerfile | +| ldap | 数据库 + docker-compose + .env + Dockerfile | +| gitea | 数据库 + 代码仓库 + 配置 + docker-compose | +| oa | docker-compose + .env(无独立数据库) | + +### 共享资源备份 + +| 资源 | 内容 | +|------|------| +| nginx | 9 个反向代理配置文件 | +| ssl | Let's Encrypt 证书 | +| docker-network | webnet 网络配置 | + +## 备份存储位置 + +| 位置 | 路径 | +|------|------| +| 本地 | `/Users/niuniu/programs/docker/db-backups/` | +| 云端 | `/root/docker/db-backups/` | + +## 恢复流程 + +恢复脚本会自动处理: + +1. Docker 网络创建(webnet) +2. SSL 证书恢复 +3. Nginx 配置恢复并重载 +4. 各站点容器启动 +5. 数据库恢复 + +## 灾难恢复步骤 + +当云服务器完全不可用时,在新服务器上: + +```bash +# 1. 安装 Docker 和 docker-compose + +# 2. 下载备份文件到本地 +scp user@backup-server:/path/to/backup/*.tar.gz ./ + +# 3. 执行恢复 +bash scripts/backup/restore-sites.sh +``` + +## 自动备份 + +建议配置 crontab 定期执行: + +```bash +# 每天凌晨 3 点执行站点备份 +0 3 * * * cd /Users/niuniu/programs/docker && bash scripts/backup/backup-sites.sh >> /var/log/backup.log 2>&1 +``` + +## 注意事项 + +- 备份前会自动执行 WAL checkpoint,确保数据库完整性 +- 恢复会重启相关容器,短暂影响服务 +- Gitea 代码仓库备份较大(~130M),可考虑单独备份频率 +- 建议定期验证备份文件可恢复性 diff --git a/backup-db.sh b/backup/backup-db.sh similarity index 82% rename from backup-db.sh rename to backup/backup-db.sh index 9786943..1b1a343 100755 --- a/backup-db.sh +++ b/backup/backup-db.sh @@ -23,7 +23,8 @@ ssh txjp "docker exec lldap sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCA ssh txjp "mkdir -p /tmp/db-backup && \ docker cp assets-ai:/app/data/assets.db /tmp/db-backup/assets-${TIMESTAMP}.db && \ docker cp issue-ai:/app/data/issue.db /tmp/db-backup/issue-${TIMESTAMP}.db && \ - docker cp lldap:/data/users.db /tmp/db-backup/ldap-${TIMESTAMP}.db" + docker cp lldap:/data/users.db /tmp/db-backup/ldap-${TIMESTAMP}.db && \ + docker cp gitea-ai:/data/gitea.db /tmp/db-backup/gitea-${TIMESTAMP}.db" # 确保本地备份目录存在 mkdir -p "${LOCAL_DIR}" @@ -33,7 +34,8 @@ CLOUD_DIR="/root/docker/db-backups" ssh txjp "mkdir -p ${CLOUD_DIR} && \ cp /tmp/db-backup/assets-${TIMESTAMP}.db ${CLOUD_DIR}/assets-${TIMESTAMP}.db && \ cp /tmp/db-backup/issue-${TIMESTAMP}.db ${CLOUD_DIR}/issue-${TIMESTAMP}.db && \ - cp /tmp/db-backup/ldap-${TIMESTAMP}.db ${CLOUD_DIR}/ldap-${TIMESTAMP}.db" + cp /tmp/db-backup/ldap-${TIMESTAMP}.db ${CLOUD_DIR}/ldap-${TIMESTAMP}.db && \ + cp /tmp/db-backup/gitea-${TIMESTAMP}.db ${CLOUD_DIR}/gitea-${TIMESTAMP}.db" log "云端备份已保存至: ${CLOUD_DIR}" # 复制到本地 @@ -42,10 +44,11 @@ mkdir -p "${LOCAL_DIR}" scp "txjp:/tmp/db-backup/assets-${TIMESTAMP}.db" "${LOCAL_DIR}/" scp "txjp:/tmp/db-backup/issue-${TIMESTAMP}.db" "${LOCAL_DIR}/" scp "txjp:/tmp/db-backup/ldap-${TIMESTAMP}.db" "${LOCAL_DIR}/" +scp "txjp:/tmp/db-backup/gitea-${TIMESTAMP}.db" "${LOCAL_DIR}/" # 验证备份文件为有效 SQLite 数据库 log "验证备份完整性..." -for f in "${LOCAL_DIR}/assets-${TIMESTAMP}.db" "${LOCAL_DIR}/issue-${TIMESTAMP}.db" "${LOCAL_DIR}/ldap-${TIMESTAMP}.db"; do +for f in "${LOCAL_DIR}/assets-${TIMESTAMP}.db" "${LOCAL_DIR}/issue-${TIMESTAMP}.db" "${LOCAL_DIR}/ldap-${TIMESTAMP}.db" "${LOCAL_DIR}/gitea-${TIMESTAMP}.db"; do if ! sqlite3 "$f" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then echo "[错误] 备份文件无效或损坏: $f" exit 1 @@ -58,6 +61,6 @@ ssh txjp "rm -rf /tmp/db-backup" log "备份完成:${TIMESTAMP}" echo " 云端: ${CLOUD_DIR}/" -ssh txjp "ls -lh ${CLOUD_DIR}/assets-${TIMESTAMP}.db ${CLOUD_DIR}/issue-${TIMESTAMP}.db ${CLOUD_DIR}/ldap-${TIMESTAMP}.db 2>/dev/null" +ssh txjp "ls -lh ${CLOUD_DIR}/assets-${TIMESTAMP}.db ${CLOUD_DIR}/issue-${TIMESTAMP}.db ${CLOUD_DIR}/ldap-${TIMESTAMP}.db ${CLOUD_DIR}/gitea-${TIMESTAMP}.db 2>/dev/null" echo " 本地:" -ls -lh "${LOCAL_DIR}"/assets-${TIMESTAMP}.db "${LOCAL_DIR}"/issue-${TIMESTAMP}.db "${LOCAL_DIR}"/ldap-${TIMESTAMP}.db 2>/dev/null +ls -lh "${LOCAL_DIR}"/assets-${TIMESTAMP}.db "${LOCAL_DIR}"/issue-${TIMESTAMP}.db "${LOCAL_DIR}"/ldap-${TIMESTAMP}.db "${LOCAL_DIR}"/gitea-${TIMESTAMP}.db 2>/dev/null diff --git a/backup/backup-full.sh b/backup/backup-full.sh new file mode 100755 index 0000000..1636efa --- /dev/null +++ b/backup/backup-full.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# 完整灾难恢复备份脚本 +# 包含:数据库、代码仓库、用户数据、配置文件、SSL证书 +# 用法:bash backup-full.sh + +set -e + +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +LOCAL_DIR="/Users/niuniu/programs/docker/db-backups" +BACKUP_NAME="full-backup-${TIMESTAMP}" + +log() { echo "[$(date '+%H:%M:%S')] $1"; } +warn() { echo "[$(date '+%H:%M:%S')] [警告] $1"; } + +log "开始完整灾难恢复备份..." + +# ============================================================ +# 1. 数据库备份(WAL checkpoint + docker cp) +# ============================================================ +log "执行 WAL checkpoint..." +ssh txjp "docker exec assets-ai node -e \"const D=require('better-sqlite3');const db=new D('/app/data/assets.db');db.pragma('wal_checkpoint(TRUNCATE)');db.close();\"" 2>/dev/null || warn "assets checkpoint 失败" +ssh txjp "docker exec issue-ai node -e \"const D=require('better-sqlite3');const db=new D('/app/data/issue.db');db.pragma('wal_checkpoint(TRUNCATE)');db.close();\"" 2>/dev/null || warn "issue checkpoint 失败" +ssh txjp "docker exec lldap sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || warn "ldap checkpoint 失败" + +log "导出数据库..." +ssh txjp "mkdir -p /tmp/full-backup/databases && \ + docker cp assets-ai:/app/data/assets.db /tmp/full-backup/databases/assets.db && \ + docker cp issue-ai:/app/data/issue.db /tmp/full-backup/databases/issue.db && \ + docker cp lldap:/data/users.db /tmp/full-backup/databases/ldap.db && \ + docker cp gitea-ai:/data/gitea.db /tmp/full-backup/databases/gitea.db" + +# ============================================================ +# 2. Gitea 代码仓库 + 配置 +# ============================================================ +log "备份 Gitea 代码仓库..." +ssh txjp "mkdir -p /tmp/full-backup/gitea && \ + cp -a /root/docker/gitea-ai/data/git /tmp/full-backup/gitea/ && \ + cp -a /root/docker/gitea-ai/data/gitea/conf /tmp/full-backup/gitea/ 2>/dev/null || true" + +# ============================================================ +# 3. 用户上传文件 +# ============================================================ +log "备份用户上传文件..." +ssh txjp "mkdir -p /tmp/full-backup/uploads && \ + cp -a /root/docker/assets-ai/data/uploads /tmp/full-backup/uploads/assets-uploads 2>/dev/null || true && \ + cp -a /root/docker/issue-ai/data/uploads /tmp/full-backup/uploads/issue-uploads 2>/dev/null || true && \ + cp -a /root/docker/issue-ai/data/reports /tmp/full-backup/uploads/issue-reports 2>/dev/null || true" + +# ============================================================ +# 4. Nginx 配置 +# ============================================================ +log "备份 Nginx 配置..." +ssh txjp "mkdir -p /tmp/full-backup/nginx && \ + cp -a /root/docker/nginx-proxy-ai/conf.d/* /tmp/full-backup/nginx/ 2>/dev/null || true" + +# ============================================================ +# 5. 各站点 docker-compose.yml + .env +# ============================================================ +log "备份站点配置文件..." +ssh txjp "mkdir -p /tmp/full-backup/configs && \ + for site in assets-ai issue-ai oa-ai ldap-ai gitea-ai; do \ + mkdir -p /tmp/full-backup/configs/\$site && \ + cp /root/docker/\$site/docker-compose.yml /tmp/full-backup/configs/\$site/ 2>/dev/null || true && \ + cp /root/docker/\$site/.env /tmp/full-backup/configs/\$site/ 2>/dev/null || true && \ + cp /root/docker/\$site/Dockerfile /tmp/full-backup/configs/\$site/ 2>/dev/null || true; \ + done" + +# ============================================================ +# 6. SSL 证书 +# ============================================================ +log "备份 SSL 证书..." +ssh txjp "mkdir -p /tmp/full-backup/ssl && \ + cp -a /etc/letsencrypt/live /tmp/full-backup/ssl/ 2>/dev/null || true && \ + cp -a /etc/letsencrypt/archive /tmp/full-backup/ssl/ 2>/dev/null || true && \ + cp -a /etc/letsencrypt/renewal /tmp/full-backup/ssl/ 2>/dev/null || true" + +# ============================================================ +# 7. 打包压缩 +# ============================================================ +log "打包压缩..." +ssh txjp "cd /tmp && tar czf ${BACKUP_NAME}.tar.gz full-backup/ && rm -rf /tmp/full-backup" + +# ============================================================ +# 8. 复制到本地 +# ============================================================ +log "复制到本地..." +mkdir -p "${LOCAL_DIR}" +scp "txjp:/tmp/${BACKUP_NAME}.tar.gz" "${LOCAL_DIR}/" + +# ============================================================ +# 9. 云端保留一份 +# ============================================================ +CLOUD_DIR="/root/docker/db-backups" +ssh txjp "mkdir -p ${CLOUD_DIR} && cp /tmp/${BACKUP_NAME}.tar.gz ${CLOUD_DIR}/" +log "云端备份已保存至: ${CLOUD_DIR}" + +# ============================================================ +# 10. 清理云端临时文件 +# ============================================================ +ssh txjp "rm -f /tmp/${BACKUP_NAME}.tar.gz" + +# ============================================================ +# 11. 验证备份 +# ============================================================ +log "验证备份完整性..." +cd "${LOCAL_DIR}" +if tar tzf "${BACKUP_NAME}.tar.gz" > /dev/null 2>&1; then + log "备份文件验证通过" +else + echo "[错误] 备份文件损坏" + exit 1 +fi + +# 显示备份内容 +log "备份内容清单:" +tar tzf "${BACKUP_NAME}.tar.gz" | head -30 + +echo "" +log "备份完成:${TIMESTAMP}" +echo " 文件: ${LOCAL_DIR}/${BACKUP_NAME}.tar.gz" +echo " 大小: $(du -h ${LOCAL_DIR}/${BACKUP_NAME}.tar.gz | cut -f1)" +echo "" +echo " 云端: ${CLOUD_DIR}/${BACKUP_NAME}.tar.gz" +ssh txjp "ls -lh ${CLOUD_DIR}/${BACKUP_NAME}.tar.gz 2>/dev/null" diff --git a/backup/backup-sites.sh b/backup/backup-sites.sh new file mode 100755 index 0000000..c718abb --- /dev/null +++ b/backup/backup-sites.sh @@ -0,0 +1,188 @@ +#!/bin/bash +# 站点独立备份脚本 +# 每个站点单独一个备份文件,共享资源(nginx/ssl/network)单独备份 +# 用法:bash backup-sites.sh [站点名] +# 无参数:备份所有站点 +# 指定站点:仅备份指定站点 + +set -e + +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +LOCAL_DIR="/Users/niuniu/programs/docker/db-backups" +REMOTE_BASE="/root/docker" +CLOUD_DIR="/root/docker/db-backups" + +log() { echo "[$(date '+%H:%M:%S')] $1"; } +warn() { echo "[$(date '+%H:%M:%S')] [警告] $1"; } + +# 要备份的站点列表 +ALL_SITES="assets issue ldap gitea oa" + +# ============================================================ +# 备份单个站点 +# ============================================================ +backup_site() { + local site="$1" + local backup_name="${site}-${TIMESTAMP}.tar.gz" + + log "备份 ${site}..." + + # 创建临时目录 + ssh txjp "rm -rf /tmp/backup-${site} && mkdir -p /tmp/backup-${site}/data" + + case "$site" in + assets) + # WAL checkpoint + ssh txjp "docker exec assets-ai node -e \"const D=require('better-sqlite3');const db=new D('/app/data/assets.db');db.pragma('wal_checkpoint(TRUNCATE)');db.close();\"" 2>/dev/null || true + # 数据库 + ssh txjp "docker cp assets-ai:/app/data/assets.db /tmp/backup-${site}/data/" + # 上传文件 + ssh txjp "cp -a /root/docker/assets-ai/data/uploads /tmp/backup-${site}/data/ 2>/dev/null || true" + # 配置文件 + ssh txjp "cp /root/docker/assets-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true" + ssh txjp "cp /root/docker/assets-ai/.env /tmp/backup-${site}/ 2>/dev/null || true" + ssh txjp "cp /root/docker/assets-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true" + ;; + issue) + # WAL checkpoint + ssh txjp "docker exec issue-ai node -e \"const D=require('better-sqlite3');const db=new D('/app/data/issue.db');db.pragma('wal_checkpoint(TRUNCATE)');db.close();\"" 2>/dev/null || true + # 数据库 + ssh txjp "docker cp issue-ai:/app/data/issue.db /tmp/backup-${site}/data/" + # 上传文件和报告 + ssh txjp "cp -a /root/docker/issue-ai/data/uploads /tmp/backup-${site}/data/ 2>/dev/null || true" + ssh txjp "cp -a /root/docker/issue-ai/data/reports /tmp/backup-${site}/data/ 2>/dev/null || true" + # 配置文件 + ssh txjp "cp /root/docker/issue-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true" + ssh txjp "cp /root/docker/issue-ai/.env /tmp/backup-${site}/ 2>/dev/null || true" + ssh txjp "cp /root/docker/issue-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true" + ;; + ldap) + # WAL checkpoint + ssh txjp "docker exec lldap sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCATE);'" 2>/dev/null || true + # 数据库 + ssh txjp "docker cp lldap:/data/users.db /tmp/backup-${site}/data/" + # 配置文件 + ssh txjp "cp /root/docker/ldap-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true" + ssh txjp "cp /root/docker/ldap-ai/.env /tmp/backup-${site}/ 2>/dev/null || true" + ssh txjp "cp /root/docker/ldap-ai/Dockerfile /tmp/backup-${site}/ 2>/dev/null || true" + ;; + gitea) + # 数据库(gitea 不允许以 root 运行 dump,直接复制数据库文件) + ssh txjp "docker cp gitea-ai:/data/gitea.db /tmp/backup-${site}/data/" + # 代码仓库 + ssh txjp "cp -a /root/docker/gitea-ai/data/git /tmp/backup-${site}/ 2>/dev/null || true" + # 配置 + ssh txjp "cp -a /root/docker/gitea-ai/data/gitea/conf /tmp/backup-${site}/gitea-conf 2>/dev/null || true" + # docker-compose + ssh txjp "cp /root/docker/gitea-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true" + ;; + oa) + # 配置文件 + ssh txjp "cp /root/docker/oa-ai/docker-compose.yml /tmp/backup-${site}/ 2>/dev/null || true" + ssh txjp "cp /root/docker/oa-ai/.env /tmp/backup-${site}/ 2>/dev/null || true" + # OA 没有独立数据库,数据存在 assets/issue 中 + ;; + esac + + # 打包 + ssh txjp "cd /tmp && tar czf ${backup_name} backup-${site}/" + + # 复制到本地 + mkdir -p "${LOCAL_DIR}" + scp "txjp:/tmp/${backup_name}" "${LOCAL_DIR}/" + + # 云端保留 + ssh txjp "mkdir -p ${CLOUD_DIR} && cp /tmp/${backup_name} ${CLOUD_DIR}/" + + # 清理临时文件 + ssh txjp "rm -rf /tmp/backup-${site} /tmp/${backup_name}" + + log "${site} 备份完成: ${backup_name}" +} + +# ============================================================ +# 备份共享资源(nginx + ssl + docker网络) +# ============================================================ +backup_shared() { + local backup_name="shared-${TIMESTAMP}.tar.gz" + + log "备份共享资源..." + + ssh txjp "rm -rf /tmp/backup-shared && mkdir -p /tmp/backup-shared" + + # Nginx 配置 + ssh txjp "mkdir -p /tmp/backup-shared/nginx && cp -a /root/docker/nginx-proxy-ai/conf.d/* /tmp/backup-shared/nginx/ 2>/dev/null || true" + + # SSL 证书 + ssh txjp "mkdir -p /tmp/backup-shared/ssl && \ + cp -a /etc/letsencrypt/live /tmp/backup-shared/ssl/ 2>/dev/null || true && \ + cp -a /etc/letsencrypt/archive /tmp/backup-shared/ssl/ 2>/dev/null || true && \ + cp -a /etc/letsencrypt/renewal /tmp/backup-shared/ssl/ 2>/dev/null || true" + + # Docker 网络配置 + ssh txjp "cat > /tmp/backup-shared/docker-network.sh << 'NETEOF' +#!/bin/bash +# 恢复 docker 网络 +if ! docker network inspect webnet >/dev/null 2>&1; then + docker network create --driver bridge --subnet 172.18.0.0/16 webnet + echo 'Docker 网络 webnet 已创建' +else + echo 'Docker 网络 webnet 已存在' +fi +NETEOF" + + # Nginx docker-compose(用于恢复 nginx 容器) + ssh txjp "cp /root/docker/nginx-proxy-ai/docker-compose.yml /tmp/backup-shared/ 2>/dev/null || true" + + # 打包 + ssh txjp "cd /tmp && tar czf ${backup_name} backup-shared/" + + # 复制到本地 + mkdir -p "${LOCAL_DIR}" + scp "txjp:/tmp/${backup_name}" "${LOCAL_DIR}/" + + # 云端保留 + ssh txjp "mkdir -p ${CLOUD_DIR} && cp /tmp/${backup_name} ${CLOUD_DIR}/" + + # 清理 + ssh txjp "rm -rf /tmp/backup-shared /tmp/${backup_name}" + + log "共享资源备份完成: ${backup_name}" +} + +# ============================================================ +# 主流程 +# ============================================================ +log "开始站点独立备份..." + +# 解析参数 +TARGET_SITES="${1:-all}" + +if [[ "$TARGET_SITES" == "all" ]]; then + # 备份所有站点 + for site in $ALL_SITES; do + backup_site "$site" + done + backup_shared +else + # 备份指定站点 + for site in $TARGET_SITES; do + if echo "$ALL_SITES" | grep -qw "$site"; then + backup_site "$site" + else + warn "未知站点: $site" + fi + done +fi + +# ============================================================ +# 显示备份结果 +# ============================================================ +echo "" +log "备份完成!" +echo "" +echo "本地备份文件:" +ls -lh ${LOCAL_DIR}/*-${TIMESTAMP}.tar.gz 2>/dev/null +echo "" +echo "云端备份文件:" +ssh txjp "ls -lh ${CLOUD_DIR}/*-${TIMESTAMP}.tar.gz 2>/dev/null" diff --git a/restore-db.sh b/backup/restore-db.sh similarity index 84% rename from restore-db.sh rename to backup/restore-db.sh index 9269d59..a1d06bc 100755 --- a/restore-db.sh +++ b/backup/restore-db.sh @@ -20,8 +20,11 @@ set -e if [ $# -lt 3 ]; then echo "用法: $0 <备份文件名>" echo "" + echo "支持格式: .db 或 .tar.gz(自动解压)" + echo "" echo "示例:" echo " $0 local issue issue-2026-04-29_1753.db" + echo " $0 local issue issue-20260622_101858.tar.gz" echo " $0 cloud ldap ldap-2026-05-15_1200.db" echo " $0 cloud assets assets-2026-04-29_1753.db" echo "" @@ -71,6 +74,7 @@ esac # ============================================ restore_local() { local BACKUP_PATH="${LOCAL_BACKUP_DIR}/${BACKUP_FILE}" + local DB_FILE="$BACKUP_PATH" echo "========================================" echo " 本地数据库恢复" @@ -87,55 +91,78 @@ restore_local() { exit 1 fi - # 2. 验证备份文件是有效的 SQLite 数据库 - if ! sqlite3 "$BACKUP_PATH" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then + # 2. 如果是 .tar.gz 文件,解压获取 .db 文件 + if [[ "$BACKUP_PATH" == *.tar.gz ]]; then + echo "[信息] 检测到 tar.gz 压缩包,正在解压..." + TEMP_DIR=$(mktemp -d) + tar xzf "$BACKUP_PATH" -C "$TEMP_DIR" + + # 查找 .db 文件 + DB_FILE=$(find "$TEMP_DIR" -name "*.db" -type f | head -1) + if [ -z "$DB_FILE" ]; then + echo "[错误] 压缩包中未找到 .db 文件" + rm -rf "$TEMP_DIR" + exit 1 + fi + echo "[信息] 解压得到数据库文件: $(basename $DB_FILE)" + fi + + # 3. 验证备份文件是有效的 SQLite 数据库 + if ! sqlite3 "$DB_FILE" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then echo "[错误] 备份文件不是有效的 SQLite 数据库" + [ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR" exit 1 fi - # 3. 确认操作 + # 4. 确认操作 echo "[警告] 这将覆盖本地 ${CONTAINER} 数据库!" echo " 当前数据库: ${LOCAL_DB_PATH}" - echo " 将替换为: ${BACKUP_PATH}" + echo " 将替换为: ${DB_FILE}" echo "" read -p "确认恢复? (输入 yes 继续): " CONFIRM if [ "$CONFIRM" != "yes" ]; then echo "已取消" + [ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR" exit 0 fi - # 4. 备份当前数据库(安全起见) + # 5. 备份当前数据库(安全起见) local SAFE_BACKUP="${LOCAL_DB_PATH}.before-restore-$(date +%Y%m%d_%H%M%S)" if [ -f "$LOCAL_DB_PATH" ]; then cp "$LOCAL_DB_PATH" "$SAFE_BACKUP" echo "[备份] 当前数据库已备份至: ${SAFE_BACKUP}" fi - # 5. WAL checkpoint on backup (确保数据完整) - sqlite3 "$BACKUP_PATH" "PRAGMA wal_checkpoint(TRUNCATE)" 2>/dev/null || true + # 6. WAL checkpoint on backup (确保数据完整) + sqlite3 "$DB_FILE" "PRAGMA wal_checkpoint(TRUNCATE)" 2>/dev/null || true - # 6. 停止本地容器(如果在运行) + # 7. 停止本地容器(如果在运行) if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then echo "[操作] 停止容器 ${CONTAINER}..." docker stop "$CONTAINER" fi - # 7. 替换数据库 - cp "$BACKUP_PATH" "$LOCAL_DB_PATH" + # 8. 替换数据库 + cp "$DB_FILE" "$LOCAL_DB_PATH" rm -f "${LOCAL_DB_PATH}-shm" "${LOCAL_DB_PATH}-wal" echo "[完成] 数据库已替换" - # 8. 启动容器 + # 9. 清理临时文件 + [ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR" + + # 10. 启动容器 echo "[操作] 启动容器 ${CONTAINER}..." docker start "$CONTAINER" - # 9. 验证 + # 11. 验证 sleep 2 echo "" echo "[验证] 恢复后数据:" if [ "$SERVICE" = "issue" ]; then echo " 工单数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets")" echo " 配件名称: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets WHERE parts_name IS NOT NULL AND parts_name != ''") 条" + elif [ "$SERVICE" = "ldap" ]; then + echo " 用户数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM users")" else echo " 设备数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM assets")" fi diff --git a/backup/restore-full.sh b/backup/restore-full.sh new file mode 100755 index 0000000..20dabfa --- /dev/null +++ b/backup/restore-full.sh @@ -0,0 +1,272 @@ +#!/bin/bash +# 灾难恢复脚本 - 从完整备份恢复站点 +# 用法:bash restore-full.sh [备份文件名] +# 无参数则自动检测当前目录下的最新备份 + +set -e + +BACKUP_DIR="/Users/niuniu/programs/docker/db-backups" +REMOTE_BASE="/root/docker" + +log() { echo "[$(date '+%H:%M:%S')] $1"; } +warn() { echo "[$(date '+%H:%M:%S')] [警告] $1"; } +error() { echo "[$(date '+%H:%M:%S')] [错误] $1"; exit 1; } + +# ============================================================ +# 1. 选择备份文件 +# ============================================================ +if [[ -n "$1" ]]; then + BACKUP_FILE="$1" +else + echo "" + echo "==========================================" + echo " 可用的完整备份文件" + echo "==========================================" + echo "" + + # 列出本地备份 + if [[ -d "$BACKUP_DIR" ]]; then + local_files=$(ls -t ${BACKUP_DIR}/full-backup-*.tar.gz 2>/dev/null | head -10) + if [[ -n "$local_files" ]]; then + echo "本地备份:" + index=1 + while IFS= read -r file; do + filename=$(basename "$file") + filesize=$(du -h "$file" | cut -f1) + echo " $index) $filename ($filesize)" + files[$index]="$file" + ((index++)) + done <<< "$local_files" + echo "" + fi + fi + + # 列出云端备份 + echo "云端备份:" + cloud_files=$(ssh txjp "ls -t /root/docker/db-backups/full-backup-*.tar.gz 2>/dev/null | head -10" 2>/dev/null || true) + if [[ -n "$cloud_files" ]]; then + while IFS= read -r file; do + filename=$(basename "$file") + echo " $index) [云端] $filename" + files[$index]="cloud:$file" + ((index++)) + done <<< "$cloud_files" + echo "" + fi + + if [[ ${#files[@]} -eq 0 ]]; then + error "未找到任何备份文件" + fi + + printf "请选择备份文件 (1-$((index-1))): " + read choice + + if [[ -z "${files[$choice]}" ]]; then + error "无效选择" + fi + + BACKUP_FILE="${files[$choice]}" +fi + +# ============================================================ +# 2. 下载云端备份到临时目录 +# ============================================================ +RESTORE_DIR="/tmp/restore-$$" +mkdir -p "$RESTORE_DIR" + +if [[ "$BACKUP_FILE" == cloud:* ]]; then + cloud_path="${BACKUP_FILE#cloud:}" + log "从云端下载备份..." + scp "txjp:$cloud_path" "$RESTORE_DIR/" + BACKUP_FILE="$RESTORE_DIR/$(basename $cloud_path)" +fi + +# ============================================================ +# 3. 解压备份 +# ============================================================ +log "解压备份文件..." +cd "$RESTORE_DIR" +tar xzf "$BACKUP_FILE" + +# 检测备份内容 +BACKUP_CONTENT="$RESTORE_DIR/full-backup" +if [[ ! -d "$BACKUP_CONTENT" ]]; then + error "备份文件格式错误" +fi + +log "备份内容:" +ls -la "$BACKUP_CONTENT/" + +# ============================================================ +# 4. 选择要恢复的站点 +# ============================================================ +echo "" +echo "==========================================" +echo " 选择要恢复的内容" +echo "==========================================" +echo "" +echo " 1) assets - 资产管理系统" +echo " 2) issue - 工单系统" +echo " 3) ldap - 用户目录服务" +echo " 4) gitea - 代码仓库" +echo " 5) nginx - 反向代理配置" +echo " 6) 全部恢复" +echo "" +printf "请选择 (1-6): " +read site_choice + +# ============================================================ +# 5. 执行恢复 +# ============================================================ +restore_assets() { + log "恢复 assets 站点..." + + # 恢复数据库 + if [[ -f "$BACKUP_CONTENT/databases/assets.db" ]]; then + log "恢复 assets 数据库..." + scp "$BACKUP_CONTENT/databases/assets.db" txjp:/tmp/assets.db + ssh txjp "docker cp /tmp/assets.db assets-ai:/app/data/assets.db && rm /tmp/assets.db" + ssh txjp "docker restart assets-ai" + log "assets 数据库恢复完成" + fi + + # 恢复配置 + if [[ -d "$BACKUP_CONTENT/configs/assets-ai" ]]; then + log "恢复 assets 配置..." + ssh txjp "mkdir -p $REMOTE_BASE/assets-ai" + scp "$BACKUP_CONTENT/configs/assets-ai/"* txjp:$REMOTE_BASE/assets-ai/ 2>/dev/null || true + fi + + # 恢复上传文件 + if [[ -d "$BACKUP_CONTENT/uploads/assets-uploads" ]]; then + log "恢复 assets 上传文件..." + ssh txjp "mkdir -p $REMOTE_BASE/assets-ai/data" + scp -r "$BACKUP_CONTENT/uploads/assets-uploads/"* txjp:$REMOTE_BASE/assets-ai/data/uploads/ 2>/dev/null || true + fi +} + +restore_issue() { + log "恢复 issue 站点..." + + # 恢复数据库 + if [[ -f "$BACKUP_CONTENT/databases/issue.db" ]]; then + log "恢复 issue 数据库..." + scp "$BACKUP_CONTENT/databases/issue.db" txjp:/tmp/issue.db + ssh txjp "docker cp /tmp/issue.db issue-ai:/app/data/issue.db && rm /tmp/issue.db" + ssh txjp "docker restart issue-ai" + log "issue 数据库恢复完成" + fi + + # 恢复配置 + if [[ -d "$BACKUP_CONTENT/configs/issue-ai" ]]; then + log "恢复 issue 配置..." + ssh txjp "mkdir -p $REMOTE_BASE/issue-ai" + scp "$BACKUP_CONTENT/configs/issue-ai/"* txjp:$REMOTE_BASE/issue-ai/ 2>/dev/null || true + fi + + # 恢复上传文件 + if [[ -d "$BACKUP_CONTENT/uploads/issue-uploads" ]]; then + log "恢复 issue 上传文件..." + ssh txjp "mkdir -p $REMOTE_BASE/issue-ai/data" + scp -r "$BACKUP_CONTENT/uploads/issue-uploads/"* txjp:$REMOTE_BASE/issue-ai/data/uploads/ 2>/dev/null || true + fi + + # 恢复报告文件 + if [[ -d "$BACKUP_CONTENT/uploads/issue-reports" ]]; then + log "恢复 issue 报告文件..." + ssh txjp "mkdir -p $REMOTE_BASE/issue-ai/data" + scp -r "$BACKUP_CONTENT/uploads/issue-reports/"* txjp:$REMOTE_BASE/issue-ai/data/reports/ 2>/dev/null || true + fi +} + +restore_ldap() { + log "恢复 ldap 站点..." + + # 恢复数据库 + if [[ -f "$BACKUP_CONTENT/databases/ldap.db" ]]; then + log "恢复 ldap 数据库..." + scp "$BACKUP_CONTENT/databases/ldap.db" txjp:/tmp/ldap.db + ssh txjp "docker cp /tmp/ldap.db lldap:/data/users.db && rm /tmp/ldap.db" + ssh txjp "docker restart lldap" + log "ldap 数据库恢复完成" + fi + + # 恢复配置 + if [[ -d "$BACKUP_CONTENT/configs/ldap-ai" ]]; then + log "恢复 ldap 配置..." + ssh txjp "mkdir -p $REMOTE_BASE/ldap-ai" + scp "$BACKUP_CONTENT/configs/ldap-ai/"* txjp:$REMOTE_BASE/ldap-ai/ 2>/dev/null || true + fi +} + +restore_gitea() { + log "恢复 gitea 站点..." + + # 恢复数据库 + if [[ -f "$BACKUP_CONTENT/databases/gitea.db" ]]; then + log "恢复 gitea 数据库..." + scp "$BACKUP_CONTENT/databases/gitea.db" txjp:/tmp/gitea.db + ssh txjp "docker cp /tmp/gitea.db gitea-ai:/data/gitea.db && rm /tmp/gitea.db" + fi + + # 恢复代码仓库 + if [[ -d "$BACKUP_CONTENT/gitea/git" ]]; then + log "恢复 gitea 代码仓库..." + ssh txjp "mkdir -p $REMOTE_BASE/gitea-ai/data" + scp -r "$BACKUP_CONTENT/gitea/git" txjp:$REMOTE_BASE/gitea-ai/data/ + fi + + # 恢复配置 + if [[ -d "$BACKUP_CONTENT/gitea/conf" ]]; then + log "恢复 gitea 配置..." + ssh txjp "mkdir -p $REMOTE_BASE/gitea-ai/data/gitea" + scp -r "$BACKUP_CONTENT/gitea/conf" txjp:$REMOTE_BASE/gitea-ai/data/gitea/ + fi + + # 恢复 docker-compose + if [[ -d "$BACKUP_CONTENT/configs/gitea-ai" ]]; then + log "恢复 gitea docker-compose..." + scp "$BACKUP_CONTENT/configs/gitea-ai/"* txjp:$REMOTE_BASE/gitea-ai/ 2>/dev/null || true + fi + + # 重启容器 + ssh txjp "cd $REMOTE_BASE/gitea-ai && docker compose down && docker compose up -d" + log "gitea 恢复完成" +} + +restore_nginx() { + log "恢复 nginx 配置..." + + if [[ -d "$BACKUP_CONTENT/nginx" ]]; then + ssh txjp "mkdir -p $REMOTE_BASE/nginx-proxy-ai/conf.d" + scp "$BACKUP_CONTENT/nginx/"* txjp:$REMOTE_BASE/nginx-proxy-ai/conf.d/ + ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" + log "nginx 配置恢复完成" + fi +} + +# 执行恢复 +case "$site_choice" in + 1) restore_assets ;; + 2) restore_issue ;; + 3) restore_ldap ;; + 4) restore_gitea ;; + 5) restore_nginx ;; + 6) + restore_assets + restore_issue + restore_ldap + restore_gitea + restore_nginx + ;; + *) error "无效选择" ;; +esac + +# ============================================================ +# 6. 清理临时文件 +# ============================================================ +log "清理临时文件..." +rm -rf "$RESTORE_DIR" + +echo "" +log "恢复完成!" diff --git a/backup/restore-sites.sh b/backup/restore-sites.sh new file mode 100755 index 0000000..530a2d9 --- /dev/null +++ b/backup/restore-sites.sh @@ -0,0 +1,367 @@ +#!/bin/bash +# 站点恢复脚本 - 支持选择恢复单个站点或全部 +# 用法:bash restore-sites.sh [备份时间戳] +# 无参数则自动检测最新备份 + +set -e + +BACKUP_DIR="/Users/niuniu/programs/docker/db-backups" +REMOTE_BASE="/root/docker" + +log() { echo "[$(date '+%H:%M:%S')] $1"; } +warn() { echo "[$(date '+%H:%M:%S')] [警告] $1"; } +error() { echo "[$(date '+%H:%M:%S')] [错误] $1"; exit 1; } + +# ============================================================ +# 1. 选择备份时间戳 +# ============================================================ +if [[ -n "$1" ]]; then + TIMESTAMP="$1" +else + echo "" + echo "==========================================" + echo " 可用的备份时间戳" + echo "==========================================" + echo "" + + # 列出可用的时间戳 + timestamps=$(ls ${BACKUP_DIR}/assets-*.tar.gz 2>/dev/null | sed 's/.*assets-\(.*\)\.tar\.gz/\1/' | sort -r | head -10) + + if [[ -z "$timestamps" ]]; then + error "未找到任何备份文件" + fi + + index=1 + while IFS= read -r ts; do + echo " $index) $ts" + ts_list[$index]="$ts" + ((index++)) + done <<< "$timestamps" + + echo "" + printf "请选择备份时间戳 (1-$((index-1))): " + read choice + + if [[ -z "${ts_list[$choice]}" ]]; then + error "无效选择" + fi + + TIMESTAMP="${ts_list[$choice]}" +fi + +# ============================================================ +# 2. 检测可用的备份文件 +# ============================================================ +echo "" +log "检测备份文件: ${TIMESTAMP}" + +LOCAL_BACKUP="${BACKUP_DIR}" +CLOUD_BACKUP="txjp:/root/docker/db-backups" + +# 检查本地备份 +check_backup() { + local site="$1" + local found="" + + # 检查本地 + if [[ -f "${LOCAL_BACKUP}/${site}-${TIMESTAMP}.tar.gz" ]]; then + found="local:${LOCAL_BACKUP}/${site}-${TIMESTAMP}.tar.gz" + # 检查云端 + elif ssh txjp "test -f ${CLOUD_BACKUP}/${site}-${TIMESTAMP}.tar.gz" 2>/dev/null; then + found="cloud:${site}-${TIMESTAMP}.tar.gz" + fi + + echo "$found" +} + +# ============================================================ +# 3. 选择要恢复的站点 +# ============================================================ +echo "" +echo "==========================================" +echo " 可恢复的站点" +echo "==========================================" +echo "" + +SITES="shared assets issue ldap gitea oa" +index=1 +declare -A site_status + +for site in $SITES; do + backup=$(check_backup "$site") + if [[ -n "$backup" ]]; then + if [[ "$backup" == local:* ]]; then + status="✅ 本地" + else + status="✅ 云端" + fi + echo " $index) ${site} ${status}" + site_list[$index]="$site" + site_source[$index]="$backup" + ((index++)) + else + echo " $index) ${site} ❌ 未找到" + ((index++)) + fi +done + +echo "" +echo " $index) 全部恢复" +echo "" +printf "请选择要恢复的内容 (1-$index): " +read choice + +if [[ "$choice" == "$index" ]]; then + # 全部恢复 + restore_list=("${site_list[@]}") +else + restore_list=("${site_list[$choice]}") +fi + +# ============================================================ +# 4. 下载并解压备份文件 +# ============================================================ +RESTORE_DIR="/tmp/restore-$$" +mkdir -p "$RESTORE_DIR" + +download_and_extract() { + local site="$1" + local source="${site_source[$choice]}" + local file_path="" + + if [[ "$source" == local:* ]]; then + file_path="${source#local:}" + elif [[ "$source" == cloud:* ]]; then + local filename="${source#cloud:}" + log "从云端下载 ${site} 备份..." + scp "${CLOUD_BACKUP}/${filename}" "$RESTORE_DIR/" + file_path="$RESTORE_DIR/${filename}" + fi + + # 解压 + cd "$RESTORE_DIR" + tar xzf "$file_path" +} + +# ============================================================ +# 5. 恢复函数 +# ============================================================ +restore_shared() { + log "恢复共享资源..." + + download_and_extract "shared" + + # 恢复 docker 网络 + if [[ -f "$RESTORE_DIR/backup-shared/docker-network.sh" ]]; then + log "创建 Docker 网络..." + bash "$RESTORE_DIR/backup-shared/docker-network.sh" + fi + + # 恢复 nginx 配置 + if [[ -d "$RESTORE_DIR/backup-shared/nginx" ]]; then + log "恢复 nginx 配置..." + ssh txjp "mkdir -p $REMOTE_BASE/nginx-proxy-ai/conf.d" + scp "$RESTORE_DIR/backup-shared/nginx/"* txjp:$REMOTE_BASE/nginx-proxy-ai/conf.d/ + fi + + # 恢复 SSL 证书 + if [[ -d "$RESTORE_DIR/backup-shared/ssl" ]]; then + log "恢复 SSL 证书..." + ssh txjp "cp -a $RESTORE_DIR/backup-shared/ssl/live /etc/letsencrypt/ 2>/dev/null || true" + ssh txjp "cp -a $RESTORE_DIR/backup-shared/ssl/archive /etc/letsencrypt/ 2>/dev/null || true" + ssh txjp "cp -a $RESTORE_DIR/backup-shared/ssl/renewal /etc/letsencrypt/ 2>/dev/null || true" + fi + + # 恢复 nginx 容器配置 + if [[ -f "$RESTORE_DIR/backup-shared/docker-compose.yml" ]]; then + log "恢复 nginx docker-compose..." + scp "$RESTORE_DIR/backup-shared/docker-compose.yml" txjp:$REMOTE_BASE/nginx-proxy-ai/ + fi +} + +restore_assets() { + log "恢复 assets 站点..." + + download_and_extract "assets" + + # 创建目录 + ssh txjp "mkdir -p $REMOTE_BASE/assets-ai/data" + + # 恢复数据库 + if [[ -f "$RESTORE_DIR/backup-assets/data/assets.db" ]]; then + log "恢复 assets 数据库..." + scp "$RESTORE_DIR/backup-assets/data/assets.db" txjp:/tmp/assets.db + ssh txjp "docker cp /tmp/assets.db assets-ai:/app/data/assets.db && rm /tmp/assets.db" + fi + + # 恢复上传文件 + if [[ -d "$RESTORE_DIR/backup-assets/data/uploads" ]]; then + log "恢复 assets 上传文件..." + scp -r "$RESTORE_DIR/backup-assets/data/uploads/"* txjp:$REMOTE_BASE/assets-ai/data/uploads/ 2>/dev/null || true + fi + + # 恢复配置文件 + for f in docker-compose.yml .env Dockerfile; do + if [[ -f "$RESTORE_DIR/backup-assets/$f" ]]; then + scp "$RESTORE_DIR/backup-assets/$f" txjp:$REMOTE_BASE/assets-ai/ + fi + done + + # 重启容器 + log "重启 assets 容器..." + ssh txjp "cd $REMOTE_BASE/assets-ai && docker compose down 2>/dev/null || true && docker compose up -d" +} + +restore_issue() { + log "恢复 issue 站点..." + + download_and_extract "issue" + + # 创建目录 + ssh txjp "mkdir -p $REMOTE_BASE/issue-ai/data" + + # 恢复数据库 + if [[ -f "$RESTORE_DIR/backup-issue/data/issue.db" ]]; then + log "恢复 issue 数据库..." + scp "$RESTORE_DIR/backup-issue/data/issue.db" txjp:/tmp/issue.db + ssh txjp "docker cp /tmp/issue.db issue-ai:/app/data/issue.db && rm /tmp/issue.db" + fi + + # 恢复上传文件 + if [[ -d "$RESTORE_DIR/backup-issue/data/uploads" ]]; then + log "恢复 issue 上传文件..." + scp -r "$RESTORE_DIR/backup-issue/data/uploads/"* txjp:$REMOTE_BASE/issue-ai/data/uploads/ 2>/dev/null || true + fi + + # 恢复报告文件 + if [[ -d "$RESTORE_DIR/backup-issue/data/reports" ]]; then + log "恢复 issue 报告文件..." + scp -r "$RESTORE_DIR/backup-issue/data/reports/"* txjp:$REMOTE_BASE/issue-ai/data/reports/ 2>/dev/null || true + fi + + # 恢复配置文件 + for f in docker-compose.yml .env Dockerfile; do + if [[ -f "$RESTORE_DIR/backup-issue/$f" ]]; then + scp "$RESTORE_DIR/backup-issue/$f" txjp:$REMOTE_BASE/issue-ai/ + fi + done + + # 重启容器 + log "重启 issue 容器..." + ssh txjp "cd $REMOTE_BASE/issue-ai && docker compose down 2>/dev/null || true && docker compose up -d" +} + +restore_ldap() { + log "恢复 ldap 站点..." + + download_and_extract "ldap" + + # 创建目录 + ssh txjp "mkdir -p $REMOTE_BASE/ldap-ai/data" + + # 恢复数据库 + if [[ -f "$RESTORE_DIR/backup-ldap/data/users.db" ]]; then + log "恢复 ldap 数据库..." + scp "$RESTORE_DIR/backup-ldap/data/users.db" txjp:/tmp/ldap.db + ssh txjp "docker cp /tmp/ldap.db lldap:/data/users.db && rm /tmp/ldap.db" + fi + + # 恢复配置文件 + for f in docker-compose.yml .env Dockerfile; do + if [[ -f "$RESTORE_DIR/backup-ldap/$f" ]]; then + scp "$RESTORE_DIR/backup-ldap/$f" txjp:$REMOTE_BASE/ldap-ai/ + fi + done + + # 重启容器 + log "重启 ldap 容器..." + ssh txjp "cd $REMOTE_BASE/ldap-ai && docker compose down 2>/dev/null || true && docker compose up -d" +} + +restore_gitea() { + log "恢复 gitea 站点..." + + download_and_extract "gitea" + + # 创建目录 + ssh txjp "mkdir -p $REMOTE_BASE/gitea-ai/data" + + # 恢复数据库 + if [[ -f "$RESTORE_DIR/backup-gitea/data/gitea.db" ]]; then + log "恢复 gitea 数据库..." + scp "$RESTORE_DIR/backup-gitea/data/gitea.db" txjp:/tmp/gitea.db + ssh txjp "docker cp /tmp/gitea.db gitea-ai:/data/gitea.db && rm /tmp/gitea.db" + fi + + # 恢复代码仓库 + if [[ -d "$RESTORE_DIR/backup-gitea/git" ]]; then + log "恢复 gitea 代码仓库..." + scp -r "$RESTORE_DIR/backup-gitea/git" txjp:$REMOTE_BASE/gitea-ai/data/ + fi + + # 恢复配置 + if [[ -d "$RESTORE_DIR/backup-gitea/gitea-conf" ]]; then + log "恢复 gitea 配置..." + ssh txjp "mkdir -p $REMOTE_BASE/gitea-ai/data/gitea" + scp -r "$RESTORE_DIR/backup-gitea/gitea-conf" txjp:$REMOTE_BASE/gitea-ai/data/gitea/ + fi + + # 恢复 docker-compose + if [[ -f "$RESTORE_DIR/backup-gitea/docker-compose.yml" ]]; then + scp "$RESTORE_DIR/backup-gitea/docker-compose.yml" txjp:$REMOTE_BASE/gitea-ai/ + fi + + # 重启容器 + log "重启 gitea 容器..." + ssh txjp "cd $REMOTE_BASE/gitea-ai && docker compose down 2>/dev/null || true && docker compose up -d" +} + +restore_oa() { + log "恢复 oa 站点..." + + download_and_extract "oa" + + # 恢复配置文件 + for f in docker-compose.yml .env; do + if [[ -f "$RESTORE_DIR/backup-oa/$f" ]]; then + scp "$RESTORE_DIR/backup-oa/$f" txjp:$REMOTE_BASE/oa-ai/ + fi + done + + # 重启容器 + log "重启 oa 容器..." + ssh txjp "cd $REMOTE_BASE/oa-ai && docker compose down 2>/dev/null || true && docker compose up -d" +} + +# ============================================================ +# 6. 执行恢复 +# ============================================================ +for site in "${restore_list[@]}"; do + case "$site" in + shared) restore_shared ;; + assets) restore_assets ;; + issue) restore_issue ;; + ldap) restore_ldap ;; + gitea) restore_gitea ;; + oa) restore_oa ;; + esac +done + +# ============================================================ +# 7. 重载 nginx +# ============================================================ +log "重载 nginx 配置..." +ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" 2>/dev/null || warn "nginx 重载失败,请手动检查" + +# ============================================================ +# 8. 清理临时文件 +# ============================================================ +log "清理临时文件..." +rm -rf "$RESTORE_DIR" + +# ============================================================ +# 9. 验证恢复结果 +# ============================================================ +echo "" +log "恢复完成!验证容器状态..." +ssh txjp "docker ps --format 'table {{.Names}}\t{{.Status}}' | grep -E '(assets|issue|ldap|gitea|oa|nginx)'" diff --git a/deploy-ai.sh b/deploy-ai.sh index 553a006..2f2b4a5 100755 --- a/deploy-ai.sh +++ b/deploy-ai.sh @@ -67,8 +67,9 @@ echo " 4) issue.tlyq.ai (工单系统)" echo " 5) assets.tlyq.ai (资产管理系统)" echo " 6) oa.tlyq.ai (OA 统一门户 — 含 nginx 配置)" echo " 7) ldap.tlyq.ai (LLDAP 用户目录服务)" + echo " 8) git.tlyq.ai (Gitea 代码托管)" echo "" -printf "请输入编号 (1/2/3/4/5/6/7): " +printf "请输入编号 (1/2/3/4/5/6/7/8): " read choice SITE=""; LOCAL_DIR=""; REMOTE_DIR=""; CONTAINER="" @@ -80,6 +81,7 @@ case "$choice" in 5) SITE="assets"; LOCAL_DIR="/Users/niuniu/programs/docker/assets-ai"; REMOTE_DIR="/root/docker/assets-ai"; CONTAINER="assets-ai" ;; 6) SITE="oa"; LOCAL_DIR="/Users/niuniu/programs/docker/oa-ai"; REMOTE_DIR="/root/docker/oa-ai"; CONTAINER="oa-ai" ;; 7) SITE="ldap"; LOCAL_DIR="/Users/niuniu/programs/docker/ldap-ai"; REMOTE_DIR="/root/docker/ldap-ai"; CONTAINER="lldap" ;; + 8) SITE="gitea"; LOCAL_DIR="/Users/niuniu/programs/docker/gitea-ai"; REMOTE_DIR="/root/docker/gitea-ai"; CONTAINER="gitea-ai" ;; *) echo "无效选择"; exit 1 ;; esac @@ -327,7 +329,27 @@ build_on_server() { ssh txjp "test -f $REMOTE_DIR/Dockerfile" 2>/dev/null && build_flag="--build" ssh txjp "cd $REMOTE_DIR && docker compose up -d $build_flag && docker compose restart" - # 7. 保存快照 + # 7. 清理构建产生的悬空镜像和缓存(根据磁盘使用率动态调整) + log "清理构建缓存..." + ssh txjp "docker image prune -f --filter 'label=com.docker.compose.project=${SITE}-ai' 2>/dev/null || true" + + # 根据磁盘使用率动态调整构建缓存清理策略 + DISK_USAGE=$(ssh txjp "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50") + if [ "$DISK_USAGE" -gt 95 ]; then + warn "磁盘使用率 ${DISK_USAGE}%,执行紧急清理" + ssh txjp "docker builder prune -f 2>/dev/null || true" + elif [ "$DISK_USAGE" -gt 85 ]; then + info "磁盘使用率 ${DISK_USAGE}%,执行激进清理(6小时)" + ssh txjp "docker builder prune -f --filter 'until=6h' 2>/dev/null || true" + elif [ "$DISK_USAGE" -gt 70 ]; then + info "磁盘使用率 ${DISK_USAGE}%,执行正常清理(24小时)" + ssh txjp "docker builder prune -f --filter 'until=24h' 2>/dev/null || true" + else + info "磁盘使用率 ${DISK_USAGE}%,执行保守清理(48小时)" + ssh txjp "docker builder prune -f --filter 'until=48h' 2>/dev/null || true" + fi + + # 8. 保存快照 ssh txjp "echo '$local_md5' > ${SNAPSHOT_FILE}" } @@ -420,7 +442,11 @@ CHECKEOF build_on_server || exit 1 info "部署 nginx 配置..." ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/oa-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true - docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" ;; + docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" + # 批量同步 LLDAP email 至各站点(新服务器首次部署填充历史数据) + info "同步 LLDAP 邮箱至各站点..." + ssh txjp "node /root/docker/oa-ai/scripts/sync-emails-to-sites.js 2>/dev/null" || warn "邮箱同步失败(不影响 OA 主流程)" + ;; ldap) info "部署 LLDAP..." scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml @@ -438,6 +464,11 @@ CHECKEOF sed -i '/3890:3890/d' docker-compose.yml 2>/dev/null || true && \ docker compose up -d --build && docker compose restart" log "LLDAP 已部署" ;; + gitea) + info "部署 Gitea..." + scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml + ssh txjp "cd $REMOTE_DIR && docker compose up -d && docker restart nginx-ai" + log "Gitea 已部署" ;; esac # ============================================================ @@ -457,6 +488,7 @@ case "$SITE" in assets) URL="https://assets.tlyq.ai" ;; oa) URL="https://oa.tlyq.ai" ;; ldap) URL="http://localhost:6178" ;; + gitea) URL="https://git.tlyq.ai" ;; esac if [[ "$SITE" == "ldap" ]]; then diff --git a/sites-manage-cloud.sh b/sites-manage-cloud.sh index 90af7b5..5bc2dda 100644 --- a/sites-manage-cloud.sh +++ b/sites-manage-cloud.sh @@ -79,6 +79,23 @@ log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; } warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; } err() { printf "${RED}[✗]${NC} %s\n" "$1"; } +# 检查 Docker 服务是否运行 +check_docker() { + if $IS_LOCAL_SERVER; then + # 本地模式:检查本地 Docker + if ! docker info >/dev/null 2>&1; then + err "Docker 服务未启动,请先启动 Docker" + exit 1 + fi + else + # SSH 模式:检查远程 Docker + if ! ssh $SERVER "docker info" >/dev/null 2>&1; then + err "远程服务器 Docker 服务未运行" + exit 1 + fi + fi +} + expand_sites() { local result=() for s in "$@"; do @@ -186,6 +203,11 @@ if [ ${#TARGETS[@]} -eq 0 ]; then mapfile -t TARGETS < <(expand_sites ai) fi +# 检查 Docker 服务(help 命令除外) +if [ "$ACTION" != "help" ] && [ "$ACTION" != "--help" ] && [ "$ACTION" != "-h" ]; then + check_docker +fi + case $ACTION in start) for s in "${TARGETS[@]}"; do remote_start "$s"; done diff --git a/sites-manage-local.sh b/sites-manage-local.sh index b24551a..c0f38f1 100755 --- a/sites-manage-local.sh +++ b/sites-manage-local.sh @@ -31,6 +31,14 @@ LDAP_WEB_PORT=6178 is_port() { lsof -i :"$1" >/dev/null 2>&1; } is_running() { docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^$1$"; } +# 检查 Docker 服务是否运行 +check_docker() { + if ! docker info >/dev/null 2>&1; then + err "Docker 服务未启动,请先启动 Docker Desktop" + exit 1 + fi +} + # ============================================================ # 单个 Docker 站点操作 # ============================================================ @@ -264,6 +272,9 @@ show_help() { # ============================================================ # 入口 # ============================================================ +# 检查 Docker 服务 +check_docker + ACTION=${1:-} shift || true