Spaces:
Running
Running
File size: 6,279 Bytes
f5d7f2b 2f37df2 f5d7f2b 2f37df2 f5d7f2b 2f37df2 f5d7f2b 2f37df2 f5d7f2b 2f37df2 f5d7f2b 2f37df2 f5d7f2b | 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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | #!/usr/bin/env python3
import argparse
import json
import os
import shutil
import tarfile
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from huggingface_hub import HfApi, hf_hub_download
try:
from huggingface_hub.errors import EntryNotFoundError, HfHubHTTPError
except ImportError:
from huggingface_hub.utils import EntryNotFoundError, HfHubHTTPError
DEFAULT_EXCLUDES = {
".cache",
"__pycache__",
"tmp",
"temp",
}
def env(name: str, default: str | None = None) -> str | None:
value = os.environ.get(name)
return value if value not in (None, "") else default
def required_env(name: str) -> str:
value = env(name)
if not value:
raise SystemExit(f"{name} is required")
return value
def should_exclude(path: Path) -> bool:
parts = set(path.parts)
if parts & DEFAULT_EXCLUDES:
return True
name = path.name
return name.endswith((".log", ".pid", ".pyc", ".tmp", ".lock"))
def state_has_content(root: Path) -> bool:
if not root.exists():
return False
for child in root.rglob("*"):
if child.is_file() and not should_exclude(child.relative_to(root)):
return True
return False
def state_stats(root: Path) -> tuple[int, int]:
files = 0
bytes_total = 0
if not root.exists():
return files, bytes_total
for child in root.rglob("*"):
if not child.is_file():
continue
rel = child.relative_to(root)
if should_exclude(rel):
continue
files += 1
try:
bytes_total += child.stat().st_size
except OSError:
pass
return files, bytes_total
def add_tree(tar: tarfile.TarFile, root: Path) -> None:
for item in root.rglob("*"):
rel = item.relative_to(root)
if should_exclude(rel):
continue
tar.add(item, arcname=str(rel), recursive=False)
def validate_member(target: Path, member: tarfile.TarInfo) -> Path:
destination = (target / member.name).resolve()
target_resolved = target.resolve()
if target_resolved != destination and target_resolved not in destination.parents:
raise RuntimeError(f"Unsafe archive member: {member.name}")
return destination
def safe_extract(archive: Path, target: Path) -> None:
with tarfile.open(archive, "r:gz") as tar:
for member in tar.getmembers():
validate_member(target, member)
tar.extractall(target)
def backup() -> None:
repo = required_env("HERMES_BACKUP_REPO")
token = required_env("HF_BACKUP_TOKEN")
root = Path(env("HERMES_BACKUP_DIR", "/home/hermeswebui/.hermes")).resolve()
filename = env("HERMES_BACKUP_FILENAME", "hermes-state.tar.gz")
file_count, byte_count = state_stats(root)
print(f"Hermes state backup source: {root} ({file_count} files, {byte_count} bytes).")
if file_count == 0:
print("Hermes state backup skipped: no meaningful local state yet.")
return
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
api = HfApi(token=token)
with tempfile.TemporaryDirectory(prefix="hermes-backup-") as tmp:
tmpdir = Path(tmp)
archive = tmpdir / filename
manifest = tmpdir / "manifest.json"
with tarfile.open(archive, "w:gz") as tar:
add_tree(tar, root)
archive_size = archive.stat().st_size
manifest.write_text(
json.dumps(
{
"created_at": timestamp,
"source": "Acrabohan/hermes-webui",
"path_in_repo": filename,
"file_count": file_count,
"source_bytes": byte_count,
"archive_bytes": archive_size,
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
api.upload_file(
path_or_fileobj=str(archive),
path_in_repo=filename,
repo_id=repo,
repo_type="dataset",
token=token,
commit_message=f"Backup Hermes state {timestamp}",
)
api.upload_file(
path_or_fileobj=str(manifest),
path_in_repo="manifest.json",
repo_id=repo,
repo_type="dataset",
token=token,
commit_message=f"Update Hermes backup manifest {timestamp}",
)
print(f"Hermes state backup uploaded to dataset {repo} ({archive_size} bytes).")
def restore() -> None:
repo = required_env("HERMES_BACKUP_REPO")
token = required_env("HF_BACKUP_TOKEN")
root = Path(env("HERMES_BACKUP_DIR", "/home/hermeswebui/.hermes")).resolve()
filename = env("HERMES_BACKUP_FILENAME", "hermes-state.tar.gz")
force = env("HERMES_FORCE_RESTORE", "0") == "1"
if state_has_content(root) and not force:
print("Hermes state restore skipped: local state already exists.")
return
root.mkdir(parents=True, exist_ok=True)
try:
archive_path = hf_hub_download(
repo_id=repo,
filename=filename,
repo_type="dataset",
token=token,
)
except EntryNotFoundError:
print(f"Hermes state restore skipped: {filename} not found in {repo}.")
return
except HfHubHTTPError as exc:
if getattr(exc.response, "status_code", None) == 404:
print(f"Hermes state restore skipped: backup dataset or file not found.")
return
raise
if force and state_has_content(root):
stamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
backup_existing = root.parent / f"{root.name}.pre-restore-{stamp}"
shutil.move(str(root), str(backup_existing))
root.mkdir(parents=True, exist_ok=True)
print(f"Existing Hermes state moved to {backup_existing}.")
safe_extract(Path(archive_path), root)
print(f"Hermes state restored from dataset {repo}.")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=("backup", "restore"))
args = parser.parse_args()
if args.command == "backup":
backup()
else:
restore()
if __name__ == "__main__":
main()
|