Files
furumi-ng/furumi-mount-macos/src/main.rs
2026-03-17 15:03:36 +00:00

131 lines
4.1 KiB
Rust

pub mod nfs;
use clap::Parser;
use furumi_client_core::FurumiClient;
use nfsserve::tcp::{NFSTcp, NFSTcpListener};
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tracing::info;
#[derive(Parser, Debug)]
#[command(version, about = "Furumi-ng: mount remote filesystem via encrypted gRPC + NFS (macOS)")]
struct Args {
/// Server address (IP:PORT, will use https:// by default unless --no-tls is specified)
#[arg(short, long, env = "FURUMI_SERVER", default_value = "0.0.0.0:50051")]
server: String,
/// Authentication Bearer token (leave empty if auth is disabled on server)
#[arg(short, long, env = "FURUMI_TOKEN", default_value = "")]
token: String,
/// Mount point directory
#[arg(short, long, env = "FURUMI_MOUNT")]
mount: PathBuf,
/// Disable TLS encryption
#[arg(long, default_value_t = false)]
no_tls: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let args = Args::parse();
if !args.mount.exists() || !args.mount.is_dir() {
eprintln!(
"Error: Mount point {:?} does not exist or is not a directory",
args.mount
);
std::process::exit(1);
}
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let full_addr = if args.no_tls {
if args.server.starts_with("http://") || args.server.starts_with("https://") {
args.server.clone()
} else {
format!("http://{}", args.server)
}
} else if args.server.starts_with("http://") || args.server.starts_with("https://") {
args.server.clone()
} else {
format!("https://{}", args.server)
};
let client = rt.block_on(async { FurumiClient::connect(&full_addr, &args.token).await })?;
let furumi_nfs = nfs::FurumiNfs::new(client);
let mount_point = args.mount.clone();
let mount_point_umount = args.mount.clone();
// Set up signal handler for graceful shutdown
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
eprintln!("\nReceived signal, unmounting...");
r.store(false, Ordering::SeqCst);
})?;
rt.block_on(async move {
// Bind NFS server to a random available port on localhost
let listener = NFSTcpListener::bind("127.0.0.1:0", furumi_nfs)
.await
.expect("Failed to bind NFS listener");
let port = listener.get_listen_port();
info!("NFS server listening on 127.0.0.1:{}", port);
// Start handling NFS connections BEFORE mounting —
// mount_nfs needs to talk to the server during mount.
let handle = tokio::spawn(async move {
if let Err(e) = listener.handle_forever().await {
eprintln!("NFS server error: {}", e);
}
});
// Mount via macOS native mount_nfs
let mount_path = mount_point.to_string_lossy().to_string();
let opts = format!(
"rdonly,locallocks,noresvport,nfsvers=3,tcp,rsize=1048576,port={},mountport={}",
port, port
);
let status = Command::new("mount_nfs")
.args(["-o", &opts, "127.0.0.1:/", &mount_path])
.status()
.expect("Failed to execute mount_nfs");
if !status.success() {
handle.abort();
eprintln!("Error: mount_nfs failed with status {}", status);
std::process::exit(1);
}
println!("Mounted Furumi-ng v{} to {:?}", env!("CARGO_PKG_VERSION"), mount_path);
// Wait for shutdown signal
while running.load(Ordering::SeqCst) {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
// Unmount
let _ = Command::new("diskutil")
.arg("unmount")
.arg("force")
.arg(mount_point_umount.to_string_lossy().as_ref())
.status();
handle.abort();
println!("Unmounted successfully.");
});
Ok(())
}