mirror of
https://github.com/mgree/ffs.git
synced 2024-08-17 07:10:35 +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.
|
* `--pretty` flag for JSON and TOML.
|
||||||
* Wrote INSTALL.md.
|
* Wrote INSTALL.md.
|
||||||
* Improved manpage.
|
* Improved manpage.
|
||||||
|
* `--new` flag for starting from an empty filesystem.
|
||||||
|
|
||||||
## 0.1.0 - 2021-06-26
|
## 0.1.0 - 2021-06-26
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ ffs - the file filesystem
|
|||||||
# SYNOPSIS
|
# SYNOPSIS
|
||||||
|
|
||||||
| ffs \[*FLAGS*\] \[*OPTIONS*\] \[*INPUT*\]
|
| ffs \[*FLAGS*\] \[*OPTIONS*\] \[*INPUT*\]
|
||||||
|
| ffs \[*FLAGS*\] \[*OPTIONS*\] --new \[*OUTPUT*\]
|
||||||
| ffs *--completions* *SHELL*
|
| ffs *--completions* *SHELL*
|
||||||
| ffs \[*-h*\|*--help*\]
|
| ffs \[*-h*\|*--help*\]
|
||||||
| ffs \[*-V*\|*--version*\]
|
| ffs \[*-V*\|*--version*\]
|
||||||
@ -103,6 +104,14 @@ installed on your system to use *ffs*.
|
|||||||
|
|
||||||
: Sets the output file for saving changes (defaults to stdout)
|
: 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*
|
--completions *SHELL*
|
||||||
|
|
||||||
: Generate shell completions (and exits) [possible values: bash, fish,
|
: 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
|
formats to filesystems. Here are the different types and how they're
|
||||||
mapped to a filesystem:
|
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
|
boolean
|
||||||
|
|
||||||
: Mapped to a **file**. Either *true* or *false*.
|
: Mapped to a **file**. Either *true* or *false*.
|
||||||
|
|
||||||
bytes
|
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
|
datetime
|
||||||
|
|
||||||
@ -165,9 +181,9 @@ list
|
|||||||
named elements, starting from 0. Filenames will be padded with zeros
|
named elements, starting from 0. Filenames will be padded with zeros
|
||||||
to ensure proper sorting; use *--unpadded* to disable padding. While
|
to ensure proper sorting; use *--unpadded* to disable padding. While
|
||||||
mounted, you are free to use whatever filenames you like in a list
|
mounted, you are free to use whatever filenames you like in a list
|
||||||
directory. When list directories are serialized back to a format,
|
directory. When list directories are saved, filenames are ignored
|
||||||
filenames are ignored and the sorted order of the files (in the
|
and the sorted order of the files (in the current locale) will be
|
||||||
current locale) will be used to determine the list order.
|
used to determine the list order.
|
||||||
|
|
||||||
named
|
named
|
||||||
|
|
||||||
@ -229,6 +245,15 @@ umount commits
|
|||||||
# changes are written back to commits.json (-i is in-place mode)
|
# 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:
|
To mount a JSON file and write back out a YAML file, you could run:
|
||||||
|
|
||||||
```shell
|
```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
|
.PD 0
|
||||||
.P
|
.P
|
||||||
.PD
|
.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]
|
ffs \f[I]--completions\f[R] \f[I]SHELL\f[R]
|
||||||
.PD 0
|
.PD 0
|
||||||
.P
|
.P
|
||||||
@ -92,8 +96,25 @@ specified when running on stdin
|
|||||||
-o, --output \f[I]OUTPUT\f[R]
|
-o, --output \f[I]OUTPUT\f[R]
|
||||||
Sets the output file for saving changes (defaults to stdout)
|
Sets the output file for saving changes (defaults to stdout)
|
||||||
.TP
|
.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]
|
--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
|
.TP
|
||||||
-s, --source \f[I]SOURCE_FORMAT\f[R]
|
-s, --source \f[I]SOURCE_FORMAT\f[R]
|
||||||
Specify the source format explicitly (by default, automatically inferred
|
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.
|
these formats to filesystems.
|
||||||
Here are the different types and how they\[aq]re mapped to a filesystem:
|
Here are the different types and how they\[aq]re mapped to a filesystem:
|
||||||
.TP
|
.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
|
boolean
|
||||||
Mapped to a \f[B]file\f[R].
|
Mapped to a \f[B]file\f[R].
|
||||||
Either \f[I]true\f[R] or \f[I]false\f[R].
|
Either \f[I]true\f[R] or \f[I]false\f[R].
|
||||||
.TP
|
.TP
|
||||||
bytes
|
bytes
|
||||||
Mapped to a \f[B]file\f[R].
|
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
|
.TP
|
||||||
datetime
|
datetime
|
||||||
Mapped to a \f[B]file\f[R].
|
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.
|
\f[I]--unpadded\f[R] to disable padding.
|
||||||
While mounted, you are free to use whatever filenames you like in a list
|
While mounted, you are free to use whatever filenames you like in a list
|
||||||
directory.
|
directory.
|
||||||
When list directories are serialized back to a format, filenames are
|
When list directories are saved, filenames are ignored and the sorted
|
||||||
ignored and the sorted order of the files (in the current locale) will
|
order of the files (in the current locale) will be used to determine the
|
||||||
be used to determine the list order.
|
list order.
|
||||||
.TP
|
.TP
|
||||||
named
|
named
|
||||||
Mapped to a \f[B]directory\f[R].
|
Mapped to a \f[B]directory\f[R].
|
||||||
@ -214,6 +241,17 @@ umount commits
|
|||||||
\f[R]
|
\f[R]
|
||||||
.fi
|
.fi
|
||||||
.PP
|
.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:
|
To mount a JSON file and write back out a YAML file, you could run:
|
||||||
.IP
|
.IP
|
||||||
.nf
|
.nf
|
||||||
|
@ -134,6 +134,15 @@ pub fn app() -> App<'static, 'static> {
|
|||||||
.short("m")
|
.short("m")
|
||||||
.takes_value(true)
|
.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(
|
||||||
Arg::with_name("INPUT")
|
Arg::with_name("INPUT")
|
||||||
.help("Sets the input file ('-' means STDIN)")
|
.help("Sets the input file ('-' means STDIN)")
|
||||||
|
@ -36,11 +36,13 @@ pub struct Config {
|
|||||||
pub enum Input {
|
pub enum Input {
|
||||||
Stdin,
|
Stdin,
|
||||||
File(PathBuf),
|
File(PathBuf),
|
||||||
|
Empty,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Input {
|
impl std::fmt::Display for Input {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
match self {
|
match self {
|
||||||
|
Input::Empty => write!(f, "<empty>"),
|
||||||
Input::Stdin => write!(f, "<stdin>"),
|
Input::Stdin => write!(f, "<stdin>"),
|
||||||
Input::File(file) => write!(f, "{}", file.display()),
|
Input::File(file) => write!(f, "{}", file.display()),
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use tracing::{debug, error, info, instrument, warn};
|
|||||||
|
|
||||||
use fuser::FileType;
|
use fuser::FileType;
|
||||||
|
|
||||||
use super::config::{Config, Output};
|
use super::config::{Config, Input, Output};
|
||||||
use super::fs::{DirEntry, DirType, Entry, Inode, FS};
|
use super::fs::{DirEntry, DirType, Entry, Inode, FS};
|
||||||
|
|
||||||
use ::toml as serde_toml;
|
use ::toml as serde_toml;
|
||||||
@ -24,6 +24,7 @@ pub enum Format {
|
|||||||
/// Types classifying string data.
|
/// Types classifying string data.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Typ {
|
pub enum Typ {
|
||||||
|
Auto,
|
||||||
Null,
|
Null,
|
||||||
Boolean,
|
Boolean,
|
||||||
Integer,
|
Integer,
|
||||||
@ -53,6 +54,7 @@ impl std::fmt::Display for Typ {
|
|||||||
f,
|
f,
|
||||||
"{}",
|
"{}",
|
||||||
match self {
|
match self {
|
||||||
|
Typ::Auto => "auto",
|
||||||
Typ::Null => "null",
|
Typ::Null => "null",
|
||||||
Typ::Boolean => "boolean",
|
Typ::Boolean => "boolean",
|
||||||
Typ::Bytes => "bytes",
|
Typ::Bytes => "bytes",
|
||||||
@ -95,7 +97,9 @@ impl FromStr for Typ {
|
|||||||
fn from_str(s: &str) -> Result<Self, ()> {
|
fn from_str(s: &str) -> Result<Self, ()> {
|
||||||
let s = s.trim().to_lowercase();
|
let s = s.trim().to_lowercase();
|
||||||
|
|
||||||
if s == "null" {
|
if s == "auto" {
|
||||||
|
Ok(Typ::Auto)
|
||||||
|
} else if s == "null" {
|
||||||
Ok(Typ::Null)
|
Ok(Typ::Null)
|
||||||
} else if s == "boolean" || s == "bool" {
|
} else if s == "boolean" || s == "bool" {
|
||||||
Ok(Typ::Boolean)
|
Ok(Typ::Boolean)
|
||||||
@ -127,10 +131,37 @@ impl Format {
|
|||||||
/// particular `Config`.
|
/// particular `Config`.
|
||||||
///
|
///
|
||||||
/// NB there is no check that `self == fs.config.input_format`!
|
/// NB there is no check that `self == fs.config.input_format`!
|
||||||
#[instrument(level = "info", skip(reader, config))]
|
#[instrument(level = "info", skip(config))]
|
||||||
pub fn load(&self, reader: Box<dyn std::io::Read>, config: Config) -> FS {
|
pub fn load(&self, config: Config) -> FS {
|
||||||
let mut inodes: Vec<Option<Inode>> = Vec::new();
|
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 {
|
match self {
|
||||||
Format::Json => {
|
Format::Json => {
|
||||||
info!("reading json value");
|
info!("reading json value");
|
||||||
@ -478,6 +509,19 @@ mod json {
|
|||||||
|
|
||||||
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
||||||
match typ {
|
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 => {
|
Typ::Boolean => {
|
||||||
if contents == "true" {
|
if contents == "true" {
|
||||||
Value::Bool(true)
|
Value::Bool(true)
|
||||||
@ -613,6 +657,21 @@ mod toml {
|
|||||||
|
|
||||||
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
||||||
match typ {
|
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 => {
|
Typ::Boolean => {
|
||||||
if contents == "true" {
|
if contents == "true" {
|
||||||
Value::Boolean(true)
|
Value::Boolean(true)
|
||||||
@ -808,6 +867,21 @@ mod yaml {
|
|||||||
|
|
||||||
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
fn from_string(typ: Typ, contents: String, _config: &Config) -> Self {
|
||||||
match typ {
|
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 => {
|
Typ::Boolean => {
|
||||||
if contents == "true" {
|
if contents == "true" {
|
||||||
Value(Yaml::Boolean(true))
|
Value(Yaml::Boolean(true))
|
||||||
|
@ -950,7 +950,7 @@ impl Filesystem for FS {
|
|||||||
|
|
||||||
// create the inode entry
|
// create the inode entry
|
||||||
let (entry, kind) = if file_type == libc::S_IFREG as u32 {
|
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 {
|
} else {
|
||||||
assert_eq!(file_type, libc::S_IFDIR as u32);
|
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() },
|
None => config.gid = unsafe { libc::getegid() },
|
||||||
}
|
}
|
||||||
|
|
||||||
config.input = match args.value_of("INPUT") {
|
match args.value_of("NEW") {
|
||||||
Some(input_source) => {
|
Some(target_file) => {
|
||||||
if input_source == "-" {
|
if args.occurrences_of("INPUT") != 0 {
|
||||||
Input::Stdin
|
error!("It doesn't make sense to set `--new` with a specified input file.");
|
||||||
} 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());
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.cleanup_mount = false;
|
let output = PathBuf::from(target_file);
|
||||||
Some(mount_point)
|
|
||||||
}
|
if output.exists() {
|
||||||
None => {
|
error!("Output file {} already exists.", output.display());
|
||||||
match &config.input {
|
std::process::exit(1);
|
||||||
Input::Stdin => {
|
}
|
||||||
error!("You must specify a mount point when reading from stdin.");
|
|
||||||
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 that file already exists, give up and tell the user about --mount.
|
||||||
if mount_dir.exists() {
|
if mount_dir.exists() {
|
||||||
error!("Inferred mountpoint '{mount}' for input file '{file}', but '{mount}' already exists. Use `--mount MOUNT` to specify a mountpoint.",
|
error!("Inferred mountpoint '{mount}' for output file '{file}', but '{mount}' already exists. Use `--mount MOUNT` to specify a mountpoint.",
|
||||||
mount = mount_dir.display(), file = file.display());
|
mount = mount_dir.display(), file = output.display());
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the mountpoint can't be created, give up and tell the user about --mount.
|
// 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) {
|
if let Err(e) = std::fs::create_dir(&mount_dir) {
|
||||||
error!(
|
error!("Couldn't create mountpoint '{}': {}. Use `--mount MOUNT` to specify a mountpoint.",
|
||||||
"Couldn't create mountpoint '{}': {}. Use `--mount MOUNT` to specify a mountpoint.",
|
mount_dir.display(),
|
||||||
mount_dir.display(),
|
e
|
||||||
e
|
);
|
||||||
);
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,98 +215,192 @@ fn main() {
|
|||||||
config.cleanup_mount = true;
|
config.cleanup_mount = true;
|
||||||
Some(mount_dir)
|
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 {
|
config.input = Input::Empty;
|
||||||
Input::Stdin => Format::Json,
|
config.output = Output::File(output);
|
||||||
Input::File(input_source) => match input_source
|
config.input_format = format;
|
||||||
.extension()
|
config.output_format = format;
|
||||||
.and_then(|s| s.to_str())
|
config.mount = mount;
|
||||||
.ok_or(format::ParseFormatError::NoFormatProvided)
|
}
|
||||||
.and_then(|s| s.parse::<Format>())
|
None => {
|
||||||
{
|
// configure input
|
||||||
Ok(format) => format,
|
config.input = match args.value_of("INPUT") {
|
||||||
Err(_) => {
|
Some(input_source) => {
|
||||||
warn!(
|
if input_source == "-" {
|
||||||
"Unrecognized format {}, defaulting to JSON.",
|
Input::Stdin
|
||||||
input_source.display()
|
} else {
|
||||||
);
|
let input_source = PathBuf::from(input_source);
|
||||||
Format::Json
|
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
|
// configure output
|
||||||
.value_of("OUTPUT")
|
config.output = if let Some(output) = args.value_of("OUTPUT") {
|
||||||
.and_then(|s| Path::new(s).extension())
|
Output::File(PathBuf::from(output))
|
||||||
.and_then(|s| s.to_str())
|
} 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(source_format) => source_format,
|
||||||
Ok(format) => format,
|
Err(e) => {
|
||||||
Err(_) => {
|
match e {
|
||||||
warn!(
|
format::ParseFormatError::NoSuchFormat(s) => {
|
||||||
"Unrecognized format {}, defaulting to input format '{}'.",
|
warn!("Unrecognized format '{}', inferring from input.", s)
|
||||||
s, config.input_format
|
}
|
||||||
);
|
format::ParseFormatError::NoFormatProvided => {
|
||||||
config.input_format
|
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 cleanup_mount = config.cleanup_mount;
|
||||||
let input_format = config.input_format;
|
let input_format = config.input_format;
|
||||||
let reader: Box<dyn std::io::Read> = match &config.input {
|
let fs = input_format.load(config);
|
||||||
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);
|
|
||||||
|
|
||||||
info!("mounting on {:?} with options {:?}", mount, options);
|
info!("mounting on {:?} with options {:?}", mount, options);
|
||||||
fuser::mount2(fs, &mount, &options).unwrap();
|
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