Added macos client
This commit is contained in:
159
Cargo.lock
generated
159
Cargo.lock
generated
@@ -91,7 +91,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -103,7 +103,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -136,7 +136,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -147,7 +147,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -266,12 +266,27 @@ version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "bytestream"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04f720842a717d6afaf69fee2dc69b771edc165f12cc3eb1b0e8eeef53a86454"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
@@ -327,7 +342,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -460,7 +475,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -518,6 +533,17 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -606,6 +632,24 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "furumi-mount-macos"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"clap",
|
||||
"ctrlc",
|
||||
"furumi-client-core",
|
||||
"furumi-common",
|
||||
"libc",
|
||||
"nfsserve",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "furumi-server"
|
||||
version = "0.1.0"
|
||||
@@ -708,7 +752,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1032,6 +1076,18 @@ version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -1129,6 +1185,26 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
|
||||
[[package]]
|
||||
name = "nfsserve"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d73615e054238e6bf5e554407b5b23e82fc63616db459057c51b794799eda6fb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"byteorder",
|
||||
"bytestream",
|
||||
"filetime",
|
||||
"futures",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-attributes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
@@ -1188,6 +1264,17 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@@ -1282,7 +1369,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -1330,7 +1417,7 @@ checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1351,6 +1438,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
@@ -1379,7 +1472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1456,7 +1549,7 @@ dependencies = [
|
||||
"prost",
|
||||
"prost-types",
|
||||
"regex",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
@@ -1470,7 +1563,7 @@ dependencies = [
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1585,6 +1678,15 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
@@ -1804,7 +1906,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1933,6 +2035,17 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@@ -1958,7 +2071,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2006,7 +2119,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2017,7 +2130,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2085,7 +2198,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2166,7 +2279,7 @@ dependencies = [
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2237,7 +2350,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2391,7 +2504,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -2587,7 +2700,7 @@ dependencies = [
|
||||
"heck",
|
||||
"indexmap 2.13.0",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
@@ -2603,7 +2716,7 @@ dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
@@ -2689,7 +2802,7 @@ checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -3,6 +3,12 @@ members = [
|
||||
"furumi-common",
|
||||
"furumi-server",
|
||||
"furumi-client-core",
|
||||
"furumi-mount-linux"
|
||||
"furumi-mount-linux",
|
||||
"furumi-mount-macos"
|
||||
]
|
||||
default-members = [
|
||||
"furumi-common",
|
||||
"furumi-server",
|
||||
"furumi-client-core",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
18
furumi-mount-macos/Cargo.toml
Normal file
18
furumi-mount-macos/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "furumi-mount-macos"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
furumi-common = { path = "../furumi-common" }
|
||||
furumi-client-core = { path = "../furumi-client-core" }
|
||||
anyhow = "1.0.102"
|
||||
nfsserve = "0.10.2"
|
||||
async-trait = "0.1.89"
|
||||
clap = { version = "4.5.60", features = ["derive", "env"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-stream = "0.1.18"
|
||||
ctrlc = "3.5.2"
|
||||
libc = "0.2.183"
|
||||
125
furumi-mount-macos/src/main.rs
Normal file
125
furumi-mount-macos/src/main.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
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 to {:?}", 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("umount").arg(mount_point_umount.to_string_lossy().as_ref()).status();
|
||||
handle.abort();
|
||||
println!("Unmounted successfully.");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
406
furumi-mount-macos/src/nfs.rs
Normal file
406
furumi-mount-macos/src/nfs.rs
Normal file
@@ -0,0 +1,406 @@
|
||||
use async_trait::async_trait;
|
||||
use furumi_client_core::FurumiClient;
|
||||
use nfsserve::nfs::{
|
||||
fattr3, fileid3, filename3, ftype3, mode3, nfspath3, nfsstat3, nfstime3, sattr3, specdata3,
|
||||
};
|
||||
use nfsserve::vfs::{DirEntry, NFSFileSystem, ReadDirResult, VFSCapabilities};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use tokio_stream::StreamExt;
|
||||
use tracing::{debug, error};
|
||||
|
||||
// ── ID Mapper ──────────────────────────────────────────────────
|
||||
|
||||
/// Maps NFS file IDs (u64) to remote virtual filesystem paths and vice versa.
|
||||
/// ID 1 is permanently assigned to the root directory ("/").
|
||||
/// ID 0 is reserved by NFS and never used.
|
||||
struct IdMapper {
|
||||
id_to_path: HashMap<fileid3, String>,
|
||||
path_to_id: HashMap<String, fileid3>,
|
||||
next_id: fileid3,
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
fn new() -> Self {
|
||||
let mut id_to_path = HashMap::new();
|
||||
let mut path_to_id = HashMap::new();
|
||||
|
||||
// Root is always ID 1 (0 is reserved in NFS)
|
||||
id_to_path.insert(1, "/".to_string());
|
||||
path_to_id.insert("/".to_string(), 1);
|
||||
|
||||
Self {
|
||||
id_to_path,
|
||||
path_to_id,
|
||||
next_id: 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_insert(&mut self, path: &str) -> fileid3 {
|
||||
if let Some(&id) = self.path_to_id.get(path) {
|
||||
return id;
|
||||
}
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.id_to_path.insert(id, path.to_string());
|
||||
self.path_to_id.insert(path.to_string(), id);
|
||||
id
|
||||
}
|
||||
|
||||
fn get_path(&self, id: fileid3) -> Option<&str> {
|
||||
self.id_to_path.get(&id).map(|s| s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
// ── FurumiNfs ──────────────────────────────────────────────────
|
||||
|
||||
pub struct FurumiNfs {
|
||||
client: FurumiClient,
|
||||
mapper: Mutex<IdMapper>,
|
||||
}
|
||||
|
||||
impl FurumiNfs {
|
||||
pub fn new(client: FurumiClient) -> Self {
|
||||
Self {
|
||||
client,
|
||||
mapper: Mutex::new(IdMapper::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the full remote path from a parent directory ID and a child filename.
|
||||
fn resolve_child_path(&self, dirid: fileid3, name: &filename3) -> Result<String, nfsstat3> {
|
||||
let mapper = self.mapper.lock().unwrap();
|
||||
let parent = mapper.get_path(dirid).ok_or(nfsstat3::NFS3ERR_STALE)?;
|
||||
let name_str = String::from_utf8_lossy(&name.0);
|
||||
let path = if parent == "/" {
|
||||
format!("/{}", name_str)
|
||||
} else {
|
||||
format!("{}/{}", parent, name_str)
|
||||
};
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Convert a gRPC AttrResponse into NFS fattr3, assigning/looking up the file ID.
|
||||
fn make_fattr(
|
||||
&self,
|
||||
path: &str,
|
||||
attr: &furumi_common::proto::AttrResponse,
|
||||
) -> fattr3 {
|
||||
let mut mapper = self.mapper.lock().unwrap();
|
||||
let id = mapper.get_or_insert(path);
|
||||
|
||||
let is_dir = attr.mode & (libc::S_IFDIR as u32) != 0;
|
||||
let ftype = if is_dir {
|
||||
ftype3::NF3DIR
|
||||
} else {
|
||||
ftype3::NF3REG
|
||||
};
|
||||
|
||||
let mtime = nfstime3 {
|
||||
seconds: attr.mtime as u32,
|
||||
nseconds: 0,
|
||||
};
|
||||
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let gid = unsafe { libc::getgid() };
|
||||
|
||||
fattr3 {
|
||||
ftype,
|
||||
mode: (attr.mode & 0o777) as mode3,
|
||||
nlink: if is_dir { 2 } else { 1 },
|
||||
uid,
|
||||
gid,
|
||||
size: attr.size,
|
||||
used: attr.size,
|
||||
rdev: specdata3::default(),
|
||||
fsid: 0,
|
||||
fileid: id,
|
||||
atime: mtime,
|
||||
mtime,
|
||||
ctime: mtime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NFSFileSystem for FurumiNfs {
|
||||
fn capabilities(&self) -> VFSCapabilities {
|
||||
VFSCapabilities::ReadOnly
|
||||
}
|
||||
|
||||
fn root_dir(&self) -> fileid3 {
|
||||
1
|
||||
}
|
||||
|
||||
async fn lookup(&self, dirid: fileid3, filename: &filename3) -> Result<fileid3, nfsstat3> {
|
||||
let name_str = String::from_utf8_lossy(&filename.0);
|
||||
debug!("lookup: dirid={} name={}", dirid, name_str);
|
||||
|
||||
let path = self.resolve_child_path(dirid, filename)?;
|
||||
|
||||
let attr = self
|
||||
.client
|
||||
.get_attr(&path)
|
||||
.await
|
||||
.map_err(|_| nfsstat3::NFS3ERR_NOENT)?;
|
||||
|
||||
let mut mapper = self.mapper.lock().unwrap();
|
||||
let id = mapper.get_or_insert(&path);
|
||||
|
||||
// Sanity: verify the attr makes sense (file exists)
|
||||
let _ = attr;
|
||||
debug!("lookup: {} -> id={}", path, id);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn getattr(&self, id: fileid3) -> Result<fattr3, nfsstat3> {
|
||||
debug!("getattr: id={}", id);
|
||||
|
||||
let path = {
|
||||
let mapper = self.mapper.lock().unwrap();
|
||||
mapper
|
||||
.get_path(id)
|
||||
.ok_or(nfsstat3::NFS3ERR_STALE)?
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let attr = self
|
||||
.client
|
||||
.get_attr(&path)
|
||||
.await
|
||||
.map_err(|_| nfsstat3::NFS3ERR_IO)?;
|
||||
|
||||
Ok(self.make_fattr(&path, &attr))
|
||||
}
|
||||
|
||||
async fn setattr(&self, _id: fileid3, _setattr: sattr3) -> Result<fattr3, nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_ROFS)
|
||||
}
|
||||
|
||||
async fn read(
|
||||
&self,
|
||||
id: fileid3,
|
||||
offset: u64,
|
||||
count: u32,
|
||||
) -> Result<(Vec<u8>, bool), nfsstat3> {
|
||||
debug!("read: id={} offset={} count={}", id, offset, count);
|
||||
|
||||
let path = {
|
||||
let mapper = self.mapper.lock().unwrap();
|
||||
mapper
|
||||
.get_path(id)
|
||||
.ok_or(nfsstat3::NFS3ERR_STALE)?
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let mut stream = self
|
||||
.client
|
||||
.read_file(&path, offset, count, count)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("read_file error: {:?}", e);
|
||||
nfsstat3::NFS3ERR_IO
|
||||
})?;
|
||||
|
||||
let mut data = Vec::new();
|
||||
while let Some(chunk_res) = stream.next().await {
|
||||
match chunk_res {
|
||||
Ok(chunk) => data.extend_from_slice(&chunk.data),
|
||||
Err(e) => {
|
||||
error!("read stream error: {:?}", e);
|
||||
return Err(nfsstat3::NFS3ERR_IO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EOF if we got less data than requested
|
||||
let eof = (data.len() as u32) < count;
|
||||
Ok((data, eof))
|
||||
}
|
||||
|
||||
async fn write(&self, _id: fileid3, _offset: u64, _data: &[u8]) -> Result<fattr3, nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_ROFS)
|
||||
}
|
||||
|
||||
async fn create(
|
||||
&self,
|
||||
_dirid: fileid3,
|
||||
_filename: &filename3,
|
||||
_attr: sattr3,
|
||||
) -> Result<(fileid3, fattr3), nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_ROFS)
|
||||
}
|
||||
|
||||
async fn create_exclusive(
|
||||
&self,
|
||||
_dirid: fileid3,
|
||||
_filename: &filename3,
|
||||
) -> Result<fileid3, nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_ROFS)
|
||||
}
|
||||
|
||||
async fn mkdir(
|
||||
&self,
|
||||
_dirid: fileid3,
|
||||
_dirname: &filename3,
|
||||
) -> Result<(fileid3, fattr3), nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_ROFS)
|
||||
}
|
||||
|
||||
async fn remove(&self, _dirid: fileid3, _filename: &filename3) -> Result<(), nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_ROFS)
|
||||
}
|
||||
|
||||
async fn rename(
|
||||
&self,
|
||||
_from_dirid: fileid3,
|
||||
_from_filename: &filename3,
|
||||
_to_dirid: fileid3,
|
||||
_to_filename: &filename3,
|
||||
) -> Result<(), nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_ROFS)
|
||||
}
|
||||
|
||||
async fn readdir(
|
||||
&self,
|
||||
dirid: fileid3,
|
||||
start_after: fileid3,
|
||||
max_entries: usize,
|
||||
) -> Result<ReadDirResult, nfsstat3> {
|
||||
debug!(
|
||||
"readdir: dirid={} start_after={} max={}",
|
||||
dirid, start_after, max_entries
|
||||
);
|
||||
|
||||
let path = {
|
||||
let mapper = self.mapper.lock().unwrap();
|
||||
mapper
|
||||
.get_path(dirid)
|
||||
.ok_or(nfsstat3::NFS3ERR_STALE)?
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let entries = self.client.read_dir(&path).await.map_err(|e| {
|
||||
error!("read_dir error: {:?}", e);
|
||||
nfsstat3::NFS3ERR_IO
|
||||
})?;
|
||||
|
||||
// Build full list with IDs and attributes
|
||||
let mut nfs_entries: Vec<DirEntry> = Vec::new();
|
||||
|
||||
for entry in &entries {
|
||||
let child_path = if path == "/" {
|
||||
format!("/{}", entry.name)
|
||||
} else {
|
||||
format!("{}/{}", path, entry.name)
|
||||
};
|
||||
|
||||
let is_dir = entry.r#type == 4; // DT_DIR
|
||||
|
||||
let child_id = {
|
||||
let mut mapper = self.mapper.lock().unwrap();
|
||||
mapper.get_or_insert(&child_path)
|
||||
};
|
||||
|
||||
// Try to get real attributes, fall back to synthetic ones
|
||||
let attr = match self.client.get_attr(&child_path).await {
|
||||
Ok(a) => self.make_fattr(&child_path, &a),
|
||||
Err(_) => {
|
||||
let mut mapper = self.mapper.lock().unwrap();
|
||||
let id = mapper.get_or_insert(&child_path);
|
||||
let ftype = if is_dir {
|
||||
ftype3::NF3DIR
|
||||
} else {
|
||||
ftype3::NF3REG
|
||||
};
|
||||
fattr3 {
|
||||
ftype,
|
||||
mode: if is_dir { 0o755 } else { 0o644 },
|
||||
nlink: if is_dir { 2 } else { 1 },
|
||||
uid: unsafe { libc::getuid() },
|
||||
gid: unsafe { libc::getgid() },
|
||||
size: 0,
|
||||
used: 0,
|
||||
rdev: specdata3::default(),
|
||||
fsid: 0,
|
||||
fileid: id,
|
||||
atime: nfstime3::default(),
|
||||
mtime: nfstime3::default(),
|
||||
ctime: nfstime3::default(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
nfs_entries.push(DirEntry {
|
||||
fileid: child_id,
|
||||
name: entry.name.as_bytes().into(),
|
||||
attr,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle pagination: skip entries up to and including start_after
|
||||
let filtered: Vec<DirEntry> = if start_after > 0 {
|
||||
let skip_pos = nfs_entries
|
||||
.iter()
|
||||
.position(|e| e.fileid == start_after)
|
||||
.map(|p| p + 1)
|
||||
.unwrap_or(0);
|
||||
nfs_entries.into_iter().skip(skip_pos).collect()
|
||||
} else {
|
||||
nfs_entries
|
||||
};
|
||||
|
||||
let end = filtered.len() <= max_entries;
|
||||
let entries: Vec<DirEntry> = filtered.into_iter().take(max_entries).collect();
|
||||
|
||||
Ok(ReadDirResult { entries, end })
|
||||
}
|
||||
|
||||
async fn symlink(
|
||||
&self,
|
||||
_dirid: fileid3,
|
||||
_linkname: &filename3,
|
||||
_symlink: &nfspath3,
|
||||
_attr: &sattr3,
|
||||
) -> Result<(fileid3, fattr3), nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_ROFS)
|
||||
}
|
||||
|
||||
async fn readlink(&self, _id: fileid3) -> Result<nfspath3, nfsstat3> {
|
||||
Err(nfsstat3::NFS3ERR_NOTSUPP)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_id_mapper_root() {
|
||||
let mapper = IdMapper::new();
|
||||
assert_eq!(mapper.get_path(1).unwrap(), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_mapper_assignment() {
|
||||
let mut mapper = IdMapper::new();
|
||||
|
||||
let id1 = mapper.get_or_insert("/hello.txt");
|
||||
assert_eq!(id1, 2);
|
||||
|
||||
let id2 = mapper.get_or_insert("/dir/world.mkv");
|
||||
assert_eq!(id2, 3);
|
||||
|
||||
assert_eq!(mapper.get_path(2).unwrap(), "/hello.txt");
|
||||
assert_eq!(mapper.get_path(3).unwrap(), "/dir/world.mkv");
|
||||
|
||||
// Same path returns same ID
|
||||
assert_eq!(mapper.get_or_insert("/hello.txt"), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_mapper_non_existent() {
|
||||
let mapper = IdMapper::new();
|
||||
assert!(mapper.get_path(999).is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user