Replit Agent commited on
Commit
5b7d989
Β·
1 Parent(s): 6f91c8f

fix: scope WATCH_DIRS to opencode subdirs only; add sdk/android cache ignores; full tracebacks in logs

Browse files
Files changed (1) hide show
  1. sync_engine.py +109 -30
sync_engine.py CHANGED
@@ -2,8 +2,8 @@
2
  """
3
  OpenCode Sync Engine
4
  ====================
5
- Persists /projects/default (workspace), /data/share (OpenCode DB),
6
- and /data/config (OpenCode config) to a private HF Dataset repo.
7
 
8
  Usage:
9
  python3 /sync_engine.py restore β€” pull dataset β†’ local dirs at startup
@@ -20,6 +20,7 @@ import os
20
  import time
21
  import hashlib
22
  import logging
 
23
  import shutil
24
  import tempfile
25
  from pathlib import Path
@@ -29,7 +30,7 @@ from pathlib import Path
29
  # ---------------------------------------------------------------------------
30
  logging.basicConfig(
31
  level=logging.INFO,
32
- format="[%(asctime)s] [SYNC] %(message)s",
33
  datefmt="%H:%M:%S",
34
  stream=sys.stdout,
35
  force=True,
@@ -43,19 +44,61 @@ log = logging.getLogger("sync")
43
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
44
  HF_DATASET = os.environ.get("HF_DATASET", "Jishnupg/OpenCode-Storage")
45
 
46
- # Local dirs β†’ dataset path prefixes
 
 
 
 
 
 
 
 
 
47
  WATCH_DIRS: dict[str, Path] = {
48
  "workspace": Path("/projects/default"),
49
- "share": Path("/data/share"),
50
- "config": Path("/data/config"),
51
  }
52
 
53
- # Files/dirs to never sync
54
  IGNORE_NAMES: set[str] = {
55
- ".git", "node_modules", "__pycache__", ".cache",
56
- ".npm", ".yarn", ".pnpm", "dist", "build",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
- IGNORE_SUFFIXES: set[str] = {".tmp", ".sock", ".pid", ".lock", ".pyc"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  MAX_FILE_BYTES = 50 * 1_024 * 1_024 # 50 MB hard limit per file
60
 
61
  POLL_INTERVAL_SECS = 15 # how often to check for changes
@@ -68,17 +111,31 @@ CHECKPOINT_INTERVAL_SECS = 300 # forced full sync every 5 min
68
 
69
  def _should_skip(path: Path) -> bool:
70
  """Return True if this file should not be synced."""
 
71
  for part in path.parts:
72
  if part in IGNORE_NAMES:
 
73
  return True
 
 
 
 
 
 
 
74
  if path.suffix in IGNORE_SUFFIXES:
 
75
  return True
 
 
76
  try:
77
- if path.stat().st_size > MAX_FILE_BYTES:
78
- log.warning(f"Skipping large file ({path.stat().st_size // 1024} KB): {path}")
 
79
  return True
80
  except OSError:
81
  return True
 
82
  return False
83
 
84
 
@@ -130,11 +187,14 @@ class SyncEngine:
130
  from huggingface_hub import HfApi
131
  self._api = HfApi(token=HF_TOKEN)
132
  log.info(f"Sync engine ready β†’ {HF_DATASET}")
 
 
 
133
  except ImportError:
134
  log.error("huggingface_hub not installed β€” sync disabled")
135
 
136
  # ------------------------------------------------------------------
137
- # Restore (startup)
138
  # ------------------------------------------------------------------
139
 
140
  def _ensure_repo(self) -> bool:
@@ -153,6 +213,7 @@ class SyncEngine:
153
  " β†’ Create it manually at https://huggingface.co/new-dataset "
154
  "(name: OpenCode-Storage, private) then restart the Space."
155
  )
 
156
  return False
157
 
158
  def _test_write(self) -> bool:
@@ -175,12 +236,17 @@ class SyncEngine:
175
  log.error("=" * 60)
176
  log.error("❌ SYNC WRITE FAILED β€” files will NOT be saved to dataset")
177
  log.error(f" Error: {exc}")
 
178
  log.error(" Fix: go to https://huggingface.co/settings/tokens")
179
  log.error(" Create a token with 'Write' scope (not Read-only).")
180
  log.error(" Then update HF_TOKEN in Space Settings β†’ Secrets.")
181
  log.error("=" * 60)
182
  return False
183
 
 
 
 
 
184
  def restore(self) -> None:
185
  """Download the entire dataset snapshot and restore local dirs."""
186
  if not self._api:
@@ -211,6 +277,7 @@ class SyncEngine:
211
  except Exception as exc:
212
  # Empty repo or network error β€” not fatal, start fresh
213
  log.warning(f"Restore skipped or partial: {exc}")
 
214
 
215
  def _copy_snapshot(self, repo_root: Path) -> int:
216
  """Copy files from a local snapshot dir to their actual locations."""
@@ -228,6 +295,7 @@ class SyncEngine:
228
  dest_file.parent.mkdir(parents=True, exist_ok=True)
229
  try:
230
  shutil.copy2(src_file, dest_file)
 
231
  count += 1
232
  except Exception as exc:
233
  log.warning(f" Could not restore {dest_file}: {exc}")
@@ -242,17 +310,24 @@ class SyncEngine:
242
  result: dict[str, str] = {}
243
  for prefix, base in WATCH_DIRS.items():
244
  if not base.exists():
 
245
  continue
246
  try:
 
 
247
  for path in base.rglob("*"):
248
  if not path.is_file():
249
  continue
250
  if _should_skip(path):
 
251
  continue
252
  rp = _repo_path(prefix, path, base)
253
  result[rp] = _md5(path)
 
 
254
  except Exception as exc:
255
  log.warning(f"Scan error in {base}: {exc}")
 
256
  return result
257
 
258
  # ------------------------------------------------------------------
@@ -290,45 +365,45 @@ class SyncEngine:
290
  for rp in to_upload:
291
  local = _local_path(rp)
292
  if local and local.exists():
 
293
  ops.append(
294
  CommitOperationAdd(
295
  path_in_repo=rp,
296
  path_or_fileobj=str(local),
297
  )
298
  )
 
 
 
299
  for rp in to_delete:
 
300
  ops.append(CommitOperationDelete(path_in_repo=rp))
301
 
302
  if not ops:
303
  self._known = current
304
  return 0, 0
305
 
306
- n_up = len(to_upload)
307
- n_del = len(to_delete)
308
  msg = f"sync: +{n_up} ~{n_del}"
309
 
310
- self._api.create_commit(
 
311
  repo_id=HF_DATASET,
312
  repo_type="dataset",
313
  commit_message=msg,
314
  operations=ops,
315
  )
 
 
316
 
317
- log.info(f"↑ committed: +{n_up} uploads, -{n_del} deletes β†’ {HF_DATASET}")
318
- if to_upload[:5]:
319
- for rp in to_upload[:5]:
320
- log.info(f" + {rp}")
321
- if n_up > 5:
322
- log.info(f" ... and {n_up - 5} more")
323
- if to_delete:
324
- for rp in to_delete[:3]:
325
- log.info(f" - {rp}")
326
-
327
  self._known = current
328
- return n_up, n_del
329
 
330
  except Exception as exc:
331
- log.error(f"Commit failed (will retry next cycle): {exc}")
 
332
  # Don't update _known so we retry next cycle
333
  return 0, 0
334
 
@@ -347,6 +422,9 @@ class SyncEngine:
347
  log.info("=== WATCH: establishing baseline scan ===")
348
  self._known = self._scan()
349
  log.info(f"Baseline: {len(self._known)} files tracked")
 
 
 
350
 
351
  last_checkpoint = time.monotonic()
352
 
@@ -359,7 +437,7 @@ class SyncEngine:
359
  if forced:
360
  # Force a full rescan to catch anything missed
361
  self._known = {}
362
- log.info("[CHECKPOINT] Forced full sync")
363
 
364
  n_up, n_del = self.sync_once()
365
 
@@ -370,6 +448,7 @@ class SyncEngine:
370
 
371
  except Exception as exc:
372
  log.error(f"Watch cycle error: {exc}")
 
373
 
374
 
375
  # ---------------------------------------------------------------------------
@@ -379,7 +458,7 @@ class SyncEngine:
379
  def main() -> None:
380
  mode = sys.argv[1] if len(sys.argv) > 1 else "watch"
381
 
382
- # Ensure all local dirs exist (they may not if /data was previously a volume mount)
383
  for d in [
384
  "/data/share/opencode",
385
  "/data/config/opencode",
 
2
  """
3
  OpenCode Sync Engine
4
  ====================
5
+ Persists /projects/default (workspace), /data/share/opencode (OpenCode DB),
6
+ and /data/config/opencode (OpenCode config) to a private HF Dataset repo.
7
 
8
  Usage:
9
  python3 /sync_engine.py restore β€” pull dataset β†’ local dirs at startup
 
20
  import time
21
  import hashlib
22
  import logging
23
+ import traceback
24
  import shutil
25
  import tempfile
26
  from pathlib import Path
 
30
  # ---------------------------------------------------------------------------
31
  logging.basicConfig(
32
  level=logging.INFO,
33
+ format="[%(asctime)s] [SYNC] %(levelname)s %(message)s",
34
  datefmt="%H:%M:%S",
35
  stream=sys.stdout,
36
  force=True,
 
44
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
45
  HF_DATASET = os.environ.get("HF_DATASET", "Jishnupg/OpenCode-Storage")
46
 
47
+ # ─── CRITICAL FIX ────────────────────────────────────────────────────────────
48
+ # Scope watched dirs to the SPECIFIC OpenCode subdirectories, NOT the entire
49
+ # /data/share or /data/config XDG roots. Those roots contain Android SDK caches,
50
+ # sdkbin-*, sdkinf-* files, npm/pip caches, etc. β€” thousands of unrelated
51
+ # system files that must never be synced.
52
+ #
53
+ # "workspace" β†’ /projects/default (the user's actual project files)
54
+ # "share" β†’ /data/share/opencode (OpenCode SQLite DB only)
55
+ # "config" β†’ /data/config/opencode (OpenCode config JSON only)
56
+ # ─────────────────────────────────────────────────────────────────────────────
57
  WATCH_DIRS: dict[str, Path] = {
58
  "workspace": Path("/projects/default"),
59
+ "share": Path("/data/share/opencode"), # was /data/share β€” TOO BROAD
60
+ "config": Path("/data/config/opencode"), # was /data/config β€” TOO BROAD
61
  }
62
 
63
+ # Exact directory-name segments to skip anywhere in the path
64
  IGNORE_NAMES: set[str] = {
65
+ ".git",
66
+ "node_modules",
67
+ "__pycache__",
68
+ ".cache",
69
+ ".npm",
70
+ ".yarn",
71
+ ".pnpm",
72
+ ".pip",
73
+ "dist",
74
+ "build",
75
+ # Android / SDK
76
+ ".android",
77
+ ".gradle",
78
+ ".kotlin",
79
+ "sdktools",
80
+ # Temp / logs / misc
81
+ "tmp",
82
+ "temp",
83
+ "logs",
84
+ ".local", # catches ~/.local caches outside our watched subdirs
85
  }
86
+
87
+ # Name *prefixes* to skip (catches sdkbin-*, sdkinf-*, tmp-*, etc.)
88
+ IGNORE_PREFIXES: tuple[str, ...] = (
89
+ "sdkbin-",
90
+ "sdkinf-",
91
+ "tmp-",
92
+ "temp-",
93
+ ".~",
94
+ )
95
+
96
+ # File suffixes to skip
97
+ IGNORE_SUFFIXES: set[str] = {
98
+ ".tmp", ".sock", ".pid", ".lock", ".pyc",
99
+ ".log", ".bak", ".swp", ".swo",
100
+ }
101
+
102
  MAX_FILE_BYTES = 50 * 1_024 * 1_024 # 50 MB hard limit per file
103
 
104
  POLL_INTERVAL_SECS = 15 # how often to check for changes
 
111
 
112
  def _should_skip(path: Path) -> bool:
113
  """Return True if this file should not be synced."""
114
+ # Check every path component
115
  for part in path.parts:
116
  if part in IGNORE_NAMES:
117
+ log.debug(f"Skip (ignored name '{part}'): {path}")
118
  return True
119
+ # Prefix check (sdkbin-*, sdkinf-*, etc.)
120
+ for pfx in IGNORE_PREFIXES:
121
+ if part.startswith(pfx):
122
+ log.debug(f"Skip (ignored prefix '{pfx}'): {path}")
123
+ return True
124
+
125
+ # Suffix check
126
  if path.suffix in IGNORE_SUFFIXES:
127
+ log.debug(f"Skip (ignored suffix '{path.suffix}'): {path}")
128
  return True
129
+
130
+ # Size check
131
  try:
132
+ size = path.stat().st_size
133
+ if size > MAX_FILE_BYTES:
134
+ log.warning(f"Skip (too large {size // 1024} KB): {path}")
135
  return True
136
  except OSError:
137
  return True
138
+
139
  return False
140
 
141
 
 
187
  from huggingface_hub import HfApi
188
  self._api = HfApi(token=HF_TOKEN)
189
  log.info(f"Sync engine ready β†’ {HF_DATASET}")
190
+ log.info(f"Watching directories:")
191
+ for prefix, path in WATCH_DIRS.items():
192
+ log.info(f" [{prefix}] {path}")
193
  except ImportError:
194
  log.error("huggingface_hub not installed β€” sync disabled")
195
 
196
  # ------------------------------------------------------------------
197
+ # Repo setup
198
  # ------------------------------------------------------------------
199
 
200
  def _ensure_repo(self) -> bool:
 
213
  " β†’ Create it manually at https://huggingface.co/new-dataset "
214
  "(name: OpenCode-Storage, private) then restart the Space."
215
  )
216
+ log.error(traceback.format_exc())
217
  return False
218
 
219
  def _test_write(self) -> bool:
 
236
  log.error("=" * 60)
237
  log.error("❌ SYNC WRITE FAILED β€” files will NOT be saved to dataset")
238
  log.error(f" Error: {exc}")
239
+ log.error(traceback.format_exc())
240
  log.error(" Fix: go to https://huggingface.co/settings/tokens")
241
  log.error(" Create a token with 'Write' scope (not Read-only).")
242
  log.error(" Then update HF_TOKEN in Space Settings β†’ Secrets.")
243
  log.error("=" * 60)
244
  return False
245
 
246
+ # ------------------------------------------------------------------
247
+ # Restore (startup)
248
+ # ------------------------------------------------------------------
249
+
250
  def restore(self) -> None:
251
  """Download the entire dataset snapshot and restore local dirs."""
252
  if not self._api:
 
277
  except Exception as exc:
278
  # Empty repo or network error β€” not fatal, start fresh
279
  log.warning(f"Restore skipped or partial: {exc}")
280
+ log.warning(traceback.format_exc())
281
 
282
  def _copy_snapshot(self, repo_root: Path) -> int:
283
  """Copy files from a local snapshot dir to their actual locations."""
 
295
  dest_file.parent.mkdir(parents=True, exist_ok=True)
296
  try:
297
  shutil.copy2(src_file, dest_file)
298
+ log.info(f" Restored: {prefix}/{rel} β†’ {dest_file}")
299
  count += 1
300
  except Exception as exc:
301
  log.warning(f" Could not restore {dest_file}: {exc}")
 
310
  result: dict[str, str] = {}
311
  for prefix, base in WATCH_DIRS.items():
312
  if not base.exists():
313
+ log.debug(f"Watched dir does not exist yet: {base}")
314
  continue
315
  try:
316
+ file_count = 0
317
+ skip_count = 0
318
  for path in base.rglob("*"):
319
  if not path.is_file():
320
  continue
321
  if _should_skip(path):
322
+ skip_count += 1
323
  continue
324
  rp = _repo_path(prefix, path, base)
325
  result[rp] = _md5(path)
326
+ file_count += 1
327
+ log.debug(f"Scan [{prefix}] {base}: {file_count} tracked, {skip_count} skipped")
328
  except Exception as exc:
329
  log.warning(f"Scan error in {base}: {exc}")
330
+ log.warning(traceback.format_exc())
331
  return result
332
 
333
  # ------------------------------------------------------------------
 
365
  for rp in to_upload:
366
  local = _local_path(rp)
367
  if local and local.exists():
368
+ log.info(f" UPLOAD ← {local} β†’ {HF_DATASET}/{rp}")
369
  ops.append(
370
  CommitOperationAdd(
371
  path_in_repo=rp,
372
  path_or_fileobj=str(local),
373
  )
374
  )
375
+ else:
376
+ log.warning(f" UPLOAD skipped (file vanished): {rp}")
377
+
378
  for rp in to_delete:
379
+ log.info(f" DELETE from dataset: {rp}")
380
  ops.append(CommitOperationDelete(path_in_repo=rp))
381
 
382
  if not ops:
383
  self._known = current
384
  return 0, 0
385
 
386
+ n_up = sum(1 for op in ops if not isinstance(op, __import__("huggingface_hub").CommitOperationDelete))
387
+ n_del = sum(1 for op in ops if isinstance(op, __import__("huggingface_hub").CommitOperationDelete))
388
  msg = f"sync: +{n_up} ~{n_del}"
389
 
390
+ log.info(f"Creating commit '{msg}' on {HF_DATASET} ({len(ops)} operations) ...")
391
+ commit_info = self._api.create_commit(
392
  repo_id=HF_DATASET,
393
  repo_type="dataset",
394
  commit_message=msg,
395
  operations=ops,
396
  )
397
+ log.info(f"βœ… Commit created: {commit_info.commit_url if hasattr(commit_info, 'commit_url') else 'ok'}")
398
+ log.info(f"↑ Pushed: +{n_up} uploads, -{n_del} deletes β†’ {HF_DATASET}")
399
 
400
+ # Update known state after successful commit
 
 
 
 
 
 
 
 
 
401
  self._known = current
402
+ return len(to_upload), len(to_delete)
403
 
404
  except Exception as exc:
405
+ log.error(f"❌ Commit FAILED (will retry next cycle): {exc}")
406
+ log.error(traceback.format_exc())
407
  # Don't update _known so we retry next cycle
408
  return 0, 0
409
 
 
422
  log.info("=== WATCH: establishing baseline scan ===")
423
  self._known = self._scan()
424
  log.info(f"Baseline: {len(self._known)} files tracked")
425
+ log.info(f"Polling every {POLL_INTERVAL_SECS}s for changes in:")
426
+ for prefix, path in WATCH_DIRS.items():
427
+ log.info(f" [{prefix}] {path}")
428
 
429
  last_checkpoint = time.monotonic()
430
 
 
437
  if forced:
438
  # Force a full rescan to catch anything missed
439
  self._known = {}
440
+ log.info("[CHECKPOINT] Forced full sync β€” clearing known state")
441
 
442
  n_up, n_del = self.sync_once()
443
 
 
448
 
449
  except Exception as exc:
450
  log.error(f"Watch cycle error: {exc}")
451
+ log.error(traceback.format_exc())
452
 
453
 
454
  # ---------------------------------------------------------------------------
 
458
  def main() -> None:
459
  mode = sys.argv[1] if len(sys.argv) > 1 else "watch"
460
 
461
+ # Ensure all local dirs exist
462
  for d in [
463
  "/data/share/opencode",
464
  "/data/config/opencode",