use clap::{Parser, Subcommand};
use std::io::{self};
use std::path::PathBuf;
use std::process;
use bindle_file::{Bindle, Compress};
#[derive(Parser)]
#[command(name = "bindle")]
#[command(version = "1.0")]
#[command(author = "zshipko")]
#[command(about = "Append-only file collection")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
List {
#[arg(value_name = "BINDLE_FILE")]
bindle_file: PathBuf,
},
Add {
#[arg(value_name = "BINDLE_FILE")]
bindle_file: PathBuf,
name: String,
file_path: Option<PathBuf>,
#[arg(short, long)]
compress: bool,
#[arg(short, long, conflicts_with = "file_path")]
data: Option<String>,
#[arg(long)]
vacuum: bool,
},
Cat {
#[arg(value_name = "BINDLE_FILE")]
bindle_file: PathBuf,
name: String,
},
Remove {
#[arg(value_name = "BINDLE_FILE")]
bindle_file: PathBuf,
name: String,
#[arg(long)]
vacuum: bool,
},
Pack {
#[arg(value_name = "BINDLE_FILE")]
bindle_file: PathBuf,
#[arg(value_name = "SRC_DIR")]
src_dir: PathBuf,
#[arg(short, long)]
compress: bool,
#[arg(short, long)]
append: bool,
#[arg(long)]
vacuum: bool,
},
Unpack {
#[arg(value_name = "BINDLE_FILE")]
bindle_file: PathBuf,
#[arg(value_name = "DEST_DIR")]
dest_dir: PathBuf,
},
Vacuum {
#[arg(value_name = "BINDLE_FILE")]
bindle_file: PathBuf,
},
}
fn main() {
let cli = Cli::parse();
if let Err(e) = handle_command(cli.command) {
eprintln!("ERROR {}", e);
process::exit(1);
}
}
fn handle_command(command: Commands) -> io::Result<()> {
let init = |path: PathBuf| match Bindle::open(&path) {
Ok(bindle) => bindle,
Err(e) => {
eprintln!("ERROR unable to open {}: {}", path.display(), e);
process::exit(1);
}
};
let init_load = |path: PathBuf| match Bindle::load(&path) {
Ok(bindle) => bindle,
Err(e) => {
eprintln!("ERROR unable to open {}: {}", path.display(), e);
process::exit(1);
}
};
match command {
Commands::List { bindle_file } => {
println!(
"{:<30} {:<12} {:<12} {:<10}",
"NAME", "SIZE", "PACKED", "RATIO"
);
println!("{}", "-".repeat(70));
if !bindle_file.exists() {
return Ok(());
}
let b = init_load(bindle_file);
for (name, entry) in b.index().iter() {
let size = entry.uncompressed_size();
let packed = entry.compressed_size();
let ratio = if size > 0 {
(packed as f64 / size as f64) * 100.0
} else {
100.0
};
println!("{:<30} {:<12} {:<12} {:.1}%", name, size, packed, ratio);
}
}
Commands::Add {
name,
file_path,
data: data_arg,
compress,
bindle_file,
vacuum,
} => {
let mut b = init(bindle_file.clone());
let compress_mode = if compress {
Compress::Zstd
} else {
Compress::None
};
let size = if let Some(d) = data_arg {
let bytes = d.into_bytes();
let len = bytes.len();
b.add(&name, &bytes, compress_mode)?;
len
} else if let Some(path) = file_path {
b.add_file(&name, &path, compress_mode)?;
std::fs::metadata(&path)?.len() as usize
} else {
let mut writer = b.writer(&name, compress_mode)?;
let size = io::copy(&mut io::stdin(), &mut writer)?;
writer.close()?;
size as usize
};
println!(
"ADD '{}' -> {} ({} bytes)",
name,
bindle_file.display(),
size
);
b.save()?;
if vacuum {
println!("VACUUM {}", bindle_file.display());
b.vacuum()?;
}
println!("OK");
}
Commands::Cat { name, bindle_file } => {
let b = init_load(bindle_file.clone());
match b.read_to(name.as_str(), io::stdout()) {
Ok(_n) => {}
Err(e) => {
return Err(io::Error::new(io::ErrorKind::NotFound, e));
}
}
}
Commands::Remove {
name,
bindle_file,
vacuum,
} => {
let mut b = init(bindle_file.clone());
if b.remove(&name) {
println!("REMOVE '{}' from {}", name, bindle_file.display());
b.save()?;
if vacuum {
println!("VACUUM {}", bindle_file.display());
b.vacuum()?;
}
println!("OK");
} else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("ERROR '{}' not found in {}", name, bindle_file.display()),
));
}
}
Commands::Pack {
bindle_file,
src_dir,
compress,
append,
vacuum,
} => {
println!("PACK {} -> {}", src_dir.display(), bindle_file.display());
let mut b = init(bindle_file.clone());
if !append {
b.clear();
}
b.pack(
src_dir,
if compress {
Compress::Zstd
} else {
Compress::None
},
)?;
b.save()?;
if vacuum {
println!("VACUUM {}", bindle_file.display());
b.vacuum()?;
}
println!("OK");
}
Commands::Unpack {
bindle_file,
dest_dir,
} => {
println!("UNPACK {} -> {}", bindle_file.display(), dest_dir.display());
let b = init_load(bindle_file);
b.unpack(dest_dir)?;
println!("OK");
}
Commands::Vacuum { bindle_file } => {
println!("VACUUM {}", bindle_file.display());
let mut b = init_load(bindle_file);
b.vacuum()?;
println!("OK");
}
}
Ok(())
}