Spaces:
Runtime error
Runtime error
File size: 7,668 Bytes
484ec23 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | """Резервное копирование данных SalGram в приватный репозиторий Hugging Face.
Зачем: на бесплатном HF Space диск эфемерный — при пересборке/перезапуске/
«засыпании» Space всё в контейнере пропадает. Этот модуль раз в
HF_BACKUP_INTERVAL секунд снимает консистентную копию БД и выгружает её
(вместе с ключом шифрования и медиафайлами) в ПРИВАТНЫЙ dataset-репозиторий,
а при старте контейнера восстанавливает оттуда. Так код остаётся в публичном
Space, а данные лежат только в приватном репо — их видишь и качаешь только ты.
Включается только если заданы переменные окружения:
HF_BACKUP_REPO — id приватного репо, напр. "username/salgram-data"
HF_TOKEN — токен с правом записи в этот репо
HF_BACKUP_INTERVAL — (необяз.) период в секундах, по умолчанию 1200 (20 мин)
Без них модуль ничего не делает — локальная разработка и Amvera не затронуты.
"""
import json
import os
import shutil
import sqlite3
import tempfile
import threading
import time
import traceback
REPO_ENV = "HF_BACKUP_REPO"
TOKEN_ENV = "HF_TOKEN"
INTERVAL_ENV = "HF_BACKUP_INTERVAL"
DEFAULT_INTERVAL = 1200 # 20 минут
REPO_TYPE = "dataset"
DB_NAME = "salgram.db"
EXTRA_FILES = ("secret.key", "vapid.json") # без них восстановленную БД не расшифровать
MEDIA_DIRS = ("avatars", "photos", "files", "wallpapers", "gifs", "audio")
_INDEX = ".hf_uploaded.json" # локальный список уже выгруженных файлов (чтобы не слать повторно)
def _config():
"""(repo, token, interval) если бэкап настроен, иначе None."""
repo = os.environ.get(REPO_ENV)
token = os.environ.get(TOKEN_ENV)
if not repo or not token:
return None
try:
interval = int(os.environ.get(INTERVAL_ENV, DEFAULT_INTERVAL))
except ValueError:
interval = DEFAULT_INTERVAL
return repo, token, max(60, interval)
def _log(msg):
print(f"[hf_backup] {msg}", flush=True)
def _snapshot_db(data_dir, dst_path):
"""Консистентная копия БД через online-backup API SQLite (безопасно при WAL)."""
src = sqlite3.connect(os.path.join(data_dir, DB_NAME), timeout=30)
try:
dst = sqlite3.connect(dst_path)
try:
with dst:
src.backup(dst)
finally:
dst.close()
finally:
src.close()
def restore_on_boot(data_dir):
"""Если локальной БД нет — скачать последнюю копию из приватного репо.
Вызывать ДО _load_key() и init_db(): восстановленные ключ и БД нужны им.
"""
cfg = _config()
if not cfg:
return
repo, token, _ = cfg
if os.path.exists(os.path.join(data_dir, DB_NAME)):
_log("локальная БД уже на месте — восстановление пропущено")
return
try:
from huggingface_hub import snapshot_download
snap = snapshot_download(repo_id=repo, repo_type=REPO_TYPE, token=token)
except Exception as e: # репо ещё нет / пустой / нет сети — стартуем с чистой БД
_log(f"копий пока нет или репо недоступен ({e!r}) — старт с чистой БД")
return
count = 0
for root, _dirs, files in os.walk(snap):
for name in files:
if name.startswith("."):
continue # служебные .gitattributes / .cache
rel = os.path.relpath(os.path.join(root, name), snap)
target = os.path.join(data_dir, rel)
os.makedirs(os.path.dirname(target) or ".", exist_ok=True)
# snapshot_download кладёт симлинки на кеш — копируем именно содержимое.
shutil.copyfile(os.path.realpath(os.path.join(root, name)), target)
count += 1
_log(f"восстановлено файлов из бэкапа: {count}")
def _remote_files(repo, token):
"""Список файлов, уже лежащих в репо (для первичного заполнения индекса)."""
try:
from huggingface_hub import HfApi
return set(HfApi(token=token).list_repo_files(repo, repo_type=REPO_TYPE))
except Exception:
return set()
def backup_now(data_dir):
"""Один цикл бэкапа: снимок БД + новые медиа/ключи -> приватный репо."""
cfg = _config()
if not cfg:
return
repo, token, _ = cfg
from huggingface_hub import CommitOperationAdd, HfApi
api = HfApi(token=token)
api.create_repo(repo, repo_type=REPO_TYPE, private=True, exist_ok=True)
index_path = os.path.join(data_dir, _INDEX)
if os.path.exists(index_path):
with open(index_path) as f:
uploaded = set(json.load(f))
else: # первый бэкап в этом контейнере — берём, что уже есть в репо
uploaded = _remote_files(repo, token)
with tempfile.TemporaryDirectory() as tmp:
snap = os.path.join(tmp, DB_NAME)
_snapshot_db(data_dir, snap)
ops = [CommitOperationAdd(DB_NAME, snap)] # БД меняется всегда — шлём каждый раз
added = []
for name in EXTRA_FILES:
path = os.path.join(data_dir, name)
if os.path.exists(path) and name not in uploaded:
ops.append(CommitOperationAdd(name, path))
added.append(name)
for d in MEDIA_DIRS:
ddir = os.path.join(data_dir, d)
if not os.path.isdir(ddir):
continue
for name in sorted(os.listdir(ddir)):
rel = f"{d}/{name}"
path = os.path.join(ddir, name)
if os.path.isfile(path) and rel not in uploaded:
ops.append(CommitOperationAdd(rel, path))
added.append(rel)
api.create_commit(repo_id=repo, repo_type=REPO_TYPE, operations=ops,
commit_message="SalGram backup")
uploaded.update(added)
with open(index_path, "w") as f:
json.dump(sorted(uploaded), f)
return len(added)
def start(data_dir):
"""Запустить фоновый поток периодического бэкапа (no-op, если не настроено)."""
cfg = _config()
if not cfg:
return
_, _, interval = cfg
def loop():
while True:
time.sleep(interval)
try:
n = backup_now(data_dir)
_log(f"бэкап выполнен (+{n} новых файлов)")
except Exception:
_log("ошибка бэкапа:\n" + traceback.format_exc())
threading.Thread(target=loop, name="hf-backup", daemon=True).start()
_log(f"периодический бэкап включён: каждые {interval} с -> {os.environ[REPO_ENV]}")
|