Skip to content
Snippets Groups Projects
Select Git revision
  • 32a71dd33c50da2032603a34d0cdfd910120d194
  • main default
  • master protected
  • ci_2025
  • review_2025
  • 2023
6 results

README.md

Blame
  • push.rs 20.57 KiB
    use crate::commit;
    use crate::configt;
    use crate::list;
    use crate::remote;
    use colored::Colorize;
    use regex::Regex;
    use std::io::{self, Write};
    use std::path::PathBuf;
    use std::process::{exit, Command, Stdio};
    
    /// Get the file that will contains the archive listing in it
    /// # Return
    /// The path of the file archive_list inside the .borg repository of the
    /// current project
    fn get_list_file() -> PathBuf {
        let (mut borg_folder, _) = commit::check_path();
        borg_folder.push("archive_list");
        borg_folder
    }
    
    /// Get the location were the remote archiive list ill be stored
    /// # Return
    /// The path of the file archive_list_remote inside the .borg repository of the
    /// current project
    fn get_remote_list_file() -> PathBuf {
        let (mut borg_folder, _) = commit::check_path();
        borg_folder.push("archive_list_remote");
        borg_folder
    }
    
    /// Get the file that contains borg config
    /// # Return
    /// The path of the file config inside the .borg repository of the
    /// current project
    fn get_config_file() -> PathBuf {
        let (mut borg_folder, _) = commit::check_path();
        borg_folder.push("config");
        borg_folder
    }
    
    /// Write the content `content`into a file `mfile`
    /// # Arguments
    /// - `mfile`: The file to write
    /// - `content`: The content to write in the file
    fn write_file(mfile: &PathBuf, content: &str) {
        match std::fs::write(mfile, content) {
            Err(e) => {
                eprintln!(
                    "{}: Unable to write file '{}' - {}",
                    "error".red(),
                    mfile.to_str().unwrap().yellow(),
                    e.to_string()
                )
            }
            Ok(_) => (),
        };
    }
    
    /// Create archive_list file with theresults of gblk lists
    /// # Return
    /// The path of the file archive_list inside the .borg repository of the
    /// current project
    fn create_archive_list() -> PathBuf {
        let output = list::borg_list_launcher(0, 0, "");
        let content = match output.status.code().unwrap() {
            0 => format!("{}", String::from_utf8(output.stdout).unwrap()),
            num => {
                eprintln!("{}", String::from_utf8(output.stderr).unwrap());
                exit(num);
            }
        };
        let archive_file = get_list_file();
        write_file(&archive_file, &content);
        archive_file
    }
    
    /// function that deletes a given file
    /// # Arguments
    /// - `my_file`: The file to remove
    fn delete_file(my_file: &PathBuf) {
        match std::fs::remove_file(&my_file) {
            Err(e) => {
                eprintln!(
                    "{}: unable to remove '{}' - {}",
                    "error".red(),
                    my_file.display(),
                    e
                );
                exit(1);
            }
            Ok(_) => (),
        };
    }
    
    /// Gets the content of a file inside the local filesystem
    /// # Arguments:
    /// - `remote_dir`: The path of a file inside the local filesystem
    /// - `mfile`: The file for which we want to get the content
    /// # Results:
    /// return the content of the file in the local filesystem
    pub fn get_content_dir_file_locally(remote_dir: &PathBuf, mfile: &str) -> String {
        let mut remote_file = remote_dir.to_owned();
        remote_file.push(mfile);
        match std::fs::read_to_string(&remote_file) {
            Err(e) => {
                eprintln!(
                    "{}: Unable to read {}. {}",
                    "error".red(),
                    remote_file.display(),
                    e
                );
                exit(1);
            }
            Ok(content) => content,
        }
    }
    
    /// Get the content of a file in a remote server
    /// # Arguments
    /// - remote_dir: A path corresponding to a remote dir
    /// - adress: The adress of the remove sever
    /// - mfile: The file for which we want to recover the content
    /// # Return
    /// The content of the file `mfile` in the remote server
    pub fn get_content_dir_file_remotly(remote_dir: &PathBuf, adress: &str, mfile: &str) -> String {
        let mut remote_file = remote_dir.to_owned();
        remote_file.push(mfile);
        let args = vec![adress, "cat", remote_file.to_str().unwrap()];
        let output = Command::new("ssh")
            .args(&args)
            .stdout(Stdio::piped())
            .stdin(Stdio::inherit())
            .output()
            .unwrap();
        // extract the raw bytes that we captured and interpret them as a string
        let stdout = String::from_utf8(output.stdout).unwrap();
        if stdout.len() == 0 {
            eprintln!("{}: The file {} is empty or doesn't exits in {}. Maybe the directory is not a borg folder.",
        "error".red(), mfile, remote_dir.display());
            exit(1);
        }
        stdout
    }
    
    /// Get the content of a file `mfile` located into either remotely or locally
    /// # Arguments
    /// - remote_dir: A path corresponding to the remote dir
    /// - adress: The adress of the remove sever
    /// - mfile: The file for which we want to recover the content
    /// # Return
    /// The content of the file `mfile` in the remote server or in the local filesystem
    fn get_content_file(remote_dir: &PathBuf, adress: &str, mfile: &str) -> String {
        if adress != "file" {
            return get_content_dir_file_remotly(remote_dir, adress, mfile);
        }
        return get_content_dir_file_locally(remote_dir, mfile);
    }
    
    /// Get the contained into the content string
    /// # Arguments
    /// - `content` : The content of a config file
    /// # Return
    /// The id inside the string corresponding to a borg config file
    fn get_id(content: &str) -> String {
        let re = Regex::new(r"id = ([a-z0-9]*)\n").unwrap();
        let res = re.captures(content);
        let my_id = match res {
            None => {
                eprintln!("{}: No id found", "error".red());
                exit(1);
            }
            Some(e) => e[1].to_owned(),
        };
        my_id
    }
    
    /// Get the current project id
    /// # Return
    /// The id inside .borg/config file
    pub(crate) fn get_project_id() -> String {
        let config_file = get_config_file();
        let current_config = match std::fs::read_to_string(config_file) {
            Err(_) => {
                eprintln!("{}: Unable to read current config file !", "error".red());
                exit(1);
            }
            Ok(e) => e,
        };
        let current_id = get_id(&current_config);
        current_id
    }
    
    /// compare the id of the current config with the id of the remote config
    /// # Arguments
    /// - `remote_dir`: The path to the remote directory
    /// - `adress`: The location of the remote directory
    /// # Return
    /// - A boolean, true if distant and local id are the same, false else,
    /// - A string: the current project id
    /// - Another string: The remote project id
    fn compare_id(remote_dir: &PathBuf, adress: &str) -> (bool, String, String) {
        let current_id = get_project_id();
        let distant_id = get_id(&get_content_file(remote_dir, adress, "config"));
        (current_id == distant_id, current_id, distant_id)
    }
    
    /// Asks the user a question and get the answer
    /// # Arguments:
    /// - `question`: The question asked to a user
    /// # Return
    /// True if the user agree, false else
    pub fn user_question(question: &str) -> bool {
        print!("{} ? [y/n] : ", question);
        io::stdout().flush().unwrap();
        let mut user_input = String::new();
        match std::io::stdin().read_line(&mut user_input) {
            Ok(_) => (),
            Err(_) => {
                eprintln!("{}: Unable to read user input", "error".red());
                exit(1);
            }
        };
        let user_input = user_input.trim();
        user_input.to_lowercase() == "y"
    }
    
    /// Create a file containing the archives located
    /// # Arguments
    /// - `remote_dir`: The path to the remote directory
    /// - `adress`: The location of the remote directory
    /// # Return
    /// The path to the destination archive list
    fn create_destination_archive_list(remote_dir: &PathBuf, adress: &str) -> PathBuf {
        let content = get_content_file(remote_dir, adress, "archive_list");
        let remote_list_file = get_remote_list_file();
        write_file(&remote_list_file, &content);
        remote_list_file
    }
    
    /// Function that show differences between local_archive_list and remote_archive_list
    /// # Arguments
    /// - `local_archive_list`: Files containing archive list defined in current
    ///   borg folder
    /// - `remote_archive_list`: Files containing archive list defined remotly
    /// - `cmd`: The kind of command executed either pull or push
    fn show_diff_archive_list(local_archive_list: &PathBuf, remote_archive_list: &PathBuf, cmd: &str) {
        let args = if cmd == "push" {
            vec![remote_archive_list, local_archive_list]
        } else {
            vec![local_archive_list, remote_archive_list]
        };
        let mut output = Command::new("delta")
            .args(&args)
            .stdout(Stdio::inherit())
            .stdin(Stdio::inherit())
            .spawn()
            .unwrap();
        match output.wait() {
            Err(e) => {
                eprintln!("{}: delta terminaned with an error: {}", "error".red(), e);
                exit(1);
            }
            Ok(_) => {
                let output = Command::new("delta")
                    .args(&args)
                    .stdout(Stdio::piped())
                    .output()
                    .unwrap();
                let s = String::from_utf8(output.stdout).unwrap();
                if s.len() == 0 {
                    println!(
                        "{}: No differences between remote and current archive lists. Skipping {} command.",
                        "info".blue(),
                        cmd.green()
                    );
                    exit(0);
                }
            }
        };
    }
    
    /// Creates current and remote list files and display their differences using delta
    ///
    /// # Description:
    /// This function creates current and remote list files and display their differences using delta.
    /// Then it remove both files
    ///
    /// # Arguments
    /// - `cmd`: A command name, it should be `push` or `pull`
    /// - `remote_dir`: The remote dir located by the remote
    /// - `adress`: The location of the remote folder
    fn create_and_show_diff(cmd: &str, remote_dir: &PathBuf, adress: &str) {
        let remote_archive_list = create_destination_archive_list(remote_dir, adress);
        let local_archive_list = create_archive_list();
        show_diff_archive_list(&local_archive_list, &remote_archive_list, cmd);
        delete_file(&local_archive_list);
        delete_file(&remote_archive_list);
    }
    
    /// Function that check if a file exist on the local filesystem
    /// # Arguments
    /// - `remote_dir`: The remote dir located by the remote
    /// # Return
    /// true if the `remote_dir` is a directory, false else
    fn check_dir_exist_local(remote_dir: &PathBuf) -> bool {
        remote_dir.is_dir()
    }
    
    /// Function that check if a file exist on the local filesystem
    /// # Arguments
    /// - `remote_file`: The remote file located inside the remote
    /// # Return
    /// true if the `remote_dir` is a directory, false else
    fn check_file_exist_local(remote_file: &PathBuf) -> bool {
        remote_file.is_file()
    }
    
    /// Function that check if a dir exist on a remote filesystem
    /// # Arguments
    /// - `remote_dir`: The remote dir located by the remote
    /// - `adress`: The adress of the remote folder
    /// # Return
    /// true if the `remote_dir` is a directory, false else
    fn check_file_exist_remote(remote_tar: &PathBuf, adress: &str) -> bool {
        let args = vec![adress, "ls", remote_tar.to_str().unwrap()];
        let output = Command::new("ssh")
            .args(&args)
            .stdout(Stdio::piped())
            .output()
            .unwrap();
        match output.status.code().unwrap() {
            2 => false,
            0 => true,
            e => {
                eprintln!("{}: unexpected exit status !", e);
                exit(1)
            }
        }
    }
    
    /// Function that check if a file exist on a remote or the local filesystem
    /// # Arguments
    /// - `remote_dir`: The remote dir located by the remote
    /// - `adress`: The adress of the remote folder
    /// # Return
    /// true if the `remote_dir` is a directory, false else
    pub(crate) fn check_file_exist(remote_dir: &PathBuf, adress: &str) -> bool {
        if adress == "file" {
            check_file_exist_local(remote_dir)
        } else {
            check_file_exist_remote(remote_dir, adress)
        }
    }
    
    /// Function that check if a dir exist on a remote or the local filesystem
    /// # Arguments
    /// - `remote_dir`: The remote dir located by the remote
    /// - `adress`: The adress of the remote folder
    /// # Return
    /// true if the `remote_dir` is a directory, false else
    pub(crate) fn check_dir_exist(remote_dir: &PathBuf, adress: &str) -> bool {
        if adress == "file" {
            check_dir_exist_local(remote_dir)
        } else {
            check_file_exist_remote(remote_dir, adress)
        }
    }
    
    /// Function that performs some checks before pushing or pulling a borg archive
    ///
    /// # Description
    /// This function perform several checks for push and pull commands.
    /// - Pull command:
    ///  1. Checks if the remote directory exists. If it doesn't stops the pull
    ///  2. Checks if the remote and the local borg id are the same. Stops the pull
    ///     if they are different
    ///  3. Show the differences between the remote and the local archive lists and ask the user if he
    ///     wants to continue the pull
    /// - Push Command
    ///  1. Checks if the remote directory exists. If it doesn't skip checks 2 and
    ///     3 and perform push command
    ///  2. Checks if the remote and the local borg id are the same. Stops the push
    ///     if they are different
    ///  3. Show the differences between the remote and the local archive lists and ask the user if he
    ///     wants to continue the push
    ///
    /// # Arguments
    /// - `cmd`: A command name, it should be `push` or `pull`
    /// - `remote_dir`: The remote dir located by the remote
    /// - `adress`: The location of the remote folder
    pub(crate) fn handle_existing_remote_dir(remote_dir: &PathBuf, adress: &str, cmd: &str) {
        if !check_dir_exist(remote_dir, adress) {
            if cmd == "push" {
                return ();
            } else {
                eprintln!(
                    "{}: remote dir {} not found",
                    "error".red(),
                    format!("{}:{}", adress, remote_dir.display())
                );
                exit(1);
            }
        }
        let (same_id, current_id, remote_id) = compare_id(remote_dir, adress);
        if !same_id {
            eprintln!("{}: remote ({}) and current ({}) borg id are different. Maybe the remote borg folder doesn't come from your local project !",
            "error".red(),
            remote_id,
            current_id);
            exit(1);
        }
        create_and_show_diff(cmd, remote_dir, adress);
        let answer = user_question(&format!(
            "Do you want to continue the {} command",
            cmd.green()
        ));
        if !answer {
            // If the user want to abord the command, we abord it
            exit(0);
        }
    }
    
    /// Get the remote path of a remote
    /// # Arguments
    /// - `remote`: A remote name
    /// # Results
    /// The remote path associated
    fn get_remote_path(remote: &str) -> String {
        let dic = remote::get_dic_remotes();
        let v = match dic.get(remote) {
            None => {
                eprintln!(
                    "{}: The remote {} was not found !",
                    "error".red(),
                    remote.yellow()
                );
                exit(1);
            }
            Some(v) => v,
        };
        v.0.url.to_owned()
    }
    
    /// Split the path of the remote to get the path and adress
    /// # Arguments
    /// - `url`: A complete url path in adress:path form
    /// # Return
    /// A tuple containing the adress and the url
    pub(crate) fn split_path(url: &str) -> (String, PathBuf) {
        let res = url.split(":").collect::<Vec<&str>>();
        if res.len() == 1 {
            return (
                String::from("file"),
                match PathBuf::from(url).canonicalize() {
                    Ok(path) => path,
                    Err(e) => {
                        eprintln!("{}: Problem with {}. {}", "error".red(), url, e);
                        exit(1);
                    }
                },
            );
        } else if res.len() == 2 {
            if res[0].to_lowercase() == "file" {
                return (
                    String::from("file"),
                    match PathBuf::from(res[1]).canonicalize() {
                        Ok(path) => path,
                        Err(e) => {
                            eprintln!("{}: Problem with {}. {}", "error".red(), &res[1], e);
                            exit(1);
                        }
                    },
                );
            }
            return (res[0].to_owned(), PathBuf::from(res[1]));
        }
        eprintln!("{}: Invalid url !", "error".red());
        exit(1);
    }
    
    /// Get the adress and url from a remote path
    /// # Arguments
    /// - `remote`: The name of the remote.
    /// # Return
    /// A tuple containing the adress and the url
    pub(crate) fn get_adress_and_url(remote: &str) -> (String, PathBuf) {
        let url = get_remote_path(remote);
        split_path(&url)
    }
    
    /// Check if the source and destination path are the same
    /// # Arguments
    /// - `borg_folder`: Path to borg folder
    /// - `url`: The destination path
    /// - `adress`: to location of the `url` path, 'file' for a local file
    /// remote for a remote file
    pub(crate) fn check_dest_and_copy(borg_folder: &PathBuf, url: &PathBuf, adress: &str) -> () {
        if !check_dir_exist(url, adress) {
            eprintln!("{}: {} no such directory", "error".red(), url.display());
            exit(1);
        }
        if adress != "file" {
            return ();
        }
        let res = borg_folder.parent().unwrap().to_str().unwrap();
        if res == url.to_str().unwrap() {
            eprintln!(
                "{}: It appears that destination and source url are the same",
                "error".red()
            );
            exit(1);
        }
    }
    
    /// Function used to copy a tar archive into the current working directory
    /// # Arguments
    /// - `borg_folder`: Path to borg folder
    /// - `remote_dir`: Path where the borg folder will be copied
    /// - `adress`: to location of the `url` path, 'file' for a local file
    /// - `cmd`: puhs or pull
    pub(crate) fn copy_file(
        borg_folder: &PathBuf,
        remote_dir: &PathBuf,
        adress: &str,
        cmd: &str,
    ) -> () {
        let remote_path = if adress != "file" {
            format!("{}:{}", adress, remote_dir.display())
        } else {
            remote_dir.to_str().unwrap().to_string()
        };
    
        let tmp = if cmd == "pull" || cmd == "clone" {
            format!("{}/", &remote_path)
        } else {
            format!("{}/", &borg_folder.display())
        };
    
        let args = if cmd == "pull" {
            vec![
                "-a",
                "--info=progress2",
                "--human-readable",
                "--exclude=config",
                "--exclude=.gblkconfig",
                "--exclude=archive_list",
                "--exclude=archive_list_remote",
                &tmp,
                borg_folder.to_str().unwrap(),
            ]
        } else if cmd == "clone" {
            vec![
                "-a",
                "--info=progress2",
                "--human-readable",
                "--exclude=.gblkconfig",
                "--exclude=archive_list",
                "--exclude=archive_list_remote",
                &tmp,
                borg_folder.to_str().unwrap(),
            ]
        } else {
            vec![
                "-a",
                "--info=progress2",
                "--human-readable",
                "--exclude=.gblkconfig",
                "--exclude=hints*",
                "--exclude=index*",
                "--exclude=integrity*",
                &tmp,
                &remote_path,
            ]
        };
        let local_archive_list = if cmd == "push" {
            Some(create_archive_list())
        } else {
            None
        };
        let mut output = Command::new("rsync")
            .args(&args)
            .stdout(Stdio::inherit())
            .stdin(Stdio::inherit())
            .spawn()
            .unwrap();
        let ecode = match output.wait() {
            Err(e) => {
                if cmd == "push" {
                    delete_file(&local_archive_list.unwrap());
                    eprintln!("{}: rsync terminated with an error: {} !", "error".red(), e);
                } else {
                    eprintln!(
                        "You .borg folder may be corrupted, restore it with {}",
                        "gblk restore".green()
                    )
                }
                exit(1);
            }
            Ok(r) => {
                if cmd == "push" {
                    delete_file(&local_archive_list.unwrap());
                }
                r
            }
        };
        match ecode.code().unwrap() {
            0 => (),
            num => {
                exit(num);
            }
        };
    }
    
    /// Function that return the name of  the folder that will containg the content
    /// of the .borg folder on the remote directory
    ///
    /// # Arguments
    /// - `url`: The path where the remote borg archive is or will be stored
    /// # Return
    /// The path `url/borg_archive_<PROJECT_DIR>` where <PROJECT_DIR> is the name
    /// of the folder at the root of the project
    pub(crate) fn get_remote_dir(url: &PathBuf) -> PathBuf {
        let mut remote_dir = url.to_owned();
        let name_folder = configt::get_project_name();
        remote_dir.push(name_folder);
        remote_dir
    }
    
    /// Create a push the tar achive on the selected remote path
    /// # Arguments
    /// - `remote`: The name of a remote
    pub fn push(remote: &str) -> () {
        let (borg_folder, _) = commit::check_path();
        let borg_folder = borg_folder.canonicalize().unwrap();
        let (adress, url) = get_adress_and_url(remote);
        check_dest_and_copy(&borg_folder, &url, &adress);
        let remote_dir = get_remote_dir(&url);
        handle_existing_remote_dir(&remote_dir, &adress, "push");
        copy_file(&borg_folder, &remote_dir, &adress, "push");
    }