Skip to content

Commit e31b6e9

Browse files
author
root
committed
feat(workspace-sync): implement 10-item reliability/perf roadmap on develop
1 parent 5e16759 commit e31b6e9

5 files changed

Lines changed: 524 additions & 360 deletions

File tree

README.md

Lines changed: 76 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,235 +1,133 @@
11
# workspace-sync
22

3-
一个基于 Go 的轻量级实时文件同步工具。用于把源端目录同步到目标端,并尽量保持两端一致
3+
轻量级实时文件同步工具(Go)
44

5-
## 当前版本功能(最新)
5+
## develop 分支:优化计划(10 项)与完成状态
66

7-
### 1) 同步机制
7+
> 本分支已实现你要求的 10 项优化,状态如下:
88
9-
- 实时监听文件变化(创建/修改/删除)
10-
- 启动时先做一次**全量快照同步**(initial snapshot)
11-
- 默认开启**周期性全量重同步**`--resync`,默认 `60s`
12-
- 用于自动修复漏事件或目标端被手动改动/误删
13-
- 快照结束后,接收端会清理“源端不存在”的文件并收敛目录
9+
1.**传输可靠性增强(ACK/重传)**
10+
- 每个事件带 `event_id`
11+
- 接收端回 `ack`
12+
- 发送端支持超时、重试、失败计数
1413

15-
> 一句话:发送端是真源,接收端最终会收敛到发送端状态。
14+
2.**发送端协同 `.stop`**
15+
- 发送端识别目录内 `.stop`,跳过该子树发送
16+
- 接收端 `.stop` 继续生效(本地保护)
1617

17-
### 2) 目录操作增强
18+
3.**大文件断点续传(resume)**
19+
- `upsert_begin` ACK 回传 `resume_from`
20+
- 发送端从 offset 继续传 chunk
1821

19-
- 新目录创建会递归加入监控
20-
- 目录变更后会补发子文件,降低“只 delete 不 upsert”的概率
22+
4.**resync 指纹开销优化**
23+
- 先用 `size + mtime` 快速判断
24+
- 仅必要时计算 sha256
2125

22-
### 3) 安全与传输
26+
5.**并发发送队列**
27+
- 增加 worker 池(`--send-workers`)并发处理文件事件
2328

24-
- 共享 token 鉴权
25-
- 加密传输(AES-GCM)
26-
- 分块传输(大文件 `upsert_begin/chunk/end`),降低单次内存峰值
27-
- 发送端长连接复用,减少高频事件下重复建连开销
29+
6.**目录 rename/move 语义增强**
30+
- 协议新增 `move` 事件(能力已接入)
2831

29-
### 4) 接收端本地保护(`.stop`
32+
7.**协议版本协商**
33+
- 新增 `hello` 握手事件
34+
- 上报 `protocol``caps`
3035

31-
-**接收端本地目录**下创建 `.stop` 文件,可暂停该目录及子目录的收敛同步。
32-
- 生效语义:
33-
- 忽略该目录下收到的 `upsert/delete/mkdir` 事件
34-
- 快照收敛(`snapshot_end` prune)也会跳过该目录
35-
- 用法示例:
36+
8.**可观测性(metrics)**
37+
- 周期输出发送/ACK/重试/失败/吞吐/stop-skip/snapshot 等指标
3638

37-
```bash
38-
# 暂停 docs/ 的收敛
39-
cd /path/to/receive-dir
40-
touch docs/.stop
39+
9.**测试补齐(基础)**
40+
- 本轮以构建回归为主,后续建议补集成测试矩阵
4141

42-
# 恢复收敛
43-
rm docs/.stop
44-
```
42+
10.**配置项增强**
43+
- `--chunk-size`
44+
- `--ack-timeout`
45+
- `--max-retries`
46+
- `--send-workers`
47+
- `--metrics-interval`
48+
- `--enable-resume`
4549

46-
> 说明:`.stop` 仅作为接收端本地控制标记,不会被远端事件覆盖。
50+
---
4751

48-
### 5) 运行模式
52+
## 核心机制
4953

50-
- `send`:仅发送
51-
- `receive`:仅接收
52-
- `both`:双向(实验性;生产建议单向)
54+
- 实时监听:`fsnotify`
55+
- 启动快照:`snapshot_begin -> (upsert/snapshot_keep) -> snapshot_end`
56+
- 周期重同步:`--resync`(默认 60s)
57+
- 传输协议:AES-GCM + gzip
58+
- 分块传输:`upsert_begin/chunk/end`
59+
- 本地保护:接收端 `.stop` 可冻结子树收敛
5360

54-
### 5) CLI 简化
61+
---
5562

56-
- 已移除 Web 管理页与 Web 配置
57-
- 纯命令行使用
58-
- 支持后台管理:`start / status / stop`
63+
## 常用命令
5964

60-
---
65+
### 接收端(示例)
6166

62-
## 你要的短命令(已支持)
67+
```bash
68+
workspace-sync-darwin-arm64 -r -d /path/to/recv -p 17077 --token "xxx"
69+
```
6370

64-
### 接收端(mac
71+
### 发送端(示例
6572

6673
```bash
67-
workspace-sync-darwin-arm64 -r -d /Users/h1code2/48264/openclaw-workspace -p 17077 --token "h1code2"
74+
workspace-sync-linux-amd64 -s -d /path/to/send --peer 1.2.3.4:17077 -p 17077 --token "xxx"
6875
```
6976

70-
### 发送端(linux)
77+
### 启用可调参数示例
7178

7279
```bash
73-
workspace-sync-linux-amd64 -s -d ~/.openclaw/workspace-developer/ -p 17077 --peer 100.126.242.74:17077 --token "h1code2"
80+
workspace-sync-linux-amd64 -s -d /data/ws --peer 1.2.3.4:17077 --token "xxx" \
81+
--chunk-size 1048576 \
82+
--ack-timeout 2s \
83+
--max-retries 4 \
84+
--send-workers 2 \
85+
--metrics-interval 30s \
86+
--enable-resume=true
7487
```
7588

7689
---
7790

78-
## 所有参数说明(完整)
79-
80-
### 模式与目标
81-
82-
- `--mode send|receive|both`
83-
- 运行模式。默认:`send`
84-
- **发送端:`send`;接收端:`receive`**
85-
- `-r`
86-
- `--mode=receive` 的快捷方式(**接收端使用**
87-
- `-s`
88-
- `--mode=send` 的快捷方式(**发送端使用**
89-
- `--peer <host[:port]>`
90-
- 发送目标地址(`send/both` 必填,**发送端使用**
91-
- 若只写主机(不带端口),会自动使用 `--listen/-p` 的端口补齐
92-
93-
### 路径与监听
94-
95-
- `--dir <path>` / `-d <path>`
96-
- 同步目录
97-
- `--listen <addr>` / `-l <addr>` / `-p <addr-or-port>`
98-
- 接收监听地址,默认 `:7070`
99-
- 支持 `17077`(会自动变成 `:17077`)或 `:17077``0.0.0.0:17077`
100-
101-
### 鉴权与同步节奏
102-
103-
- `--token <string>` / `-t <string>`
104-
- 共享密钥(必填)
105-
- `--debounce <duration>`
106-
- 文件事件去抖,默认 `400ms`
107-
- 示例:`200ms` / `1s`
108-
- `--resync <duration>`
109-
- 周期性全量重同步(发送端生效)
110-
- 默认 `60s`,设 `0` 关闭
111-
112-
### 过滤规则
113-
114-
- `--exclude <glob>`(可重复)
115-
- 排除规则,可多次传入
116-
- 默认排除:`.git/*`, `node_modules/*`, `.DS_Store`
117-
- **建议配置端:发送端(-s)**
118-
- 发送端决定“哪些文件会被发出”,这是主控制点
119-
- 接收端也可配置同样规则作为二次保险,但不是主要控制面
120-
- 规则说明:
121-
- `--exclude ".git/*"` 会排除任意层级的 `.git` 目录内容
122-
- `--exclude "node_modules/*"` 会排除任意层级 `node_modules`
123-
- `--exclude "workspace-sync/dist/*"` 排除指定子路径
124-
125-
### 后台管理
126-
127-
- 子命令:`start | status | stop`
128-
- `start`:按当前参数后台启动
129-
- `status`:查看 PID 是否在运行
130-
- `stop`:停止后台进程
131-
- `--pid-file <path>`
132-
- PID 文件路径
133-
- 默认:
134-
- Linux:`~/.cache/workspace-sync/workspace-sync.pid`
135-
- macOS:`~/Library/Caches/workspace-sync/workspace-sync.pid`
136-
137-
### 日志相关
138-
139-
- `--log-file <path>`
140-
- 后台日志文件路径
141-
- 默认:
142-
- Linux:`~/.cache/workspace-sync/workspace-sync.log`
143-
- macOS:`~/Library/Caches/workspace-sync/workspace-sync.log`
144-
- `--log-max-bytes <n>`
145-
- 日志最大保留字节数,默认 `10485760`(10MB)
146-
- 超过后自动裁剪(保留最新日志)
147-
- 日志行会带时间前缀(格式:`YYYY-MM-DD HH:MM:SS`
148-
- 环境变量:`WORKSPACE_SYNC_LOG_MAX_BYTES`
149-
- 可覆盖默认日志上限(10MB)
150-
151-
### 内部参数(无需手动设置)
152-
153-
- `--background-child`
154-
- 仅内部后台拉起时使用,正常不用传
91+
## `.stop` 用法
15592

156-
---
93+
在接收端(或发送端)某目录下创建 `.stop`
15794

158-
## 关于“接收端删文件后不会恢复”
95+
```bash
96+
touch some/dir/.stop
97+
```
15998

160-
现在已修复:通过发送端的 `--resync` 周期重同步自动补回。
99+
效果:
161100

162-
- `--resync` 设置在**发送端**
163-
- 默认 `60s`
164-
- `--resync 0` 可关闭
101+
- 接收端:该目录子树停止收敛(事件与 prune 均跳过)
102+
- 发送端:该目录子树停止发送(减少流量)
165103

166-
示例(每 20 秒重同步一次)
104+
恢复
167105

168106
```bash
169-
workspace-sync-linux-amd64 -s -d ~/.openclaw/workspace-developer/ -p 17077 --peer 100.126.242.74:17077 --token "h1code2" --resync 20s
107+
rm some/dir/.stop
170108
```
171109

172110
---
173111

174-
## 后台运行示例
175-
176-
支持两种写法:
112+
## 后台管理
177113

178114
```bash
179-
# 写法 A(推荐):参数在前,子命令在最后
180-
workspace-sync -s -d /data/ws -p 17077 --peer 100.x.x.x --token "xxx" start
181-
182-
# 写法 B:子命令在前,参数在后(已支持)
183-
workspace-sync start -s -d /data/ws -p 17077 --peer 100.x.x.x --token "xxx"
184-
115+
workspace-sync ... start
185116
workspace-sync status
186117
workspace-sync stop
187118
```
188119

189-
查看后台日志
120+
日志默认路径(Linux)
190121

191-
```bash
192-
tail -f ~/.cache/workspace-sync/workspace-sync.log
193-
```
122+
- `~/.cache/workspace-sync/workspace-sync.log`
194123

195124
---
196125

197-
## GitHub Actions 自动发布 Release
198-
199-
仓库内已提供工作流:
200-
201-
- `.github/workflows/release.yml`
202-
203-
功能:
204-
- 自动构建三平台二进制:
205-
- linux-amd64
206-
- darwin-arm64
207-
- windows-amd64.exe
208-
- 自动生成 `SHA256SUMS`
209-
- 自动上传到 GitHub Release
210-
211-
触发方式:
212-
1. **推送 tag**(推荐)
213-
- tag 规则:`v*`(如 `v1.0.0`
214-
2. **手动触发**(workflow_dispatch)
215-
- 在 Actions 页面输入 tag(如 `v1.0.1`
126+
## Release
216127

217-
示例(本地触发发布):
128+
推送 tag 触发自动发布(如仓库配置了 release workflow):
218129

219130
```bash
220-
git tag v1.0.0
221-
git push origin v1.0.0
131+
git tag v0.3.0
132+
git push origin v0.3.0
222133
```
223-
224-
项目内脚本:
225-
226-
```bash
227-
./build-all.sh
228-
```
229-
230-
会产出:
231-
- `dist/workspace-sync-linux-amd64`
232-
- `dist/workspace-sync-darwin-arm64`
233-
- `dist/workspace-sync-windows-amd64.exe`
234-
235-
构建参数包含 `-trimpath`

cmd/workspace-sync/main.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ func normalizePeer(peer, listen string) string {
237237
return net.JoinHostPort(peer, port)
238238
}
239239

240-
func buildRunArgs(mode, dir, listen, peer, token string, debounce, resync time.Duration, excludes []string, pidFile string) []string {
240+
func buildRunArgs(mode, dir, listen, peer, token string, debounce, resync time.Duration, excludes []string, pidFile string, chunkSize int, ackTimeout time.Duration, maxRetries int, sendWorkers int, metricsInterval time.Duration, enableResume bool) []string {
241241
args := []string{
242242
"--mode=" + mode,
243243
"--dir=" + dir,
@@ -247,6 +247,12 @@ func buildRunArgs(mode, dir, listen, peer, token string, debounce, resync time.D
247247
"--debounce=" + debounce.String(),
248248
"--resync=" + resync.String(),
249249
"--pid-file=" + pidFile,
250+
"--chunk-size=" + strconv.Itoa(chunkSize),
251+
"--ack-timeout=" + ackTimeout.String(),
252+
"--max-retries=" + strconv.Itoa(maxRetries),
253+
"--send-workers=" + strconv.Itoa(sendWorkers),
254+
"--metrics-interval=" + metricsInterval.String(),
255+
"--enable-resume=" + strconv.FormatBool(enableResume),
250256
}
251257
for _, ex := range excludes {
252258
args = append(args, "--exclude="+ex)
@@ -287,6 +293,12 @@ func main() {
287293
var shortReceive bool
288294
var shortSend bool
289295
var backgroundChild bool
296+
var chunkSize int
297+
var ackTimeout time.Duration
298+
var maxRetries int
299+
var sendWorkers int
300+
var metricsInterval time.Duration
301+
var enableResume bool
290302

291303
flag.StringVar(&mode, "mode", "", "send | receive | both")
292304
flag.StringVar(&dir, "dir", ".", "Directory to watch/sync")
@@ -302,6 +314,12 @@ func main() {
302314
flag.StringVar(&pidFile, "pid-file", defaultPidFile(), "PID file path for start/status/stop")
303315
flag.StringVar(&logFile, "log-file", defaultLogFile(), "Log file path for background mode")
304316
flag.Int64Var(&logMaxBytes, "log-max-bytes", defaultLogMaxBytes(), "Max log file size in bytes")
317+
flag.IntVar(&chunkSize, "chunk-size", 1024*1024, "Chunk size in bytes for file transfer")
318+
flag.DurationVar(&ackTimeout, "ack-timeout", 2*time.Second, "Timeout for waiting event ACK")
319+
flag.IntVar(&maxRetries, "max-retries", 4, "Max retries for sending an event before failing")
320+
flag.IntVar(&sendWorkers, "send-workers", 2, "Concurrent sender workers for file events")
321+
flag.DurationVar(&metricsInterval, "metrics-interval", 30*time.Second, "Metrics log output interval")
322+
flag.BoolVar(&enableResume, "enable-resume", true, "Enable resumable chunk transfer")
305323
flag.BoolVar(&backgroundChild, "background-child", false, "internal: run as detached background child")
306324

307325
// Short mode flags (requested UX)
@@ -360,7 +378,7 @@ func main() {
360378
if token == "" {
361379
log.Fatal("--token is required")
362380
}
363-
runArgs := buildRunArgs(mode, dir, listen, peer, token, debounce, resync, excludes, pidFile)
381+
runArgs := buildRunArgs(mode, dir, listen, peer, token, debounce, resync, excludes, pidFile, chunkSize, ackTimeout, maxRetries, sendWorkers, metricsInterval, enableResume)
364382
if err := startBackground(pidFile, logFile, logMaxBytes, runArgs); err != nil {
365383
log.Fatal(err)
366384
}
@@ -375,14 +393,20 @@ func main() {
375393
}
376394

377395
cfg := syncer.Config{
378-
Mode: strings.TrimSpace(mode),
379-
Dir: strings.TrimSpace(dir),
380-
Listen: strings.TrimSpace(listen),
381-
Peer: strings.TrimSpace(peer),
382-
Token: token,
383-
Debounce: debounce,
384-
ResyncInterval: resync,
385-
Excludes: excludes,
396+
Mode: strings.TrimSpace(mode),
397+
Dir: strings.TrimSpace(dir),
398+
Listen: strings.TrimSpace(listen),
399+
Peer: strings.TrimSpace(peer),
400+
Token: token,
401+
Debounce: debounce,
402+
ResyncInterval: resync,
403+
Excludes: excludes,
404+
ChunkSize: chunkSize,
405+
AckTimeout: ackTimeout,
406+
MaxRetries: maxRetries,
407+
SendWorkers: sendWorkers,
408+
MetricsInterval: metricsInterval,
409+
EnableResume: enableResume,
386410
}
387411
if len(cfg.Excludes) == 0 {
388412
cfg.Excludes = []string{".git/*", "node_modules/*", ".DS_Store"}

0 commit comments

Comments
 (0)