renminwansui1976 commited on
Commit
80a465e
·
unverified ·
1 Parent(s): b6074d0

更新 main.rs

Browse files
Files changed (1) hide show
  1. src/main.rs +470 -1
src/main.rs CHANGED
@@ -1 +1,470 @@
1
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use anyhow::{anyhow, Context, Result};
2
+ use reqwest::{Client, StatusCode};
3
+ use serde::{Deserialize, Serialize};
4
+ use std::collections::HashMap;
5
+ use std::env;
6
+ use std::path::{Path, PathBuf};
7
+ use std::process::Stdio;
8
+ use std::time::Duration;
9
+ use tokio::process::{Child, Command};
10
+ use tokio::signal::unix::{signal, SignalKind};
11
+ use tokio::time::interval;
12
+ use walkdir::WalkDir;
13
+
14
+ const DEFAULT_WORKSPACE: &str = "/home/user/.openclaw/workspace";
15
+ const MANIFEST_PATH: &str = ".sync_manifest.json";
16
+
17
+ #[derive(Clone, Debug)]
18
+ struct Config {
19
+ hf_token: String,
20
+ hf_dataset_id: String,
21
+ sync_interval: u64,
22
+ start_command: String,
23
+ workspace_dir: PathBuf,
24
+ }
25
+
26
+ #[derive(Clone)]
27
+ struct SyncManager {
28
+ client: Client,
29
+ config: Config,
30
+ }
31
+
32
+ #[derive(Debug, Deserialize)]
33
+ struct TreeEntry {
34
+ path: String,
35
+ #[serde(rename = "type")]
36
+ entry_type: String,
37
+ }
38
+
39
+ #[derive(Debug, Serialize, Deserialize, Default)]
40
+ struct Manifest {
41
+ files: HashMap<String, String>,
42
+ }
43
+
44
+ #[derive(Debug, Deserialize)]
45
+ struct DatasetInfo {
46
+ id: String,
47
+ }
48
+
49
+ #[derive(Debug, Serialize)]
50
+ struct CreateRepoRequest {
51
+ name: String,
52
+ #[serde(skip_serializing_if = "Option::is_none")]
53
+ organization: Option<String>,
54
+ private: bool,
55
+ #[serde(rename = "type")]
56
+ repo_type: String,
57
+ }
58
+
59
+ impl Config {
60
+ fn from_env() -> Result<Self> {
61
+ let hf_token = env::var("HF_TOKEN").context("missing HF_TOKEN")?;
62
+ let hf_dataset_id = env::var("HF_DATASET_ID").context("missing HF_DATASET_ID")?;
63
+ let sync_interval = env::var("SYNC_INTERVAL")
64
+ .ok()
65
+ .and_then(|v| v.parse::<u64>().ok())
66
+ .unwrap_or(300);
67
+ let start_command = env::var("START_COMMAND").context("missing START_COMMAND")?;
68
+
69
+ Ok(Self {
70
+ hf_token,
71
+ hf_dataset_id,
72
+ sync_interval,
73
+ start_command,
74
+ workspace_dir: PathBuf::from(DEFAULT_WORKSPACE),
75
+ })
76
+ }
77
+ }
78
+
79
+ impl SyncManager {
80
+ fn new(config: Config) -> Result<Self> {
81
+ let client = Client::builder()
82
+ .pool_idle_timeout(Duration::from_secs(90))
83
+ .build()
84
+ .context("failed to build HTTP client")?;
85
+ Ok(Self { client, config })
86
+ }
87
+
88
+ async fn init_dataset(&self) -> Result<()> {
89
+ if self.dataset_exists().await? {
90
+ println!("[hf] dataset exists: {}", self.config.hf_dataset_id);
91
+ return Ok(());
92
+ }
93
+
94
+ println!("[hf] dataset not found, creating private dataset");
95
+ self.create_dataset().await
96
+ }
97
+
98
+ async fn startup_pull(&self) -> Result<()> {
99
+ tokio::fs::create_dir_all(&self.config.workspace_dir)
100
+ .await
101
+ .context("failed to create workspace directory")?;
102
+
103
+ let entries = self.list_remote_files().await?;
104
+ if entries.is_empty() {
105
+ println!("[pull] dataset is empty, skip pull");
106
+ return Ok(());
107
+ }
108
+
109
+ for path in entries {
110
+ let content = self.download_file(&path).await?;
111
+ let local_path = self.config.workspace_dir.join(&path);
112
+ if let Some(parent) = local_path.parent() {
113
+ tokio::fs::create_dir_all(parent).await?;
114
+ }
115
+ tokio::fs::write(&local_path, content)
116
+ .await
117
+ .with_context(|| format!("failed writing {}", local_path.display()))?;
118
+ }
119
+
120
+ println!("[pull] restored {} file(s)", entries.len());
121
+ Ok(())
122
+ }
123
+
124
+ async fn push_cycle(&self) -> Result<()> {
125
+ let mut manifest = self.load_local_manifest().await?;
126
+ let mut new_manifest = Manifest::default();
127
+
128
+ let files = collect_workspace_files(&self.config.workspace_dir)?;
129
+ for file in files {
130
+ let rel = file
131
+ .strip_prefix(&self.config.workspace_dir)
132
+ .context("failed to compute relative path")?
133
+ .to_string_lossy()
134
+ .replace('\\', "/");
135
+
136
+ let bytes = tokio::fs::read(&file)
137
+ .await
138
+ .with_context(|| format!("failed to read {}", file.display()))?;
139
+ let hash = format!("{:x}", md5::compute(&bytes));
140
+ new_manifest.files.insert(rel.clone(), hash.clone());
141
+
142
+ let changed = manifest.files.get(&rel).map(|h| h != &hash).unwrap_or(true);
143
+ if changed {
144
+ self.upload_file(&rel, bytes).await?;
145
+ println!("[push] uploaded {rel}");
146
+ }
147
+ }
148
+
149
+ manifest = new_manifest;
150
+ self.save_local_manifest(&manifest).await?;
151
+ self.upload_manifest(&manifest).await?;
152
+ Ok(())
153
+ }
154
+
155
+ async fn dataset_exists(&self) -> Result<bool> {
156
+ let url = format!(
157
+ "https://huggingface.co/api/datasets/{}",
158
+ self.config.hf_dataset_id
159
+ );
160
+ let resp = self
161
+ .client
162
+ .get(url)
163
+ .bearer_auth(&self.config.hf_token)
164
+ .send()
165
+ .await
166
+ .context("dataset existence request failed")?;
167
+
168
+ match resp.status() {
169
+ StatusCode::OK => {
170
+ let info: DatasetInfo = resp.json().await.context("invalid dataset payload")?;
171
+ println!("[hf] connected dataset: {}", info.id);
172
+ Ok(true)
173
+ }
174
+ StatusCode::NOT_FOUND => Ok(false),
175
+ s => Err(anyhow!("failed checking dataset, status={s}")),
176
+ }
177
+ }
178
+
179
+ async fn create_dataset(&self) -> Result<()> {
180
+ let (organization, name) = split_dataset_id(&self.config.hf_dataset_id)?;
181
+ let payload = CreateRepoRequest {
182
+ name,
183
+ organization,
184
+ private: true,
185
+ repo_type: "dataset".to_string(),
186
+ };
187
+
188
+ let resp = self
189
+ .client
190
+ .post("https://huggingface.co/api/repos/create")
191
+ .bearer_auth(&self.config.hf_token)
192
+ .json(&payload)
193
+ .send()
194
+ .await
195
+ .context("create dataset request failed")?;
196
+
197
+ if !resp.status().is_success() {
198
+ return Err(anyhow!(
199
+ "failed creating dataset {}, status={} body={}",
200
+ self.config.hf_dataset_id,
201
+ resp.status(),
202
+ resp.text().await.unwrap_or_default()
203
+ ));
204
+ }
205
+
206
+ Ok(())
207
+ }
208
+
209
+ async fn list_remote_files(&self) -> Result<Vec<String>> {
210
+ let url = format!(
211
+ "https://huggingface.co/api/datasets/{}/tree/main?recursive=true&expand=true",
212
+ self.config.hf_dataset_id
213
+ );
214
+
215
+ let resp = self
216
+ .client
217
+ .get(url)
218
+ .bearer_auth(&self.config.hf_token)
219
+ .send()
220
+ .await
221
+ .context("list dataset files request failed")?;
222
+
223
+ if resp.status() == StatusCode::NOT_FOUND {
224
+ return Ok(Vec::new());
225
+ }
226
+
227
+ if !resp.status().is_success() {
228
+ return Err(anyhow!(
229
+ "failed listing dataset files, status={}",
230
+ resp.status()
231
+ ));
232
+ }
233
+
234
+ let entries: Vec<TreeEntry> = resp.json().await.context("invalid tree payload")?;
235
+ let files = entries
236
+ .into_iter()
237
+ .filter(|e| e.entry_type == "file")
238
+ .map(|e| e.path)
239
+ .filter(|p| p != MANIFEST_PATH)
240
+ .collect();
241
+ Ok(files)
242
+ }
243
+
244
+ async fn download_file(&self, remote_path: &str) -> Result<Vec<u8>> {
245
+ let encoded = url_encode_path(remote_path);
246
+ let url = format!(
247
+ "https://huggingface.co/datasets/{}/resolve/main/{}?download=1",
248
+ self.config.hf_dataset_id, encoded
249
+ );
250
+
251
+ let resp = self
252
+ .client
253
+ .get(url)
254
+ .bearer_auth(&self.config.hf_token)
255
+ .send()
256
+ .await
257
+ .with_context(|| format!("download failed for {remote_path}"))?;
258
+
259
+ if !resp.status().is_success() {
260
+ return Err(anyhow!(
261
+ "failed downloading {remote_path}, status={}",
262
+ resp.status()
263
+ ));
264
+ }
265
+
266
+ Ok(resp.bytes().await?.to_vec())
267
+ }
268
+
269
+ async fn upload_file(&self, remote_path: &str, bytes: Vec<u8>) -> Result<()> {
270
+ let encoded = url_encode_path(remote_path);
271
+ let url = format!(
272
+ "https://huggingface.co/api/datasets/{}/upload/main/{}",
273
+ self.config.hf_dataset_id, encoded
274
+ );
275
+
276
+ let resp = self
277
+ .client
278
+ .post(url)
279
+ .bearer_auth(&self.config.hf_token)
280
+ .header("content-type", "application/octet-stream")
281
+ .body(bytes)
282
+ .send()
283
+ .await
284
+ .with_context(|| format!("upload failed for {remote_path}"))?;
285
+
286
+ if !resp.status().is_success() {
287
+ return Err(anyhow!(
288
+ "failed uploading {remote_path}, status={} body={}",
289
+ resp.status(),
290
+ resp.text().await.unwrap_or_default()
291
+ ));
292
+ }
293
+ Ok(())
294
+ }
295
+
296
+ async fn upload_manifest(&self, manifest: &Manifest) -> Result<()> {
297
+ let content = serde_json::to_vec_pretty(manifest)?;
298
+ self.upload_file(MANIFEST_PATH, content).await
299
+ }
300
+
301
+ async fn load_local_manifest(&self) -> Result<Manifest> {
302
+ let path = self.config.workspace_dir.join(MANIFEST_PATH);
303
+ if tokio::fs::try_exists(&path).await.unwrap_or(false) {
304
+ let bytes = tokio::fs::read(path).await?;
305
+ return Ok(serde_json::from_slice(&bytes).unwrap_or_default());
306
+ }
307
+
308
+ let remote = self.download_file(MANIFEST_PATH).await;
309
+ match remote {
310
+ Ok(bytes) => Ok(serde_json::from_slice(&bytes).unwrap_or_default()),
311
+ Err(_) => Ok(Manifest::default()),
312
+ }
313
+ }
314
+
315
+ async fn save_local_manifest(&self, manifest: &Manifest) -> Result<()> {
316
+ let path = self.config.workspace_dir.join(MANIFEST_PATH);
317
+ let bytes = serde_json::to_vec_pretty(manifest)?;
318
+ tokio::fs::write(path, bytes).await?;
319
+ Ok(())
320
+ }
321
+ }
322
+
323
+ fn split_dataset_id(dataset_id: &str) -> Result<(Option<String>, String)> {
324
+ let mut parts = dataset_id.split('/');
325
+ let first = parts
326
+ .next()
327
+ .ok_or_else(|| anyhow!("invalid HF_DATASET_ID"))?;
328
+ let second = parts
329
+ .next()
330
+ .ok_or_else(|| anyhow!("invalid HF_DATASET_ID"))?;
331
+ if parts.next().is_some() {
332
+ return Err(anyhow!("invalid HF_DATASET_ID"));
333
+ }
334
+ Ok((Some(first.to_string()), second.to_string()))
335
+ }
336
+
337
+ fn collect_workspace_files(root: &Path) -> Result<Vec<PathBuf>> {
338
+ let mut files = Vec::new();
339
+ for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
340
+ if entry.file_type().is_dir() {
341
+ continue;
342
+ }
343
+
344
+ let rel = entry
345
+ .path()
346
+ .strip_prefix(root)
347
+ .context("strip prefix failed")?
348
+ .to_string_lossy()
349
+ .replace('\\', "/");
350
+
351
+ if rel == MANIFEST_PATH {
352
+ continue;
353
+ }
354
+
355
+ files.push(entry.into_path());
356
+ }
357
+ Ok(files)
358
+ }
359
+
360
+ fn url_encode_path(path: &str) -> String {
361
+ path.split('/')
362
+ .map(|segment| {
363
+ let mut out = String::new();
364
+ for b in segment.bytes() {
365
+ match b {
366
+ b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
367
+ out.push(char::from(b))
368
+ }
369
+ _ => out.push_str(&format!("%{:02X}", b)),
370
+ }
371
+ }
372
+ out
373
+ })
374
+ .collect::<Vec<_>>()
375
+ .join("/")
376
+ }
377
+
378
+ async fn spawn_openclaw(command: &str) -> Result<Child> {
379
+ let parts = shell_words::split(command).context("failed to parse START_COMMAND")?;
380
+ if parts.is_empty() {
381
+ return Err(anyhow!("START_COMMAND is empty"));
382
+ }
383
+
384
+ let mut cmd = Command::new(&parts[0]);
385
+ if parts.len() > 1 {
386
+ cmd.args(&parts[1..]);
387
+ }
388
+
389
+ cmd.stdin(Stdio::inherit())
390
+ .stdout(Stdio::inherit())
391
+ .stderr(Stdio::inherit());
392
+
393
+ cmd.spawn().context("failed to spawn OpenClaw process")
394
+ }
395
+
396
+ async fn forward_signal(child: &mut Child, sig: &str) {
397
+ let Some(pid) = child.id() else {
398
+ return;
399
+ };
400
+
401
+ let signal_num = match sig {
402
+ "TERM" => "-TERM",
403
+ "INT" => "-INT",
404
+ _ => return,
405
+ };
406
+
407
+ let status = Command::new("kill")
408
+ .arg(signal_num)
409
+ .arg(pid.to_string())
410
+ .status()
411
+ .await;
412
+
413
+ match status {
414
+ Ok(s) if s.success() => println!("[signal] forwarded {sig} to child {pid}"),
415
+ Ok(s) => eprintln!("[signal] failed forwarding {sig}, status={s}"),
416
+ Err(e) => eprintln!("[signal] failed forwarding {sig}: {e}"),
417
+ }
418
+ }
419
+
420
+ #[tokio::main]
421
+ async fn main() -> Result<()> {
422
+ let config = Config::from_env()?;
423
+ let sync = SyncManager::new(config.clone())?;
424
+
425
+ sync.init_dataset().await?;
426
+ sync.startup_pull().await?;
427
+
428
+ println!("[boot] starting command: {}", config.start_command);
429
+ let mut child = spawn_openclaw(&config.start_command).await?;
430
+
431
+ let mut ticker = interval(Duration::from_secs(config.sync_interval));
432
+ let mut sigterm = signal(SignalKind::terminate())?;
433
+ let mut sigint = signal(SignalKind::interrupt())?;
434
+
435
+ loop {
436
+ tokio::select! {
437
+ _ = ticker.tick() => {
438
+ if let Err(e) = sync.push_cycle().await {
439
+ eprintln!("[push] periodic sync failed: {e:#}");
440
+ }
441
+ }
442
+ _ = sigterm.recv() => {
443
+ println!("[signal] SIGTERM received");
444
+ forward_signal(&mut child, "TERM").await;
445
+ if let Err(e) = sync.push_cycle().await {
446
+ eprintln!("[push] final sync failed on SIGTERM: {e:#}");
447
+ }
448
+ break;
449
+ }
450
+ _ = sigint.recv() => {
451
+ println!("[signal] SIGINT received");
452
+ forward_signal(&mut child, "INT").await;
453
+ if let Err(e) = sync.push_cycle().await {
454
+ eprintln!("[push] final sync failed on SIGINT: {e:#}");
455
+ }
456
+ break;
457
+ }
458
+ status = child.wait() => {
459
+ let status = status.context("failed waiting for child process")?;
460
+ println!("[proc] child exited with status: {status}");
461
+ if let Err(e) = sync.push_cycle().await {
462
+ eprintln!("[push] final sync failed after child exit: {e:#}");
463
+ }
464
+ break;
465
+ }
466
+ }
467
+ }
468
+
469
+ Ok(())
470
+ }