feat(rust-provider): Add Rust-backed provider with XFS-safe durability via IPC bridge, TypeScript provider, tests and docs

This commit is contained in:
2026-03-05 19:36:11 +00:00
parent 61e3f3a0b6
commit 5283247bea
24 changed files with 14453 additions and 1248 deletions

View File

@@ -0,0 +1,18 @@
[package]
name = "smartfs-bin"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "smartfs-bin"
path = "src/main.rs"
[dependencies]
smartfs-protocol = { path = "../smartfs-protocol" }
smartfs-core = { path = "../smartfs-core" }
tokio.workspace = true
serde.workspace = true
serde_json.workspace = true
clap.workspace = true
base64.workspace = true

View File

@@ -0,0 +1,419 @@
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<std::fs::File>,
final_path: PathBuf,
temp_path: Option<PathBuf>,
mode: Option<u32>,
}
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<String, WriteStreamState> = 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<String, WriteStreamState>,
) -> IpcResponse {
match req.method.as_str() {
"readFile" => {
match serde_json::from_value::<ReadFileParams>(req.params.clone()) {
Ok(params) => match FsOps::read_file(&params) {
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::<WriteFileParams>(req.params.clone()) {
Ok(params) => match FsOps::write_file(&params) {
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::<AppendFileParams>(req.params.clone()) {
Ok(params) => match FsOps::append_file(&params) {
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::<PathParams>(req.params.clone()) {
Ok(params) => match FsOps::delete_file(Path::new(&params.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::<CopyMoveParams>(req.params.clone()) {
Ok(params) => match FsOps::copy_file(&params) {
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::<CopyMoveParams>(req.params.clone()) {
Ok(params) => match FsOps::move_file(&params) {
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::<PathParams>(req.params.clone()) {
Ok(params) => {
let exists = FsOps::file_exists(Path::new(&params.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::<PathParams>(req.params.clone()) {
Ok(params) => match FsOps::file_stat(Path::new(&params.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::<ListDirectoryParams>(req.params.clone()) {
Ok(params) => match FsOps::list_directory(&params) {
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::<CreateDirectoryParams>(req.params.clone()) {
Ok(params) => match FsOps::create_directory(&params) {
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::<DeleteDirectoryParams>(req.params.clone()) {
Ok(params) => match FsOps::delete_directory(&params) {
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::<PathParams>(req.params.clone()) {
Ok(params) => {
let exists = FsOps::directory_exists(Path::new(&params.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::<PathParams>(req.params.clone()) {
Ok(params) => match FsOps::directory_stat(Path::new(&params.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::<WatchParams>(req.params.clone()) {
Ok(params) => {
match watch_manager.add_watch(
params.id,
&params.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::<BatchParams>(req.params.clone()) {
Ok(params) => {
let results = FsOps::batch(&params);
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::<TransactionParams>(req.params.clone()) {
Ok(params) => match FsOps::execute_transaction(&params) {
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::<NormalizePathParams>(req.params.clone()) {
Ok(params) => {
let result = FsOps::normalize_path(&params.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::<JoinPathParams>(req.params.clone()) {
Ok(params) => {
let result = FsOps::join_path(&params.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::<ReadFileStreamParams>(req.params.clone()) {
Ok(params) => match FsOps::read_file_stream(&req.id, &params) {
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::<WriteStreamBeginParams>(req.params.clone()) {
Ok(params) => {
let final_path = PathBuf::from(&params.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::<WriteStreamChunkParams>(req.params.clone()) {
Ok(params) => {
let stream = match write_streams.get_mut(&params.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(&params.data) {
Ok(bytes) => {
if let Err(e) = stream.writer.write_all(&bytes) {
write_streams.remove(&params.stream_id);
return IpcResponse::err(req.id.clone(), format!("writeStreamChunk write: {}", e));
}
}
Err(e) => {
write_streams.remove(&params.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(&params.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<T: serde::Serialize>(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();
}
}