Spaces:
Running
Running
tkgray commited on
Commit ·
4466efd
1
Parent(s): 3815eaf
backup: keep dated daily snapshots alongside the rolling archive
Browse filesThe rolling archive is overwritten every hour, so a bad wipe would be baked
into the only copy within one interval. Also upload
backups/daily/open-webui-data-<date>.tar.gz and prune to the newest
OWUI_BACKUP_DAILY_KEEP (default 7). restore.py accepts OWUI_RESTORE_ARCHIVE to
restore a pinned snapshot instead of the rolling latest.
- backup.py +71 -9
- restore.py +5 -1
- start-with-backup.sh +3 -1
backup.py
CHANGED
|
@@ -9,8 +9,16 @@ Covers everything a user would miss after a volume wipe:
|
|
| 9 |
Deliberately EXCLUDES /data/cache (the embedding-model cache, ~1 GB, regenerates
|
| 10 |
itself on boot).
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
Requires the HF_TOKEN env var (Space secret). Never fails the app.
|
| 13 |
"""
|
|
|
|
| 14 |
import gzip
|
| 15 |
import os
|
| 16 |
import shutil
|
|
@@ -22,8 +30,23 @@ import tempfile
|
|
| 22 |
DATA_DIR = os.environ.get("OWUI_DATA_DIR", "/data")
|
| 23 |
REPO_ID = os.environ.get("OWUI_BACKUP_REPO", "tkgray/open-webui-backups")
|
| 24 |
ARCHIVE_IN_REPO = "backups/open-webui-data.tar.gz"
|
|
|
|
|
|
|
| 25 |
INCLUDE_DIRS = ("vector_db", "uploads")
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
DB_PATH = os.path.join(DATA_DIR, "webui.db")
|
| 28 |
|
| 29 |
|
|
@@ -81,6 +104,40 @@ def build_archive(payload_dir, archive_path):
|
|
| 81 |
return archive_path
|
| 82 |
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def main():
|
| 85 |
if not os.path.isfile(DB_PATH):
|
| 86 |
log(f"{DB_PATH} does not exist yet — skipping.")
|
|
@@ -103,18 +160,23 @@ def main():
|
|
| 103 |
|
| 104 |
archive = os.path.join(stage, "open-webui-data.tar.gz")
|
| 105 |
build_archive(payload, archive)
|
| 106 |
-
log(f"staged {os.path.getsize(archive)} bytes; uploading to {REPO_ID}
|
| 107 |
|
| 108 |
from huggingface_hub import HfApi
|
| 109 |
|
| 110 |
-
HfApi(token=token)
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
return 0
|
| 119 |
except Exception as exc: # noqa: BLE001 - never fail the app over a backup
|
| 120 |
log(f"Upload FAILED: {exc}")
|
|
|
|
| 9 |
Deliberately EXCLUDES /data/cache (the embedding-model cache, ~1 GB, regenerates
|
| 10 |
itself on boot).
|
| 11 |
|
| 12 |
+
Two copies go up per run: the rolling latest at backups/open-webui-data.tar.gz
|
| 13 |
+
(what restore.py reads on boot) and a dated snapshot
|
| 14 |
+
backups/daily/open-webui-data-<YYYY-MM-DD>.tar.gz. The dated snapshots are what
|
| 15 |
+
make a bad wipe survivable - the rolling file is overwritten every hour, so a
|
| 16 |
+
corrupted dataset gets baked into it within one interval. The newest
|
| 17 |
+
OWUI_BACKUP_DAILY_KEEP (default 7) dated snapshots are kept, older ones deleted.
|
| 18 |
+
|
| 19 |
Requires the HF_TOKEN env var (Space secret). Never fails the app.
|
| 20 |
"""
|
| 21 |
+
import datetime
|
| 22 |
import gzip
|
| 23 |
import os
|
| 24 |
import shutil
|
|
|
|
| 30 |
DATA_DIR = os.environ.get("OWUI_DATA_DIR", "/data")
|
| 31 |
REPO_ID = os.environ.get("OWUI_BACKUP_REPO", "tkgray/open-webui-backups")
|
| 32 |
ARCHIVE_IN_REPO = "backups/open-webui-data.tar.gz"
|
| 33 |
+
DAILY_DIR = "backups/daily"
|
| 34 |
+
DAILY_PREFIX = "open-webui-data-"
|
| 35 |
INCLUDE_DIRS = ("vector_db", "uploads")
|
| 36 |
|
| 37 |
+
|
| 38 |
+
def env_int(name, default):
|
| 39 |
+
raw = os.environ.get(name)
|
| 40 |
+
if raw is None:
|
| 41 |
+
return default
|
| 42 |
+
try:
|
| 43 |
+
return int(raw)
|
| 44 |
+
except (TypeError, ValueError):
|
| 45 |
+
return default
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
DAILY_KEEP = env_int("OWUI_BACKUP_DAILY_KEEP", 7)
|
| 49 |
+
|
| 50 |
DB_PATH = os.path.join(DATA_DIR, "webui.db")
|
| 51 |
|
| 52 |
|
|
|
|
| 104 |
return archive_path
|
| 105 |
|
| 106 |
|
| 107 |
+
def prune_daily(api, token):
|
| 108 |
+
"""Delete dated snapshots beyond the newest DAILY_KEEP.
|
| 109 |
+
|
| 110 |
+
Filenames are ISO-dated, so a plain sort is chronological. Only ever
|
| 111 |
+
deletes under DAILY_DIR, so the rolling archive and the legacy single-file
|
| 112 |
+
backup are untouchable by this path.
|
| 113 |
+
"""
|
| 114 |
+
try:
|
| 115 |
+
files = api.list_repo_files(REPO_ID, repo_type="dataset", token=token)
|
| 116 |
+
except Exception as exc: # noqa: BLE001 - a prune failure must not fail the run
|
| 117 |
+
log(f"prune skipped, listing failed: {exc}")
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
dated = sorted(
|
| 121 |
+
f for f in files
|
| 122 |
+
if f.startswith(f"{DAILY_DIR}/{DAILY_PREFIX}") and f.endswith(".tar.gz")
|
| 123 |
+
)
|
| 124 |
+
stale = dated[:-DAILY_KEEP] if DAILY_KEEP > 0 else dated
|
| 125 |
+
if not stale:
|
| 126 |
+
log(f"prune: {len(dated)} dated snapshot(s) on disk, limit {DAILY_KEEP} - nothing to delete")
|
| 127 |
+
return
|
| 128 |
+
|
| 129 |
+
from huggingface_hub import CommitOperationDelete, create_commit
|
| 130 |
+
|
| 131 |
+
create_commit(
|
| 132 |
+
repo_id=REPO_ID,
|
| 133 |
+
repo_type="dataset",
|
| 134 |
+
operations=[CommitOperationDelete(path_in_repo=path) for path in stale],
|
| 135 |
+
commit_message=f"prune {len(stale)} snapshot(s) beyond {DAILY_KEEP}-day retention",
|
| 136 |
+
token=token,
|
| 137 |
+
)
|
| 138 |
+
log(f"prune: deleted {', '.join(stale)}")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
def main():
|
| 142 |
if not os.path.isfile(DB_PATH):
|
| 143 |
log(f"{DB_PATH} does not exist yet — skipping.")
|
|
|
|
| 160 |
|
| 161 |
archive = os.path.join(stage, "open-webui-data.tar.gz")
|
| 162 |
build_archive(payload, archive)
|
| 163 |
+
log(f"staged {os.path.getsize(archive)} bytes; uploading to {REPO_ID} ...")
|
| 164 |
|
| 165 |
from huggingface_hub import HfApi
|
| 166 |
|
| 167 |
+
api = HfApi(token=token)
|
| 168 |
+
dated_in_repo = f"{DAILY_DIR}/{DAILY_PREFIX}{datetime.date.today().isoformat()}.tar.gz"
|
| 169 |
+
for path_in_repo in (ARCHIVE_IN_REPO, dated_in_repo):
|
| 170 |
+
api.upload_file(
|
| 171 |
+
path_or_fileobj=archive,
|
| 172 |
+
path_in_repo=path_in_repo,
|
| 173 |
+
repo_id=REPO_ID,
|
| 174 |
+
repo_type="dataset",
|
| 175 |
+
token=token,
|
| 176 |
+
)
|
| 177 |
+
log(f"Upload OK -> {path_in_repo}")
|
| 178 |
+
|
| 179 |
+
prune_daily(api, token)
|
| 180 |
return 0
|
| 181 |
except Exception as exc: # noqa: BLE001 - never fail the app over a backup
|
| 182 |
log(f"Upload FAILED: {exc}")
|
restore.py
CHANGED
|
@@ -8,6 +8,10 @@ single-file DB backup written by older versions of backup.sh.
|
|
| 8 |
|
| 9 |
Never fails the boot: a restore error is logged and ignored, so a first boot
|
| 10 |
with no backup yet still starts the app.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"""
|
| 12 |
import os
|
| 13 |
import shutil
|
|
@@ -16,7 +20,7 @@ import tarfile
|
|
| 16 |
|
| 17 |
DATA_DIR = os.environ.get("OWUI_DATA_DIR", "/data")
|
| 18 |
REPO_ID = os.environ.get("OWUI_BACKUP_REPO", "tkgray/open-webui-backups")
|
| 19 |
-
ARCHIVE_IN_REPO = "backups/open-webui-data.tar.gz"
|
| 20 |
LEGACY_DB_IN_REPO = "backups/webui.db"
|
| 21 |
|
| 22 |
DB_PATH = os.path.join(DATA_DIR, "webui.db")
|
|
|
|
| 8 |
|
| 9 |
Never fails the boot: a restore error is logged and ignored, so a first boot
|
| 10 |
with no backup yet still starts the app.
|
| 11 |
+
|
| 12 |
+
Set OWUI_RESTORE_ARCHIVE to restore a pinned snapshot instead of the rolling
|
| 13 |
+
latest, e.g. backups/daily/open-webui-data-2026-10-28.tar.gz (list what exists
|
| 14 |
+
with HfApi().list_repo_files('tkgray/open-webui-backups', repo_type='dataset')).
|
| 15 |
"""
|
| 16 |
import os
|
| 17 |
import shutil
|
|
|
|
| 20 |
|
| 21 |
DATA_DIR = os.environ.get("OWUI_DATA_DIR", "/data")
|
| 22 |
REPO_ID = os.environ.get("OWUI_BACKUP_REPO", "tkgray/open-webui-backups")
|
| 23 |
+
ARCHIVE_IN_REPO = os.environ.get("OWUI_RESTORE_ARCHIVE", "backups/open-webui-data.tar.gz")
|
| 24 |
LEGACY_DB_IN_REPO = "backups/webui.db"
|
| 25 |
|
| 26 |
DB_PATH = os.path.join(DATA_DIR, "webui.db")
|
start-with-backup.sh
CHANGED
|
@@ -6,7 +6,9 @@ set -eu
|
|
| 6 |
# 1. Restore if db doesn't exist
|
| 7 |
/restore.sh
|
| 8 |
|
| 9 |
-
# 2. Start background backup loop
|
|
|
|
|
|
|
| 10 |
(
|
| 11 |
BACKUP_INTERVAL="${BACKUP_INTERVAL_SECONDS:-3600}"
|
| 12 |
echo "[start] Backup loop started — interval: \${BACKUP_INTERVAL}s"
|
|
|
|
| 6 |
# 1. Restore if db doesn't exist
|
| 7 |
/restore.sh
|
| 8 |
|
| 9 |
+
# 2. Start background backup loop: hourly rolling snapshot (overwritten each run)
|
| 10 |
+
# plus one dated snapshot per day, keeping the newest OWUI_BACKUP_DAILY_KEEP
|
| 11 |
+
# (default 7) days.
|
| 12 |
(
|
| 13 |
BACKUP_INTERVAL="${BACKUP_INTERVAL_SECONDS:-3600}"
|
| 14 |
echo "[start] Backup loop started — interval: \${BACKUP_INTERVAL}s"
|