xiaoxiaxia-temp / backup-manager.py
sharween's picture
Upload backup-manager.py with huggingface_hub
ec8d6ef verified
Raw
History Blame Contribute Delete
31.6 kB
#!/usr/bin/env python3
"""
backup-manager.py WebDAV + HF Dataset /
:
WebDAV + BACKUP_WEBDAV_INTERVAL WEBDAV_MAX_BACKUPS
( 60 ) ( 30 )
HF Dataset BACKUP_HF_INTERVAL_SMALL 1 ()
( 30 )
HF Dataset BACKUP_HF_INTERVAL_BIG DATASET_MAX_BACKUPS
( 30 ) ( 30 )
: HF Dataset 1GB,
: WebDAV HF Dataset
: last_run ,
"""
import hashlib, json, os, tarfile, time, sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
import requests
#
#
#
STATE_DIR = os.environ.get("OPENCLAW_STATE_DIR", "/root/.openclaw")
# WebDAV
WEBDAV_URL = os.environ.get("WEBDAV_URL", "").rstrip("/")
WEBDAV_USER = os.environ.get("WEBDAV_USERNAME", "")
WEBDAV_PASS = os.environ.get("WEBDAV_PASSWORD", "")
WEBDAV_BACKUP_DIR = os.environ.get("WEBDAV_BACKUP_DIR", "openclaw-backup")
WEBDAV_INTERVAL = int(os.environ.get("BACKUP_WEBDAV_INTERVAL", "60"))
WEBDAV_MAX_BACKUPS = int(os.environ.get("WEBDAV_MAX_BACKUPS", "30"))
# HF Dataset
HF_REPO = os.environ.get("HF_DATASET", "")
HF_TOKEN = os.environ.get("HF_TOKEN", "")
HF_SMALL_INTERVAL = int(os.environ.get("BACKUP_HF_INTERVAL_SMALL", "30"))
HF_BIG_INTERVAL = int(os.environ.get("BACKUP_HF_INTERVAL_BIG", "30"))
DATASET_MAX_BACKUPS = int(os.environ.get("DATASET_MAX_BACKUPS", "30"))
#
FULL_NAME = "openclaw-full.tar.gz"
MANIFEST_NAME = "_incremental_manifest.json"
# HF
MAX_HF_STORAGE = 1 * 1024 * 1024 * 1024 # 1 GB
#
HASH_CHUNK_SIZE = 65536 # SHA256
WD_CHECK_TIMEOUT = 3 # WebDAV
WD_REQ_TIMEOUT = 30 # WebDAV
#
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RESET = "\033[0m"
#
#
#
def _print_banner():
""""""
global WD_SKIPPED, HF_SKIPPED
sep = "" * 50
print(f"[backup] {sep}")
wd_configured = _wd_is_configured()
if not wd_configured:
print(f"[backup] WebDAV {RED} : WEBDAV_URL / WEBDAV_USERNAME / WEBDAV_PASSWORD / WEBDAV_BACKUP_DIR {RESET}")
WD_SKIPPED = True
else:
print(f"[backup] WebDAV {GREEN} {RESET} ={WEBDAV_INTERVAL} ={WEBDAV_MAX_BACKUPS}")
WD_SKIPPED = False
if not HF_REPO or not HF_TOKEN:
hf_missing = []
if not HF_REPO: hf_missing.append("HF_DATASET")
if not HF_TOKEN: hf_missing.append("HF_TOKEN")
print(f"[backup] HF {RED} : {', '.join(hf_missing)}{RESET}")
print(f"[backup] HF {RED} {RESET}")
HF_SKIPPED = True
else:
print(f"[backup] HF {GREEN} {RESET} ={HF_SMALL_INTERVAL}")
print(f"[backup] HF {GREEN} {RESET} ={HF_BIG_INTERVAL} ={DATASET_MAX_BACKUPS}")
HF_SKIPPED = False
print(f"[backup] {sep}")
# banner import
_DISPLAYED_BANNER = False
def _ensure_banner():
global _DISPLAYED_BANNER
if not _DISPLAYED_BANNER:
_print_banner()
_DISPLAYED_BANNER = True
#
#
#
def _create_tar() -> str:
""" STATE_DIR tar.gz, ."""
tarpath = f"/tmp/openclaw-backup-{int(time.time()*1000)}.tar.gz"
try:
with tarfile.open(tarpath, "w:gz") as tar:
root = Path(STATE_DIR)
if root.exists():
for item in root.iterdir():
if item.exists():
name = item.name
if name.startswith("_last_") or name.endswith(".tmp") or name == ".lock":
continue
try:
tar.add(str(item), arcname=name)
except (PermissionError, FileNotFoundError) as e:
print(f"[backup] {name}: {e}")
continue
else:
print(f"[backup] STATE_DIR {STATE_DIR} , ")
root.mkdir(parents=True, exist_ok=True)
size = os.path.getsize(tarpath)
print(f"[backup] Archive created ({size/1024/1024:.1f} MB)")
except (OSError, tarfile.TarError) as e:
print(f"[backup] : {e}")
if os.path.exists(tarpath):
os.remove(tarpath)
raise #
return tarpath
def _file_hash(path: str) -> str:
""" SHA256 , ."""
try:
h = hashlib.sha256()
with open(path, "rb") as f:
while True:
chunk = f.read(HASH_CHUNK_SIZE)
if not chunk:
break
h.update(chunk)
return h.hexdigest()
except (OSError, PermissionError, FileNotFoundError) as e:
print(f"[backup] : {path} {e}")
return ""
def _today_str() -> str:
return time.strftime("%Y%m%d")
#
# WebDAV HTTP
#
def _wd_auth():
return (WEBDAV_USER, WEBDAV_PASS) if WEBDAV_USER else None
def _wd_url(path=""):
return f"{WEBDAV_URL}/{WEBDAV_BACKUP_DIR}/{path.lstrip('/')}"
def _wd_req(method, path="", **kwargs):
url = _wd_url(path)
kwargs.setdefault("timeout", WD_REQ_TIMEOUT) # /
resp = requests.request(method, url, auth=_wd_auth(), **kwargs)
resp.raise_for_status()
return resp
def _wd_check():
""" WebDAV (3s ), False"""
try:
url = _wd_url("")
resp = requests.request("PROPFIND", url, auth=_wd_auth(), timeout=3)
resp.raise_for_status() # 401/403/404
return True
except Exception:
return False
def wd_exists(path):
try:
_wd_req("PROPFIND", path)
return True
except Exception:
return False
def wd_upload(path, data):
return _wd_req("PUT", path, data=data)
def wd_download(path):
return _wd_req("GET", path).content
def wd_delete(path):
""" WebDAV """
try:
_wd_req("DELETE", path)
return True
except Exception as e:
print(f"[backup] WebDAV : {path} {e}")
return False
def wd_mkdir(parts):
""" WebDAV """
try:
_wd_req("MKCOL", "")
except Exception:
pass
for i in range(1, len(parts) + 1):
p = "/".join(parts[:i])
try:
_wd_req("MKCOL", p)
except Exception:
pass
def wd_list(path=""):
""" WebDAV """
try:
import xml.etree.ElementTree as ET
resp = requests.request(
"PROPFIND", _wd_url(path),
auth=_wd_auth(), timeout=3,
headers={"Depth": "1"},
)
resp.raise_for_status()
root = ET.fromstring(resp.content)
ns = {"d": "DAV:"}
items = []
for el in root.findall(".//d:response", ns):
href_el = el.find("d:href", ns)
if href_el is not None and href_el.text:
name = href_el.text.rstrip("/").split("/")[-1]
if name:
items.append(name)
return [x for x in items if x != WEBDAV_BACKUP_DIR.split("/")[-1]]
except Exception as e:
print(f"[backup] WebDAV : {e}")
return []
#
# WebDAV (Manifest)
#
def _load_manifest() -> dict:
try:
data = wd_download(MANIFEST_NAME)
return json.loads(data)
except Exception:
return {}
def _save_manifest(manifest: dict):
wd_upload(MANIFEST_NAME, json.dumps(manifest, indent=2).encode())
def _scan_and_upload_changes(root: Path, manifest: dict) -> int:
""" WebDAV. ."""
changed = 0
for fpath in root.rglob("*"):
if not fpath.is_file():
continue
rel = str(fpath.relative_to(root))
if rel == MANIFEST_NAME or rel.startswith("_incremental"):
continue
basename = fpath.name
if basename in (".lock", ".pid", ".tmp") or basename.endswith(".log"):
continue
if "node_modules" in rel:
continue
if rel.startswith("_last_"):
continue
cur_h = _file_hash(str(fpath))
prev = manifest.get(rel, {})
if cur_h != prev.get("sha256"):
try:
parts = ["files", *rel.split("/")]
wd_mkdir(parts[:-1])
wd_upload("/".join(parts), fpath.read_bytes())
manifest[rel] = {
"sha256": cur_h,
"mtime": fpath.stat().st_mtime,
"size": fpath.stat().st_size,
}
changed += 1
except Exception as e:
err_str = str(e)
# 409 =
if "409" in err_str or "Conflict" in err_str:
manifest[rel] = {
"sha256": cur_h,
"mtime": fpath.stat().st_mtime,
"size": fpath.stat().st_size,
}
changed += 1
else:
print(f"[backup] (): {rel} {e}")
return changed
def incremental_backup() -> int:
""", WebDAV. ."""
root = Path(STATE_DIR)
if not root.exists():
return 0
# WebDAV
if not _wd_is_configured():
print(f"[backup] WebDAV , ")
return 0
# WebDAV
if not _wd_check():
print(f"[backup] WebDAV , ")
return 0
manifest = _load_manifest()
changed = _scan_and_upload_changes(root, manifest)
if changed:
try:
_save_manifest(manifest)
except Exception as e:
print(f"[backup] Manifest : {e}")
return changed
#
# WebDAV +
#
def _wd_cleanup():
""" WEBDAV_MAX_BACKUPS WebDAV """
files = wd_list("")
now = datetime.now()
deleted = 0
for f in files:
if not f.startswith("openclaw-full-") or not f.endswith(".tar.gz"):
continue
if f == FULL_NAME:
continue #
try:
date_str = f.replace("openclaw-full-", "").replace(".tar.gz", "")
dt = datetime.strptime(date_str, "%Y%m%d")
days_old = (now - dt).days
if days_old > WEBDAV_MAX_BACKUPS:
if wd_delete(f):
print(f"[backup] WebDAV : {f} ({days_old})")
deleted += 1
except ValueError:
continue
if deleted:
print(f"[backup] WebDAV {deleted} ")
return deleted
def _wd_is_configured() -> bool:
""" WebDAV 4 """
return bool(WEBDAV_URL and WEBDAV_USER and WEBDAV_PASS and WEBDAV_BACKUP_DIR)
def full_backup():
""" tar WebDAV (: openclaw-full-YYYYMMDD.tar.gz)"""
tarpath = _create_tar()
if _wd_is_configured():
# WebDAV
if not _wd_check():
print(f"[backup] WebDAV , ")
os.remove(tarpath)
return
wd_mkdir([])
filename = f"openclaw-full-{_today_str()}.tar.gz"
try:
with open(tarpath, "rb") as f:
wd_upload(filename, f.read())
print(f"[backup] WebDAV {filename}")
#
with open(tarpath, "rb") as f:
wd_upload(FULL_NAME, f.read())
#
_wd_cleanup()
except Exception as e:
print(f"[backup] WebDAV : {e}")
os.remove(tarpath)
#
# HF Dataset
#
def _hf_list_backup_files() -> dict | None:
"""
HF Dataset .
: {: (bytes)}
{}
None API
"""
if not HF_REPO or not HF_TOKEN:
return {}
try:
from huggingface_hub import HfApi
try:
api = HfApi(timeout=60)
except TypeError:
api = HfApi() # timeout
backups = {}
for entry in api.list_repo_tree(HF_REPO, repo_type="dataset", token=HF_TOKEN):
name = entry.rfilename
if name.startswith("openclaw-full") and name.endswith(".tar.gz"):
backups[name] = entry.size if hasattr(entry, "size") else 0
return backups
except Exception as e:
print(f"[backup] HF Dataset list API : {e}")
return None # "" ( {})
def hf_small_backup():
""": tar.gz openclaw-full.tar.gz (), """
if not HF_REPO or not HF_TOKEN:
return
tarpath = _create_tar()
tar_size = os.path.getsize(tarpath)
#
backups = _hf_list_backup_files()
if backups is None: # API
print(f"[backup] HF: Dataset API ")
os.remove(tarpath)
return
existing_total = sum(backups.values())
if existing_total + tar_size > MAX_HF_STORAGE:
print(f"{RED}[backup] HF: 1GB! "
f"( {existing_total/1024/1024:.1f}MB + "
f" {tar_size/1024/1024:.1f}MB > 1GB){RESET}")
os.remove(tarpath)
return
try:
from huggingface_hub import HfApi
try:
api = HfApi(timeout=60)
except TypeError:
api = HfApi() # timeout
with open(tarpath, "rb") as f:
api.upload_file(
path_or_fileobj=f,
path_in_repo=FULL_NAME,
repo_id=HF_REPO,
repo_type="dataset",
token=HF_TOKEN,
)
print(f"[backup] HF {FULL_NAME}")
except Exception as e:
print(f"{RED}[backup] HF: {e}{RESET}")
finally:
if os.path.exists(tarpath):
os.remove(tarpath)
#
# HF Dataset +
#
def _hf_cleanup():
""" DATASET_MAX_BACKUPS HF """
backups = _hf_list_backup_files()
if not backups: # None(API) {} ()
return 0
from huggingface_hub import HfApi
api = HfApi(timeout=60)
now = datetime.now()
deleted = 0
for name in backups:
if name == FULL_NAME or not name.endswith(".tar.gz"):
continue
try:
date_str = name.replace("openclaw-full-", "").replace(".tar.gz", "")
dt = datetime.strptime(date_str, "%Y%m%d")
days_old = (now - dt).days
if days_old > DATASET_MAX_BACKUPS:
api.delete_file(
path_in_repo=name,
repo_id=HF_REPO,
repo_type="dataset",
token=HF_TOKEN,
)
print(f"[backup] HF : {name} ({days_old})")
deleted += 1
except (ValueError, Exception):
continue
if deleted:
print(f"[backup] HF {deleted} ")
return deleted
def _hf_make_space(backups: dict, tar_size: int, api) -> bool:
""", True, False."""
print(f"[backup] HF "
f"( {sum(backups.values())/1024/1024:.1f}MB + "
f" {tar_size/1024/1024:.1f}MB > 1GB)")
print(f"[backup] ...")
dated_big = []
for name, sz in backups.items():
if name == FULL_NAME:
continue
try:
ds = name.replace("openclaw-full-", "").replace(".tar.gz", "")
_ = datetime.strptime(ds, "%Y%m%d")
dated_big.append((ds, name))
except ValueError:
continue
dated_big.sort()
for ds, name in dated_big:
try:
api.delete_file(
path_in_repo=name,
repo_id=HF_REPO,
repo_type="dataset",
token=HF_TOKEN,
)
print(f"[backup] {name} ( {ds})")
del backups[name]
if sum(backups.values()) + tar_size <= MAX_HF_STORAGE:
return True
except Exception:
continue
print(f"{RED}[backup] HF: 1GB "
f"( {tar_size/1024/1024:.1f}MB > 1GB){RESET}")
return False
def _hf_upload_backup(tarpath: str, tar_size: int, filename: str, api) -> None:
""" HF Dataset."""
# &
backups = _hf_list_backup_files()
if backups is None: # API
print(f"[backup] HF: Dataset API ")
os.remove(tarpath)
return
existing_total = sum(backups.values())
if existing_total + tar_size > MAX_HF_STORAGE:
if not _hf_make_space(backups, tar_size, api):
os.remove(tarpath)
return
print(f"[backup] , ")
backups = _hf_list_backup_files() #
if backups is None:
backups = {}
# ,
if filename in backups:
try:
api.delete_file(
path_in_repo=filename,
repo_id=HF_REPO,
repo_type="dataset",
token=HF_TOKEN,
)
except Exception:
print(f"[backup] (): {filename}")
#
with open(tarpath, "rb") as f:
api.upload_file(
path_or_fileobj=f,
path_in_repo=filename,
repo_id=HF_REPO,
repo_type="dataset",
token=HF_TOKEN,
)
print(f"[backup] HF {filename}")
#
_hf_cleanup()
def hf_large_backup():
"""
: tar.gz openclaw-full-YYYYMMDD.tar.gz
,
"""
if not HF_REPO or not HF_TOKEN:
return
today = _today_str()
filename = f"openclaw-full-{today}.tar.gz"
# Skip if today's backup already exists (prevents re-upload on restart)
existing = _hf_list_backup_files()
if existing and filename in existing:
print(f"[backup] Today's large backup already exists ({filename}), skipping")
return
tarpath = _create_tar()
tar_size = os.path.getsize(tarpath)
try:
from huggingface_hub import HfApi
try:
api = HfApi(timeout=60)
except TypeError:
api = HfApi() # timeout
_hf_upload_backup(tarpath, tar_size, filename, api)
except Exception as e:
print(f"{RED}[backup] HF: {e}{RESET}")
finally:
if os.path.exists(tarpath):
os.remove(tarpath)
#
#
#
def _hf_download(filename: str = FULL_NAME) -> str | None:
""" HF Dataset ."""
if not HF_REPO or not HF_TOKEN:
return None
try:
from huggingface_hub import hf_hub_download
try:
return hf_hub_download(
repo_id=HF_REPO, filename=filename,
repo_type="dataset", token=HF_TOKEN,
timeout=60,
)
except TypeError:
return hf_hub_download(
repo_id=HF_REPO, filename=filename,
repo_type="dataset", token=HF_TOKEN,
)
except Exception as e:
print(f"[restore] HF download failed: {e}")
return None
def _stop_openviking():
"""Stop OpenViking server via PID file."""
import signal
pid_file = "/tmp/openviking.pid"
if not os.path.exists(pid_file):
print("[backup] OpenViking not running (no PID file)")
return True
try:
with open(pid_file) as f:
pid = int(f.read().strip())
print(f"[backup] Stopping OpenViking (PID: {pid})...")
os.kill(pid, signal.SIGTERM)
for _ in range(20):
try:
os.kill(pid, 0)
time.sleep(0.5)
except ProcessLookupError:
print("[backup] OpenViking stopped")
return True
print("[backup] OpenViking did not stop gracefully, sending SIGKILL")
os.kill(pid, signal.SIGKILL)
time.sleep(0.5)
return True
except (FileNotFoundError, ValueError, ProcessLookupError, OSError) as e:
print(f"[backup] OpenViking stop error: {e}")
return True
def _start_openviking():
"""Start OpenViking server."""
import subprocess
config = os.path.expanduser("~/.openviking/ov.conf")
if not os.path.exists(config):
print("[backup] OpenViking config not found, cannot start")
return False
try:
with open("/tmp/openviking-server.log", "w") as f:
proc = subprocess.Popen(
["nohup", "openviking-server", "--config", config],
stdout=f, stderr=subprocess.STDOUT,
preexec_fn=os.setpgrp,
)
with open("/tmp/openviking.pid", "w") as f:
f.write(str(proc.pid))
print(f"[backup] OpenViking started (PID: {proc.pid})")
return True
except Exception as e:
print(f"[backup] OpenViking start failed: {e}")
return False
def _restore_from_webdav(root: Path, target: str, source: str) -> bool:
""" WebDAV True"""
if not WEBDAV_URL or not wd_exists(target):
return False
print(f"[restore] Downloading {target} from WebDAV...")
data = wd_download(target)
tarpath = f"/tmp/{target}"
with open(tarpath, "wb") as f:
f.write(data)
with tarfile.open(tarpath, "r:gz") as tar:
tar.extractall(path=STATE_DIR)
os.remove(tarpath)
# Apply incremental overrides only for default full backup (auto mode)
if source == "auto" and target == FULL_NAME:
try:
manifest = _load_manifest()
count = 0
for rel in manifest:
p = root / rel
p.parent.mkdir(parents=True, exist_ok=True)
try:
data = wd_download(f"files/{rel}")
p.write_bytes(data)
count += 1
except Exception as e:
print(f"[restore] : {rel} {e}")
print(f"[restore] Applied {count} incremental file overrides")
except Exception:
print("[restore] No incremental manifest found (clean start)")
print("[restore] Restore from WebDAV complete")
return True
def _restore_from_dataset(target: str) -> bool:
""" HF Dataset True"""
print(f"[restore] Trying HF Dataset: {target}...")
path = _hf_download(target)
if not path:
return False
with tarfile.open(path, "r:gz") as tar:
tar.extractall(path=STATE_DIR)
print("[restore] Restore from HF Dataset complete")
return True
def restore(source="auto", filename=None):
"""
:
- source="auto": WebDAV ( ) HF Dataset
- source="webdav": WebDAV filename
- source="dataset": HF Dataset filename
filename None openclaw-full.tar.gz
"""
root = Path(STATE_DIR)
root.mkdir(parents=True, exist_ok=True)
restored = False
target = filename or FULL_NAME
# Strategy 1: WebDAV
if source in ("webdav", "auto"):
restored = _restore_from_webdav(root, target, source)
# Strategy 2: HF Dataset fallback
if source in ("dataset", "auto") and not restored:
restored = _restore_from_dataset(target)
if not restored:
print("[restore] No backup found fresh start")
def list_backups() -> dict:
""" {webdav: [], dataset: []}."""
result: dict = {"webdav": [], "dataset": []}
# WebDAV banner
if _wd_is_configured():
try:
files = wd_list("")
result["webdav"] = sorted([f for f in files if f.endswith(".tar.gz")])
except Exception as e:
print(f"[backup] WebDAV list error: {e}")
# HF Dataset
if HF_REPO and HF_TOKEN:
try:
backups = _hf_list_backup_files()
if backups:
result["dataset"] = sorted(backups.keys())
except Exception as e:
print(f"[backup] HF list error: {e}")
return result
#
# (Tick ) OVvectordb
#
def _get_hf_file_mtime(filename: str) -> float:
""" HF Dataset (unix timestamp) 0"""
try:
from huggingface_hub import HfApi
try:
api = HfApi(timeout=60)
except TypeError:
api = HfApi()
for entry in api.list_repo_tree(HF_REPO, repo_type="dataset", token=HF_TOKEN, expand=True):
if getattr(entry, 'rfilename', '') == filename:
lc = getattr(entry, 'last_commit', None)
if lc and hasattr(lc, 'date'):
return lc.date.timestamp()
return 0.0 #
return 0.0 # file not found
except Exception as e:
print(f"[backup] HF : {e}")
return 0.0
def _get_wd_last_backup_time() -> float:
""" WebDAV (unix timestamp) 0"""
try:
files = wd_list("")
latest = 0.0
for f in files:
if f.startswith("openclaw-full") and f.endswith(".tar.gz"):
url = _wd_url(f)
r = requests.head(url, auth=_wd_auth(), timeout=WD_REQ_TIMEOUT)
if 'last-modified' in r.headers:
from email.utils import parsedate_to_datetime
dt = parsedate_to_datetime(r.headers['last-modified'])
ts = dt.timestamp()
if ts > latest:
latest = ts
return latest
except Exception:
return 0.0
def _get_hf_large_backup_mtime() -> float:
""" HF Dataset (unix timestamp) 0"""
try:
from huggingface_hub import HfApi
try:
api = HfApi(timeout=60)
except TypeError:
api = HfApi()
latest_ts = 0.0
for entry in api.list_repo_tree(HF_REPO, repo_type="dataset", token=HF_TOKEN, expand=True):
name = getattr(entry, 'rfilename', '')
if name.startswith("openclaw-full-") and name.endswith(".tar.gz") and name != FULL_NAME:
lc = getattr(entry, 'last_commit', None)
if lc and hasattr(lc, 'date'):
ts = lc.date.timestamp()
if ts > latest_ts:
latest_ts = ts
return latest_ts
except Exception:
return 0.0
def _do_one_tick():
"""
tick: OV OV
"""
now = time.time()
today = _today_str()
need_stop_ov = False
# 1. WebDAV
if not WD_SKIPPED:
last_wd = _get_wd_last_backup_time()
if last_wd == 0.0 or (now - last_wd) >= WEBDAV_INTERVAL * 60:
need_stop_ov = True
# 2. HF
if not HF_SKIPPED:
last_hf_small = _get_hf_file_mtime(FULL_NAME)
if last_hf_small == 0.0 or (now - last_hf_small) >= HF_SMALL_INTERVAL * 60:
need_stop_ov = True
# 3. HF
hf_big_due = False
if not HF_SKIPPED:
# OV
today_file = f"openclaw-full-{_today_str()}.tar.gz"
try:
from huggingface_hub import HfApi
try:
api_pre = HfApi(timeout=60)
except TypeError:
api_pre = HfApi()
for entry in api_pre.list_repo_tree(HF_REPO, repo_type="dataset", token=HF_TOKEN, expand=True):
if getattr(entry, 'rfilename', '') == today_file and getattr(entry, 'last_commit', None):
today_exists = True
break
else:
today_exists = False
except Exception:
today_exists = False
if not today_exists:
last_big_mtime = _get_hf_large_backup_mtime()
hf_big_due = (last_big_mtime == 0.0) or (now - last_big_mtime >= HF_BIG_INTERVAL * 60)
if hf_big_due:
need_stop_ov = True
if not need_stop_ov:
return #
# OV OV
ov_was_running = False
import signal
try:
with open("/tmp/openviking.pid") as f:
ov_pid = int(f.read().strip())
os.kill(ov_pid, 0)
ov_was_running = True
except (FileNotFoundError, ValueError, ProcessLookupError, OSError):
ov_was_running = False
if ov_was_running:
_stop_openviking()
time.sleep(1)
# WebDAV
if not WD_SKIPPED:
last_wd = _get_wd_last_backup_time()
if last_wd == 0.0 or (now - last_wd) >= WEBDAV_INTERVAL * 60:
try:
c = incremental_backup()
print(f"[backup] WebDAV : {c} ")
full_backup()
except Exception as e:
print(f"[backup] WebDAV (): {e}")
# HF
if not HF_SKIPPED:
last_hf_small = _get_hf_file_mtime(FULL_NAME)
if last_hf_small == 0.0 or (now - last_hf_small) >= HF_SMALL_INTERVAL * 60:
try:
hf_small_backup()
except Exception as e:
print(f"[backup] HF (): {e}")
# HF
if not HF_SKIPPED and hf_big_due:
try:
hf_large_backup()
except Exception as e:
print(f"[backup] HF (): {e}")
if ov_was_running:
_start_openviking()
def scheduler_loop():
"""
Tick ()
OV vectordb
"""
_ensure_banner()
print(f"[backup] tick ")
print(f"[backup] WebDAV ={WEBDAV_INTERVAL} ={WEBDAV_MAX_BACKUPS}")
print(f"[backup] HF ={HF_SMALL_INTERVAL}")
print(f"[backup] HF ={HF_BIG_INTERVAL} ={DATASET_MAX_BACKUPS}")
print(f"[backup] tick 5")
while True:
time.sleep(5)
_do_one_tick()
#
# CLI
#
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="WebDAV + HF Dataset /")
parser.add_argument("cmd", nargs="?", default="restore",
choices=["restore", "list", "incremental", "full", "small", "large", "scheduler", "check"],
help=" (default: restore)")
parser.add_argument("--source", default="auto",
help=": auto|webdav|dataset ( restore )")
parser.add_argument("--filename", default=None,
help=" ( restore )")
args = parser.parse_args()
# list/check banner stdout JSON
if args.cmd not in ("list", "check"):
_ensure_banner()
if args.cmd == "restore":
restore(source=args.source, filename=args.filename)
elif args.cmd == "list":
import json
print(json.dumps(list_backups(), ensure_ascii=False))
elif args.cmd == "incremental":
c = incremental_backup()
print(f"[backup] WebDAV : {c} ")
elif args.cmd == "full":
full_backup()
elif args.cmd == "small":
hf_small_backup()
elif args.cmd == "large":
hf_large_backup()
elif args.cmd == "scheduler":
scheduler_loop()
elif args.cmd == "check":
import json
result = {"backup_today": False, "last_backup": None}
try:
from huggingface_hub import HfApi
try:
api = HfApi(timeout=60)
except TypeError:
api = HfApi()
for entry in api.list_repo_tree(HF_REPO, repo_type="dataset", token=HF_TOKEN, expand=True):
name = getattr(entry, 'rfilename', '')
if name == FULL_NAME: #
lc = getattr(entry, 'last_commit', None)
if lc and hasattr(lc, 'date'):
# UTC (UTC+8)
bj_time = lc.date.astimezone(timezone(timedelta(hours=8)))
result["last_backup"] = bj_time.strftime("%Y-%m-%d %H:%M:%S")
if name.startswith("openclaw-full-") and name.endswith(".tar.gz") and name != FULL_NAME:
date_str = name.replace("openclaw-full-", "").replace(".tar.gz", "")
if date_str == _today_str():
lc = getattr(entry, 'last_commit', None)
if lc and hasattr(lc, 'date'):
bj_time = lc.date.astimezone(timezone(timedelta(hours=8)))
result["backup_today"] = True
result["last_backup"] = bj_time.strftime("%Y-%m-%d %H:%M:%S")
except Exception as e:
print(f"[backup] : {e}")
print(json.dumps(result))
else:
print(f": {sys.argv[0]} {{restore|incremental|full|small|large|scheduler}}")
sys.exit(1)