Spaces:
Sleeping
Sleeping
更新 main.rs
Browse files- src/main.rs +41 -16
src/main.rs
CHANGED
|
@@ -14,13 +14,18 @@ use tokio::process::{Child, Command};
|
|
| 14 |
use tokio::time::{interval, timeout};
|
| 15 |
use walkdir::WalkDir;
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
| 18 |
const STATE_FILE: &str = ".hf-sync-state.json";
|
| 19 |
const FINAL_WAIT_TIMEOUT: Duration = Duration::from_secs(20);
|
| 20 |
|
| 21 |
#[derive(Debug, Clone)]
|
| 22 |
struct Config {
|
| 23 |
token: String,
|
|
|
|
|
|
|
| 24 |
dataset_id: String,
|
| 25 |
sync_interval: Duration,
|
| 26 |
workspace: PathBuf,
|
|
@@ -144,7 +149,13 @@ async fn run() -> Result<()> {
|
|
| 144 |
|
| 145 |
fn load_config() -> Result<Config> {
|
| 146 |
let token = env::var("HF_TOKEN").context("HF_TOKEN is required")?;
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
validate_dataset_id(&dataset_id)?;
|
| 149 |
|
| 150 |
let sync_interval_secs: u64 = env::var("SYNC_INTERVAL")
|
|
@@ -169,7 +180,9 @@ fn validate_dataset_id(dataset_id: &str) -> Result<()> {
|
|
| 169 |
let owner = parts.next().unwrap_or_default();
|
| 170 |
let repo = parts.next().unwrap_or_default();
|
| 171 |
if owner.is_empty() || repo.is_empty() || parts.next().is_some() {
|
| 172 |
-
return Err(anyhow!(
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
Ok(())
|
| 175 |
}
|
|
@@ -204,11 +217,24 @@ async fn ensure_dataset_exists(client: &reqwest::Client, cfg: &Config) -> Result
|
|
| 204 |
Ok(())
|
| 205 |
}
|
| 206 |
StatusCode::NOT_FOUND => {
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
status => {
|
| 214 |
let body = response.text().await.unwrap_or_default();
|
|
@@ -221,7 +247,7 @@ async fn create_private_dataset(client: &reqwest::Client, cfg: &Config) -> Resul
|
|
| 221 |
let (owner, name) = cfg
|
| 222 |
.dataset_id
|
| 223 |
.split_once('/')
|
| 224 |
-
.ok_or_else(|| anyhow!("
|
| 225 |
|
| 226 |
let me_url = "https://huggingface.co/api/whoami-v2";
|
| 227 |
let username = client
|
|
@@ -263,7 +289,7 @@ async fn create_private_dataset(client: &reqwest::Client, cfg: &Config) -> Resul
|
|
| 263 |
}
|
| 264 |
|
| 265 |
async fn pull_workspace(client: &reqwest::Client, cfg: &Config) -> Result<()> {
|
| 266 |
-
eprintln!("pulling workspace from
|
| 267 |
let remote_files = list_remote_files(client, cfg).await?;
|
| 268 |
|
| 269 |
for file in &remote_files {
|
|
@@ -287,7 +313,7 @@ async fn pull_workspace(client: &reqwest::Client, cfg: &Config) -> Result<()> {
|
|
| 287 |
f.write_all(&bytes).await?;
|
| 288 |
}
|
| 289 |
|
| 290 |
-
eprintln!("pull complete
|
| 291 |
Ok(())
|
| 292 |
}
|
| 293 |
|
|
@@ -311,7 +337,7 @@ async fn list_remote_files(client: &reqwest::Client, cfg: &Config) -> Result<Vec
|
|
| 311 |
}
|
| 312 |
|
| 313 |
async fn push_workspace(client: &reqwest::Client, cfg: &Config) -> Result<()> {
|
| 314 |
-
eprintln!("pushing workspace updates to
|
| 315 |
let state_path = cfg.workspace.join(STATE_FILE);
|
| 316 |
let mut state = load_state(&state_path).await?;
|
| 317 |
|
|
@@ -433,7 +459,6 @@ async fn load_state(path: &Path) -> Result<SyncState> {
|
|
| 433 |
if !path.exists() {
|
| 434 |
return Ok(SyncState::default());
|
| 435 |
}
|
| 436 |
-
|
| 437 |
let raw = tokio::fs::read(path).await?;
|
| 438 |
let state = serde_json::from_slice(&raw).context("failed to parse sync state file")?;
|
| 439 |
Ok(state)
|
|
@@ -449,7 +474,7 @@ fn spawn_child_from_args() -> Result<Child> {
|
|
| 449 |
let args: Vec<String> = env::args().skip(1).collect();
|
| 450 |
if args.is_empty() {
|
| 451 |
return Err(anyhow!(
|
| 452 |
-
"no command provided
|
| 453 |
));
|
| 454 |
}
|
| 455 |
|
|
@@ -470,7 +495,7 @@ fn forward_sigterm(child: &mut Child) {
|
|
| 470 |
let ret = unsafe { libc::kill(id as libc::pid_t, libc::SIGTERM) };
|
| 471 |
if ret != 0 {
|
| 472 |
eprintln!(
|
| 473 |
-
"failed to forward SIGTERM:
|
| 474 |
std::io::Error::last_os_error()
|
| 475 |
);
|
| 476 |
}
|
|
@@ -495,7 +520,7 @@ async fn wait_for_child_shutdown(child: &mut Child) {
|
|
| 495 |
let ret = unsafe { libc::kill(id as libc::pid_t, libc::SIGKILL) };
|
| 496 |
if ret != 0 {
|
| 497 |
eprintln!(
|
| 498 |
-
"failed to SIGKILL child:
|
| 499 |
std::io::Error::last_os_error()
|
| 500 |
);
|
| 501 |
}
|
|
|
|
| 14 |
use tokio::time::{interval, timeout};
|
| 15 |
use walkdir::WalkDir;
|
| 16 |
|
| 17 |
+
/// The OpenClaw config directory we sync to/from HuggingFace.
|
| 18 |
+
/// Must match the path OpenClaw itself writes to (HOME=/home/node).
|
| 19 |
+
const WORKSPACE_DIR: &str = "/home/node/.openclaw";
|
| 20 |
+
|
| 21 |
const STATE_FILE: &str = ".hf-sync-state.json";
|
| 22 |
const FINAL_WAIT_TIMEOUT: Duration = Duration::from_secs(20);
|
| 23 |
|
| 24 |
#[derive(Debug, Clone)]
|
| 25 |
struct Config {
|
| 26 |
token: String,
|
| 27 |
+
/// HuggingFace dataset repo in the form "owner/name".
|
| 28 |
+
/// Read from OPENCLAW_DATASET_REPO (canonical HuggingClaw variable).
|
| 29 |
dataset_id: String,
|
| 30 |
sync_interval: Duration,
|
| 31 |
workspace: PathBuf,
|
|
|
|
| 149 |
|
| 150 |
fn load_config() -> Result<Config> {
|
| 151 |
let token = env::var("HF_TOKEN").context("HF_TOKEN is required")?;
|
| 152 |
+
|
| 153 |
+
// Accept OPENCLAW_DATASET_REPO (canonical HuggingClaw name) with
|
| 154 |
+
// HF_DATASET_ID as a legacy fallback so old Space configs keep working.
|
| 155 |
+
let dataset_id = env::var("OPENCLAW_DATASET_REPO")
|
| 156 |
+
.or_else(|_| env::var("HF_DATASET_ID"))
|
| 157 |
+
.context("OPENCLAW_DATASET_REPO (or HF_DATASET_ID) is required")?;
|
| 158 |
+
|
| 159 |
validate_dataset_id(&dataset_id)?;
|
| 160 |
|
| 161 |
let sync_interval_secs: u64 = env::var("SYNC_INTERVAL")
|
|
|
|
| 180 |
let owner = parts.next().unwrap_or_default();
|
| 181 |
let repo = parts.next().unwrap_or_default();
|
| 182 |
if owner.is_empty() || repo.is_empty() || parts.next().is_some() {
|
| 183 |
+
return Err(anyhow!(
|
| 184 |
+
"OPENCLAW_DATASET_REPO must be in the form owner/name"
|
| 185 |
+
));
|
| 186 |
}
|
| 187 |
Ok(())
|
| 188 |
}
|
|
|
|
| 217 |
Ok(())
|
| 218 |
}
|
| 219 |
StatusCode::NOT_FOUND => {
|
| 220 |
+
// Only auto-create if the user has opted in.
|
| 221 |
+
let auto_create = env::var("AUTO_CREATE_DATASET")
|
| 222 |
+
.map(|v| matches!(v.to_lowercase().as_str(), "1" | "true" | "yes" | "on"))
|
| 223 |
+
.unwrap_or(false);
|
| 224 |
+
|
| 225 |
+
if auto_create {
|
| 226 |
+
eprintln!(
|
| 227 |
+
"dataset {} not found, AUTO_CREATE_DATASET=true — creating private dataset",
|
| 228 |
+
cfg.dataset_id
|
| 229 |
+
);
|
| 230 |
+
create_private_dataset(client, cfg).await
|
| 231 |
+
} else {
|
| 232 |
+
Err(anyhow!(
|
| 233 |
+
"dataset {} not found. Create it on huggingface.co/new-dataset \
|
| 234 |
+
or set AUTO_CREATE_DATASET=true to create it automatically.",
|
| 235 |
+
cfg.dataset_id
|
| 236 |
+
))
|
| 237 |
+
}
|
| 238 |
}
|
| 239 |
status => {
|
| 240 |
let body = response.text().await.unwrap_or_default();
|
|
|
|
| 247 |
let (owner, name) = cfg
|
| 248 |
.dataset_id
|
| 249 |
.split_once('/')
|
| 250 |
+
.ok_or_else(|| anyhow!("OPENCLAW_DATASET_REPO must be in the form owner/name"))?;
|
| 251 |
|
| 252 |
let me_url = "https://huggingface.co/api/whoami-v2";
|
| 253 |
let username = client
|
|
|
|
| 289 |
}
|
| 290 |
|
| 291 |
async fn pull_workspace(client: &reqwest::Client, cfg: &Config) -> Result<()> {
|
| 292 |
+
eprintln!("pulling workspace from HuggingFace dataset: {}", cfg.dataset_id);
|
| 293 |
let remote_files = list_remote_files(client, cfg).await?;
|
| 294 |
|
| 295 |
for file in &remote_files {
|
|
|
|
| 313 |
f.write_all(&bytes).await?;
|
| 314 |
}
|
| 315 |
|
| 316 |
+
eprintln!("pull complete: {} files restored", remote_files.len());
|
| 317 |
Ok(())
|
| 318 |
}
|
| 319 |
|
|
|
|
| 337 |
}
|
| 338 |
|
| 339 |
async fn push_workspace(client: &reqwest::Client, cfg: &Config) -> Result<()> {
|
| 340 |
+
eprintln!("pushing workspace updates to HuggingFace");
|
| 341 |
let state_path = cfg.workspace.join(STATE_FILE);
|
| 342 |
let mut state = load_state(&state_path).await?;
|
| 343 |
|
|
|
|
| 459 |
if !path.exists() {
|
| 460 |
return Ok(SyncState::default());
|
| 461 |
}
|
|
|
|
| 462 |
let raw = tokio::fs::read(path).await?;
|
| 463 |
let state = serde_json::from_slice(&raw).context("failed to parse sync state file")?;
|
| 464 |
Ok(state)
|
|
|
|
| 474 |
let args: Vec<String> = env::args().skip(1).collect();
|
| 475 |
if args.is_empty() {
|
| 476 |
return Err(anyhow!(
|
| 477 |
+
"no command provided — pass the main process command as entrypoint arguments"
|
| 478 |
));
|
| 479 |
}
|
| 480 |
|
|
|
|
| 495 |
let ret = unsafe { libc::kill(id as libc::pid_t, libc::SIGTERM) };
|
| 496 |
if ret != 0 {
|
| 497 |
eprintln!(
|
| 498 |
+
"failed to forward SIGTERM: {}",
|
| 499 |
std::io::Error::last_os_error()
|
| 500 |
);
|
| 501 |
}
|
|
|
|
| 520 |
let ret = unsafe { libc::kill(id as libc::pid_t, libc::SIGKILL) };
|
| 521 |
if ret != 0 {
|
| 522 |
eprintln!(
|
| 523 |
+
"failed to SIGKILL child: {}",
|
| 524 |
std::io::Error::last_os_error()
|
| 525 |
);
|
| 526 |
}
|