feat: 完善备份和恢复脚本
- 新增备份脚本目录结构 - 新增站点独立备份脚本 - 新增完整备份脚本 - 新增数据库备份脚本 - 新增站点恢复脚本 - 新增完整恢复脚本 - 更新 README 文档
This commit is contained in:
parent
502d1341fb
commit
4b65bfa850
56
README.md
56
README.md
|
|
@ -4,16 +4,29 @@
|
||||||
|
|
||||||
## 脚本清单
|
## 脚本清单
|
||||||
|
|
||||||
|
### 部署与管理
|
||||||
|
|
||||||
| 脚本 | 用途 | 目标环境 |
|
| 脚本 | 用途 | 目标环境 |
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| `sites-manage-local.sh` | 本地站点启停(npm run dev + Docker) | 本地 |
|
| `sites-manage-local.sh` | 本地站点启停(npm run dev + Docker) | 本地 |
|
||||||
| `sites-manage-cloud.sh` | 云服务器站点启停(SSH / 服务器本地运行) | 云端 |
|
| `sites-manage-cloud.sh` | 云服务器站点启停(SSH / 服务器本地运行) | 云端 |
|
||||||
| `deploy-ai.sh` | 部署站点到 tlyq.ai(txjp 服务器) | 云端 |
|
| `deploy-ai.sh` | 部署站点到 tlyq.ai(txjp 服务器) | 云端 |
|
||||||
| `deploy-cc.sh` | 部署站点到 tlyq.cc(tgz 服务器) | 云端 |
|
| `deploy-cc.sh` | 部署站点到 tlyq.cc(tgz 服务器) | 云端 |
|
||||||
| `backup-db.sh` | 备份数据库(assets / issue / ldap) | 云端 → 本地 |
|
|
||||||
| `restore-db.sh` | 恢复数据库到本地或云端 | 本地 / 云端 |
|
|
||||||
| `edit-site-cc.sh` | www.tlyq.cc 官网内容管理 | 云端 |
|
| `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 — 本地站点管理
|
## sites-manage-local.sh — 本地站点管理
|
||||||
|
|
@ -69,36 +82,26 @@ bash scripts/deploy-ai.sh --restart # 仅重启容器
|
||||||
|
|
||||||
LDAP 部署特殊处理:上传 Dockerfile + docker-compose.yml,自动创建 .env,去除端口映射,`--build` 构建含 sqlite3 的镜像。
|
LDAP 部署特殊处理:上传 Dockerfile + docker-compose.yml,自动创建 .env,去除端口映射,`--build` 构建含 sqlite3 的镜像。
|
||||||
|
|
||||||
## backup-db.sh — 数据库备份
|
## backup/ — 备份与恢复
|
||||||
|
|
||||||
```bash
|
```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`
|
||||||
|
|
||||||
| 数据库 | 服务 | 内容 |
|
备份内容:数据库 + 上传文件 + 配置文件 + nginx + SSL 证书
|
||||||
|--------|------|------|
|
|
||||||
| assets-{时间戳}.db | assets-ai | 设备资产 |
|
|
||||||
| issue-{时间戳}.db | issue-ai | 工单、配件 |
|
|
||||||
| ldap-{时间戳}.db | lldap | **全部用户账号** |
|
|
||||||
|
|
||||||
流程:WAL checkpoint → 云端打包 → 云端持久保留 → 下载到本地 → 完整性验证。
|
> 详细文档见 [backup/README.md](backup/README.md)
|
||||||
|
|
||||||
> 详细文档见 [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 → 启动 → 验证。
|
|
||||||
|
|
||||||
## 备份存储路径
|
## 备份存储路径
|
||||||
|
|
||||||
|
|
@ -106,7 +109,6 @@ bash scripts/restore-db.sh local assets assets-20260518_1200.db
|
||||||
|------|------|
|
|------|------|
|
||||||
| 云端持久备份 | `/root/docker/db-backups/` |
|
| 云端持久备份 | `/root/docker/db-backups/` |
|
||||||
| 本地备份 | `db-backups/` |
|
| 本地备份 | `db-backups/` |
|
||||||
| 云端临时 | `/tmp/db-backup/`(完成后清理) |
|
|
||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),可考虑单独备份频率
|
||||||
|
- 建议定期验证备份文件可恢复性
|
||||||
|
|
@ -23,7 +23,8 @@ ssh txjp "docker exec lldap sqlite3 /data/users.db 'PRAGMA wal_checkpoint(TRUNCA
|
||||||
ssh txjp "mkdir -p /tmp/db-backup && \
|
ssh txjp "mkdir -p /tmp/db-backup && \
|
||||||
docker cp assets-ai:/app/data/assets.db /tmp/db-backup/assets-${TIMESTAMP}.db && \
|
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 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}"
|
mkdir -p "${LOCAL_DIR}"
|
||||||
|
|
@ -33,7 +34,8 @@ CLOUD_DIR="/root/docker/db-backups"
|
||||||
ssh txjp "mkdir -p ${CLOUD_DIR} && \
|
ssh txjp "mkdir -p ${CLOUD_DIR} && \
|
||||||
cp /tmp/db-backup/assets-${TIMESTAMP}.db ${CLOUD_DIR}/assets-${TIMESTAMP}.db && \
|
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/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}"
|
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/assets-${TIMESTAMP}.db" "${LOCAL_DIR}/"
|
||||||
scp "txjp:/tmp/db-backup/issue-${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/ldap-${TIMESTAMP}.db" "${LOCAL_DIR}/"
|
||||||
|
scp "txjp:/tmp/db-backup/gitea-${TIMESTAMP}.db" "${LOCAL_DIR}/"
|
||||||
|
|
||||||
# 验证备份文件为有效 SQLite 数据库
|
# 验证备份文件为有效 SQLite 数据库
|
||||||
log "验证备份完整性..."
|
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
|
if ! sqlite3 "$f" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then
|
||||||
echo "[错误] 备份文件无效或损坏: $f"
|
echo "[错误] 备份文件无效或损坏: $f"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -58,6 +61,6 @@ ssh txjp "rm -rf /tmp/db-backup"
|
||||||
|
|
||||||
log "备份完成:${TIMESTAMP}"
|
log "备份完成:${TIMESTAMP}"
|
||||||
echo " 云端: ${CLOUD_DIR}/"
|
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 " 本地:"
|
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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -20,8 +20,11 @@ set -e
|
||||||
if [ $# -lt 3 ]; then
|
if [ $# -lt 3 ]; then
|
||||||
echo "用法: $0 <local|cloud> <issue|assets|ldap> <备份文件名>"
|
echo "用法: $0 <local|cloud> <issue|assets|ldap> <备份文件名>"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "支持格式: .db 或 .tar.gz(自动解压)"
|
||||||
|
echo ""
|
||||||
echo "示例:"
|
echo "示例:"
|
||||||
echo " $0 local issue issue-2026-04-29_1753.db"
|
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 ldap ldap-2026-05-15_1200.db"
|
||||||
echo " $0 cloud assets assets-2026-04-29_1753.db"
|
echo " $0 cloud assets assets-2026-04-29_1753.db"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -71,6 +74,7 @@ esac
|
||||||
# ============================================
|
# ============================================
|
||||||
restore_local() {
|
restore_local() {
|
||||||
local BACKUP_PATH="${LOCAL_BACKUP_DIR}/${BACKUP_FILE}"
|
local BACKUP_PATH="${LOCAL_BACKUP_DIR}/${BACKUP_FILE}"
|
||||||
|
local DB_FILE="$BACKUP_PATH"
|
||||||
|
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo " 本地数据库恢复"
|
echo " 本地数据库恢复"
|
||||||
|
|
@ -87,55 +91,78 @@ restore_local() {
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. 验证备份文件是有效的 SQLite 数据库
|
# 2. 如果是 .tar.gz 文件,解压获取 .db 文件
|
||||||
if ! sqlite3 "$BACKUP_PATH" "SELECT count(*) FROM sqlite_master" > /dev/null 2>&1; then
|
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 数据库"
|
echo "[错误] 备份文件不是有效的 SQLite 数据库"
|
||||||
|
[ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. 确认操作
|
# 4. 确认操作
|
||||||
echo "[警告] 这将覆盖本地 ${CONTAINER} 数据库!"
|
echo "[警告] 这将覆盖本地 ${CONTAINER} 数据库!"
|
||||||
echo " 当前数据库: ${LOCAL_DB_PATH}"
|
echo " 当前数据库: ${LOCAL_DB_PATH}"
|
||||||
echo " 将替换为: ${BACKUP_PATH}"
|
echo " 将替换为: ${DB_FILE}"
|
||||||
echo ""
|
echo ""
|
||||||
read -p "确认恢复? (输入 yes 继续): " CONFIRM
|
read -p "确认恢复? (输入 yes 继续): " CONFIRM
|
||||||
if [ "$CONFIRM" != "yes" ]; then
|
if [ "$CONFIRM" != "yes" ]; then
|
||||||
echo "已取消"
|
echo "已取消"
|
||||||
|
[ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. 备份当前数据库(安全起见)
|
# 5. 备份当前数据库(安全起见)
|
||||||
local SAFE_BACKUP="${LOCAL_DB_PATH}.before-restore-$(date +%Y%m%d_%H%M%S)"
|
local SAFE_BACKUP="${LOCAL_DB_PATH}.before-restore-$(date +%Y%m%d_%H%M%S)"
|
||||||
if [ -f "$LOCAL_DB_PATH" ]; then
|
if [ -f "$LOCAL_DB_PATH" ]; then
|
||||||
cp "$LOCAL_DB_PATH" "$SAFE_BACKUP"
|
cp "$LOCAL_DB_PATH" "$SAFE_BACKUP"
|
||||||
echo "[备份] 当前数据库已备份至: ${SAFE_BACKUP}"
|
echo "[备份] 当前数据库已备份至: ${SAFE_BACKUP}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. WAL checkpoint on backup (确保数据完整)
|
# 6. WAL checkpoint on backup (确保数据完整)
|
||||||
sqlite3 "$BACKUP_PATH" "PRAGMA wal_checkpoint(TRUNCATE)" 2>/dev/null || true
|
sqlite3 "$DB_FILE" "PRAGMA wal_checkpoint(TRUNCATE)" 2>/dev/null || true
|
||||||
|
|
||||||
# 6. 停止本地容器(如果在运行)
|
# 7. 停止本地容器(如果在运行)
|
||||||
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
|
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
|
||||||
echo "[操作] 停止容器 ${CONTAINER}..."
|
echo "[操作] 停止容器 ${CONTAINER}..."
|
||||||
docker stop "$CONTAINER"
|
docker stop "$CONTAINER"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 7. 替换数据库
|
# 8. 替换数据库
|
||||||
cp "$BACKUP_PATH" "$LOCAL_DB_PATH"
|
cp "$DB_FILE" "$LOCAL_DB_PATH"
|
||||||
rm -f "${LOCAL_DB_PATH}-shm" "${LOCAL_DB_PATH}-wal"
|
rm -f "${LOCAL_DB_PATH}-shm" "${LOCAL_DB_PATH}-wal"
|
||||||
echo "[完成] 数据库已替换"
|
echo "[完成] 数据库已替换"
|
||||||
|
|
||||||
# 8. 启动容器
|
# 9. 清理临时文件
|
||||||
|
[ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"
|
||||||
|
|
||||||
|
# 10. 启动容器
|
||||||
echo "[操作] 启动容器 ${CONTAINER}..."
|
echo "[操作] 启动容器 ${CONTAINER}..."
|
||||||
docker start "$CONTAINER"
|
docker start "$CONTAINER"
|
||||||
|
|
||||||
# 9. 验证
|
# 11. 验证
|
||||||
sleep 2
|
sleep 2
|
||||||
echo ""
|
echo ""
|
||||||
echo "[验证] 恢复后数据:"
|
echo "[验证] 恢复后数据:"
|
||||||
if [ "$SERVICE" = "issue" ]; then
|
if [ "$SERVICE" = "issue" ]; then
|
||||||
echo " 工单数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM tickets")"
|
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 != ''") 条"
|
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
|
else
|
||||||
echo " 设备数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM assets")"
|
echo " 设备数: $(sqlite3 "$LOCAL_DB_PATH" "SELECT COUNT(*) FROM assets")"
|
||||||
fi
|
fi
|
||||||
|
|
@ -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 "恢复完成!"
|
||||||
|
|
@ -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)'"
|
||||||
38
deploy-ai.sh
38
deploy-ai.sh
|
|
@ -67,8 +67,9 @@ echo " 4) issue.tlyq.ai (工单系统)"
|
||||||
echo " 5) assets.tlyq.ai (资产管理系统)"
|
echo " 5) assets.tlyq.ai (资产管理系统)"
|
||||||
echo " 6) oa.tlyq.ai (OA 统一门户 — 含 nginx 配置)"
|
echo " 6) oa.tlyq.ai (OA 统一门户 — 含 nginx 配置)"
|
||||||
echo " 7) ldap.tlyq.ai (LLDAP 用户目录服务)"
|
echo " 7) ldap.tlyq.ai (LLDAP 用户目录服务)"
|
||||||
|
echo " 8) git.tlyq.ai (Gitea 代码托管)"
|
||||||
echo ""
|
echo ""
|
||||||
printf "请输入编号 (1/2/3/4/5/6/7): "
|
printf "请输入编号 (1/2/3/4/5/6/7/8): "
|
||||||
read choice
|
read choice
|
||||||
|
|
||||||
SITE=""; LOCAL_DIR=""; REMOTE_DIR=""; CONTAINER=""
|
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" ;;
|
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" ;;
|
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" ;;
|
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 ;;
|
*) echo "无效选择"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|
@ -327,7 +329,27 @@ build_on_server() {
|
||||||
ssh txjp "test -f $REMOTE_DIR/Dockerfile" 2>/dev/null && build_flag="--build"
|
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"
|
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}"
|
ssh txjp "echo '$local_md5' > ${SNAPSHOT_FILE}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -420,7 +442,11 @@ CHECKEOF
|
||||||
build_on_server || exit 1
|
build_on_server || exit 1
|
||||||
info "部署 nginx 配置..."
|
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
|
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)
|
ldap)
|
||||||
info "部署 LLDAP..."
|
info "部署 LLDAP..."
|
||||||
scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml
|
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 && \
|
sed -i '/3890:3890/d' docker-compose.yml 2>/dev/null || true && \
|
||||||
docker compose up -d --build && docker compose restart"
|
docker compose up -d --build && docker compose restart"
|
||||||
log "LLDAP 已部署" ;;
|
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
|
esac
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -457,6 +488,7 @@ case "$SITE" in
|
||||||
assets) URL="https://assets.tlyq.ai" ;;
|
assets) URL="https://assets.tlyq.ai" ;;
|
||||||
oa) URL="https://oa.tlyq.ai" ;;
|
oa) URL="https://oa.tlyq.ai" ;;
|
||||||
ldap) URL="http://localhost:6178" ;;
|
ldap) URL="http://localhost:6178" ;;
|
||||||
|
gitea) URL="https://git.tlyq.ai" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ "$SITE" == "ldap" ]]; then
|
if [[ "$SITE" == "ldap" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,23 @@ log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; }
|
||||||
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
|
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
|
||||||
err() { printf "${RED}[✗]${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() {
|
expand_sites() {
|
||||||
local result=()
|
local result=()
|
||||||
for s in "$@"; do
|
for s in "$@"; do
|
||||||
|
|
@ -186,6 +203,11 @@ if [ ${#TARGETS[@]} -eq 0 ]; then
|
||||||
mapfile -t TARGETS < <(expand_sites ai)
|
mapfile -t TARGETS < <(expand_sites ai)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 检查 Docker 服务(help 命令除外)
|
||||||
|
if [ "$ACTION" != "help" ] && [ "$ACTION" != "--help" ] && [ "$ACTION" != "-h" ]; then
|
||||||
|
check_docker
|
||||||
|
fi
|
||||||
|
|
||||||
case $ACTION in
|
case $ACTION in
|
||||||
start)
|
start)
|
||||||
for s in "${TARGETS[@]}"; do remote_start "$s"; done
|
for s in "${TARGETS[@]}"; do remote_start "$s"; done
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ LDAP_WEB_PORT=6178
|
||||||
is_port() { lsof -i :"$1" >/dev/null 2>&1; }
|
is_port() { lsof -i :"$1" >/dev/null 2>&1; }
|
||||||
is_running() { docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^$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 站点操作
|
# 单个 Docker 站点操作
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -264,6 +272,9 @@ show_help() {
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 入口
|
# 入口
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
# 检查 Docker 服务
|
||||||
|
check_docker
|
||||||
|
|
||||||
ACTION=${1:-}
|
ACTION=${1:-}
|
||||||
shift || true
|
shift || true
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue