feat(rust-provider): Add Rust-backed provider with XFS-safe durability via IPC bridge, TypeScript provider, tests and docs
This commit is contained in:
18
rust/crates/smartfs-bin/Cargo.toml
Normal file
18
rust/crates/smartfs-bin/Cargo.toml
Normal 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
|
||||
419
rust/crates/smartfs-bin/src/main.rs
Normal file
419
rust/crates/smartfs-bin/src/main.rs
Normal 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(¶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::<WriteFileParams>(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::<AppendFileParams>(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::<PathParams>(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::<CopyMoveParams>(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::<CopyMoveParams>(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::<PathParams>(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::<PathParams>(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::<ListDirectoryParams>(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::<CreateDirectoryParams>(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::<DeleteDirectoryParams>(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::<PathParams>(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::<PathParams>(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::<WatchParams>(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::<BatchParams>(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::<TransactionParams>(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::<NormalizePathParams>(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::<JoinPathParams>(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::<ReadFileStreamParams>(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::<WriteStreamBeginParams>(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::<WriteStreamChunkParams>(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<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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user