420 lines
17 KiB
Rust
420 lines
17 KiB
Rust
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();
|
|
}
|
|
}
|