Skip to content

Commit f35923d

Browse files
committed
feat: derive per-mount export/volume name from the mounted target
Multiple concurrent mq-mount instances all exposed the same fixed "mq-mount" NFS export name / WinFSP filesystem-service name, making them indistinguishable in `mount`/`df` output or the Windows volume list. Derive a sanitized name like `mq-notes` from the mounted file/directory instead, falling back to `mq-mount` when nothing ASCII-safe can be derived.
1 parent 0c28f81 commit f35923d

3 files changed

Lines changed: 69 additions & 14 deletions

File tree

‎src/backend/nfs.rs‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,25 +213,25 @@ fn nfs_mount_opts(nolock_flag: &str, port: u16, readonly: bool) -> String {
213213
}
214214

215215
#[cfg(target_os = "macos")]
216-
fn mount_nfs(mountpoint: &Path, port: u16, readonly: bool) -> miette::Result<()> {
216+
fn mount_nfs(mountpoint: &Path, port: u16, readonly: bool, name: &str) -> miette::Result<()> {
217217
let opts = nfs_mount_opts("nolocks", port, readonly);
218218
run_mount_command(Command::new("mount_nfs").args([
219219
"-o",
220220
&opts,
221-
"localhost:/mq-mount",
221+
&format!("localhost:/{name}"),
222222
&mountpoint.to_string_lossy(),
223223
]))
224224
}
225225

226226
#[cfg(target_os = "linux")]
227-
fn mount_nfs(mountpoint: &Path, port: u16, readonly: bool) -> miette::Result<()> {
227+
fn mount_nfs(mountpoint: &Path, port: u16, readonly: bool, name: &str) -> miette::Result<()> {
228228
let opts = nfs_mount_opts("nolock", port, readonly);
229229
run_mount_command(Command::new("mount").args([
230230
"-t",
231231
"nfs",
232232
"-o",
233233
&opts,
234-
"localhost:/mq-mount",
234+
&format!("localhost:/{name}"),
235235
&mountpoint.to_string_lossy(),
236236
]))
237237
}
@@ -259,25 +259,31 @@ fn unmount(mountpoint: &Path) -> miette::Result<()> {
259259
run_mount_command(Command::new("umount").arg(mountpoint))
260260
}
261261

262-
pub fn run(filesystem: MqFs, mountpoint: &Path, file_count: usize, readonly: bool) -> miette::Result<()> {
262+
pub fn run(filesystem: MqFs, mountpoint: &Path, file_count: usize, readonly: bool, name: &str) -> miette::Result<()> {
263263
tokio::runtime::Builder::new_multi_thread()
264264
.enable_all()
265265
.build()
266266
.map_err(|e| miette::miette!("failed to start async runtime: {e}"))?
267-
.block_on(run_mounted(filesystem, mountpoint, file_count, readonly))
267+
.block_on(run_mounted(filesystem, mountpoint, file_count, readonly, name))
268268
}
269269

270-
async fn run_mounted(filesystem: MqFs, mountpoint: &Path, file_count: usize, readonly: bool) -> miette::Result<()> {
270+
async fn run_mounted(
271+
filesystem: MqFs,
272+
mountpoint: &Path,
273+
file_count: usize,
274+
readonly: bool,
275+
name: &str,
276+
) -> miette::Result<()> {
271277
let mut listener = NFSTcpListener::bind("127.0.0.1:0", NfsAdapter(filesystem))
272278
.await
273279
.map_err(|e| miette::miette!("failed to start NFS server: {e}"))?;
274-
listener.with_export_name("mq-mount");
280+
listener.with_export_name(name);
275281
let port = listener.get_listen_port();
276282
let server = tokio::spawn(async move {
277283
let _ = listener.handle_forever().await;
278284
});
279285

280-
mount_nfs(mountpoint, port, readonly)?;
286+
mount_nfs(mountpoint, port, readonly, name)?;
281287
tracing::info!("mounted {file_count} file(s) at {}", mountpoint.display());
282288

283289
tokio::signal::ctrl_c().await.ok();

‎src/backend/winfsp.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,12 @@ impl<T: MountFs> FileSystemContext for WinFspAdapter<T> {
306306
}
307307
}
308308

309-
pub fn run(filesystem: MqFs, mountpoint: &Path, file_count: usize, readonly: bool) -> miette::Result<()> {
309+
pub fn run(filesystem: MqFs, mountpoint: &Path, file_count: usize, readonly: bool, name: &str) -> miette::Result<()> {
310310
let init = winfsp::winfsp_init().map_err(|e| miette::miette!("failed to initialize WinFSP: {e:?}"))?;
311311

312312
let filesystem = std::sync::Mutex::new(Some(filesystem));
313313
let mountpoint_owned = mountpoint.to_path_buf();
314+
let filesystem_name = name.to_string();
314315

315316
let mut service = FileSystemServiceBuilder::new()
316317
.with_start(move || {
@@ -329,7 +330,7 @@ pub fn run(filesystem: MqFs, mountpoint: &Path, file_count: usize, readonly: boo
329330
.unicode_on_disk(true)
330331
.persistent_acls(false)
331332
.read_only_volume(readonly)
332-
.filesystem_name("mq-mount")
333+
.filesystem_name(&filesystem_name)
333334
.volume_creation_time(filetime(SystemTime::now()));
334335
let params = FileSystemParams::default_params(volume_params);
335336

@@ -344,7 +345,7 @@ pub fn run(filesystem: MqFs, mountpoint: &Path, file_count: usize, readonly: boo
344345
}
345346
Ok(())
346347
})
347-
.build("mq-mount", init)
348+
.build(name, init)
348349
.map_err(|e| miette::miette!("failed to build WinFSP service: {e:?}"))?;
349350

350351
service

‎src/main.rs‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,61 @@ mod app {
6565
})
6666
.collect::<miette::Result<Vec<_>>>()?;
6767
let file_count = entries.len();
68+
let name = mount_name(&entries);
6869
let filesystem = MqFs::new(entries, cli.readonly, cli.allow_other)
6970
.map_err(|e| miette::miette!("failed to read source file(s): {e}"))?;
7071

7172
#[cfg(unix)]
72-
return crate::backend::nfs::run(filesystem, mountpoint, file_count, cli.readonly);
73+
return crate::backend::nfs::run(filesystem, mountpoint, file_count, cli.readonly, &name);
7374
#[cfg(windows)]
74-
return crate::backend::winfsp::run(filesystem, mountpoint, file_count, cli.readonly);
75+
return crate::backend::winfsp::run(filesystem, mountpoint, file_count, cli.readonly, &name);
76+
}
77+
78+
/// Derives an export/volume name identifying this mount instance (e.g.
79+
/// `mq-notes`), so multiple concurrently mounted instances are
80+
/// distinguishable in `mount`/`df` output or the Windows Explorer volume
81+
/// list instead of all showing up as the same fixed name.
82+
fn mount_name(entries: &[(PathBuf, Vec<String>)]) -> String {
83+
let mut top_level: Vec<&str> = Vec::new();
84+
for (_, mount_path) in entries {
85+
if let Some(first) = mount_path.first().map(String::as_str)
86+
&& !top_level.contains(&first)
87+
{
88+
top_level.push(first);
89+
}
90+
}
91+
let label = match top_level.as_slice() {
92+
[] => String::new(),
93+
[only] => sanitize(only),
94+
[first, rest @ ..] => format!("{}+{}", sanitize(first), rest.len()),
95+
};
96+
let label = if label.is_empty() { "mount".to_string() } else { label };
97+
format!("mq-{}", truncate(&label, 40))
98+
}
99+
100+
/// Keeps only ASCII alphanumerics, collapsing every other run of
101+
/// characters (spaces, punctuation, non-ASCII) into a single `-`, so the
102+
/// result is safe to use as an NFS export name or WinFSP filesystem/service name.
103+
fn sanitize(name: &str) -> String {
104+
let mut out = String::new();
105+
let mut last_dash = false;
106+
for c in name.chars() {
107+
if c.is_ascii_alphanumeric() {
108+
out.push(c);
109+
last_dash = false;
110+
} else if !last_dash && !out.is_empty() {
111+
out.push('-');
112+
last_dash = true;
113+
}
114+
}
115+
out.trim_end_matches('-').to_string()
116+
}
117+
118+
fn truncate(s: &str, max_chars: usize) -> &str {
119+
match s.char_indices().nth(max_chars) {
120+
Some((idx, _)) => &s[..idx],
121+
None => s,
122+
}
75123
}
76124

77125
fn collect_entries(sources: &[PathBuf]) -> miette::Result<Vec<(PathBuf, Vec<String>)>> {

0 commit comments

Comments
 (0)