Ensure scheduled auto backups explicitly include .env in backup archives.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-02 16:38:51 +08:00
parent 8ebe1a3c77
commit b0afff53af
2 changed files with 28 additions and 21 deletions
+27 -20
View File
@@ -516,20 +516,21 @@ def _backup_sqlite(src_path: str, dst_path: str) -> None:
def _env_backup_info() -> tuple[Optional[Path], str]: def _env_backup_info() -> tuple[Optional[Path], str]:
"""返回 (源 .env 路径, 恢复到应用目录的相对路径)。""" """返回 (源 .env 路径, 恢复到应用目录的相对路径)。"""
from modules.core.paths import CONFIG_DIR, LEGACY_ENV_FILE, ROOT, resolve_env_file from modules.core.paths import CONFIG_DIR, ENV_FILE, LEGACY_ENV_FILE
src = Path(resolve_env_file()) if ENV_FILE.is_file():
if not src.is_file(): return ENV_FILE, "config/.env"
return None, "config/.env" if LEGACY_ENV_FILE.is_file():
try: return LEGACY_ENV_FILE, ".env"
if src.resolve().parent == CONFIG_DIR.resolve(): return None, "config/.env"
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(): def _copy_env_into_backup(work: Path) -> tuple[bool, str]:
return src, ".env" env_src, env_restore_path = _env_backup_info()
except Exception: if env_src and env_src.is_file():
pass shutil.copy2(env_src, work / ".env")
return src, "config/.env" return True, env_restore_path
return False, env_restore_path
def _write_restore_script(dest: Path, *, env_restore_path: str = "") -> None: def _write_restore_script(dest: Path, *, env_restore_path: str = "") -> None:
@@ -565,7 +566,7 @@ echo "详见 RESTORE.md 与 docs/BACKUP.md"
dest.write_text(script, encoding="utf-8") dest.write_text(script, encoding="utf-8")
def create_backup(*, include_uploads: bool = True) -> tuple[str, str]: def create_backup(*, include_uploads: bool = True, include_env: bool = True) -> tuple[str, str]:
"""创建 tar.gz 备份,返回 (文件名, 说明)。""" """创建 tar.gz 备份,返回 (文件名, 说明)。"""
if not os.path.isfile(DB_PATH): if not os.path.isfile(DB_PATH):
raise FileNotFoundError(f"数据库不存在: {DB_PATH}") raise FileNotFoundError(f"数据库不存在: {DB_PATH}")
@@ -586,11 +587,12 @@ def create_backup(*, include_uploads: bool = True) -> tuple[str, str]:
if include_uploads and upload_src.is_dir(): if include_uploads and upload_src.is_dir():
shutil.copytree(upload_src, work / "uploads", dirs_exist_ok=True) shutil.copytree(upload_src, work / "uploads", dirs_exist_ok=True)
env_src, env_restore_path = _env_backup_info()
includes_env = False includes_env = False
if env_src and env_src.is_file(): env_restore_path = "config/.env"
shutil.copy2(env_src, work / ".env") if include_env:
includes_env = True includes_env, env_restore_path = _copy_env_into_backup(work)
if not includes_env:
logger.warning("backup: .env not found (checked config/.env and root .env)")
manifest = { manifest = {
"app": "qihuo", "app": "qihuo",
@@ -617,7 +619,8 @@ def create_backup(*, include_uploads: bool = True) -> tuple[str, str]:
tar.add(work, arcname=folder_name) tar.add(work, arcname=folder_name)
size_mb = out_path.stat().st_size / (1024 * 1024) size_mb = out_path.stat().st_size / (1024 * 1024)
return filename, f"备份已生成 {filename}{size_mb:.2f} MB" env_note = "含 .env" if includes_env else "未含 .env"
return filename, f"备份已生成 {filename}{size_mb:.2f} MB{env_note}"
def list_backups(*, with_manifest: bool = True) -> list[dict]: def list_backups(*, with_manifest: bool = True) -> list[dict]:
@@ -678,13 +681,14 @@ def run_backup_job(
get_setting: Callable[[str, str], str], get_setting: Callable[[str, str], str],
set_setting: Callable[[str, str], None], set_setting: Callable[[str, str], None],
include_uploads: bool = True, include_uploads: bool = True,
include_env: bool = True,
) -> tuple[str, str]: ) -> tuple[str, str]:
keep = DEFAULT_KEEP_COUNT keep = DEFAULT_KEEP_COUNT
try: try:
keep = max(5, min(200, int(get_setting(BACKUP_KEEP_KEY, str(DEFAULT_KEEP_COUNT)) or DEFAULT_KEEP_COUNT))) keep = max(5, min(200, int(get_setting(BACKUP_KEEP_KEY, str(DEFAULT_KEEP_COUNT)) or DEFAULT_KEEP_COUNT)))
except ValueError: except ValueError:
pass pass
filename, msg = create_backup(include_uploads=include_uploads) filename, msg = create_backup(include_uploads=include_uploads, include_env=include_env)
set_setting(BACKUP_LAST_KEY, datetime.now(TZ).isoformat(timespec="seconds")) set_setting(BACKUP_LAST_KEY, datetime.now(TZ).isoformat(timespec="seconds"))
removed = prune_old_backups(keep) removed = prune_old_backups(keep)
if removed: if removed:
@@ -697,6 +701,7 @@ def schedule_backup(
get_setting: Callable[[str, str], str], get_setting: Callable[[str, str], str],
set_setting: Callable[[str, str], None], set_setting: Callable[[str, str], None],
include_uploads: bool = True, include_uploads: bool = True,
include_env: bool = True,
) -> tuple[bool, str]: ) -> tuple[bool, str]:
if _backup_lock.locked(): if _backup_lock.locked():
return False, "备份进行中,请稍后再试" return False, "备份进行中,请稍后再试"
@@ -709,6 +714,7 @@ def schedule_backup(
get_setting=get_setting, get_setting=get_setting,
set_setting=set_setting, set_setting=set_setting,
include_uploads=include_uploads, include_uploads=include_uploads,
include_env=include_env,
) )
except Exception as exc: except Exception as exc:
logger.exception("backup failed: %s", exc) logger.exception("backup failed: %s", exc)
@@ -751,6 +757,7 @@ def start_backup_worker(
get_setting=get_setting_fn, get_setting=get_setting_fn,
set_setting=set_setting_fn, set_setting=set_setting_fn,
include_uploads=True, include_uploads=True,
include_env=True,
) )
logger.info("auto backup: %s%s", filename, msg) logger.info("auto backup: %s%s", filename, msg)
except Exception as exc: except Exception as exc:
+1 -1
View File
@@ -536,7 +536,7 @@
<div class="field"> <div class="field">
<label style="display:flex;align-items:center;gap:.45rem;cursor:pointer"> <label style="display:flex;align-items:center;gap:.45rem;cursor:pointer">
<input type="checkbox" name="backup_auto_enabled" value="1" {% if backup_auto_enabled %}checked{% endif %}> <input type="checkbox" name="backup_auto_enabled" value="1" {% if backup_auto_enabled %}checked{% endif %}>
<span>启用每日自动备份</span> <span>启用每日自动备份(含 <code>futures.db</code><code>uploads/</code><code>.env</code></span>
</label> </label>
</div> </div>
<div class="field"> <div class="field">