1use super::blob_handlers::{handle_get_blob, handle_put_blob};
2use super::mapping;
3use super::server_handlers::{handle_get_file, handle_get_file_api, handle_put_file};
4use crate::core::server::BraidLayer;
5use crate::core::Result;
6use crate::fs::state::{Command, DaemonState};
7use axum::{
8 extract::State,
9 routing::{delete, put},
10 Json, Router,
11};
12use serde::Deserialize;
13use std::net::SocketAddr;
14
15#[derive(Deserialize)]
16pub struct SyncParams {
17 url: String,
18}
19
20#[derive(Deserialize)]
21pub struct PushParams {
22 url: String,
23 content: String,
24 content_type: Option<String>,
25}
26
27#[derive(Deserialize)]
28pub struct CookieParams {
29 pub domain: String,
30 pub value: String,
31}
32
33#[derive(Deserialize)]
34pub struct IdentityParams {
35 pub domain: String,
36 pub email: String,
37}
38
39#[derive(Deserialize)]
40pub struct MountParams {
41 pub port: Option<u16>,
42 pub mount_point: Option<String>,
43}
44
45pub async fn run_server(port: u16, state: DaemonState) -> Result<()> {
46 let mut app = Router::new()
47 .route("/api/sync", put(handle_sync))
48 .route("/api/sync", delete(handle_unsync))
49 .route("/api/push", put(handle_push))
50 .route("/api/get", axum::routing::get(handle_get_file_api))
51 .route("/api/cookie", put(handle_cookie))
52 .route("/api/identity", put(handle_identity));
53
54 #[cfg(feature = "nfs")]
55 {
56 app = app
57 .route("/api/mount", put(handle_mount))
58 .route("/api/mount", delete(handle_unmount));
59 }
60
61 let app = app
62 .route(
63 "/.braidfs/config",
64 axum::routing::get(handle_braidfs_config),
65 )
66 .route(
67 "/.braidfs/errors",
68 axum::routing::get(handle_braidfs_errors),
69 )
70 .route(
71 "/.braidfs/get_version/{fullpath}/{hash}",
72 axum::routing::get(handle_get_version),
73 )
74 .route(
75 "/.braidfs/set_version/{fullpath}/{parents}",
76 axum::routing::put(handle_set_version),
77 )
78 .route("/api/blob/{hash}", axum::routing::get(handle_get_blob))
79 .route("/api/blob", put(handle_put_blob))
80 .route("/{*path}", axum::routing::get(handle_get_file))
81 .route("/{*path}", put(handle_put_file))
82 .layer(BraidLayer::new().middleware())
83 .with_state(state);
84
85 let addr = SocketAddr::from(([127, 0, 0, 1], port));
86 tracing::info!("Daemon API listening on {}", addr);
87
88 let listener = tokio::net::TcpListener::bind(addr).await?;
89 axum::serve(listener, app).await?;
90
91 Ok(())
92}
93
94async fn handle_sync(
95 State(state): State<DaemonState>,
96 Json(params): Json<SyncParams>,
97) -> Json<serde_json::Value> {
98 tracing::info!("IPC Command: Sync {}", params.url);
99
100 if let Err(e) = state
101 .tx_cmd
102 .send(Command::Sync {
103 url: params.url.clone(),
104 })
105 .await
106 {
107 tracing::error!("Failed to send sync command: {}", e);
108 return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
109 }
110
111 Json(serde_json::json!({ "status": "ok", "url": params.url }))
112}
113
114async fn handle_unsync(
115 State(state): State<DaemonState>,
116 Json(params): Json<SyncParams>,
117) -> Json<serde_json::Value> {
118 tracing::info!("IPC Command: Unsync {}", params.url);
119
120 if let Err(e) = state
121 .tx_cmd
122 .send(Command::Unsync {
123 url: params.url.clone(),
124 })
125 .await
126 {
127 tracing::error!("Failed to send unsync command: {}", e);
128 return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
129 }
130
131 Json(serde_json::json!({ "status": "ok", "url": params.url }))
132}
133
134async fn handle_push(
135 State(state): State<DaemonState>,
136 Json(params): Json<PushParams>,
137) -> Json<serde_json::Value> {
138 tracing::info!(
139 "IPC Command: Push {} ({} bytes)",
140 params.url,
141 params.content.len()
142 );
143
144 let path = match mapping::url_to_path(¶ms.url) {
146 Ok(p) => p,
147 Err(e) => {
148 tracing::error!("Failed to map URL to path: {}", e);
149 return Json(
150 serde_json::json!({ "status": "error", "message": format!("Path mapping failed: {}", e) }),
151 );
152 }
153 };
154
155 if let Some(parent) = path.parent() {
156 if let Err(e) = tokio::fs::create_dir_all(parent).await {
157 tracing::error!("Failed to create parent directory: {}", e);
158 return Json(
159 serde_json::json!({ "status": "error", "message": format!("Directory creation failed: {}", e) }),
160 );
161 }
162 }
163
164 let parents = {
166 let store = state.version_store.read().await;
167 store
168 .get(¶ms.url)
169 .map(|v| v.current_version.clone())
170 .unwrap_or_default()
171 };
172
173 let original_content = {
175 let cache = state.content_cache.read().await;
176 cache.get(¶ms.url).cloned()
177 };
178
179 match crate::fs::sync::sync_local_to_remote(
181 &path,
182 ¶ms.url,
183 &parents,
184 original_content,
185 params.content.clone(),
186 params.content_type,
187 state.clone(),
188 )
189 .await
190 {
191 Ok(()) => {
192 tracing::info!("Successfully pushed {} to remote", params.url);
193
194 if let Err(e) = tokio::fs::write(&path, ¶ms.content).await {
196 tracing::error!("Failed to write file after successful sync: {}", e);
197 return Json(
198 serde_json::json!({ "status": "error", "message": format!("Server accepted but local write failed: {}", e) }),
199 );
200 }
201 state.pending.add(path.clone());
203
204 Json(serde_json::json!({ "status": "ok", "url": params.url }))
205 }
206 Err(e) => {
207 tracing::error!("Push failed for {}: {}", params.url, e);
208 let err_str = e.to_string();
209 let status = if err_str.contains("401") || err_str.contains("Unauthorized") {
210 "unauthorized"
211 } else if err_str.contains("403") || err_str.contains("Forbidden") {
212 "forbidden"
213 } else {
214 "error"
215 };
216
217 tracing::error!("Server error detail: {}", e);
218
219 Json(serde_json::json!({
220 "status": status,
221 "message": format!("Push failed: {}", e),
222 "domain": url::Url::parse(¶ms.url).ok().and_then(|u| u.domain().map(|d| d.to_string()))
223 }))
224 }
225 }
226}
227
228async fn handle_cookie(
229 State(state): State<DaemonState>,
230 Json(params): Json<CookieParams>,
231) -> Json<serde_json::Value> {
232 tracing::info!("IPC Command: SetCookie {}={}", params.domain, params.value);
233
234 if let Err(e) = state
235 .tx_cmd
236 .send(Command::SetCookie {
237 domain: params.domain.clone(),
238 value: params.value.clone(),
239 })
240 .await
241 {
242 tracing::error!("Failed to send cookie command: {}", e);
243 return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
244 }
245
246 Json(serde_json::json!({ "status": "ok", "domain": params.domain }))
247}
248
249async fn handle_identity(
250 State(state): State<DaemonState>,
251 Json(params): Json<IdentityParams>,
252) -> Json<serde_json::Value> {
253 tracing::info!(
254 "IPC Command: SetIdentity {}={}",
255 params.domain,
256 params.email
257 );
258
259 if let Err(e) = state
260 .tx_cmd
261 .send(Command::SetIdentity {
262 domain: params.domain.clone(),
263 email: params.email.clone(),
264 })
265 .await
266 {
267 tracing::error!("Failed to send identity command: {}", e);
268 return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
269 }
270
271 Json(serde_json::json!({ "status": "ok", "domain": params.domain }))
272}
273
274async fn handle_braidfs_config(State(state): State<DaemonState>) -> Json<serde_json::Value> {
276 let config = state.config.read().await;
277 Json(serde_json::json!({
278 "sync": config.sync,
279 "cookies": config.cookies,
280 "port": config.port,
281 "debounce_ms": config.debounce_ms,
282 "ignore_patterns": config.ignore_patterns,
283 }))
284}
285
286static ERRORS: std::sync::OnceLock<std::sync::Mutex<Vec<String>>> = std::sync::OnceLock::new();
288
289fn get_errors() -> &'static std::sync::Mutex<Vec<String>> {
290 ERRORS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
291}
292
293pub fn log_error(text: &str) {
295 tracing::error!("LOGGING ERROR: {}", text);
296 if let Ok(mut errors) = get_errors().lock() {
297 errors.push(format!(
298 "{}: {}",
299 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
300 text
301 ));
302 if errors.len() > 100 {
304 errors.remove(0);
305 }
306 }
307}
308
309async fn handle_braidfs_errors() -> String {
311 if let Ok(errors) = get_errors().lock() {
312 errors.join("\n")
313 } else {
314 "Error reading error log".to_string()
315 }
316}
317
318async fn handle_get_version(
320 axum::extract::Path((fullpath, hash)): axum::extract::Path<(String, String)>,
321 State(state): State<DaemonState>,
322) -> Json<serde_json::Value> {
323 use percent_encoding::percent_decode_str;
324 let fullpath = percent_decode_str(&fullpath)
325 .decode_utf8_lossy()
326 .to_string();
327 let hash = percent_decode_str(&hash).decode_utf8_lossy().to_string();
328
329 tracing::debug!("get_version: {} hash={}", fullpath, hash);
330
331 let versions = state.version_store.read().await;
333 if let Some(version) = versions.get_version_by_hash(&fullpath, &hash) {
334 Json(serde_json::json!(version))
335 } else {
336 Json(serde_json::json!(null))
337 }
338}
339
340async fn handle_set_version(
342 axum::extract::Path((fullpath, parents)): axum::extract::Path<(String, String)>,
343 State(state): State<DaemonState>,
344 body: String,
345) -> Json<serde_json::Value> {
346 use percent_encoding::percent_decode_str;
347 let fullpath = percent_decode_str(&fullpath)
348 .decode_utf8_lossy()
349 .to_string();
350 let parents_json = percent_decode_str(&parents).decode_utf8_lossy().to_string();
351
352 let parents: Vec<String> = serde_json::from_str(&parents_json).unwrap_or_default();
353
354 tracing::info!("set_version: {} parents={:?}", fullpath, parents);
355
356 match mapping::path_to_url(std::path::Path::new(&fullpath)) {
357 Ok(url) => {
358 let mut store = state.version_store.write().await;
359 let my_id = crate::fs::PEER_ID.read().await.clone();
360
361 let version_id = format!(
363 "{}-{}",
364 my_id,
365 uuid::Uuid::new_v4().to_string()[..8].to_string()
366 );
367
368 store.update(
369 &url,
370 vec![crate::core::Version::new(&version_id)],
371 parents
372 .into_iter()
373 .map(|p| crate::core::Version::new(&p))
374 .collect(),
375 );
376 let _ = store.save().await;
377
378 let mut cache = state.content_cache.write().await;
380 cache.insert(url, body);
381
382 Json(serde_json::json!({ "status": "ok", "version": version_id }))
383 }
384 Err(e) => {
385 tracing::error!("Failed to map path to URL: {}", e);
386 Json(serde_json::json!({ "status": "error", "message": e.to_string() }))
387 }
388 }
389}
390
391#[cfg(unix)]
394pub async fn is_read_only(path: &std::path::Path) -> std::io::Result<bool> {
395 use std::os::unix::fs::PermissionsExt;
396 let metadata = tokio::fs::metadata(path).await?;
397 let mode = metadata.permissions().mode();
398 Ok((mode & 0o200) == 0)
400}
401
402#[cfg(windows)]
403pub async fn is_read_only(path: &std::path::Path) -> std::io::Result<bool> {
404 let metadata = tokio::fs::metadata(path).await?;
405 Ok(metadata.permissions().readonly())
406}
407
408#[cfg(unix)]
411pub async fn set_read_only(path: &std::path::Path, read_only: bool) -> std::io::Result<()> {
412 use std::os::unix::fs::PermissionsExt;
413 let metadata = tokio::fs::metadata(path).await?;
414 let mut perms = metadata.permissions();
415 let mode = perms.mode();
416
417 let new_mode = if read_only {
418 mode & !0o222 } else {
420 mode | 0o200 };
422
423 perms.set_mode(new_mode);
424 tokio::fs::set_permissions(path, perms).await
425}
426
427#[cfg(windows)]
428pub async fn set_read_only(path: &std::path::Path, read_only: bool) -> std::io::Result<()> {
429 let metadata = tokio::fs::metadata(path).await?;
430 let mut perms = metadata.permissions();
431 perms.set_readonly(read_only);
432 tokio::fs::set_permissions(path, perms).await
433}
434
435#[cfg(feature = "nfs")]
436async fn handle_mount(
437 State(state): State<DaemonState>,
438 Json(params): Json<MountParams>,
439) -> Json<serde_json::Value> {
440 let port = params.port.unwrap_or(2049);
441 tracing::info!("IPC Command: Mount on port {}", port);
442
443 if let Err(e) = state
444 .tx_cmd
445 .send(Command::Mount {
446 port,
447 mount_point: params.mount_point,
448 })
449 .await
450 {
451 tracing::error!("Failed to send mount command: {}", e);
452 return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
453 }
454
455 Json(serde_json::json!({ "status": "ok", "port": port }))
456}
457
458#[cfg(feature = "nfs")]
459async fn handle_unmount(State(state): State<DaemonState>) -> Json<serde_json::Value> {
460 tracing::info!("IPC Command: Unmount");
461
462 if let Err(e) = state.tx_cmd.send(Command::Unmount).await {
463 tracing::error!("Failed to send unmount command: {}", e);
464 return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
465 }
466
467 Json(serde_json::json!({ "status": "ok" }))
468}
469async fn handle_push_binary(
470 State(state): State<DaemonState>,
471 query: axum::extract::Query<SyncParams>,
472 headers: axum::http::HeaderMap,
473 body: axum::body::Bytes,
474) -> Json<serde_json::Value> {
475 tracing::info!(
476 "IPC Command: Push Binary {} ({} bytes)",
477 query.url,
478 body.len()
479 );
480
481 let path = match mapping::url_to_path(&query.url) {
483 Ok(p) => p,
484 Err(e) => {
485 return Json(
486 serde_json::json!({ "status": "error", "message": format!("Path mapping failed: {}", e) }),
487 );
488 }
489 };
490
491 if let Some(parent) = path.parent() {
492 let _ = tokio::fs::create_dir_all(parent).await;
493 }
494
495 let parents = {
497 let store = state.version_store.read().await;
498 store
499 .get(&query.url)
500 .map(|v| v.current_version.clone())
501 .unwrap_or_default()
502 };
503
504 let content_type = headers
505 .get(axum::http::header::CONTENT_TYPE)
506 .and_then(|v| v.to_str().ok())
507 .map(|s| s.to_string());
508
509 match crate::fs::sync::sync_binary_to_remote(
511 &path,
512 &query.url,
513 &parents,
514 body.clone(),
515 content_type,
516 state.clone(),
517 )
518 .await
519 {
520 Ok(()) => {
521 if let Err(e) = tokio::fs::write(&path, &body).await {
523 return Json(
524 serde_json::json!({ "status": "error", "message": format!("Server accepted but local write failed: {}", e) }),
525 );
526 }
527 state.pending.add(path);
528
529 Json(serde_json::json!({ "status": "ok", "url": query.url }))
530 }
531 Err(e) => Json(
532 serde_json::json!({ "status": "error", "message": format!("Binary push failed: {}", e) }),
533 ),
534 }
535}