braid_core/fs/
server_handlers.rs1use super::mapping;
2use crate::core::server::BraidState;
3use crate::core::{Update, Version};
4use crate::fs::state::DaemonState;
5use axum::{
6 extract::{Path, State},
7 http::StatusCode,
8 response::{IntoResponse, Response},
9 Extension,
10};
11use serde::Deserialize;
12use std::sync::Arc;
13
14#[derive(Deserialize)]
15pub struct GetParams {
16 pub url: String,
17}
18
19pub async fn handle_get_file(
20 Path(path): Path<String>,
21 State(state): State<DaemonState>,
22 Extension(braid_state): Extension<Arc<BraidState>>,
23) -> Response {
24 tracing::info!("GET /{} (subscribe={})", path, braid_state.subscribe);
25
26 let is_braid_url = path.starts_with("http://") || path.starts_with("https://");
28
29 if !is_braid_url && path != ".braidfs/config" && path != ".braidfs/errors" {
30 tracing::debug!("Path not found: {}", path);
31 return (
32 StatusCode::NOT_FOUND,
33 axum::response::Html(
34 r#"Nothing to see here. Use a Braid URL or check <a href=".braidfs/config">.braidfs/config</a>"#
35 )
36 ).into_response();
37 }
38
39 let file_path = match mapping::url_to_path(&path) {
41 Ok(p) => p,
42 Err(e) => {
43 tracing::error!("Path mapping error: {:?}", e);
44 return (StatusCode::BAD_REQUEST, format!("Invalid path: {}", e)).into_response();
45 }
46 };
47
48 let content = match tokio::fs::read_to_string(&file_path).await {
50 Ok(c) => c,
51 Err(_) => {
52 if is_braid_url {
53 tracing::info!("[On-Demand] Fetching {} from remote...", path);
54 let fetch_req = crate::core::BraidRequest::new().with_method("GET");
55 match state.client.fetch(&path, fetch_req).await {
56 Ok(res) if (200..300).contains(&res.status) => {
57 let body = String::from_utf8_lossy(&res.body).to_string();
58 if let Some(p) = file_path.parent() {
60 let _ = tokio::fs::create_dir_all(p).await;
61 }
62 let _ = tokio::fs::write(&file_path, &body).await;
63
64 let peer_id = crate::fs::PEER_ID.read().await.clone();
66 let mut merges = state.active_merges.write().await;
67 let merge = merges.entry(path.clone()).or_insert_with(|| {
68 let mut m = state
69 .merge_registry
70 .create("diamond", &peer_id)
71 .expect("Failed to create diamond merge");
72 m.initialize(&body);
73 m
74 });
75
76 body
77 }
78 _ => {
79 return (StatusCode::NOT_FOUND, "Resource not found on remote")
80 .into_response();
81 }
82 }
83 } else {
84 if let Some(parent) = file_path.parent() {
85 let _ = tokio::fs::create_dir_all(parent).await;
86 }
87 let empty_content = if path == ".braidfs/config" {
88 r#"{"sync":{},"cookies":{},"port":45678}"#
89 } else {
90 ""
91 };
92 let _ = tokio::fs::write(&file_path, empty_content).await;
93 empty_content.to_string()
94 }
95 }
96 };
97
98 let version = {
100 let store = state.version_store.read().await;
101 store
102 .get(&path)
103 .map(|v| v.current_version.clone())
104 .unwrap_or_else(|| vec!["initial".to_string()])
105 };
106
107 let update = Update::snapshot(Version::new(version[0].clone()), content.clone());
109 update.into_response()
110}
111
112pub async fn handle_get_file_api(
113 State(state): State<DaemonState>,
114 Extension(braid_state): Extension<Arc<BraidState>>,
115 axum::extract::Query(params): axum::extract::Query<GetParams>,
116) -> Response {
117 handle_get_file(Path(params.url), State(state), Extension(braid_state)).await
118}
119
120pub async fn handle_put_file(
121 Path(path): Path<String>,
122 State(state): State<DaemonState>,
123 Extension(braid_state): Extension<Arc<BraidState>>,
124 _headers: axum::http::HeaderMap,
125 body: String,
126) -> Response {
127 tracing::info!("PUT /{}", path);
128
129 let is_braid_url = path.starts_with("http://") || path.starts_with("https://");
131
132 if !is_braid_url && path != ".braidfs/config" && path != ".braidfs/errors" {
133 tracing::warn!("PUT not allowed for path: {}", path);
134 return (StatusCode::NOT_FOUND, "Not found").into_response();
135 }
136
137 let file_path = match mapping::url_to_path(&path) {
139 Ok(p) => p,
140 Err(e) => {
141 tracing::error!("Path mapping error: {:?}", e);
142 return (StatusCode::BAD_REQUEST, format!("Invalid path: {}", e)).into_response();
143 }
144 };
145
146 if let Some(parent) = file_path.parent() {
148 if let Err(e) = tokio::fs::create_dir_all(parent).await {
149 tracing::error!("Failed to create directory: {:?}", e);
150 return (
151 StatusCode::INTERNAL_SERVER_ERROR,
152 "Directory creation failed",
153 )
154 .into_response();
155 }
156 }
157
158 if let Err(e) = tokio::fs::write(&file_path, &body).await {
159 tracing::error!("Failed to write file: {:?}", e);
160 return (StatusCode::INTERNAL_SERVER_ERROR, "File write failed").into_response();
161 }
162
163 {
165 let mut cache = state.content_cache.write().await;
166 cache.insert(path.clone().to_string(), body);
167 }
168
169 if let Some(version) = &braid_state.version {
171 let mut store = state.version_store.write().await;
172 store.update(
173 &path,
174 version.clone(),
175 braid_state.parents.clone().unwrap_or_default(),
176 );
177 let _ = store.save().await;
178 }
179
180 tracing::info!("File written: {}", path);
181 (StatusCode::OK, "File updated").into_response()
182}