Skip to main content

braid_core/fs/
server_handlers.rs

1use 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    // If it's a Braid URL, we allow it for on-demand sync
27    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    // Map URL path to filesystem path
40    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    // 4. Read file content (or fetch if missing and it's a URL)
49    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                        // Lazy persist
59                        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                        // Initialize merge state if not exists
65                        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    // Get current version
99    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    // Send snapshot response
108    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    // allow PUT to urls and .braidfs/config/errors
130    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    // Map URL path to filesystem path
138    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    // Write content to file
147    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    // Update content cache
164    {
165        let mut cache = state.content_cache.write().await;
166        cache.insert(path.clone().to_string(), body);
167    }
168
169    // Update version store if version was provided
170    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}