use anyhow::Result;
use clap::Parser;
use tracing::instrument;
#[derive(Clone, Debug)]
struct Dirwidth {
value: Vec<usize>,
}
impl std::str::FromStr for Dirwidth {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
if s.is_empty() {
anyhow::bail!(
"Invalid dirwidth specification: must contain at least one value (e.g., \"3,2\")"
);
}
let value = s
.split(',')
.map(str::parse::<usize>)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| anyhow::anyhow!("Invalid dirwidth specification '{}': {}", s, e))?;
if let Some((index, _)) = value.iter().enumerate().find(|(_, &v)| v == 0) {
anyhow::bail!(
"Invalid dirwidth specification '{}': value at position {} is 0. All values must be greater than 0.",
s,
index + 1
);
}
Ok(Dirwidth { value })
}
}
#[derive(Clone, Parser, Debug)]
#[command(
name = "filegen",
version,
about = "Generate sample filesets for testing",
long_about = "`filegen` generates sample filesets with configurable directory structure and file sizes.
EXAMPLE:
# Generate a test fileset with 2 levels, 10 files per dir, 1MB each
filegen /tmp 3,2 10 1M --progress
This creates a directory tree at /tmp/filegen/ with 3 top-level dirs, each containing 2 subdirs, with 10 files of 1MB each in every directory."
)]
struct Args {
#[arg()]
root: std::path::PathBuf,
#[arg(value_name = "SPEC", help_heading = "Generation options")]
dirwidth: Dirwidth,
#[arg(value_name = "N", help_heading = "Generation options")]
numfiles: usize,
#[arg(value_name = "SIZE", help_heading = "Generation options")]
filesize: String,
#[arg(
long,
default_value = "4K",
value_name = "SIZE",
help_heading = "Generation options"
)]
bufsize: String,
#[arg(long, help_heading = "Generation options")]
leaf_files: bool,
#[arg(long, help_heading = "Progress & output")]
progress: bool,
#[arg(long, value_name = "TYPE", help_heading = "Progress & output")]
progress_type: Option<common::ProgressType>,
#[arg(long, value_name = "DELAY", help_heading = "Progress & output")]
progress_delay: Option<String>,
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, help_heading = "Progress & output")]
verbose: u8,
#[arg(long, help_heading = "Progress & output")]
summary: bool,
#[arg(short = 'q', long = "quiet", help_heading = "Progress & output")]
quiet: bool,
#[arg(long, value_name = "N", help_heading = "Performance & throttling")]
max_open_files: Option<usize>,
#[arg(
long,
default_value = "0",
value_name = "N",
help_heading = "Performance & throttling"
)]
ops_throttle: usize,
#[arg(
long,
default_value = "0",
value_name = "N",
help_heading = "Performance & throttling"
)]
iops_throttle: usize,
#[arg(
long,
default_value = "0",
value_name = "SIZE",
help_heading = "Performance & throttling"
)]
chunk_size: u64,
#[arg(
long,
default_value = "0",
value_name = "N",
help_heading = "Advanced settings"
)]
max_workers: usize,
#[arg(
long,
default_value = "0",
value_name = "N",
help_heading = "Advanced settings"
)]
max_blocking_threads: usize,
}
#[instrument]
async fn async_main(args: Args) -> Result<common::filegen::Summary> {
use anyhow::Context;
let filesize = args
.filesize
.parse::<bytesize::ByteSize>()
.unwrap()
.as_u64() as usize;
let writebuf = args.bufsize.parse::<bytesize::ByteSize>().unwrap().as_u64() as usize;
let root = args.root.join("filegen");
tokio::fs::create_dir(&root)
.await
.with_context(|| format!("Error creating {:?}", &root))
.map_err(|err| common::filegen::Error::new(err, common::filegen::Summary::default()))?;
let prog_track = common::get_progress();
prog_track.directories_created.inc();
let mut summary = common::filegen::Summary {
directories_created: 1,
..Default::default()
};
let config = common::filegen::FileGenConfig {
root: root.clone(),
dirwidth: args.dirwidth.value.clone(),
numfiles: args.numfiles,
filesize,
writebuf,
chunk_size: args.chunk_size,
leaf_files: args.leaf_files,
};
let filegen_summary = common::filegen::filegen(prog_track, &config).await?;
summary = summary + filegen_summary;
Ok(summary)
}
fn main() -> Result<(), anyhow::Error> {
let args = Args::parse();
let func = {
let args = args.clone();
|| async_main(args)
};
let output = common::OutputConfig {
quiet: args.quiet,
verbose: args.verbose,
print_summary: args.summary,
..Default::default()
};
let runtime = common::RuntimeConfig {
max_workers: args.max_workers,
max_blocking_threads: args.max_blocking_threads,
};
let max_open_files = args.max_open_files.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
});
let throttle = common::ThrottleConfig {
max_open_files: Some(max_open_files),
ops_throttle: args.ops_throttle,
iops_throttle: args.iops_throttle,
chunk_size: args.chunk_size,
};
let tracing = common::TracingConfig {
remote_layer: None,
debug_log_file: None,
chrome_trace_prefix: None,
flamegraph_prefix: None,
trace_identifier: "filegen".to_string(),
profile_level: None,
tokio_console: false,
tokio_console_port: None,
};
let res = common::run(
if args.progress || args.progress_type.is_some() {
Some(common::ProgressSettings {
progress_type: common::GeneralProgressType::User(
args.progress_type.unwrap_or_default(),
),
progress_delay: args.progress_delay,
})
} else {
None
},
output,
runtime,
throttle,
tracing,
func,
);
if res.is_none() {
std::process::exit(1);
}
Ok(())
}