cockup 0.2.0

Yet another backup tool for various configurations
use super::{
    copy::copy,
    paths::{check_overlap, remove, resolved},
    state::CopyState,
};
use crate::{
    config::Symlinks,
    manifest::{Link, TargetLink},
    report,
};
use std::{
    fs,
    path::{Path, PathBuf},
};

pub(super) fn restore_link(
    src: &Path,
    dst: &Path,
    metadata: bool,
    ancestors: &[PathBuf],
    state: &mut CopyState,
    key: &Path,
) -> Option<Result<(), String>> {
    if let Some(index) = state
        .manifest
        .links
        .iter()
        .position(|l| l.backup_path == key)
    {
        let link = state.manifest.links.remove(index);
        let updating = fs::symlink_metadata(dst).is_ok();
        let result = (|| -> Result<(), String> {
            check_overlap(src, dst)?;
            if link.mode == Symlinks::Dereference {
                for entry in &link.target_chain {
                    check_overlap(src, &state.mapped(&entry.location))?;
                }
                copy(
                    src,
                    &state.mapped(&link.resolved_target),
                    metadata,
                    Symlinks::Referece,
                    ancestors,
                    state,
                )?;
                if state.dry_run {
                    for entry in link.target_chain.iter().rev() {
                        let location = state.mapped(&entry.location);
                        println!(
                            "Would restore symlink: {} -> {}",
                            location.display(),
                            state
                                .link_target(&entry.location, &entry.target, &location)
                                .display()
                        );
                    }
                }
                if !state.dry_run {
                    for entry in link.target_chain.iter().rev() {
                        let location = state.mapped(&entry.location);
                        fs::create_dir_all(location.parent().unwrap())
                            .map_err(|e| e.to_string())?;
                        remove(&location).map_err(|e| e.to_string())?;
                        std::os::unix::fs::symlink(
                            state.link_target(&entry.location, &entry.target, &location),
                            &location,
                        )
                        .map_err(|e| e.to_string())?;
                    }
                }
            }
            if state.dry_run {
                println!(
                    "Would {} symlink: {} -> {}",
                    if updating { "replace" } else { "restore" },
                    dst.display(),
                    state
                        .link_target(&link.location, &link.target, dst)
                        .display()
                );
                return Ok(());
            }
            if let Some(parent) = dst.parent() {
                fs::create_dir_all(parent).map_err(|e| e.to_string())?;
            }
            remove(dst).map_err(|e| e.to_string())?;
            std::os::unix::fs::symlink(state.link_target(&link.location, &link.target, dst), dst)
                .map_err(|e| e.to_string())?;
            Ok(())
        })();
        state
            .manifest
            .links
            .insert(index.min(state.manifest.links.len()), link);
        if result.is_ok() && !state.dry_run {
            report::copied("Symlink", updating, dst);
        }
        return Some(result);
    }
    None
}

pub(super) fn choose_mode(
    src: &Path,
    key: &Path,
    symlinks: Symlinks,
    link: bool,
    state: &mut CopyState,
) -> Result<Symlinks, String> {
    let planned = state.planned.iter().find(|l| l.backup_path == key);
    let choice = if link
        && !state.planning
        && !state.restore
        && let Some(planned) = planned
    {
        planned.mode
    } else if link && symlinks == Symlinks::Prompt {
        if state.prompt_choice.is_none() {
            let prompt = format!(
                "Symlink {} -> {}:\n[r]eferece link or [d]ereference target? (applies to all symlinks)",
                src.display(),
                fs::read_link(src).map_err(|e| e.to_string())?.display()
            );
            loop {
                report::warning(&prompt);
                match report::input("")?.to_lowercase().as_str() {
                    "r" | "referece" => {
                        state.prompt_choice = Some(Ok(Symlinks::Referece));
                        break;
                    }
                    "d" | "dereference" => {
                        state.prompt_choice = Some(Ok(Symlinks::Dereference));
                        break;
                    }
                    _ => report::warning("Please enter r or d."),
                }
            }
        }
        state.prompt_choice.as_ref().unwrap().clone()?
    } else {
        symlinks
    };
    Ok(choice)
}

pub(super) fn record_link(
    src: &Path,
    key: &Path,
    choice: Symlinks,
    link: bool,
    state: &CopyState,
) -> Result<Option<Link>, String> {
    let record = if link && !state.restore {
        let target = fs::read_link(src).map_err(|e| e.to_string())?;
        let target_path = if target.is_absolute() {
            target.clone()
        } else {
            src.parent().unwrap().join(&target)
        };
        let mut target_chain = Vec::new();
        if choice == Symlinks::Dereference {
            let mut next = target_path.clone();
            while fs::symlink_metadata(&next)
                .map_err(|e| e.to_string())?
                .is_symlink()
            {
                let target = fs::read_link(&next).map_err(|e| e.to_string())?;
                target_chain.push(TargetLink {
                    location: next.clone(),
                    target: target.clone(),
                });
                next = if target.is_absolute() {
                    target
                } else {
                    next.parent().unwrap().join(target)
                };
            }
        }
        Some(Link {
            location: src.to_path_buf(),
            target,
            resolved_target: resolved(&target_path).map_err(|e| e.to_string())?,
            mode: choice,
            backup_path: key.to_path_buf(),
            target_chain,
        })
    } else {
        None
    };
    Ok(record)
}