Phase 6a: read-only FUSE mount over the blob store
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 3s
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Failing after 3s
First slice of Phase 6. Ships a minimal, feature-gated `claw-fuse` binary that mounts the local blob store read-only as a POSIX filesystem: <mount>/blobs/<blob-id-hex> ← file, content = assembled blob <mount>/blobs/ ← dir, ls shows all blob-ids <mount>/ ← dir, contains `blobs` Lets an operator `tar -tvzf`, `md5sum`, or grep at a cached tarball without wiring a client. Debug + audit tool for now; warm-tier git-worktrees + write path come in later slices. Feature-gated so my macOS dev box doesn't need macFUSE headers to build the rest of the tree: * Cargo.toml declares `[[bin]] name = "claw-fuse"` with `required-features = ["fuse"]`. * Feature `fuse` pulls in `fuser = "0.15"`, target-restricted to `cfg(target_os = "linux")` — dep resolution never considers fuser on other platforms. * `cargo build` (default) leaves claw-fuse out entirely. `cargo build --features fuse --bin claw-fuse` on Linux builds it. Design notes baked into the impl: * Inode allocation is lazy — first `lookup` for a hex assigns an inode. Avoids pre-indexing the full blob store at mount time which would be O(blobs) fs walk before FUSE is even ready. * getattr / read validate that the manifest exists on every call — no stale-inode reads if a blob is GC'd out from under us mid-mount. Extra read cost is negligible against the per-request FUSE overhead. * size = manifest.total_size (bytes reported without touching chunk files) so `ls -l` is cheap. * runtime = current-thread tokio, block_on per callback. fuser is sync; a full tokio worker pool would just add scheduling overhead when callbacks are already serialized by the kernel. No new tests here — Filesystem impls are integration-heavy and the underlying BlobStore methods are already covered. The `fuse` feature build itself will be smoke-tested on tank. 381 tests pass unchanged (feature-gated bin doesn't affect the existing test surface).
This commit is contained in:
Generated
+59
@@ -379,6 +379,7 @@ dependencies = [
|
|||||||
"chitchat",
|
"chitchat",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"fuser",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"libc",
|
"libc",
|
||||||
"quinn",
|
"quinn",
|
||||||
@@ -555,6 +556,22 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fuser"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"nix",
|
||||||
|
"page_size",
|
||||||
|
"pkg-config",
|
||||||
|
"smallvec",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -1067,6 +1084,18 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@@ -1150,6 +1179,16 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "page_size"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -2653,6 +2692,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|||||||
@@ -14,6 +14,27 @@ path = "src/main.rs"
|
|||||||
name = "claw-cargo"
|
name = "claw-cargo"
|
||||||
path = "src/claw_cargo.rs"
|
path = "src/claw_cargo.rs"
|
||||||
|
|
||||||
|
# Phase 6 (2026-07-14): read-only FUSE mount that exposes the blob
|
||||||
|
# store as a filesystem. Gated behind the `fuse` cargo feature +
|
||||||
|
# Linux-only in dep resolution so my macOS dev box doesn't need
|
||||||
|
# macFUSE headers installed to build the other bins.
|
||||||
|
#
|
||||||
|
# Build with: cargo build --features fuse --bin claw-fuse
|
||||||
|
[[bin]]
|
||||||
|
name = "claw-fuse"
|
||||||
|
path = "src/claw_fuse.rs"
|
||||||
|
required-features = ["fuse"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
# Phase 6: enables the claw-fuse binary + pulls in the fuser dep.
|
||||||
|
fuse = ["dep:fuser"]
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
# v0.15 — Rust FUSE bindings. Only pulled on Linux + only when
|
||||||
|
# the `fuse` feature is on; keeps macOS + non-fuse builds clean.
|
||||||
|
fuser = { version = "0.15", optional = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4", features = ["derive", "env"] }
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
//! Phase 6 (2026-07-14): read-only FUSE mount over the clawstor
|
||||||
|
//! blob store.
|
||||||
|
//!
|
||||||
|
//! Layout at the mount point:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! <mount>/blobs/<blob-id-hex> # regular file, content = assembled blob bytes
|
||||||
|
//! <mount>/blobs/ # dir, ls shows every blob-id in the store
|
||||||
|
//! <mount>/ # dir, contains a single `blobs` entry
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Read-only. No writes, no metadata mutation, no permissions changes.
|
||||||
|
//! Small, obviously safe first slice — later slices will layer warm-tier
|
||||||
|
//! git worktrees + smart-clean modes on top.
|
||||||
|
//!
|
||||||
|
//! Build gate: this file is compiled only when `--features fuse` is set
|
||||||
|
//! (see Cargo.toml). Linux-only dep on the fuser crate.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use fuser::{FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{Duration, UNIX_EPOCH};
|
||||||
|
|
||||||
|
// Same module tree as main.rs / claw_cargo.rs — bin-only crate,
|
||||||
|
// so we redeclare the mods here. Only `cluster::blob` is actually
|
||||||
|
// used from this bin.
|
||||||
|
mod actions;
|
||||||
|
mod cargo_init;
|
||||||
|
mod cluster;
|
||||||
|
mod config;
|
||||||
|
mod daemon;
|
||||||
|
mod head_watch;
|
||||||
|
mod hot;
|
||||||
|
mod manifest;
|
||||||
|
mod restore;
|
||||||
|
mod serve;
|
||||||
|
|
||||||
|
use cluster::blob::{BlobId, BlobStore};
|
||||||
|
|
||||||
|
const TTL: Duration = Duration::from_secs(1);
|
||||||
|
// Inode number layout:
|
||||||
|
// * 1 = root ("/")
|
||||||
|
// * 2 = "/blobs" directory
|
||||||
|
// * 100_000.. = individual blob files. We assign these lazily on
|
||||||
|
// the first lookup so we don't have to pre-index
|
||||||
|
// the whole store at mount time.
|
||||||
|
const ROOT_INO: u64 = 1;
|
||||||
|
const BLOBS_DIR_INO: u64 = 2;
|
||||||
|
const FIRST_BLOB_INO: u64 = 100_000;
|
||||||
|
|
||||||
|
/// Read-only FUSE mount over a BlobStore.
|
||||||
|
struct ClawFuse {
|
||||||
|
store: BlobStore,
|
||||||
|
/// Runtime for async BlobStore calls. fuser is sync so we
|
||||||
|
/// block_on inside each callback.
|
||||||
|
runtime: tokio::runtime::Runtime,
|
||||||
|
/// blob-hex → allocated inode. Populated on lookup.
|
||||||
|
hex_to_ino: HashMap<String, u64>,
|
||||||
|
/// inode → blob-hex. Reverse lookup for getattr / read.
|
||||||
|
ino_to_hex: HashMap<u64, String>,
|
||||||
|
next_ino: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClawFuse {
|
||||||
|
fn new(store: BlobStore) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
store,
|
||||||
|
runtime: tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?,
|
||||||
|
hex_to_ino: HashMap::new(),
|
||||||
|
ino_to_hex: HashMap::new(),
|
||||||
|
next_ino: FIRST_BLOB_INO,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir_attr(ino: u64) -> FileAttr {
|
||||||
|
FileAttr {
|
||||||
|
ino,
|
||||||
|
size: 0,
|
||||||
|
blocks: 0,
|
||||||
|
atime: UNIX_EPOCH,
|
||||||
|
mtime: UNIX_EPOCH,
|
||||||
|
ctime: UNIX_EPOCH,
|
||||||
|
crtime: UNIX_EPOCH,
|
||||||
|
kind: FileType::Directory,
|
||||||
|
perm: 0o555,
|
||||||
|
nlink: 2,
|
||||||
|
uid: unsafe { libc::getuid() },
|
||||||
|
gid: unsafe { libc::getgid() },
|
||||||
|
rdev: 0,
|
||||||
|
flags: 0,
|
||||||
|
blksize: 4096,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_attr(ino: u64, size: u64) -> FileAttr {
|
||||||
|
FileAttr {
|
||||||
|
ino,
|
||||||
|
size,
|
||||||
|
blocks: size.div_ceil(512),
|
||||||
|
atime: UNIX_EPOCH,
|
||||||
|
mtime: UNIX_EPOCH,
|
||||||
|
ctime: UNIX_EPOCH,
|
||||||
|
crtime: UNIX_EPOCH,
|
||||||
|
kind: FileType::RegularFile,
|
||||||
|
perm: 0o444,
|
||||||
|
nlink: 1,
|
||||||
|
uid: unsafe { libc::getuid() },
|
||||||
|
gid: unsafe { libc::getgid() },
|
||||||
|
rdev: 0,
|
||||||
|
flags: 0,
|
||||||
|
blksize: 4096,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a blob-hex string to an inode, allocating one if
|
||||||
|
/// this is the first lookup. Returns None if the hex doesn't
|
||||||
|
/// name a real blob on disk.
|
||||||
|
fn resolve_hex(&mut self, hex: &str) -> Option<(u64, u64)> {
|
||||||
|
// Validate: exactly 64 hex chars, decodable.
|
||||||
|
let blob_id = BlobId::from_hex(hex).ok()?;
|
||||||
|
// Confirm the blob's manifest exists — else we'd claim a
|
||||||
|
// file that no read can satisfy.
|
||||||
|
let manifest = self
|
||||||
|
.runtime
|
||||||
|
.block_on(self.store.load_manifest(&blob_id))
|
||||||
|
.ok()
|
||||||
|
.flatten()?;
|
||||||
|
let ino = *self.hex_to_ino.entry(hex.to_string()).or_insert_with(|| {
|
||||||
|
let n = self.next_ino;
|
||||||
|
self.next_ino += 1;
|
||||||
|
n
|
||||||
|
});
|
||||||
|
self.ino_to_hex.entry(ino).or_insert_with(|| hex.to_string());
|
||||||
|
Some((ino, manifest.total_size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filesystem for ClawFuse {
|
||||||
|
fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
|
||||||
|
let name = match name.to_str() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match parent {
|
||||||
|
ROOT_INO if name == "blobs" => {
|
||||||
|
reply.entry(&TTL, &Self::dir_attr(BLOBS_DIR_INO), 0);
|
||||||
|
}
|
||||||
|
BLOBS_DIR_INO => match self.resolve_hex(name) {
|
||||||
|
Some((ino, size)) => reply.entry(&TTL, &Self::file_attr(ino, size), 0),
|
||||||
|
None => reply.error(libc::ENOENT),
|
||||||
|
},
|
||||||
|
_ => reply.error(libc::ENOENT),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
|
||||||
|
match ino {
|
||||||
|
ROOT_INO | BLOBS_DIR_INO => reply.attr(&TTL, &Self::dir_attr(ino)),
|
||||||
|
_ => {
|
||||||
|
let hex = match self.ino_to_hex.get(&ino).cloned() {
|
||||||
|
Some(h) => h,
|
||||||
|
None => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let blob_id = match BlobId::from_hex(&hex) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => {
|
||||||
|
reply.error(libc::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let size = self
|
||||||
|
.runtime
|
||||||
|
.block_on(self.store.load_manifest(&blob_id))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|m| m.total_size)
|
||||||
|
.unwrap_or(0);
|
||||||
|
reply.attr(&TTL, &Self::file_attr(ino, size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readdir(
|
||||||
|
&mut self,
|
||||||
|
_req: &Request,
|
||||||
|
ino: u64,
|
||||||
|
_fh: u64,
|
||||||
|
offset: i64,
|
||||||
|
mut reply: ReplyDirectory,
|
||||||
|
) {
|
||||||
|
let mut entries: Vec<(u64, FileType, String)> = Vec::new();
|
||||||
|
match ino {
|
||||||
|
ROOT_INO => {
|
||||||
|
entries.push((ROOT_INO, FileType::Directory, ".".into()));
|
||||||
|
entries.push((ROOT_INO, FileType::Directory, "..".into()));
|
||||||
|
entries.push((BLOBS_DIR_INO, FileType::Directory, "blobs".into()));
|
||||||
|
}
|
||||||
|
BLOBS_DIR_INO => {
|
||||||
|
entries.push((BLOBS_DIR_INO, FileType::Directory, ".".into()));
|
||||||
|
entries.push((ROOT_INO, FileType::Directory, "..".into()));
|
||||||
|
let ids = match self.runtime.block_on(self.store.list_blob_ids()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => {
|
||||||
|
reply.error(libc::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for id in ids {
|
||||||
|
let hex = id.to_hex();
|
||||||
|
let ino = *self
|
||||||
|
.hex_to_ino
|
||||||
|
.entry(hex.clone())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
let n = self.next_ino;
|
||||||
|
self.next_ino += 1;
|
||||||
|
n
|
||||||
|
});
|
||||||
|
self.ino_to_hex
|
||||||
|
.entry(ino)
|
||||||
|
.or_insert_with(|| hex.clone());
|
||||||
|
entries.push((ino, FileType::RegularFile, hex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
reply.error(libc::ENOTDIR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i, (ino, kind, name)) in entries.into_iter().enumerate().skip(offset as usize) {
|
||||||
|
if reply.add(ino, (i + 1) as i64, kind, name) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(
|
||||||
|
&mut self,
|
||||||
|
_req: &Request,
|
||||||
|
ino: u64,
|
||||||
|
_fh: u64,
|
||||||
|
offset: i64,
|
||||||
|
size: u32,
|
||||||
|
_flags: i32,
|
||||||
|
_lock: Option<u64>,
|
||||||
|
reply: ReplyData,
|
||||||
|
) {
|
||||||
|
let hex = match self.ino_to_hex.get(&ino).cloned() {
|
||||||
|
Some(h) => h,
|
||||||
|
None => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let blob_id = match BlobId::from_hex(&hex) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => {
|
||||||
|
reply.error(libc::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let bytes = match self.runtime.block_on(self.store.get_bytes(&blob_id)) {
|
||||||
|
Ok(Some(b)) => b,
|
||||||
|
_ => {
|
||||||
|
reply.error(libc::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let start = (offset as usize).min(bytes.len());
|
||||||
|
let end = (start + size as usize).min(bytes.len());
|
||||||
|
reply.data(&bytes[start..end]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(name = "claw-fuse", about = "Read-only FUSE mount over clawstor blob store")]
|
||||||
|
struct Cli {
|
||||||
|
/// Path to `cluster.blob_store_root` (same value the daemon
|
||||||
|
/// uses in config.toml).
|
||||||
|
#[arg(long)]
|
||||||
|
data_dir: PathBuf,
|
||||||
|
/// Mount point (existing empty directory).
|
||||||
|
#[arg(long)]
|
||||||
|
mount: PathBuf,
|
||||||
|
/// Allow other users to access the mount. Default: current
|
||||||
|
/// user only.
|
||||||
|
#[arg(long)]
|
||||||
|
allow_other: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()))
|
||||||
|
.init();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
if !cli.mount.is_dir() {
|
||||||
|
anyhow::bail!("mount point {} does not exist or is not a directory", cli.mount.display());
|
||||||
|
}
|
||||||
|
let store = BlobStore::open(cli.data_dir.clone())
|
||||||
|
.with_context(|| format!("opening blob store at {}", cli.data_dir.display()))?;
|
||||||
|
let fs = ClawFuse::new(store)?;
|
||||||
|
|
||||||
|
let mut opts = vec![
|
||||||
|
MountOption::RO,
|
||||||
|
MountOption::FSName("clawstor".into()),
|
||||||
|
MountOption::Subtype("clawstor".into()),
|
||||||
|
MountOption::NoAtime,
|
||||||
|
];
|
||||||
|
if cli.allow_other {
|
||||||
|
opts.push(MountOption::AllowOther);
|
||||||
|
}
|
||||||
|
tracing::info!(mount = %cli.mount.display(), "mounting claw-fuse (read-only)");
|
||||||
|
// Blocks until SIGINT/umount.
|
||||||
|
fuser::mount2(fs, &cli.mount, &opts)
|
||||||
|
.with_context(|| format!("mounting FUSE at {}", cli.mount.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user