clineup 0.2.5

A command-line utility for organizing media files
Documentation
use std::{fs::File, path::Path};

fn copy_directory(source: &Path, target: &Path) -> std::io::Result<()> {
    let mut stack = Vec::new();
    stack.push((source.to_path_buf(), target.to_path_buf()));

    while let Some((src, dst)) = stack.pop() {
        if src.is_dir() {
            std::fs::create_dir_all(&dst)?;

            for entry in std::fs::read_dir(&src)? {
                let entry = entry?;
                let entry_src = entry.path();
                let entry_dst = dst.join(entry_src.file_name().unwrap());

                if entry_src.is_dir() {
                    stack.push((entry_src, entry_dst));
                } else if entry_src.is_file() {
                    std::fs::copy(entry_src, entry_dst)?;
                }
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use assert_cmd::Command;
    use predicates::prelude::*;
    use std::path::PathBuf;
    use tempdir::TempDir;
    #[test]
    fn test_cmd_help() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let assert = cmd.arg("--help").assert();
        assert.success().stdout(predicates::str::contains("USAGE"));
    }
    #[test]
    fn test_cmd_missing_folder_or_filename() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let assert = cmd
            .arg("--source=whatever")
            .arg("--destination=whatever")
            .assert();
        assert.failure().stdout(predicates::str::contains(
            "You should provide at least one of the folder or filename format.",
        ));
    }

    #[test]
    fn test_cmd_missing_folder_or_filename_but_verbose() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let assert = cmd
            .arg("--source=whatever")
            .arg("--destination=whatever")
            .arg("-vv")
            .assert();
        assert
            .failure()
            .stderr(predicates::str::contains("DEBUG"))
            .stderr(predicates::str::contains("Get config"));
    }
    #[test]
    fn test_cmd_copy_strategy() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/");
        let input_tmp_dir = TempDir::new("input_copy_strategy").unwrap();
        let output_tmp_dir = TempDir::new("output_copy_strategy").unwrap();

        // Copy content of data folder inside tmp dir :
        copy_directory(&data_path, &input_tmp_dir.path()).unwrap();
        let assert = cmd
            .arg(format!(
                "--source={}",
                format!("{}", input_tmp_dir.path().to_string_lossy())
            ))
            .arg(format!(
                "--destination={}",
                output_tmp_dir.path().to_string_lossy()
            ))
            .arg("--folder-format={%year}")
            .arg("--strategy=copy")
            .arg("-vv")
            .assert();
        assert.success();

        let expected_2023 = Path::new(output_tmp_dir.path())
            .join("2023")
            .join("Paris-20230304.jpg");
        assert!(expected_2023.exists());

        let expected_unknown_year = Path::new(output_tmp_dir.path())
            .join("Unknown Year")
            .join("Paris.png");
        assert!(expected_unknown_year.exists());

        let still_exist = Path::new(input_tmp_dir.path()).join("Paris-20230304.jpg");
        assert!(still_exist.exists());
        let expected_number_of_files = 3;
        let entries = std::fs::read_dir(Path::new(output_tmp_dir.path()).join("2023"))
            .unwrap()
            .count();
        assert!(entries == expected_number_of_files)
    }

    #[test]
    fn test_cmd_move_strategy() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/");
        let input_tmp_dir = TempDir::new("input_move_strategy").unwrap();
        let output_tmp_dir = TempDir::new("output_move_strategy").unwrap();

        // Copy content of data folder inside tmp dir :
        copy_directory(&data_path, &input_tmp_dir.path()).unwrap();
        let assert = cmd
            .arg(format!(
                "--source={}",
                format!("{}", input_tmp_dir.path().to_string_lossy())
            ))
            .arg(format!(
                "--destination={}",
                output_tmp_dir.path().to_string_lossy()
            ))
            .arg("--folder-format={%year}")
            .arg("--strategy=move")
            .arg("-vv")
            .assert();
        assert.success();

        let expected_2023 = Path::new(output_tmp_dir.path())
            .join("2023")
            .join("Paris-20230304.jpg");
        assert!(expected_2023.exists());
        let expected_unknown_year = Path::new(output_tmp_dir.path())
            .join("Unknown Year")
            .join("Paris.png");

        assert!(expected_unknown_year.exists());

        let should_not_exist = Path::new(input_tmp_dir.path()).join("Paris-20230304.jpg");
        assert!(!should_not_exist.exists());

        let expected_number_of_files = 3;
        let entries = std::fs::read_dir(Path::new(output_tmp_dir.path()).join("2023"))
            .unwrap()
            .count();
        assert!(entries == expected_number_of_files)
    }

    #[test]
    fn test_cmd_symlink_strategy() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/");
        let input_tmp_dir = TempDir::new("input_symlink_strategy").unwrap();
        let output_tmp_dir = TempDir::new("output_symlink_strategy").unwrap();

        // Copy content of data folder inside tmp dir :
        copy_directory(&data_path, &input_tmp_dir.path()).unwrap();
        let assert = cmd
            .arg(format!(
                "--source={}",
                format!("{}", input_tmp_dir.path().to_string_lossy())
            ))
            .arg(format!(
                "--destination={}",
                output_tmp_dir.path().to_string_lossy()
            ))
            .arg("--folder-format={%year}")
            .arg("--strategy=move")
            .arg("-vv")
            .assert();
        assert.success();

        let expected = Path::new(output_tmp_dir.path())
            .join("2023")
            .join("Paris-20230304.jpg");
        assert!(expected.exists());

        let expected_number_of_files = 3;
        let entries = std::fs::read_dir(Path::new(output_tmp_dir.path()).join("2023"))
            .unwrap()
            .count();
        assert!(entries == expected_number_of_files)
    }
    #[test]
    fn test_cmd_copy_strategy_recursive() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/");
        let input_tmp_dir = TempDir::new("input_copy_strategy").unwrap();
        let output_tmp_dir = TempDir::new("output_copy_strategy").unwrap();

        // Copy content of data folder inside tmp dir :
        copy_directory(&data_path, &input_tmp_dir.path()).unwrap();
        let assert = cmd
            .arg(format!(
                "--source={}",
                format!("{}", input_tmp_dir.path().to_string_lossy())
            ))
            .arg(format!(
                "--destination={}",
                output_tmp_dir.path().to_string_lossy()
            ))
            .arg("--folder-format={%year}")
            .arg("--strategy=copy")
            .arg("--recursive")
            .arg("-vv")
            .assert();
        assert.success();
        for folder in 2013..2023 {
            let path = Path::new(output_tmp_dir.path()).join(format!("{}", folder));
            assert!(path.exists())
        }
    }
    #[test]
    fn test_cmd_copy_strategy_recursive_exclude_regex() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/");
        let input_tmp_dir = TempDir::new("input_copy_strategy").unwrap();
        let output_tmp_dir = TempDir::new("output_copy_strategy").unwrap();

        // Copy content of data folder inside tmp dir :
        copy_directory(&data_path, &input_tmp_dir.path()).unwrap();
        let assert = cmd
            .arg(format!(
                "--source={}",
                format!("{}", input_tmp_dir.path().to_string_lossy())
            ))
            .arg(format!(
                "--destination={}",
                output_tmp_dir.path().to_string_lossy()
            ))
            .arg("--folder-format={%year}")
            .arg("--strategy=copy")
            .arg("--recursive")
            .arg("--exclude-regex=.*Paris-20130101.jpg.*")
            .arg("-vv")
            .assert();
        assert.success();
        let should_not_exist = Path::new(output_tmp_dir.path()).join("2013");
        assert!(!should_not_exist.exists());
        for folder in 2014..2023 {
            let path = Path::new(output_tmp_dir.path()).join(format!("{}", folder));
            assert!(path.exists())
        }
    }
    #[test]
    fn test_cmd_copy_strategy_recursive_include_regex() {
        let mut cmd = Command::cargo_bin("clineup").unwrap();
        let data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/");
        let input_tmp_dir = TempDir::new("input_copy_strategy").unwrap();
        let output_tmp_dir = TempDir::new("output_copy_strategy").unwrap();

        // Copy content of data folder inside tmp dir :
        copy_directory(&data_path, &input_tmp_dir.path()).unwrap();
        let assert = cmd
            .arg(format!(
                "--source={}",
                format!("{}", input_tmp_dir.path().to_string_lossy())
            ))
            .arg(format!(
                "--destination={}",
                output_tmp_dir.path().to_string_lossy()
            ))
            .arg("--folder-format={%year}")
            .arg("--strategy=copy")
            .arg("--recursive")
            .arg("--include-regex=.*Paris-20130101.jpg.*")
            .arg("-vv")
            .assert();
        assert.success();
        let should_exist = Path::new(output_tmp_dir.path()).join("2013");
        assert!(should_exist.exists());

        // All the others should not exist
        for folder in 2014..2023 {
            let path = Path::new(output_tmp_dir.path()).join(format!("{}", folder));
            assert!(!path.exists())
        }
    }
}