tkgray commited on
Commit
79eb867
·
verified ·
1 Parent(s): 260f29e

Upload backup.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. backup.py +127 -0
backup.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Back up Open WebUI user data to a Hugging Face dataset repo.
3
+
4
+ Covers everything a user would miss after a volume wipe:
5
+ * webui.db - users, logins, settings, chats (consistent SQLite snapshot)
6
+ * vector_db/ - RAG embeddings for uploaded documents
7
+ * uploads/ - user-uploaded files
8
+
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
17
+ import sqlite3
18
+ import sys
19
+ import tarfile
20
+ import tempfile
21
+
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
+
30
+ def log(msg):
31
+ print(f"[backup] {msg}", flush=True)
32
+
33
+
34
+ def snapshot_sqlite(src, dst):
35
+ """Write a consistent copy of a live SQLite DB using the backup API.
36
+
37
+ Copying the file directly (or tarring it) can capture a torn WAL, so use
38
+ sqlite3's own online-backup, which is safe while the app is writing.
39
+ """
40
+ source = sqlite3.connect(f"file:{src}?mode=ro", uri=True)
41
+ try:
42
+ dest = sqlite3.connect(dst)
43
+ try:
44
+ with dest:
45
+ source.backup(dest)
46
+ finally:
47
+ dest.close()
48
+ finally:
49
+ source.close()
50
+
51
+
52
+ def build_archive(payload_dir, archive_path):
53
+ """Deterministically tar.gz a staged payload.
54
+
55
+ Every member's mtime/uid/gid and the gzip header mtime are zeroed and the
56
+ member order is fixed, so an unchanged payload yields byte-identical output.
57
+ That lets the dataset host recognise a no-op upload and skip the commit,
58
+ instead of creating a fresh commit every hour.
59
+ """
60
+ entries = []
61
+ for root, _dirs, files in os.walk(payload_dir):
62
+ for name in files:
63
+ full = os.path.join(root, name)
64
+ entries.append((os.path.relpath(full, payload_dir), full))
65
+ entries.sort()
66
+
67
+ with open(archive_path, "wb") as raw:
68
+ # filename="" suppresses the gzip FNAME header, which would otherwise
69
+ # carry the temp archive path and make every run's bytes differ.
70
+ with gzip.GzipFile(filename="", fileobj=raw, mode="wb", mtime=0) as gz:
71
+ with tarfile.open(fileobj=gz, mode="w") as tf:
72
+ for rel, full in entries:
73
+ info = tf.gettarinfo(full, arcname=rel)
74
+ info.mtime = 0
75
+ info.uid = 0
76
+ info.gid = 0
77
+ info.uname = ""
78
+ info.gname = ""
79
+ with open(full, "rb") as fh:
80
+ tf.addfile(info, fh)
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.")
87
+ return 0
88
+
89
+ token = os.environ.get("HF_TOKEN")
90
+ if not token:
91
+ log("HF_TOKEN not set — skipping.")
92
+ return 0
93
+
94
+ stage = tempfile.mkdtemp()
95
+ payload = os.path.join(stage, "data")
96
+ os.makedirs(payload, exist_ok=True)
97
+ try:
98
+ snapshot_sqlite(DB_PATH, os.path.join(payload, "webui.db"))
99
+ for name in INCLUDE_DIRS:
100
+ src = os.path.join(DATA_DIR, name)
101
+ if os.path.isdir(src):
102
+ shutil.copytree(src, os.path.join(payload, name), symlinks=True)
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}")
121
+ return 1
122
+ finally:
123
+ shutil.rmtree(stage, ignore_errors=True)
124
+
125
+
126
+ if __name__ == "__main__":
127
+ sys.exit(main())