use base64::{Engine as _, engine::general_purpose::STANDARD}; use clap::Parser; use smartfs_core::{FsOps, WatchManager}; use smartfs_protocol::*; use std::collections::HashMap; use std::io::{self, BufRead, BufWriter, Write as IoWrite}; use std::path::{Path, PathBuf}; use std::os::unix::fs::PermissionsExt; #[derive(Parser)] #[command(name = "smartfs-bin", about = "SmartFS Rust filesystem backend")] struct Cli { /// Run in management/IPC mode (JSON over stdin/stdout) #[arg(long)] management: bool, } fn main() { let cli = Cli::parse(); if cli.management { run_management_mode(); } else { eprintln!("smartfs-bin: use --management flag for IPC mode"); std::process::exit(1); } } /// State for open write streams struct WriteStreamState { writer: BufWriter, final_path: PathBuf, temp_path: Option, mode: Option, } fn run_management_mode() { // Send ready event let ready = IpcEvent { event: "ready".to_string(), data: serde_json::json!({ "version": env!("CARGO_PKG_VERSION"), "provider": "rust" }), }; send_json(&ready); let watch_manager = WatchManager::new(); let mut write_streams: HashMap = HashMap::new(); let stdin = io::stdin(); for line in stdin.lock().lines() { let line = match line { Ok(l) => l, Err(_) => break, }; if line.trim().is_empty() { continue; } let request: IpcRequest = match serde_json::from_str(&line) { Ok(r) => r, Err(e) => { eprintln!("smartfs-bin: invalid JSON: {}", e); continue; } }; let response = dispatch_command(&request, &watch_manager, &mut write_streams); send_json(&response); } } fn dispatch_command( req: &IpcRequest, watch_manager: &WatchManager, write_streams: &mut HashMap, ) -> IpcResponse { match req.method.as_str() { "readFile" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::read_file(¶ms) { Ok(result) => IpcResponse::ok(req.id.clone(), result), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "writeFile" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::write_file(¶ms) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "appendFile" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::append_file(¶ms) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "deleteFile" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::delete_file(Path::new(¶ms.path)) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "copyFile" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::copy_file(¶ms) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "moveFile" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::move_file(¶ms) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "fileExists" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => { let exists = FsOps::file_exists(Path::new(¶ms.path)); IpcResponse::ok(req.id.clone(), serde_json::json!(exists)) } Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "fileStat" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::file_stat(Path::new(¶ms.path)) { Ok(stats) => { IpcResponse::ok(req.id.clone(), serde_json::to_value(&stats).unwrap()) } Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "listDirectory" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::list_directory(¶ms) { Ok(entries) => { IpcResponse::ok(req.id.clone(), serde_json::to_value(&entries).unwrap()) } Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "createDirectory" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::create_directory(¶ms) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "deleteDirectory" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::delete_directory(¶ms) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "directoryExists" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => { let exists = FsOps::directory_exists(Path::new(¶ms.path)); IpcResponse::ok(req.id.clone(), serde_json::json!(exists)) } Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "directoryStat" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::directory_stat(Path::new(¶ms.path)) { Ok(stats) => { IpcResponse::ok(req.id.clone(), serde_json::to_value(&stats).unwrap()) } Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "watch" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => { match watch_manager.add_watch( params.id, ¶ms.path, params.recursive.unwrap_or(false), ) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), } } Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "unwatchAll" => { match watch_manager.remove_all() { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), } } "batch" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => { let results = FsOps::batch(¶ms); IpcResponse::ok(req.id.clone(), serde_json::to_value(&results).unwrap()) } Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "executeTransaction" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::execute_transaction(¶ms) { Ok(()) => IpcResponse::ok_void(req.id.clone()), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "normalizePath" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => { let result = FsOps::normalize_path(¶ms.path); IpcResponse::ok(req.id.clone(), serde_json::json!(result)) } Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "joinPath" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => { let result = FsOps::join_path(¶ms.segments); IpcResponse::ok(req.id.clone(), serde_json::json!(result)) } Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "readFileStream" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => match FsOps::read_file_stream(&req.id, ¶ms) { Ok(total) => IpcResponse::ok(req.id.clone(), serde_json::json!({ "totalBytes": total })), Err(e) => IpcResponse::err(req.id.clone(), e), }, Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "writeStreamBegin" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => { let final_path = PathBuf::from(¶ms.path); // Ensure parent directory exists if let Some(parent) = final_path.parent() { if !parent.exists() { if let Err(e) = std::fs::create_dir_all(parent) { return IpcResponse::err(req.id.clone(), format!("writeStreamBegin mkdir: {}", e)); } } } let (write_path, temp_path) = if params.atomic.unwrap_or(false) { let temp = final_path.with_extension(format!( "tmp.{}", std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .unwrap() .as_nanos() )); (temp.clone(), Some(temp)) } else { (final_path.clone(), None) }; match std::fs::File::create(&write_path) { Ok(file) => { let stream_id = format!("ws_{}", req.id); write_streams.insert(stream_id.clone(), WriteStreamState { writer: BufWriter::new(file), final_path, temp_path, mode: params.mode, }); IpcResponse::ok(req.id.clone(), serde_json::json!({ "streamId": stream_id })) } Err(e) => IpcResponse::err(req.id.clone(), format!("writeStreamBegin create: {}", e)), } } Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "writeStreamChunk" => { match serde_json::from_value::(req.params.clone()) { Ok(params) => { let stream = match write_streams.get_mut(¶ms.stream_id) { Some(s) => s, None => return IpcResponse::err(req.id.clone(), format!("unknown streamId: {}", params.stream_id)), }; // Write data if non-empty if !params.data.is_empty() { match STANDARD.decode(¶ms.data) { Ok(bytes) => { if let Err(e) = stream.writer.write_all(&bytes) { write_streams.remove(¶ms.stream_id); return IpcResponse::err(req.id.clone(), format!("writeStreamChunk write: {}", e)); } } Err(e) => { write_streams.remove(¶ms.stream_id); return IpcResponse::err(req.id.clone(), format!("writeStreamChunk decode: {}", e)); } } } if params.last { // Finalize: flush, fsync, set mode, rename if atomic, fsync parent let state = write_streams.remove(¶ms.stream_id).unwrap(); let mut writer = state.writer; if let Err(e) = writer.flush() { return IpcResponse::err(req.id.clone(), format!("writeStreamChunk flush: {}", e)); } // Get inner file for fsync let file = match writer.into_inner() { Ok(f) => f, Err(e) => { return IpcResponse::err(req.id.clone(), format!("writeStreamChunk into_inner: {}", e.error())); } }; if let Err(e) = file.sync_all() { return IpcResponse::err(req.id.clone(), format!("writeStreamChunk fsync: {}", e)); } drop(file); // Set mode if requested if let Some(mode) = state.mode { let write_path = state.temp_path.as_ref().unwrap_or(&state.final_path); let _ = std::fs::set_permissions(write_path, std::fs::Permissions::from_mode(mode)); } // Rename if atomic if let Some(ref temp_path) = state.temp_path { if let Err(e) = std::fs::rename(temp_path, &state.final_path) { let _ = std::fs::remove_file(temp_path); return IpcResponse::err(req.id.clone(), format!("writeStreamChunk rename: {}", e)); } } // Fsync parent if let Some(parent) = state.final_path.parent() { let _ = std::fs::File::open(parent).and_then(|f| f.sync_all()); } } IpcResponse::ok_void(req.id.clone()) } Err(e) => IpcResponse::err(req.id.clone(), format!("invalid params: {}", e)), } } "ping" => IpcResponse::ok(req.id.clone(), serde_json::json!({ "pong": true })), other => IpcResponse::err(req.id.clone(), format!("unknown method: {}", other)), } } fn send_json(value: &T) { if let Ok(json) = serde_json::to_string(value) { let stdout = io::stdout(); let mut out = stdout.lock(); let _ = writeln!(out, "{}", json); let _ = out.flush(); } }