Collapse dashboard server status into top bar and include .env in backup restore.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-02 15:43:48 +08:00
parent aae897b7eb
commit 481086eddc
5 changed files with 149 additions and 29 deletions
+47 -6
View File
@@ -48,6 +48,7 @@ RESTORE_MD = """# qihuo 备份恢复说明
| `futures.db` | SQLite 主库(仅 SQLite 模式备份) |
| `postgres_dump.sql` | PostgreSQL 逻辑备份(仅 PostgreSQL 模式) |
| `uploads/` | 复盘截图与 K 线图(若备份时存在) |
| `.env` | 环境配置(`config/.env` 或根目录 `.env` |
| `manifest.json` | 备份元数据(含 `backend` 字段) |
| `restore.sh` | 一键恢复脚本 |
@@ -73,7 +74,7 @@ RESTORE_DIR=/opt/qihuo ./restore.sh
```
3. 在新服务器部署 qihuo 代码与 Python 环境(见 `docs/POSTGRES.md` / `docs/DEPLOY.md`
4. 配置 `.env``DATABASE_URL` 或 SQLite、`SECRET_KEY`、CTP 账号等
4. 若包内含 `.env``restore.sh` 会自动恢复到应用目录(无需手工复制
5. 重启服务:`pm2 restart qihuo`
## PostgreSQL 恢复
@@ -101,7 +102,7 @@ cp -a uploads/. /opt/qihuo/uploads/
## 注意
- 恢复前请停止 qihuo 进程
- `.env` 含敏感信息,请单独安全传输
- `.env` 含敏感信息,请妥善保管备份包
- 详见 `docs/POSTGRES.md` 与 `docs/BACKUP.md`
"""
@@ -176,7 +177,25 @@ def _backup_postgres(dst_path: str) -> None:
raise RuntimeError(f"pg_dump 失败: {proc.stderr.strip() or proc.stdout.strip()}")
def _write_restore_script(dest: Path, *, backend: str) -> None:
def _env_backup_info() -> tuple[Optional[Path], str]:
"""返回 (源 .env 路径, 恢复到应用目录的相对路径)。"""
from modules.core.paths import CONFIG_DIR, LEGACY_ENV_FILE, ROOT, resolve_env_file
src = Path(resolve_env_file())
if not src.is_file():
return None, "config/.env"
try:
if src.resolve().parent == CONFIG_DIR.resolve():
return src, "config/.env"
if src.resolve().parent == ROOT.resolve() and src.name == ".env":
if LEGACY_ENV_FILE.is_file() and not (CONFIG_DIR / ".env").is_file():
return src, ".env"
except Exception:
pass
return src, "config/.env"
def _write_restore_script(dest: Path, *, backend: str, env_restore_path: str = "") -> None:
pg_block = ""
if backend == "postgres":
pg_block = """
@@ -201,13 +220,23 @@ if [ -f "$SCRIPT_DIR/postgres_dump.sql" ]; then
psql "$DATABASE_URL" -f "$SCRIPT_DIR/postgres_dump.sql"
echo "PostgreSQL 导入完成"
fi
"""
env_block = ""
if env_restore_path:
env_block = f"""
if [ -f "$SCRIPT_DIR/.env" ]; then
ENV_DEST="$RESTORE_DIR/{env_restore_path}"
mkdir -p "$(dirname "$ENV_DEST")"
cp -f "$SCRIPT_DIR/.env" "$ENV_DEST"
echo "已复制 .env -> $ENV_DEST"
fi
"""
script = f"""#!/bin/bash
set -euo pipefail
RESTORE_DIR="${{RESTORE_DIR:-{default_restore_dir()}}}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$RESTORE_DIR/uploads"
{pg_block}
{pg_block}{env_block}
if [ -f "$SCRIPT_DIR/futures.db" ]; then
cp -f "$SCRIPT_DIR/futures.db" "$RESTORE_DIR/futures.db"
echo "已复制 futures.db -> $RESTORE_DIR/futures.db"
@@ -218,7 +247,7 @@ if [ -d "$SCRIPT_DIR/uploads" ]; then
fi
echo ""
echo "恢复完成。目标目录: $RESTORE_DIR"
echo "下一步: 确认 .env、pm2 restart qihuo"
echo "下一步: pm2 restart qihuo"
echo "详见 RESTORE.md 与 docs/POSTGRES.md"
"""
dest.write_text(script, encoding="utf-8")
@@ -251,12 +280,20 @@ def create_backup(*, include_uploads: bool = True) -> tuple[str, str]:
if include_uploads and upload_src.is_dir():
shutil.copytree(upload_src, work / "uploads", dirs_exist_ok=True)
env_src, env_restore_path = _env_backup_info()
includes_env = False
if env_src and env_src.is_file():
shutil.copy2(env_src, work / ".env")
includes_env = True
manifest = {
"app": "qihuo",
"backend": backend,
"created_at": datetime.now(TZ).isoformat(timespec="seconds"),
"db_path": DB_PATH if backend == "sqlite" else (os.getenv("DATABASE_URL") or ""),
"includes_uploads": include_uploads and upload_src.is_dir(),
"includes_env": includes_env,
"env_restore_path": env_restore_path if includes_env else "",
"default_restore_dir": default_restore_dir(),
"files": sorted(p.name for p in work.iterdir()),
}
@@ -265,7 +302,11 @@ def create_backup(*, include_uploads: bool = True) -> tuple[str, str]:
encoding="utf-8",
)
(work / "RESTORE.md").write_text(RESTORE_MD, encoding="utf-8")
_write_restore_script(work / "restore.sh", backend=backend)
_write_restore_script(
work / "restore.sh",
backend=backend,
env_restore_path=env_restore_path if includes_env else "",
)
with tarfile.open(out_path, "w:gz") as tar:
tar.add(work, arcname=folder_name)