xcp 0.24.1

xcp is a (partial) clone of the Unix `cp` command, with more user-friendly feedback and some performance optimisations. See the README for features and limitations.
/*
 * Copyright © 2018, Steve Smith <tarkasteve@gmail.com>
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 3 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

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(())
}

// Expand a list of file-paths or glob-patterns into a list of concrete paths.
// FIXME: This currently eats non-existent files that are not
// globs. Should we convert empty glob results into errors?
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()
        // Force resolve each glob Paths iterator into a vector of the results...
        .map::<result::Result<Vec<PathBuf>, _>, _>(Iterator::collect)
        // And lift all the results up to the top.
        .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());
        }
    }

    // Sanity-check all sources up-front
    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());
        }
    }


    // ========== Start copy ============

    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)
    });


    // ========== Collect output and display ============

    let pb = progress::create_bar(&opts, 0)?;

    // Gather the results as we go; our end of the channel has been
    // moved to the driver call and will end when drained.
    for stat in stat_rx {
        match stat {
            StatusUpdate::Copied(v) => pb.inc(v),
            StatusUpdate::Size(v) => pb.inc_size(v),
            StatusUpdate::Error(e) => {
                // FIXME: Optional continue?
                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(())
}