pub mod archiver;
pub mod extractor;
pub mod format;
pub(crate) mod outputs;
use clap::ValueEnum;
use ignore::WalkBuilder;
use std::collections::HashSet;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use typed_builder::TypedBuilder;
use crate::archiver::ArchiveEntries;
use crate::extractor::Entries;
use crate::format::{default_format_detector, FormatDetector};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, ValueEnum, PartialEq, Copy, Hash, Eq)]
pub enum IgnoreType {
Default,
Hidden,
GitIgnore,
GitGlobal,
GitExclude,
Ignore,
}
#[derive(Debug)]
pub enum Error {
Archiver(String),
Array(Vec<Error>),
DestIsDir(PathBuf),
DirExists(PathBuf),
Extractor(String),
Fatal(Box<dyn std::error::Error>),
FileNotFound(PathBuf),
FileExists(PathBuf),
IO(std::io::Error),
Json(serde_json::Error),
NoArgumentsGiven,
Warn(String),
UnknownFormat(String),
UnsupportedFormat(String),
Xml(serde_xml_rs::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Archiver(s) => write!(f, "Archiver error: {s}"),
Error::Array(errs) => {
errs.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
.fmt(f)
},
Error::DestIsDir(p) => write!(f, "{}: Destination is a directory", p.to_str().unwrap()),
Error::DirExists(p) => write!(f, "{}: Directory already exists", p.to_str().unwrap()),
Error::Extractor(s) => write!(f, "Extractor error: {s}"),
Error::Fatal(e) => write!(f, "Error: {e}"),
Error::FileNotFound(p) => write!(f, "{}: File not found", p.to_str().unwrap()),
Error::FileExists(p) => write!(f, "{}: File already exists", p.to_str().unwrap()),
Error::IO(e) => write!(f, "IO error: {e}"),
Error::Json(e) => write!(f, "Json error: {e}"),
Error::NoArgumentsGiven => write!(f, "No arguments given. Use --help for usage."),
Error::Warn(s) => write!(f, "Unknown error: {s}"),
Error::UnknownFormat(s) => write!(f, "{s}: Unknown format"),
Error::UnsupportedFormat(s) => write!(f, "{s}: Unsupported format"),
Error::Xml(e) => write!(f, "Xml error: {e}"),
}
}
}
impl Error {
pub fn error_or<T>(ok: T, errs: Vec<Self>) -> Result<T> {
if errs.is_empty() {
Ok(ok)
} else if errs.len() == 1 {
Err(errs.into_iter().next().unwrap())
} else {
Err(Error::Array(errs))
}
}
pub fn error_or_else<F, O>(ok: F, errs: Vec<Self>) -> Result<O>
where
F: FnOnce() -> O,
{
if errs.is_empty() {
Ok(ok())
} else if errs.len() == 1 {
Err(errs.into_iter().next().unwrap())
} else {
Err(Error::Array(errs))
}
}
}
pub fn extract<P: AsRef<Path>>(archive_file: P, config: &ExtractConfig) -> Result<()> {
let archive_file = archive_file.as_ref();
let base_dir = config.dest(archive_file)?;
let extractor = config.extractor(archive_file)?;
extractor.perform(archive_file.to_path_buf(), base_dir)
}
#[derive(TypedBuilder)]
pub struct ExtractConfig {
#[builder(setter(into), default = PathBuf::from("."))]
pub dest: PathBuf,
#[builder(default = false)]
pub overwrite: bool,
#[builder(default = false)]
pub use_archive_name_dir: bool,
#[builder(default = default_format_detector())]
pub format_detector: Box<dyn FormatDetector>,
}
impl ExtractConfig {
pub(crate) fn dest(&self, archive_file: &Path) -> Result<PathBuf> {
let dest = if self.use_archive_name_dir {
let stem = archive_file
.file_stem()
.unwrap_or_else(|| std::ffi::OsStr::new("archive"));
self.dest.join(stem)
} else {
self.dest.clone()
};
if dest.exists() && !self.overwrite {
if dest == PathBuf::from(".") || dest == PathBuf::from("..") {
Ok(dest)
} else {
Err(Error::DirExists(dest))
}
} else {
Ok(dest)
}
}
pub fn extractor(&self, archive_file: &Path) -> Result<Box<dyn crate::extractor::ToteExtractor>> {
let format = self.format_detector.detect(archive_file);
crate::extractor::create_with(archive_file, format)
}
}
pub fn entries<P: AsRef<Path>>(archive_file: P, format_detector: &dyn FormatDetector) -> Result<Entries> {
let archive_file = archive_file.as_ref();
let format = format_detector.detect(archive_file);
let extractor = crate::extractor::create_with(archive_file, format)?;
extractor.list(archive_file.to_path_buf())
}
pub fn list<P: AsRef<Path>>(archive_file: P, config: &ListConfig) -> Result<String> {
match entries(archive_file, config.format_detector.as_ref()) {
Err(e) => Err(e),
Ok(entries) => format_for_output(entries, &config.format),
}
}
fn format_for_output(entries: Entries, f: &OutputFormat) -> Result<String> {
use OutputFormat::*;
match f {
Default => outputs::to_string(&entries),
Long => outputs::to_string_long(&entries),
Json => serde_json::to_string(&entries).map_err(Error::Json),
PrettyJson => serde_json::to_string_pretty(&entries).map_err(Error::Json),
Xml => serde_xml_rs::to_string(&entries).map_err(Error::Xml),
}
}
pub struct ListConfig {
pub format: OutputFormat,
format_detector: Box<dyn FormatDetector>,
}
impl ListConfig {
pub fn new(format: OutputFormat, format_detector: Box<dyn FormatDetector>) -> Self {
Self { format, format_detector }
}
}
#[derive(ValueEnum, Debug, Clone)]
pub enum OutputFormat {
Default,
Long,
Json,
PrettyJson,
Xml,
}
pub fn archive<P: AsRef<Path>>(
archive_targets: &[P],
config: &ArchiveConfig,
) -> Result<ArchiveEntries> {
let dest_file = config.dest_file()?;
log::info!("{:?}: {}", dest_file, dest_file.exists());
let archiver = archiver::create(&dest_file)?;
if let Some(parent) = dest_file.parent() {
if !parent.exists() {
if let Err(e) = std::fs::create_dir_all(parent) {
return Err(Error::IO(e));
}
}
}
let targets = prepare_targets(archive_targets);
match std::fs::File::create(&dest_file) {
Ok(file) => match archiver.perform(file, &targets, config) {
Ok(entries) => {
let compressed = dest_file.metadata().map(|m| m.len()).unwrap_or(0);
Ok(ArchiveEntries::new(dest_file, entries, compressed))
}
Err(e) => Err(e),
},
Err(e) => Err(Error::IO(e)),
}
}
fn prepare_targets<P: AsRef<Path>>(targets: &[P]) -> Vec<PathBuf> {
targets
.iter()
.map(|p| p.as_ref().to_path_buf())
.collect()
}
#[derive(TypedBuilder, Debug, Clone)]
pub struct ArchiveConfig {
#[builder(setter(into), default = PathBuf::from("totebag.zip"))]
pub dest: PathBuf,
#[builder(default = 5)]
pub level: u8,
#[builder(default = None, setter(strip_option, into))]
pub rebase_dir: Option<PathBuf>,
#[builder(default = false)]
pub overwrite: bool,
#[builder(default = false)]
pub no_recursive: bool,
#[builder(default = vec![IgnoreType::Default], setter(into))]
pub ignore: Vec<IgnoreType>,
}
impl ArchiveConfig {
pub fn dest_file(&self) -> Result<PathBuf> {
let dest_path = self.dest.clone();
if dest_path.exists() {
if dest_path.is_file() && !self.overwrite {
Err(Error::FileExists(dest_path))
} else if self.dest.is_dir() {
Err(Error::DestIsDir(dest_path))
} else {
Ok(dest_path)
}
} else {
Ok(dest_path)
}
}
pub fn path_in_archive<P: AsRef<Path>>(&self, path: P) -> PathBuf {
let from_path = path.as_ref();
let to_path = if let Some(rebase) = &self.rebase_dir {
rebase.join(from_path)
} else {
from_path.to_path_buf()
};
log::debug!("dest_path({from_path:?}) -> {to_path:?}");
to_path
}
pub fn iter<P: AsRef<Path>>(&self, path: P) -> impl Iterator<Item = ignore::DirEntry> {
let mut builder = WalkBuilder::new(path);
build_walker_impl(self, &mut builder);
builder.build().flatten()
}
pub fn ignore_types(&self) -> Vec<IgnoreType> {
if self.ignore.is_empty() {
vec![
IgnoreType::Ignore,
IgnoreType::GitIgnore,
IgnoreType::GitGlobal,
IgnoreType::GitExclude,
]
} else {
let mut r = HashSet::<IgnoreType>::new();
for &it in &self.ignore {
if it == IgnoreType::Default {
r.insert(IgnoreType::Ignore);
r.insert(IgnoreType::GitIgnore);
r.insert(IgnoreType::GitGlobal);
r.insert(IgnoreType::GitExclude);
} else {
r.insert(it);
}
}
r.into_iter().collect()
}
}
}
fn build_walker_impl(opts: &ArchiveConfig, w: &mut WalkBuilder) {
for it in opts.ignore_types() {
match it {
IgnoreType::Default => w
.ignore(true)
.git_ignore(true)
.git_global(true)
.git_exclude(true),
IgnoreType::GitIgnore => w.git_ignore(true),
IgnoreType::GitGlobal => w.git_global(true),
IgnoreType::GitExclude => w.git_exclude(true),
IgnoreType::Hidden => w.hidden(true),
IgnoreType::Ignore => w.ignore(true),
};
}
}
mod tests {
#[test]
fn test_error_message() {
use crate::Error;
use std::path::PathBuf;
assert_eq!(
Error::Archiver("hoge".into()).to_string(),
"Archiver error: hoge"
);
assert_eq!(
Error::Extractor("hoge".into()).to_string(),
"Extractor error: hoge"
);
assert_eq!(
Error::DestIsDir(PathBuf::from("hoge")).to_string(),
"hoge: Destination is a directory"
);
assert_eq!(
Error::DirExists(PathBuf::from("hoge")).to_string(),
"hoge: Directory already exists"
);
assert_eq!(
Error::Fatal(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "hoge"))).to_string(),
"Error: hoge"
);
assert_eq!(
Error::Json(serde::de::Error::custom("hoge")).to_string(),
"Json error: hoge"
);
assert_eq!(
Error::Xml(serde_xml_rs::Error::Custom("hoge".into())).to_string(),
"Xml error: Custom: hoge"
);
assert_eq!(
Error::IO(std::io::Error::new(std::io::ErrorKind::NotFound, "hoge")).to_string(),
"IO error: hoge"
);
assert_eq!(
Error::FileNotFound("hoge".into()).to_string(),
"hoge: File not found"
);
assert_eq!(
Error::FileExists("hoge".into()).to_string(),
"hoge: File already exists"
);
assert_eq!(
Error::UnknownFormat("hoge".to_string()).to_string(),
"hoge: Unknown format"
);
assert_eq!(
Error::UnsupportedFormat("hoge".to_string()).to_string(),
"hoge: Unsupported format"
);
assert_eq!(
Error::Warn("message".to_string()).to_string(),
"Unknown error: message"
);
assert_eq!(
Error::NoArgumentsGiven.to_string(),
"No arguments given. Use --help for usage."
);
assert_eq!(
Error::Array(vec![
Error::Warn("hoge1".to_string()),
Error::Warn("hoge2".to_string())
])
.to_string(),
"Unknown error: hoge1\nUnknown error: hoge2"
);
}
}