mirror of
https://github.com/mgree/ffs.git
synced 2024-07-14 23:00:24 +03:00
--new
flag for creating files from empty (#38)
Running `--new FILE.EXT` will:
- infer the output format form `EXT`
- use `FILE.EXT` as the output
- start with a single, empty, named directory (but allocate a bit more space)
In implementing this, I realized that introducing metadata (fad45bed4b
) meant we no longer inferred types automatically. I added a type `Typ::Auto` and some inference code.
This commit is contained in:
parent
7249ae95df
commit
a1d5718ac9
@ -8,6 +8,7 @@
|
||||
* `--pretty` flag for JSON and TOML.
|
||||
* Wrote INSTALL.md.
|
||||
* Improved manpage.
|
||||
* `--new` flag for starting from an empty filesystem.
|
||||
|
||||
## 0.1.0 - 2021-06-26
|
||||
|
||||
|
@ -8,6 +8,7 @@ ffs - the file filesystem
|
||||
# SYNOPSIS
|
||||
|
||||
| ffs \[*FLAGS*\] \[*OPTIONS*\] \[*INPUT*\]
|
||||
| ffs \[*FLAGS*\] \[*OPTIONS*\] --new \[*OUTPUT*\]
|
||||
| ffs *--completions* *SHELL*
|
||||
| ffs \[*-h*\|*--help*\]
|
||||
| ffs \[*-V*\|*--version*\]
|
||||
@ -103,6 +104,14 @@ installed on your system to use *ffs*.
|
||||
|
||||
: Sets the output file for saving changes (defaults to stdout)
|
||||
|
||||
--new *NEW*
|
||||
|
||||
: Mounts an empty filesystem, inferring a mountpoint and output format. Running --new *FILE*.*EXT* is morally equivalent to running:
|
||||
```
|
||||
echo '{}' | ffs --source json -o *FILE*.*EXT* --target *EXT* -m *FILE*
|
||||
```
|
||||
where the mountpoint *FILE* will be created (and removed) by ffs.
|
||||
|
||||
--completions *SHELL*
|
||||
|
||||
: Generate shell completions (and exits) [possible values: bash, fish,
|
||||
@ -138,13 +147,20 @@ formats (currently, JSON, TOML, and YAML); *ffs* maps values in these
|
||||
formats to filesystems. Here are the different types and how they're
|
||||
mapped to a filesystem:
|
||||
|
||||
auto
|
||||
|
||||
: Automatically detected. The following order is used for UTF-8
|
||||
encodable data: null, boolean, integer, float, datetime, string. If
|
||||
data can't be encoded in UTF-8, it will always be bytes.
|
||||
|
||||
boolean
|
||||
|
||||
: Mapped to a **file**. Either *true* or *false*.
|
||||
|
||||
bytes
|
||||
|
||||
: Mapped to a **file**. When serializing back to format, it will be encoded in base64.
|
||||
: Mapped to a **file**. When saving, bytes are typically encoded in
|
||||
base64.
|
||||
|
||||
datetime
|
||||
|
||||
@ -165,9 +181,9 @@ list
|
||||
named elements, starting from 0. Filenames will be padded with zeros
|
||||
to ensure proper sorting; use *--unpadded* to disable padding. While
|
||||
mounted, you are free to use whatever filenames you like in a list
|
||||
directory. When list directories are serialized back to a format,
|
||||
filenames are ignored and the sorted order of the files (in the
|
||||
current locale) will be used to determine the list order.
|
||||
directory. When list directories are saved, filenames are ignored
|
||||
and the sorted order of the files (in the current locale) will be
|
||||
used to determine the list order.
|
||||
|
||||
named
|
||||
|
||||
@ -229,6 +245,15 @@ umount commits
|
||||
# changes are written back to commits.json (-i is in-place mode)
|
||||
```
|
||||
|
||||
If you want to create a new file wholesale, the --new flag is helpful.
|
||||
|
||||
```shell
|
||||
ffs --new file.json
|
||||
# do edits in file directory
|
||||
umount file
|
||||
# corresponding json is in file.json
|
||||
```
|
||||
|
||||
To mount a JSON file and write back out a YAML file, you could run:
|
||||
|
||||
```shell
|
||||
|
48
man/ffs.1
48
man/ffs.1
@ -11,6 +11,10 @@ ffs [\f[I]FLAGS\f[R]] [\f[I]OPTIONS\f[R]] [\f[I]INPUT\f[R]]
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
ffs [\f[I]FLAGS\f[R]] [\f[I]OPTIONS\f[R]] --new [\f[I]OUTPUT\f[R]]
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
ffs \f[I]--completions\f[R] \f[I]SHELL\f[R]
|
||||
.PD 0
|
||||
.P
|
||||
@ -92,8 +96,25 @@ specified when running on stdin
|
||||
-o, --output \f[I]OUTPUT\f[R]
|
||||
Sets the output file for saving changes (defaults to stdout)
|
||||
.TP
|
||||
--new \f[I]NEW\f[R]
|
||||
Mounts an empty filesystem, inferring a mountpoint and output format.
|
||||
Running --new \f[I]FILE\f[R].\f[I]EXT\f[R] is morally equivalent to
|
||||
running:
|
||||
.RS
|
||||
.IP
|
||||
.nf
|
||||
\f[C]
|
||||
echo \[aq]{}\[aq] | ffs --source json -o *FILE*.*EXT* --target *EXT* -m *FILE*
|
||||
\f[R]
|
||||
.fi
|
||||
.PP
|
||||
where the mountpoint \f[I]FILE\f[R] will be created (and removed) by
|
||||
ffs.
|
||||
.RE
|
||||
.TP
|
||||
--completions \f[I]SHELL\f[R]
|
||||
Generate shell completions and exit [possible values: bash, fish, zsh]
|
||||
Generate shell completions (and exits) [possible values: bash, fish,
|
||||
zsh]
|
||||
.TP
|
||||
-s, --source \f[I]SOURCE_FORMAT\f[R]
|
||||
Specify the source format explicitly (by default, automatically inferred
|
||||
@ -117,13 +138,19 @@ formats (currently, JSON, TOML, and YAML); \f[I]ffs\f[R] maps values in
|
||||
these formats to filesystems.
|
||||
Here are the different types and how they\[aq]re mapped to a filesystem:
|
||||
.TP
|
||||
auto
|
||||
Automatically detected.
|
||||
The following order is used for UTF-8 encodable data: null, boolean,
|
||||
integer, float, datetime, string.
|
||||
If data can\[aq]t be encoded in UTF-8, it will always be bytes.
|
||||
.TP
|
||||
boolean
|
||||
Mapped to a \f[B]file\f[R].
|
||||
Either \f[I]true\f[R] or \f[I]false\f[R].
|
||||
.TP
|
||||
bytes
|
||||
Mapped to a \f[B]file\f[R].
|
||||
When serializing back to format, it will be encoded in base64.
|
||||
When saving, bytes are typically encoded in base64.
|
||||
.TP
|
||||
datetime
|
||||
Mapped to a \f[B]file\f[R].
|
||||
@ -145,9 +172,9 @@ Filenames will be padded with zeros to ensure proper sorting; use
|
||||
\f[I]--unpadded\f[R] to disable padding.
|
||||
While mounted, you are free to use whatever filenames you like in a list
|
||||
directory.
|
||||
When list directories are serialized back to a format, filenames are
|
||||
ignored and the sorted order of the files (in the current locale) will
|
||||
be used to determine the list order.
|
||||
When list directories are saved, filenames are ignored and the sorted
|
||||
order of the files (in the current locale) will be used to determine the
|
||||
list order.
|
||||
.TP
|
||||
named
|
||||
Mapped to a \f[B]directory\f[R].
|
||||
@ -214,6 +241,17 @@ umount commits
|
||||
\f[R]
|
||||
.fi
|
||||
.PP
|
||||
If you want to create a new file wholesale, the --new flag is helpful.
|
||||
.IP
|
||||
.nf
|
||||
\f[C]
|
||||
ffs --new file.json
|
||||
# do edits in file directory
|
||||
umount file
|
||||
# corresponding json is in file.json
|
||||
\f[R]
|
||||
.fi
|
||||
.PP
|
||||
To mount a JSON file and write back out a YAML file, you could run:
|
||||
.IP
|
||||
.nf
|
||||
|
@ -134,6 +134,15 @@ pub fn app() -> App<'static, 'static> {
|
||||
.short("m")
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("NEW")
|
||||
.help("Mounts an empty filesystem, inferring a mountpoint and output format")
|
||||
.long("new")
|
||||
.takes_value(true)
|
||||
.conflicts_with("INPLACE")
|
||||
.conflicts_with("SOURCE_FORMAT")
|
||||
.conflicts_with("OUTPUT")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("INPUT")
|
||||
.help("Sets the input file ('-' means STDIN)")
|
||||
|
@ -36,11 +36,13 @@ pub struct Config {
|
||||
pub enum Input {
|
||||
Stdin,
|
||||
File(PathBuf),
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Input {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
match self {
|
||||
Input::Empty => write!(f, "<empty>"),
|
||||
Input::Stdin => write!(f, "<stdin>"),
|
||||
Input::File(file) => write!(f, "{}", file.display()),
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use fuser::FileType;
|
||||
|
||||
use super::config::{Config, Output};
|
||||
use super::config::{Config, Input, Output};
|
||||
use super::fs::{DirEntry, DirType, Entry, Inode, FS};
|
||||
|
||||
use ::toml as serde_toml;
|
||||
@ -24,6 +24,7 @@ pub enum Format {
|
||||
/// Types classifying string data.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Typ {
|
||||
Auto,
|
||||
Null,
|
||||
Boolean,
|
||||
Integer,
|
||||
@ -53,6 +54,7 @@ impl std::fmt::Display for Typ {
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Typ::Auto => "auto",
|
||||
Typ::Null => "null",
|
||||
Typ::Boolean => "boolean",
|
||||
Typ::Bytes => "bytes",
|
||||
@ -95,7 +97,9 @@ impl FromStr for Typ {
|
||||
fn from_str(s: &str) -> Result<Self, ()> {
|
||||
let s = s.trim().to_lowercase();
|
||||
|
||||
if s == "null" {
|
||||
if s == "auto" {
|
||||
Ok(Typ::Auto)
|
||||
} else if s == "null" {
|
||||
Ok(Typ::Null)
|
||||
} else if s == "boolean" || s == "bool" {
|
||||
Ok(Typ::Boolean)
|
||||
@ -127,10 +131,37 @@ impl Format {
|
||||
/// particular `Config`.
|
||||
///
|
||||
/// NB there is no check that `self == fs.config.input_format`!
|
||||
#[instrument(level = "info", skip(reader, config))]
|
||||
pub fn load(&self, reader: Box<dyn std::io::Read>, config: Config) -> FS {
|
||||
#[instrument(level = "info", skip(config))]
|
||||
pub fn load(&self, config: Config) -> FS {
|
||||
let mut inodes: Vec<Option<Inode>> = Vec::new();
|
||||
|
||||
let reader: Box<dyn std::io::Read> = match &config.input {
|
||||
Input::Stdin => Box::new(std::io::stdin()),
|
||||
Input::File(file) => {
|
||||
let fmt = config.input_format;
|
||||
let file = std::fs::File::open(&file).unwrap_or_else(|e| {
|
||||
error!("Unable to open {} for {} input: {}", file.display(), fmt, e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
Box::new(file)
|
||||
}
|
||||
Input::Empty => {
|
||||
// let's just reserve some space for later and get cracking
|
||||
info!("reserving space in empty filesystem");
|
||||
inodes.resize_with(1024, || None);
|
||||
|
||||
// create an empty directory
|
||||
let contents = HashMap::with_capacity(16);
|
||||
inodes[1] = Some(Inode::new(
|
||||
fuser::FUSE_ROOT_ID,
|
||||
fuser::FUSE_ROOT_ID,
|
||||
Entry::Directory(DirType::Named, contents),
|
||||
&config,
|
||||
));
|
||||
return FS::new(inodes, config);
|
||||
}
|
||||
};
|
||||
|
||||
match self {
|
||||
Format::Json => {
|
||||
info!("reading json value");
|
||||
@ -478,6 +509,19 @@ mod json {
|
||||
|
||||
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
||||
match typ {
|
||||
Typ::Auto => {
|
||||
if contents.is_empty() {
|
||||
Value::Null
|
||||
} else if contents == "true" {
|
||||
Value::Bool(true)
|
||||
} else if contents == "false" {
|
||||
Value::Bool(false)
|
||||
} else if let Ok(n) = serde_json::Number::from_str(&contents) {
|
||||
Value::Number(n)
|
||||
} else {
|
||||
Value::String(contents)
|
||||
}
|
||||
}
|
||||
Typ::Boolean => {
|
||||
if contents == "true" {
|
||||
Value::Bool(true)
|
||||
@ -613,6 +657,21 @@ mod toml {
|
||||
|
||||
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
||||
match typ {
|
||||
Typ::Auto => {
|
||||
if contents == "true" {
|
||||
Value::Boolean(true)
|
||||
} else if contents == "false" {
|
||||
Value::Boolean(false)
|
||||
} else if let Ok(n) = i64::from_str(&contents) {
|
||||
Value::Integer(n)
|
||||
} else if let Ok(n) = f64::from_str(&contents) {
|
||||
Value::Float(n)
|
||||
} else if let Ok(datetime) = str::parse(&contents) {
|
||||
Value::Datetime(datetime)
|
||||
} else {
|
||||
Value::String(contents)
|
||||
}
|
||||
}
|
||||
Typ::Boolean => {
|
||||
if contents == "true" {
|
||||
Value::Boolean(true)
|
||||
@ -808,6 +867,21 @@ mod yaml {
|
||||
|
||||
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
||||
match typ {
|
||||
Typ::Auto => {
|
||||
if contents.is_empty() {
|
||||
Value(Yaml::Null)
|
||||
} else if contents == "true" {
|
||||
Value(Yaml::Boolean(true))
|
||||
} else if contents == "false" {
|
||||
Value(Yaml::Boolean(false))
|
||||
} else if let Ok(n) = i64::from_str(&contents) {
|
||||
Value(Yaml::Integer(n))
|
||||
} else if let Ok(_n) = f64::from_str(&contents) {
|
||||
Value(Yaml::Real(contents))
|
||||
} else {
|
||||
Value(Yaml::String(contents))
|
||||
}
|
||||
}
|
||||
Typ::Boolean => {
|
||||
if contents == "true" {
|
||||
Value(Yaml::Boolean(true))
|
||||
|
@ -950,7 +950,7 @@ impl Filesystem for FS {
|
||||
|
||||
// create the inode entry
|
||||
let (entry, kind) = if file_type == libc::S_IFREG as u32 {
|
||||
(Entry::File(Typ::String, Vec::new()), FileType::RegularFile)
|
||||
(Entry::File(Typ::Auto, Vec::new()), FileType::RegularFile)
|
||||
} else {
|
||||
assert_eq!(file_type, libc::S_IFDIR as u32);
|
||||
(
|
||||
|
399
src/main.rs
399
src/main.rs
@ -129,75 +129,85 @@ fn main() {
|
||||
None => config.gid = unsafe { libc::getegid() },
|
||||
}
|
||||
|
||||
config.input = match args.value_of("INPUT") {
|
||||
Some(input_source) => {
|
||||
if input_source == "-" {
|
||||
Input::Stdin
|
||||
} else {
|
||||
let input_source = PathBuf::from(input_source);
|
||||
if !input_source.exists() {
|
||||
error!("Input file {} does not exist.", input_source.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
Input::File(input_source)
|
||||
}
|
||||
}
|
||||
None => Input::Stdin,
|
||||
};
|
||||
config.output = if let Some(output) = args.value_of("OUTPUT") {
|
||||
Output::File(PathBuf::from(output))
|
||||
} else if args.is_present("INPLACE") {
|
||||
match &config.input {
|
||||
Input::Stdin => {
|
||||
warn!(
|
||||
"In-place output `-i` with STDIN input makes no sense; outputting on STDOUT."
|
||||
);
|
||||
Output::Stdout
|
||||
}
|
||||
Input::File(input_source) => Output::File(input_source.clone()),
|
||||
}
|
||||
} else if args.is_present("NOOUTPUT") || args.is_present("QUIET") {
|
||||
Output::Quiet
|
||||
} else {
|
||||
Output::Stdout
|
||||
};
|
||||
|
||||
// infer and create mountpoint from filename as possible
|
||||
config.mount = match args.value_of("MOUNT") {
|
||||
Some(mount_point) => {
|
||||
let mount_point = PathBuf::from(mount_point);
|
||||
if !mount_point.exists() {
|
||||
error!("Mount point {} does not exist.", mount_point.display());
|
||||
match args.value_of("NEW") {
|
||||
Some(target_file) => {
|
||||
if args.occurrences_of("INPUT") != 0 {
|
||||
error!("It doesn't make sense to set `--new` with a specified input file.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
config.cleanup_mount = false;
|
||||
Some(mount_point)
|
||||
}
|
||||
None => {
|
||||
match &config.input {
|
||||
Input::Stdin => {
|
||||
error!("You must specify a mount point when reading from stdin.");
|
||||
std::process::exit(1);
|
||||
let output = PathBuf::from(target_file);
|
||||
|
||||
if output.exists() {
|
||||
error!("Output file {} already exists.", output.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let format = match args
|
||||
.value_of("TARGET_FORMAT")
|
||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
||||
.and_then(|s| s.parse::<Format>())
|
||||
{
|
||||
Ok(target_format) => target_format,
|
||||
Err(e) => {
|
||||
match e {
|
||||
format::ParseFormatError::NoSuchFormat(s) => {
|
||||
warn!(
|
||||
"Unrecognized format '{}', inferring from {}.",
|
||||
s,
|
||||
output.display(),
|
||||
)
|
||||
}
|
||||
format::ParseFormatError::NoFormatProvided => {
|
||||
debug!("Inferring output format from input.")
|
||||
}
|
||||
};
|
||||
|
||||
match output
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
||||
.and_then(|s| s.parse::<Format>())
|
||||
{
|
||||
Ok(format) => format,
|
||||
Err(_) => {
|
||||
error!(
|
||||
"Unrecognized format '{}'; use --target or a known extension to specify a format.",
|
||||
output.display()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Input::File(file) => {
|
||||
// If the input is from a file foo.EXT, then try to make a directory foo.
|
||||
let mount_dir = file.with_extension("");
|
||||
};
|
||||
|
||||
let mount = match args.value_of("MOUNT") {
|
||||
Some(mount_point) => {
|
||||
let mount_point = PathBuf::from(mount_point);
|
||||
if !mount_point.exists() {
|
||||
error!("Mount point {} does not exist.", mount_point.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
config.cleanup_mount = false;
|
||||
Some(mount_point)
|
||||
}
|
||||
None => {
|
||||
// If the output is to a file foo.EXT, then try to make a directory foo.
|
||||
let mount_dir = output.with_extension("");
|
||||
|
||||
// If that file already exists, give up and tell the user about --mount.
|
||||
if mount_dir.exists() {
|
||||
error!("Inferred mountpoint '{mount}' for input file '{file}', but '{mount}' already exists. Use `--mount MOUNT` to specify a mountpoint.",
|
||||
mount = mount_dir.display(), file = file.display());
|
||||
error!("Inferred mountpoint '{mount}' for output file '{file}', but '{mount}' already exists. Use `--mount MOUNT` to specify a mountpoint.",
|
||||
mount = mount_dir.display(), file = output.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// If the mountpoint can't be created, give up and tell the user about --mount.
|
||||
if let Err(e) = std::fs::create_dir(&mount_dir) {
|
||||
error!(
|
||||
"Couldn't create mountpoint '{}': {}. Use `--mount MOUNT` to specify a mountpoint.",
|
||||
mount_dir.display(),
|
||||
e
|
||||
);
|
||||
error!("Couldn't create mountpoint '{}': {}. Use `--mount MOUNT` to specify a mountpoint.",
|
||||
mount_dir.display(),
|
||||
e
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
@ -205,98 +215,192 @@ fn main() {
|
||||
config.cleanup_mount = true;
|
||||
Some(mount_dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
assert!(config.mount.is_some());
|
||||
|
||||
// try to autodetect the input format.
|
||||
//
|
||||
// first see if it's specified and parses okay.
|
||||
//
|
||||
// then see if we can pull it out of the extension.
|
||||
//
|
||||
// then give up and use json
|
||||
config.input_format = match args
|
||||
.value_of("SOURCE_FORMAT")
|
||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
||||
.and_then(|s| s.parse::<Format>())
|
||||
{
|
||||
Ok(source_format) => source_format,
|
||||
Err(e) => {
|
||||
match e {
|
||||
format::ParseFormatError::NoSuchFormat(s) => {
|
||||
warn!("Unrecognized format '{}', inferring from input.", s)
|
||||
}
|
||||
format::ParseFormatError::NoFormatProvided => {
|
||||
debug!("Inferring format from input.")
|
||||
}
|
||||
};
|
||||
|
||||
match &config.input {
|
||||
Input::Stdin => Format::Json,
|
||||
Input::File(input_source) => match input_source
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
||||
.and_then(|s| s.parse::<Format>())
|
||||
{
|
||||
Ok(format) => format,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Unrecognized format {}, defaulting to JSON.",
|
||||
input_source.display()
|
||||
);
|
||||
Format::Json
|
||||
config.input = Input::Empty;
|
||||
config.output = Output::File(output);
|
||||
config.input_format = format;
|
||||
config.output_format = format;
|
||||
config.mount = mount;
|
||||
}
|
||||
None => {
|
||||
// configure input
|
||||
config.input = match args.value_of("INPUT") {
|
||||
Some(input_source) => {
|
||||
if input_source == "-" {
|
||||
Input::Stdin
|
||||
} else {
|
||||
let input_source = PathBuf::from(input_source);
|
||||
if !input_source.exists() {
|
||||
error!("Input file {} does not exist.", input_source.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
Input::File(input_source)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// try to autodetect the output format.
|
||||
//
|
||||
// first see if it's specified and parses okay.
|
||||
//
|
||||
// then see if we can pull it out of the extension (if specified)
|
||||
//
|
||||
// then give up and use the input format
|
||||
config.output_format = match args
|
||||
.value_of("TARGET_FORMAT")
|
||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
||||
.and_then(|s| s.parse::<Format>())
|
||||
{
|
||||
Ok(target_format) => target_format,
|
||||
Err(e) => {
|
||||
match e {
|
||||
format::ParseFormatError::NoSuchFormat(s) => {
|
||||
warn!(
|
||||
"Unrecognized format '{}', inferring from input and output.",
|
||||
s
|
||||
)
|
||||
}
|
||||
format::ParseFormatError::NoFormatProvided => {
|
||||
debug!("Inferring output format from input.")
|
||||
}
|
||||
None => Input::Stdin,
|
||||
};
|
||||
|
||||
match args
|
||||
.value_of("OUTPUT")
|
||||
.and_then(|s| Path::new(s).extension())
|
||||
.and_then(|s| s.to_str())
|
||||
// configure output
|
||||
config.output = if let Some(output) = args.value_of("OUTPUT") {
|
||||
Output::File(PathBuf::from(output))
|
||||
} else if args.is_present("INPLACE") {
|
||||
match &config.input {
|
||||
Input::Stdin => {
|
||||
warn!(
|
||||
"In-place output `-i` with STDIN input makes no sense; outputting on STDOUT."
|
||||
);
|
||||
Output::Stdout
|
||||
}
|
||||
Input::Empty => {
|
||||
warn!(
|
||||
"In-place output `-i` with empty input makes no sense; outputting on STDOUT."
|
||||
);
|
||||
Output::Stdout
|
||||
}
|
||||
Input::File(input_source) => Output::File(input_source.clone()),
|
||||
}
|
||||
} else if args.is_present("NOOUTPUT") || args.is_present("QUIET") {
|
||||
Output::Quiet
|
||||
} else {
|
||||
Output::Stdout
|
||||
};
|
||||
|
||||
// infer and create mountpoint from filename as possible
|
||||
config.mount = match args.value_of("MOUNT") {
|
||||
Some(mount_point) => {
|
||||
let mount_point = PathBuf::from(mount_point);
|
||||
if !mount_point.exists() {
|
||||
error!("Mount point {} does not exist.", mount_point.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
config.cleanup_mount = false;
|
||||
Some(mount_point)
|
||||
}
|
||||
None => {
|
||||
match &config.input {
|
||||
Input::Stdin => {
|
||||
error!("You must specify a mount point when reading from stdin.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Input::Empty => {
|
||||
error!("You must specify a mount point when reading an empty file.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Input::File(file) => {
|
||||
// If the input is from a file foo.EXT, then try to make a directory foo.
|
||||
let mount_dir = file.with_extension("");
|
||||
// If that file already exists, give up and tell the user about --mount.
|
||||
if mount_dir.exists() {
|
||||
error!("Inferred mountpoint '{mount}' for input file '{file}', but '{mount}' already exists. Use `--mount MOUNT` to specify a mountpoint.",
|
||||
mount = mount_dir.display(), file = file.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
// If the mountpoint can't be created, give up and tell the user about --mount.
|
||||
if let Err(e) = std::fs::create_dir(&mount_dir) {
|
||||
error!(
|
||||
"Couldn't create mountpoint '{}': {}. Use `--mount MOUNT` to specify a mountpoint.",
|
||||
mount_dir.display(),
|
||||
e
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
// We did it!
|
||||
config.cleanup_mount = true;
|
||||
Some(mount_dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
assert!(config.mount.is_some());
|
||||
|
||||
// try to autodetect the input format.
|
||||
//
|
||||
// first see if it's specified and parses okay.
|
||||
//
|
||||
// then see if we can pull it out of the extension.
|
||||
//
|
||||
// then give up and use json
|
||||
config.input_format = match args
|
||||
.value_of("SOURCE_FORMAT")
|
||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
||||
.and_then(|s| s.parse::<Format>())
|
||||
{
|
||||
Some(s) => match s.parse::<Format>() {
|
||||
Ok(format) => format,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Unrecognized format {}, defaulting to input format '{}'.",
|
||||
s, config.input_format
|
||||
);
|
||||
config.input_format
|
||||
Ok(source_format) => source_format,
|
||||
Err(e) => {
|
||||
match e {
|
||||
format::ParseFormatError::NoSuchFormat(s) => {
|
||||
warn!("Unrecognized format '{}', inferring from input.", s)
|
||||
}
|
||||
format::ParseFormatError::NoFormatProvided => {
|
||||
debug!("Inferring format from input.")
|
||||
}
|
||||
};
|
||||
match &config.input {
|
||||
Input::Stdin => Format::Json,
|
||||
Input::Empty => Format::Json,
|
||||
Input::File(input_source) => match input_source
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
||||
.and_then(|s| s.parse::<Format>())
|
||||
{
|
||||
Ok(format) => format,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Unrecognized format {}, defaulting to JSON.",
|
||||
input_source.display()
|
||||
);
|
||||
Format::Json
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
None => config.input_format,
|
||||
}
|
||||
}
|
||||
};
|
||||
// try to autodetect the output format.
|
||||
//
|
||||
// first see if it's specified and parses okay.
|
||||
//
|
||||
// then see if we can pull it out of the extension (if specified)
|
||||
//
|
||||
// then give up and use the input format
|
||||
config.output_format = match args
|
||||
.value_of("TARGET_FORMAT")
|
||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
||||
.and_then(|s| s.parse::<Format>())
|
||||
{
|
||||
Ok(target_format) => target_format,
|
||||
Err(e) => {
|
||||
match e {
|
||||
format::ParseFormatError::NoSuchFormat(s) => {
|
||||
warn!(
|
||||
"Unrecognized format '{}', inferring from input and output.",
|
||||
s
|
||||
)
|
||||
}
|
||||
format::ParseFormatError::NoFormatProvided => {
|
||||
debug!("Inferring output format from input.")
|
||||
}
|
||||
};
|
||||
match args
|
||||
.value_of("OUTPUT")
|
||||
.and_then(|s| Path::new(s).extension())
|
||||
.and_then(|s| s.to_str())
|
||||
{
|
||||
Some(s) => match s.parse::<Format>() {
|
||||
Ok(format) => format,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Unrecognized format {}, defaulting to input format '{}'.",
|
||||
s, config.input_format
|
||||
);
|
||||
config.input_format
|
||||
}
|
||||
},
|
||||
None => config.input_format,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@ -331,18 +435,7 @@ fn main() {
|
||||
};
|
||||
let cleanup_mount = config.cleanup_mount;
|
||||
let input_format = config.input_format;
|
||||
let reader: Box<dyn std::io::Read> = match &config.input {
|
||||
Input::Stdin => Box::new(std::io::stdin()),
|
||||
Input::File(file) => {
|
||||
let fmt = config.input_format;
|
||||
let file = std::fs::File::open(&file).unwrap_or_else(|e| {
|
||||
error!("Unable to open {} for {} input: {}", file.display(), fmt, e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
Box::new(file)
|
||||
}
|
||||
};
|
||||
let fs = input_format.load(reader, config);
|
||||
let fs = input_format.load(config);
|
||||
|
||||
info!("mounting on {:?} with options {:?}", mount, options);
|
||||
fuser::mount2(fs, &mount, &options).unwrap();
|
||||
|
47
tests/auto.sh
Executable file
47
tests/auto.sh
Executable file
@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
|
||||
fail() {
|
||||
echo FAILED: $1
|
||||
if [ "$MNT" ]
|
||||
then
|
||||
umount "$MNT"
|
||||
rm "$FILE" "$EXP"
|
||||
rmdir "$MNT"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
MNT=$(mktemp -d)
|
||||
FILE=$(mktemp).json
|
||||
|
||||
echo '{}' >"$FILE"
|
||||
|
||||
EXP=$(mktemp)
|
||||
|
||||
printf '{"favorite_number":47,"likes":{"cats":false,"dogs":true},"mistakes":null,"name":"Michael Greenberg","website":"https://mgree.github.io"}' >"$EXP"
|
||||
|
||||
ffs -m "$MNT" -i "$FILE" &
|
||||
PID=$!
|
||||
sleep 2
|
||||
|
||||
ls "$MNT"
|
||||
[ $(ls $MNT) ] && fail nonempty1
|
||||
[ $(ls $MNT | wc -l) -eq 0 ] || fail nonempty2
|
||||
|
||||
echo 47 >"$MNT"/favorite_number
|
||||
mkdir "$MNT"/likes
|
||||
echo true >"$MNT"/likes/dogs
|
||||
echo false >"$MNT"/likes/cats
|
||||
touch "$MNT"/mistakes
|
||||
echo Michael Greenberg >"$MNT"/name
|
||||
echo https://mgree.github.io >"$MNT"/website
|
||||
|
||||
umount "$MNT" || fail unmount
|
||||
sleep 1
|
||||
kill -0 $PID >/dev/null 2>&1 && fail process
|
||||
|
||||
cat "$FILE"
|
||||
diff "$FILE" "$EXP" || fail diff
|
||||
|
||||
rm "$FILE" "$EXP"
|
||||
rmdir "$MNT"
|
43
tests/new.sh
Executable file
43
tests/new.sh
Executable file
@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
|
||||
fail() {
|
||||
echo FAILED: $1
|
||||
if [ "$MNT" ]
|
||||
then
|
||||
umount "$MNT"
|
||||
rmdir "$MNT"
|
||||
rm "$OUT" "$EXP"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# really, just for the name
|
||||
OUT=$(mktemp)
|
||||
rm "$OUT"
|
||||
MNT="$OUT"
|
||||
OUT="$OUT".json
|
||||
|
||||
EXP=$(mktemp)
|
||||
|
||||
printf '{"handles":{"github":"mgree","stevens":"mgreenbe","twitter":"mgrnbrg"},"problems":99}' >"$EXP"
|
||||
|
||||
ffs --new "$OUT" &
|
||||
PID=$!
|
||||
sleep 2
|
||||
[ "$(ls $MNT)" ] && fail nonempty
|
||||
|
||||
mkdir "$MNT"/handles
|
||||
|
||||
echo mgree >"$MNT"/handles/github
|
||||
echo mgreenbe >"$MNT"/handles/stevens
|
||||
echo mgrnbrg >"$MNT"/handles/twitter
|
||||
echo 99 >"$MNT"/problems
|
||||
|
||||
umount "$MNT" || fail unmount
|
||||
sleep 1
|
||||
kill -0 $PID >/dev/null 2>&1 && fail process
|
||||
|
||||
diff "$OUT" "$EXP" || fail diff
|
||||
|
||||
[ -e "$MNT" ] && fail mount
|
||||
rm "$OUT" "$EXP"
|
Loading…
Reference in New Issue
Block a user