pub mod archiver;
pub mod extractor;
pub mod format;
pub(crate) mod outputs;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
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::{FormatDetector, default_format_detector};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq, Copy, Hash, Eq)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
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 + Send + Sync>),
FeatureDisabled {
format: String,
feature: &'static str,
},
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::FeatureDisabled { format, feature } => write!(
f,
"{format}: support is not compiled in (rebuild with --features {feature})"
),
Error::FileNotFound(p) => write!(f, "{}: File not found", p.display()),
Error::FileExists(p) => write!(f, "{}: File already exists", p.display()),
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 std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Fatal(e) => Some(e.as_ref()),
Error::IO(e) => Some(e),
Error::Json(e) => Some(e),
Error::Xml(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::IO(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Json(e)
}
}
impl From<serde_xml_rs::Error> for Error {
fn from(e: serde_xml_rs::Error) -> Self {
Error::Xml(e)
}
}
impl Error {
pub(crate) fn unsupported_for_archiving(format: &str) -> Self {
Error::UnsupportedFormat(format!("{format} (archiving)"))
}
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 == Path::new(".") || dest == Path::new("..") {
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(Debug, Clone)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
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()
&& !parent.exists()
{
std::fs::create_dir_all(parent).map_err(Error::IO)?;
}
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(fallback = rebase_dir_opt), 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 normalized = normalize_entry_path(from_path);
let to_path = match &self.rebase_dir {
Some(rebase) => normalize_entry_path(rebase).join(&normalized),
None => normalized,
};
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()
}
}
}
pub fn normalize_entry_path<P: AsRef<Path>>(path: P) -> PathBuf {
use std::path::Component;
let mut components: Vec<std::ffi::OsString> = Vec::new();
for component in path.as_ref().components() {
match component {
Component::Prefix(_) | Component::RootDir | Component::CurDir => {}
Component::ParentDir => {
components.pop();
}
Component::Normal(c) => components.push(c.to_os_string()),
}
}
components.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),
};
}
}
#[cfg(test)]
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::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::unsupported_for_archiving("Rar").to_string(),
"Rar (archiving): Unsupported format"
);
assert_eq!(
Error::FeatureDisabled {
format: "Rar".to_string(),
feature: "rar",
}
.to_string(),
"Rar: support is not compiled in (rebuild with --features rar)"
);
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"
);
}
}