Spaces:
Running
Running
Improve state snapshotting and legacy migration
Browse filesRefactor snapshotting to use a staging directory for atomic backups and migrate legacy state from hidden directory.
- openclaw-sync.py +42 -6
openclaw-sync.py
CHANGED
|
@@ -87,20 +87,33 @@ def count_files(path: Path) -> int:
|
|
| 87 |
def snapshot_state_into_workspace() -> None:
|
| 88 |
try:
|
| 89 |
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
for source_path in OPENCLAW_HOME.iterdir():
|
| 95 |
if source_path.name in EXCLUDED_STATE_NAMES:
|
| 96 |
continue
|
| 97 |
|
| 98 |
-
backup_path =
|
| 99 |
if source_path.is_dir():
|
| 100 |
shutil.copytree(source_path, backup_path)
|
| 101 |
elif source_path.is_file():
|
| 102 |
shutil.copy2(source_path, backup_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
except Exception as exc:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
print(f"Warning: could not snapshot OpenClaw state: {exc}")
|
| 105 |
|
| 106 |
try:
|
|
@@ -135,6 +148,23 @@ def snapshot_state_into_workspace() -> None:
|
|
| 135 |
|
| 136 |
def restore_embedded_state() -> None:
|
| 137 |
state_backup_root = STATE_DIR / "openclaw"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
if state_backup_root.is_dir():
|
| 139 |
for source_path in state_backup_root.iterdir():
|
| 140 |
name = source_path.name
|
|
@@ -401,8 +431,14 @@ def loop() -> int:
|
|
| 401 |
time.sleep(INITIAL_DELAY)
|
| 402 |
print(f"Workspace sync started: every {INTERVAL}s -> {repo_id}")
|
| 403 |
|
| 404 |
-
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
while not STOP_EVENT.is_set():
|
| 408 |
try:
|
|
|
|
| 87 |
def snapshot_state_into_workspace() -> None:
|
| 88 |
try:
|
| 89 |
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 90 |
+
# Atomic snapshot: copy to a staging dir first, then rename.
|
| 91 |
+
# This prevents a half-written (or empty) backup if we crash mid-copy,
|
| 92 |
+
# which would otherwise be uploaded and overwrite the real HF backup.
|
| 93 |
+
staging_dir = STATE_DIR / ".openclaw-staging"
|
| 94 |
+
if staging_dir.exists():
|
| 95 |
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
| 96 |
+
staging_dir.mkdir(parents=True, exist_ok=True)
|
| 97 |
|
| 98 |
for source_path in OPENCLAW_HOME.iterdir():
|
| 99 |
if source_path.name in EXCLUDED_STATE_NAMES:
|
| 100 |
continue
|
| 101 |
|
| 102 |
+
backup_path = staging_dir / source_path.name
|
| 103 |
if source_path.is_dir():
|
| 104 |
shutil.copytree(source_path, backup_path)
|
| 105 |
elif source_path.is_file():
|
| 106 |
shutil.copy2(source_path, backup_path)
|
| 107 |
+
|
| 108 |
+
# Atomically swap staging → real backup dir
|
| 109 |
+
if OPENCLAW_STATE_BACKUP_DIR.exists():
|
| 110 |
+
shutil.rmtree(OPENCLAW_STATE_BACKUP_DIR, ignore_errors=True)
|
| 111 |
+
staging_dir.rename(OPENCLAW_STATE_BACKUP_DIR)
|
| 112 |
except Exception as exc:
|
| 113 |
+
# Clean up staging on failure so it doesn't interfere next time
|
| 114 |
+
staging_dir = STATE_DIR / ".openclaw-staging"
|
| 115 |
+
if staging_dir.exists():
|
| 116 |
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
| 117 |
print(f"Warning: could not snapshot OpenClaw state: {exc}")
|
| 118 |
|
| 119 |
try:
|
|
|
|
| 148 |
|
| 149 |
def restore_embedded_state() -> None:
|
| 150 |
state_backup_root = STATE_DIR / "openclaw"
|
| 151 |
+
|
| 152 |
+
# Migration fix: old backups stored state in ".huggingclaw-state/openclaw"
|
| 153 |
+
# (hidden dir). If new path doesn't exist but old hidden path does, use it
|
| 154 |
+
# and migrate it to the new path so future syncs write to the right place.
|
| 155 |
+
if not state_backup_root.is_dir():
|
| 156 |
+
legacy_state = WORKSPACE / ".huggingclaw-state" / "openclaw"
|
| 157 |
+
if legacy_state.is_dir():
|
| 158 |
+
print("Found legacy state backup at .huggingclaw-state/; migrating to huggingclaw-state/...")
|
| 159 |
+
try:
|
| 160 |
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
| 161 |
+
shutil.copytree(legacy_state, state_backup_root)
|
| 162 |
+
legacy_root = WORKSPACE / ".huggingclaw-state"
|
| 163 |
+
shutil.rmtree(legacy_root, ignore_errors=True)
|
| 164 |
+
print("Legacy state migrated and .huggingclaw-state/ removed.")
|
| 165 |
+
except Exception as exc:
|
| 166 |
+
print(f"Warning: could not migrate legacy state: {exc}")
|
| 167 |
+
|
| 168 |
if state_backup_root.is_dir():
|
| 169 |
for source_path in state_backup_root.iterdir():
|
| 170 |
name = source_path.name
|
|
|
|
| 431 |
time.sleep(INITIAL_DELAY)
|
| 432 |
print(f"Workspace sync started: every {INTERVAL}s -> {repo_id}")
|
| 433 |
|
| 434 |
+
# Take a fingerprint of the workspace AS RESTORED (after snapshotting state)
|
| 435 |
+
# so the first loop iteration only uploads if something genuinely changed.
|
| 436 |
+
# Previously this was None, which forced an unconditional upload every restart
|
| 437 |
+
# — even when restore had failed silently and the workspace was empty.
|
| 438 |
+
snapshot_state_into_workspace()
|
| 439 |
+
last_fingerprint = fingerprint_dir(WORKSPACE)
|
| 440 |
+
last_marker = metadata_marker(WORKSPACE)
|
| 441 |
+
print("Initial workspace fingerprint captured.")
|
| 442 |
|
| 443 |
while not STOP_EVENT.is_set():
|
| 444 |
try:
|