mod options;
mod progress;
use std::path::PathBuf;
use std::{result, thread};
use std::sync::Arc;
use glob::{glob, Paths};
use libfs::is_same_file;
use libxcp::config::Config;
use libxcp::drivers::load_driver;
use libxcp::errors::{Result, XcpError};
use libxcp::feedback::{ChannelUpdater, StatusUpdate, StatusUpdater};
use log::{error, info};
use crate::options::Opts;
fn init_logging(opts: &Opts) -> Result<()> {
use simplelog::{ColorChoice, Config, LevelFilter, SimpleLogger, TermLogger, TerminalMode};
let log_level = match opts.verbose {
0 => LevelFilter::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
TermLogger::init(
log_level,
Config::default(),
TerminalMode::Mixed,
ColorChoice::Auto,
).or_else(
|_| SimpleLogger::init(log_level, Config::default())
)?;
Ok(())
}
fn expand_globs(patterns: &[String]) -> Result<Vec<PathBuf>> {
let paths = patterns.iter()
.map(|s| glob(s.as_str()))
.collect::<result::Result<Vec<Paths>, _>>()?
.iter_mut()
.map::<result::Result<Vec<PathBuf>, _>, _>(Iterator::collect)
.collect::<result::Result<Vec<Vec<PathBuf>>, _>>()?
.iter()
.flat_map(ToOwned::to_owned)
.collect::<Vec<PathBuf>>();
Ok(paths)
}
fn expand_sources(source_list: &[String], opts: &Opts) -> Result<Vec<PathBuf>> {
if opts.glob {
expand_globs(source_list)
} else {
let pb = source_list.iter()
.map(PathBuf::from)
.collect::<Vec<PathBuf>>();
Ok(pb)
}
}
fn main() -> Result<()> {
let opts = options::parse_args()?;
init_logging(&opts)?;
let (dest, source_patterns) = opts
.paths
.split_last()
.ok_or(XcpError::InvalidArguments("Insufficient arguments".to_string()))
.map(|(d, s)| (PathBuf::from(d), s))?;
if source_patterns.len() > 1 && !dest.is_dir() {
return Err(XcpError::InvalidDestination("Multiple sources and destination is not a directory.").into());
}
let sources = expand_sources(source_patterns, &opts)?;
if sources.is_empty() {
return Err(XcpError::InvalidSource("No source files found.").into());
}
let config = Arc::new(Config::from(&opts));
let updater = ChannelUpdater::new(&config);
let stat_rx = updater.rx_channel();
let stats: Arc<dyn StatusUpdater> = Arc::new(updater);
let driver = load_driver(opts.driver, &config)?;
let handle = if sources.len() == 1 && dest.is_file() {
let source = sources[0].clone();
if opts.no_clobber {
return Err(XcpError::DestinationExists("Destination file exists and --no-clobber is set.", dest).into());
}
if is_same_file(&source, &dest)? {
return Err(XcpError::DestinationExists("Source and destination is the same file.", dest).into());
}
info!("Copying file {:?} to {:?}", source, dest);
thread::spawn(move || -> Result<()> {
driver.copy_single(&source, &dest, stats)
})
} else {
for source in &sources {
info!("Copying source {:?} to {:?}", source, dest);
if !source.exists() {
return Err(XcpError::InvalidSource("Source does not exist.").into());
}
if source.is_dir() && !opts.recursive {
return Err(XcpError::InvalidSource("Source is directory and --recursive not specified.").into());
}
if source == &dest {
return Err(XcpError::InvalidSource("Cannot copy a directory into itself").into());
}
if dest.exists() && !dest.is_dir() {
return Err(XcpError::InvalidDestination("Source is directory but target exists and is not a directory").into());
}
}
thread::spawn(move || -> Result<()> {
driver.copy_all(sources, &dest, stats)
})
};
let pb = progress::create_bar(&opts, 0)?;
for stat in stat_rx {
match stat {
StatusUpdate::Copied(v) => pb.inc(v),
StatusUpdate::Size(v) => pb.inc_size(v),
StatusUpdate::Error(e) => {
error!("Received error: {}", e);
return Err(e.into());
}
}
}
handle.join()
.map_err(|_| XcpError::CopyError("Error during copy operation".to_string()))??;
info!("Copy complete");
pb.end();
Ok(())
}