Skip to content

Commit 5e16759

Browse files
author
root
committed
feat(workspace-sync): receiver-side .stop marker to pause subtree convergence
1 parent 2878cd2 commit 5e16759

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,29 @@
2323

2424
- 共享 token 鉴权
2525
- 加密传输(AES-GCM)
26+
- 分块传输(大文件 `upsert_begin/chunk/end`),降低单次内存峰值
27+
- 发送端长连接复用,减少高频事件下重复建连开销
2628

27-
### 4) 运行模式
29+
### 4) 接收端本地保护(`.stop`
30+
31+
-**接收端本地目录**下创建 `.stop` 文件,可暂停该目录及子目录的收敛同步。
32+
- 生效语义:
33+
- 忽略该目录下收到的 `upsert/delete/mkdir` 事件
34+
- 快照收敛(`snapshot_end` prune)也会跳过该目录
35+
- 用法示例:
36+
37+
```bash
38+
# 暂停 docs/ 的收敛
39+
cd /path/to/receive-dir
40+
touch docs/.stop
41+
42+
# 恢复收敛
43+
rm docs/.stop
44+
```
45+
46+
> 说明:`.stop` 仅作为接收端本地控制标记,不会被远端事件覆盖。
47+
48+
### 5) 运行模式
2849

2950
- `send`:仅发送
3051
- `receive`:仅接收

internal/syncer/app.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ func (a *App) applyEvent(evt fileEvent) error {
189189
if isExcluded(rel, a.cfg.Excludes) {
190190
return nil
191191
}
192+
if shouldIgnoreStopMarker(rel) {
193+
// receiver-local control file; never mutate it from remote
194+
return nil
195+
}
196+
if stopRoot, stopped := a.findStopRoot(rel); stopped {
197+
logf("<- skipped by .stop (%s): %s %s\n", stopRoot, evt.Type, rel)
198+
return nil
199+
}
192200
full := filepath.Join(a.cfg.Dir, rel)
193201
if !pathWithinBase(full, a.cfg.Dir) {
194202
return fmt.Errorf("path escape: %s", rel)
@@ -354,6 +362,50 @@ func pathWithinBase(path, base string) bool {
354362
return true
355363
}
356364

365+
// findStopRoot returns the nearest ancestor directory (or self, if dir)
366+
// that contains a ".stop" marker file on receiver side.
367+
func (a *App) findStopRoot(rel string) (string, bool) {
368+
rel = filepath.ToSlash(filepath.Clean(rel))
369+
if rel == "" || rel == "." {
370+
if fileExists(filepath.Join(a.cfg.Dir, ".stop")) {
371+
return ".", true
372+
}
373+
return "", false
374+
}
375+
376+
parts := strings.Split(rel, "/")
377+
for i := len(parts); i >= 1; i-- {
378+
cand := strings.Join(parts[:i], "/")
379+
full := filepath.Join(a.cfg.Dir, filepath.FromSlash(cand))
380+
if st, err := os.Stat(full); err == nil && !st.IsDir() {
381+
// if current candidate is a file path, check parent directory marker
382+
if i == len(parts) {
383+
continue
384+
}
385+
}
386+
if fileExists(filepath.Join(full, ".stop")) {
387+
return cand, true
388+
}
389+
}
390+
if fileExists(filepath.Join(a.cfg.Dir, ".stop")) {
391+
return ".", true
392+
}
393+
return "", false
394+
}
395+
396+
func fileExists(path string) bool {
397+
st, err := os.Stat(path)
398+
return err == nil && !st.IsDir()
399+
}
400+
401+
func shouldIgnoreStopMarker(rel string) bool {
402+
rel = filepath.ToSlash(filepath.Clean(rel))
403+
if rel == ".stop" {
404+
return true
405+
}
406+
return strings.HasSuffix(rel, "/.stop")
407+
}
408+
357409
func (a *App) suppress(rel string, d time.Duration) {
358410
a.mu.Lock()
359411
defer a.mu.Unlock()
@@ -424,6 +476,16 @@ func (a *App) snapshotEnd(id string) error {
424476
}
425477
return nil
426478
}
479+
if shouldIgnoreStopMarker(rel) {
480+
// receiver-local stop markers should never be removed by reconcile
481+
return nil
482+
}
483+
if stopRoot, stopped := a.findStopRoot(rel); stopped {
484+
if d.IsDir() && rel == stopRoot {
485+
return filepath.SkipDir
486+
}
487+
return nil
488+
}
427489
if d.IsDir() {
428490
return nil
429491
}

0 commit comments

Comments
 (0)