mod options;
mod progress;
use std::path::PathBuf;
use std::{result, thread};
use std::sync::Arc;
use glob::{glob, Paths};
use libxcp::config::{Config, Reflink};
use libxcp::drivers::load_driver;
use libxcp::errors::{Result, XcpError};
use libxcp::feedback::{ChannelUpdater, StatusUpdate, StatusUpdater};
use log::{error, info, warn};
use crate::options::Opts;
fn init_logging(opts: &Opts) -> Result<()> {
use simplelog::{ColorChoice, Config, SimpleLogger, TermLogger, TerminalMode};
TermLogger::init(
opts.log_level(),
Config::default(),
TerminalMode::Mixed,
ColorChoice::Auto,
).or_else(
|_| SimpleLogger::init(opts.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 opts_check(opts: &Opts) -> Result<()> {
#[cfg(any(target_os = "linux", target_os = "android"))]
if opts.reflink == Reflink::Never {
warn!("--reflink=never is selected, however the Linux kernel may override this.");
}
if opts.no_clobber && opts.force {
return Err(XcpError::InvalidArguments("--force and --noclobber cannot be set at the same time.".to_string()).into());
}
Ok(())
}
fn main() -> Result<()> {
let opts = Opts::from_args()?;
init_logging(&opts)?;
opts_check(&opts)?;
let (dest, source_patterns) = match opts.target_directory {
Some(ref d) => { (d, opts.paths.as_slice()) }
None => {
opts.paths.split_last().ok_or(XcpError::InvalidArguments("Insufficient arguments".to_string()))?
}
};
let dest = PathBuf::from(dest);
let sources = expand_sources(source_patterns, &opts)?;
if sources.is_empty() {
return Err(XcpError::InvalidSource("No source files found.").into());
} else if !dest.is_dir() {
if sources.len() == 1 && sources[0].is_dir() && dest.exists() {
return Err(XcpError::InvalidDestination("Cannot copy a directory to a file.").into());
} else if sources.len() > 1 {
return Err(XcpError::InvalidDestination("Multiple sources and destination is not a directory.").into());
}
}
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());
}
let sourcedir = source
.components()
.next_back()
.ok_or(XcpError::InvalidSource("Failed to find source directory name."))?;
let target_base = if dest.exists() && dest.is_dir() && !opts.no_target_directory {
dest.join(sourcedir)
} else {
dest.to_path_buf()
};
if source == &target_base {
return Err(XcpError::InvalidSource("Source is same as destination").into());
}
}
let config = Arc::new(Config::from(&opts));
let driver = load_driver(opts.driver, &config)?;
let updater = ChannelUpdater::new(&config);
let stat_rx = updater.rx_channel();
let stats: Arc<dyn StatusUpdater> = Arc::new(updater);
let handle = thread::spawn(move || -> Result<()> {
driver.copy(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(())
}