Skip to content

Commit 44db419

Browse files
committed
rage-mount-dir: Implement transparent read-only FUSE overlay
This creates a read-only view over the source directory.
1 parent c028790 commit 44db419

File tree

6 files changed

+632
-1
lines changed

6 files changed

+632
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rage/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ secrecy = "0.8"
5959
# rage-mount dependencies
6060
fuse_mt = { version = "0.5.1", optional = true }
6161
libc = { version = "0.2", optional = true }
62+
nix = { version = "0.20", optional = true }
6263
tar = { version = "0.4", optional = true }
6364
time = { version = "0.1", optional = true }
6465
zip = { version = "0.5.9", optional = true }
@@ -71,7 +72,7 @@ man = "0.3"
7172

7273
[features]
7374
default = ["ssh"]
74-
mount = ["fuse_mt", "libc", "tar", "time", "zip"]
75+
mount = ["fuse_mt", "libc", "nix", "tar", "time", "zip"]
7576
ssh = ["age/ssh"]
7677
unstable = ["age/unstable"]
7778

@@ -87,3 +88,8 @@ bench = false
8788
name = "rage-mount"
8889
required-features = ["mount"]
8990
bench = false
91+
92+
[[bin]]
93+
name = "rage-mount-dir"
94+
required-features = ["mount"]
95+
bench = false

rage/i18n/en-US/rage.ftl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,12 @@ rec-dec-recipient-flag = Did you mean to use {-flag-identity} to specify a priva
142142
info-decrypting = Decrypting {$filename}
143143
info-mounting-as-fuse = Mounting as FUSE filesystem
144144
145+
err-mnt-missing-source = Missing source.
145146
err-mnt-missing-filename = Missing filename.
146147
err-mnt-missing-mountpoint = Missing mountpoint.
147148
err-mnt-missing-types = Missing {-flag-mnt-types}.
149+
err-mnt-must-be-dir = Mountpoint must be a directory.
150+
err-mnt-source-must-be-dir = Source must be a directory.
148151
err-mnt-unknown-type = Unknown filesystem type "{$fs_type}"
149152
150153
## Unstable features

rage/src/bin/rage-mount-dir/main.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
use fuse_mt::FilesystemMT;
2+
use gumdrop::Options;
3+
use i18n_embed::{
4+
fluent::{fluent_language_loader, FluentLanguageLoader},
5+
DesktopLanguageRequester,
6+
};
7+
use lazy_static::lazy_static;
8+
use log::{error, info};
9+
use rust_embed::RustEmbed;
10+
use std::ffi::OsStr;
11+
use std::fmt;
12+
use std::io;
13+
use std::path::PathBuf;
14+
15+
mod overlay;
16+
mod util;
17+
18+
#[derive(RustEmbed)]
19+
#[folder = "i18n"]
20+
struct Translations;
21+
22+
const TRANSLATIONS: Translations = Translations {};
23+
24+
lazy_static! {
25+
static ref LANGUAGE_LOADER: FluentLanguageLoader = fluent_language_loader!();
26+
}
27+
28+
macro_rules! fl {
29+
($message_id:literal) => {{
30+
i18n_embed_fl::fl!(LANGUAGE_LOADER, $message_id)
31+
}};
32+
}
33+
34+
macro_rules! wfl {
35+
($f:ident, $message_id:literal) => {
36+
write!($f, "{}", fl!($message_id))
37+
};
38+
}
39+
40+
enum Error {
41+
Age(age::DecryptError),
42+
Io(io::Error),
43+
MissingMountpoint,
44+
MissingSource,
45+
MountpointMustBeDir,
46+
Nix(nix::Error),
47+
SourceMustBeDir,
48+
}
49+
50+
impl From<age::DecryptError> for Error {
51+
fn from(e: age::DecryptError) -> Self {
52+
Error::Age(e)
53+
}
54+
}
55+
56+
impl From<io::Error> for Error {
57+
fn from(e: io::Error) -> Self {
58+
Error::Io(e)
59+
}
60+
}
61+
62+
impl From<nix::Error> for Error {
63+
fn from(e: nix::Error) -> Self {
64+
Error::Nix(e)
65+
}
66+
}
67+
68+
// Rust only supports `fn main() -> Result<(), E: Debug>`, so we implement `Debug`
69+
// manually to provide the error output we want.
70+
impl fmt::Debug for Error {
71+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72+
match self {
73+
Error::Age(e) => match e {
74+
age::DecryptError::ExcessiveWork { required, .. } => {
75+
writeln!(f, "{}", e)?;
76+
write!(
77+
f,
78+
"{}",
79+
i18n_embed_fl::fl!(
80+
LANGUAGE_LOADER,
81+
"rec-dec-excessive-work",
82+
wf = required
83+
)
84+
)
85+
}
86+
_ => write!(f, "{}", e),
87+
},
88+
Error::Io(e) => write!(f, "{}", e),
89+
Error::MissingMountpoint => wfl!(f, "err-mnt-missing-mountpoint"),
90+
Error::MissingSource => wfl!(f, "err-mnt-missing-source"),
91+
Error::MountpointMustBeDir => wfl!(f, "err-mnt-must-be-dir"),
92+
Error::Nix(e) => write!(f, "{}", e),
93+
Error::SourceMustBeDir => wfl!(f, "err-mnt-source-must-be-dir"),
94+
}?;
95+
writeln!(f)?;
96+
writeln!(f, "[ {} ]", fl!("err-ux-A"))?;
97+
write!(
98+
f,
99+
"[ {}: https://str4d.xyz/rage/report {} ]",
100+
fl!("err-ux-B"),
101+
fl!("err-ux-C")
102+
)
103+
}
104+
}
105+
106+
#[derive(Debug, Options)]
107+
struct AgeMountOptions {
108+
#[options(free, help = "The directory to mount.")]
109+
directory: String,
110+
111+
#[options(free, help = "The path to mount at.")]
112+
mountpoint: String,
113+
114+
#[options(help = "Print this help message and exit.")]
115+
help: bool,
116+
117+
#[options(help = "Print version info and exit.", short = "V")]
118+
version: bool,
119+
}
120+
121+
fn mount_fs<T: FilesystemMT + Send + Sync + 'static, F>(open: F, mountpoint: PathBuf)
122+
where
123+
F: FnOnce() -> io::Result<T>,
124+
{
125+
let fuse_args: Vec<&OsStr> = vec![&OsStr::new("-o"), &OsStr::new("ro,auto_unmount")];
126+
127+
match open().map(|fs| fuse_mt::FuseMT::new(fs, 1)) {
128+
Ok(fs) => {
129+
info!("{}", fl!("info-mounting-as-fuse"));
130+
if let Err(e) = fuse_mt::mount(fs, &mountpoint, &fuse_args) {
131+
error!("{}", e);
132+
}
133+
}
134+
Err(e) => {
135+
error!("{}", e);
136+
}
137+
}
138+
}
139+
140+
fn main() -> Result<(), Error> {
141+
use std::env::args;
142+
143+
env_logger::builder()
144+
.format_timestamp(None)
145+
.filter_level(log::LevelFilter::Off)
146+
.parse_default_env()
147+
.init();
148+
149+
let requested_languages = DesktopLanguageRequester::requested_languages();
150+
i18n_embed::select(&*LANGUAGE_LOADER, &TRANSLATIONS, &requested_languages).unwrap();
151+
age::localizer().select(&requested_languages).unwrap();
152+
153+
let args = args().collect::<Vec<_>>();
154+
155+
if console::user_attended() && args.len() == 1 {
156+
// If gumdrop ever merges that PR, that can be used here
157+
// instead.
158+
println!("{} {} [OPTIONS]", fl!("usage-header"), args[0]);
159+
println!();
160+
println!("{}", AgeMountOptions::usage());
161+
162+
return Ok(());
163+
}
164+
165+
let opts = AgeMountOptions::parse_args_default_or_exit();
166+
167+
if opts.version {
168+
println!("rage-mount-dir {}", env!("CARGO_PKG_VERSION"));
169+
return Ok(());
170+
}
171+
172+
if opts.directory.is_empty() {
173+
return Err(Error::MissingSource);
174+
}
175+
if opts.mountpoint.is_empty() {
176+
return Err(Error::MissingMountpoint);
177+
}
178+
179+
let directory = PathBuf::from(opts.directory);
180+
if !directory.is_dir() {
181+
return Err(Error::SourceMustBeDir);
182+
}
183+
let mountpoint = PathBuf::from(opts.mountpoint);
184+
if !mountpoint.is_dir() {
185+
return Err(Error::MountpointMustBeDir);
186+
}
187+
188+
mount_fs(
189+
|| crate::overlay::AgeOverlayFs::new(directory.into()),
190+
mountpoint,
191+
);
192+
Ok(())
193+
}

0 commit comments

Comments
 (0)