tkgray commited on
Commit
4466efd
·
1 Parent(s): 3815eaf

backup: keep dated daily snapshots alongside the rolling archive

Browse files

The 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.

Files changed (3) hide show
  1. backup.py +71 -9
  2. restore.py +5 -1
  3. 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}:{ARCHIVE_IN_REPO} ...")
107
 
108
  from huggingface_hub import HfApi
109
 
110
- HfApi(token=token).upload_file(
111
- path_or_fileobj=archive,
112
- path_in_repo=ARCHIVE_IN_REPO,
113
- repo_id=REPO_ID,
114
- repo_type="dataset",
115
- token=token,
116
- )
117
- log("Upload OK")
 
 
 
 
 
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 (every hour, keeps last 24 snapshots)
 
 
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"