use super::server_handlers::{handle_get_file, handle_put_file};
use crate::core::server::BraidLayer;
use crate::core::Result;
use crate::fs::state::{Command, DaemonState};
use axum::{
extract::State,
routing::{delete, put},
Json, Router,
};
use serde::Deserialize;
use std::net::SocketAddr;
#[derive(Deserialize)]
pub struct SyncParams {
url: String,
}
#[derive(Deserialize)]
pub struct CookieParams {
pub domain: String,
pub value: String,
}
#[derive(Deserialize)]
pub struct IdentityParams {
pub domain: String,
pub email: String,
}
#[derive(Deserialize)]
pub struct MountParams {
pub port: Option<u16>,
}
pub async fn run_server(port: u16, state: DaemonState) -> Result<()> {
let app = Router::new()
.route("/api/sync", put(handle_sync))
.route("/api/sync", delete(handle_unsync))
.route("/api/cookie", put(handle_cookie))
.route("/api/identity", put(handle_identity))
.route("/api/mount", put(handle_mount))
.route("/api/mount", delete(handle_unmount))
.route(
"/.braidfs/config",
axum::routing::get(handle_braidfs_config),
)
.route(
"/.braidfs/errors",
axum::routing::get(handle_braidfs_errors),
)
.route(
"/.braidfs/get_version/{fullpath}/{hash}",
axum::routing::get(handle_get_version),
)
.route("/{*path}", axum::routing::get(handle_get_file))
.route("/{*path}", put(handle_put_file))
.layer(BraidLayer::new().middleware())
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], port));
tracing::info!("Daemon API listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn handle_sync(
State(state): State<DaemonState>,
Json(params): Json<SyncParams>,
) -> Json<serde_json::Value> {
tracing::info!("IPC Command: Sync {}", params.url);
if let Err(e) = state
.tx_cmd
.send(Command::Sync {
url: params.url.clone(),
})
.await
{
tracing::error!("Failed to send sync command: {}", e);
return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
}
Json(serde_json::json!({ "status": "ok", "url": params.url }))
}
async fn handle_unsync(
State(state): State<DaemonState>,
Json(params): Json<SyncParams>,
) -> Json<serde_json::Value> {
tracing::info!("IPC Command: Unsync {}", params.url);
if let Err(e) = state
.tx_cmd
.send(Command::Unsync {
url: params.url.clone(),
})
.await
{
tracing::error!("Failed to send unsync command: {}", e);
return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
}
Json(serde_json::json!({ "status": "ok", "url": params.url }))
}
async fn handle_cookie(
State(state): State<DaemonState>,
Json(params): Json<CookieParams>,
) -> Json<serde_json::Value> {
tracing::info!("IPC Command: SetCookie {}={}", params.domain, params.value);
if let Err(e) = state
.tx_cmd
.send(Command::SetCookie {
domain: params.domain.clone(),
value: params.value.clone(),
})
.await
{
tracing::error!("Failed to send cookie command: {}", e);
return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
}
Json(serde_json::json!({ "status": "ok", "domain": params.domain }))
}
async fn handle_identity(
State(state): State<DaemonState>,
Json(params): Json<IdentityParams>,
) -> Json<serde_json::Value> {
tracing::info!(
"IPC Command: SetIdentity {}={}",
params.domain,
params.email
);
if let Err(e) = state
.tx_cmd
.send(Command::SetIdentity {
domain: params.domain.clone(),
email: params.email.clone(),
})
.await
{
tracing::error!("Failed to send identity command: {}", e);
return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
}
Json(serde_json::json!({ "status": "ok", "domain": params.domain }))
}
async fn handle_braidfs_config(State(state): State<DaemonState>) -> Json<serde_json::Value> {
let config = state.config.read().await;
Json(serde_json::json!({
"sync": config.sync,
"cookies": config.cookies,
"port": config.port,
"debounce_ms": config.debounce_ms,
"ignore_patterns": config.ignore_patterns,
}))
}
static ERRORS: std::sync::OnceLock<std::sync::Mutex<Vec<String>>> = std::sync::OnceLock::new();
fn get_errors() -> &'static std::sync::Mutex<Vec<String>> {
ERRORS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
}
pub fn log_error(text: &str) {
tracing::error!("LOGGING ERROR: {}", text);
if let Ok(mut errors) = get_errors().lock() {
errors.push(format!(
"{}: {}",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
text
));
if errors.len() > 100 {
errors.remove(0);
}
}
}
async fn handle_braidfs_errors() -> String {
if let Ok(errors) = get_errors().lock() {
errors.join("\n")
} else {
"Error reading error log".to_string()
}
}
async fn handle_get_version(
axum::extract::Path((fullpath, hash)): axum::extract::Path<(String, String)>,
State(state): State<DaemonState>,
) -> Json<serde_json::Value> {
use percent_encoding::percent_decode_str;
let fullpath = percent_decode_str(&fullpath)
.decode_utf8_lossy()
.to_string();
let hash = percent_decode_str(&hash).decode_utf8_lossy().to_string();
tracing::debug!("get_version: {} hash={}", fullpath, hash);
let versions = state.version_store.read().await;
if let Some(version) = versions.get_version_by_hash(&fullpath, &hash) {
Json(serde_json::json!(version))
} else {
Json(serde_json::json!(null))
}
}
#[cfg(unix)]
pub async fn is_read_only(path: &std::path::Path) -> std::io::Result<bool> {
use std::os::unix::fs::PermissionsExt;
let metadata = tokio::fs::metadata(path).await?;
let mode = metadata.permissions().mode();
Ok((mode & 0o200) == 0)
}
#[cfg(windows)]
pub async fn is_read_only(path: &std::path::Path) -> std::io::Result<bool> {
let metadata = tokio::fs::metadata(path).await?;
Ok(metadata.permissions().readonly())
}
#[cfg(unix)]
pub async fn set_read_only(path: &std::path::Path, read_only: bool) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
let metadata = tokio::fs::metadata(path).await?;
let mut perms = metadata.permissions();
let mode = perms.mode();
let new_mode = if read_only {
mode & !0o222 } else {
mode | 0o200 };
perms.set_mode(new_mode);
tokio::fs::set_permissions(path, perms).await
}
#[cfg(windows)]
pub async fn set_read_only(path: &std::path::Path, read_only: bool) -> std::io::Result<()> {
let metadata = tokio::fs::metadata(path).await?;
let mut perms = metadata.permissions();
perms.set_readonly(read_only);
tokio::fs::set_permissions(path, perms).await
}
async fn handle_mount(
State(state): State<DaemonState>,
Json(params): Json<MountParams>,
) -> Json<serde_json::Value> {
let port = params.port.unwrap_or(2049);
tracing::info!("IPC Command: Mount on port {}", port);
if let Err(e) = state.tx_cmd.send(Command::Mount { port }).await {
tracing::error!("Failed to send mount command: {}", e);
return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
}
Json(serde_json::json!({ "status": "ok", "port": port }))
}
async fn handle_unmount(State(state): State<DaemonState>) -> Json<serde_json::Value> {
tracing::info!("IPC Command: Unmount");
if let Err(e) = state.tx_cmd.send(Command::Unmount).await {
tracing::error!("Failed to send unmount command: {}", e);
return Json(serde_json::json!({ "status": "error", "message": "Internal channel error" }));
}
Json(serde_json::json!({ "status": "ok" }))
}