Ensure scheduled auto backups explicitly include .env in backup archives.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+26
-19
@@ -516,20 +516,21 @@ def _backup_sqlite(src_path: str, dst_path: 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
|
||||
from modules.core.paths import CONFIG_DIR, ENV_FILE, LEGACY_ENV_FILE
|
||||
|
||||
src = Path(resolve_env_file())
|
||||
if not src.is_file():
|
||||
if ENV_FILE.is_file():
|
||||
return ENV_FILE, "config/.env"
|
||||
if LEGACY_ENV_FILE.is_file():
|
||||
return LEGACY_ENV_FILE, ".env"
|
||||
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 _copy_env_into_backup(work: Path) -> tuple[bool, str]:
|
||||
env_src, env_restore_path = _env_backup_info()
|
||||
if env_src and env_src.is_file():
|
||||
shutil.copy2(env_src, work / ".env")
|
||||
return True, env_restore_path
|
||||
return False, env_restore_path
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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 备份,返回 (文件名, 说明)。"""
|
||||
if not os.path.isfile(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():
|
||||
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
|
||||
env_restore_path = "config/.env"
|
||||
if include_env:
|
||||
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 = {
|
||||
"app": "qihuo",
|
||||
@@ -617,7 +619,8 @@ def create_backup(*, include_uploads: bool = True) -> tuple[str, str]:
|
||||
tar.add(work, arcname=folder_name)
|
||||
|
||||
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]:
|
||||
@@ -678,13 +681,14 @@ def run_backup_job(
|
||||
get_setting: Callable[[str, str], str],
|
||||
set_setting: Callable[[str, str], None],
|
||||
include_uploads: bool = True,
|
||||
include_env: bool = True,
|
||||
) -> tuple[str, str]:
|
||||
keep = DEFAULT_KEEP_COUNT
|
||||
try:
|
||||
keep = max(5, min(200, int(get_setting(BACKUP_KEEP_KEY, str(DEFAULT_KEEP_COUNT)) or DEFAULT_KEEP_COUNT)))
|
||||
except ValueError:
|
||||
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"))
|
||||
removed = prune_old_backups(keep)
|
||||
if removed:
|
||||
@@ -697,6 +701,7 @@ def schedule_backup(
|
||||
get_setting: Callable[[str, str], str],
|
||||
set_setting: Callable[[str, str], None],
|
||||
include_uploads: bool = True,
|
||||
include_env: bool = True,
|
||||
) -> tuple[bool, str]:
|
||||
if _backup_lock.locked():
|
||||
return False, "备份进行中,请稍后再试"
|
||||
@@ -709,6 +714,7 @@ def schedule_backup(
|
||||
get_setting=get_setting,
|
||||
set_setting=set_setting,
|
||||
include_uploads=include_uploads,
|
||||
include_env=include_env,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("backup failed: %s", exc)
|
||||
@@ -751,6 +757,7 @@ def start_backup_worker(
|
||||
get_setting=get_setting_fn,
|
||||
set_setting=set_setting_fn,
|
||||
include_uploads=True,
|
||||
include_env=True,
|
||||
)
|
||||
logger.info("auto backup: %s — %s", filename, msg)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -536,7 +536,7 @@
|
||||
<div class="field">
|
||||
<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 %}>
|
||||
<span>启用每日自动备份</span>
|
||||
<span>启用每日自动备份(含 <code>futures.db</code>、<code>uploads/</code>、<code>.env</code>)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
||||
Reference in New Issue
Block a user