Skip to content
Snippets Groups Projects
Select Git revision
  • f009236794d5c935ad9edd37406a869e6a5f8308
  • master default protected
  • doc
  • dev_doc
  • dev_push
  • dev
6 results

push.rs

Blame
  • push.rs 13.99 KiB
    // SPDX-FileCopyrightText: 2023 Nicolas Fontrodona
    //
    // SPDX-License-Identifier: AGPL-3.0-or-later
    
    use crate::commit;
    use crate::mount;
    use crate::remote;
    use colored::Colorize;
    use std::fs;
    use std::os::unix::fs::PermissionsExt;
    use std::path::PathBuf;
    use std::process::{exit, Command, Stdio};
    
    const COMPRESSION: [&str; 6] = ["gzip", "bzip2", "xz", "zstd", "lz4", "none"];
    
    /// 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 with a glob pattern exist on the local filesystem
    /// # Arguments
    /// - `remote_file`: The remote file located inside the remote
    /// # Return
    /// true if the `remote_file` is at least a file, false else
    fn check_glob_exist_local(remote_file: &PathBuf) -> Option<PathBuf> {
        let mut filenames: Vec<String> = Vec::new();
        filenames.push(
            remote_file
                .file_name()
                .unwrap()
                .to_str()
                .unwrap()
                .to_string(),
        );
        let walker =
            globwalk::GlobWalkerBuilder::from_patterns(remote_file.parent().unwrap(), &filenames)
                .max_depth(1)
                .follow_links(false)
                .build()
                .unwrap()
                .into_iter()
                .filter_map(Result::ok)
                .map(|x| x.path().to_path_buf())
                .collect::<Vec<_>>();
        if walker.len() > 0 {
            Some(walker.get(0).unwrap().to_path_buf())
        } else {
            None
        }
    }
    
    
    /// 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_glob_file_exist_remote(remote_tar: &PathBuf, adress: &str) -> Option<PathBuf> {
        let args = vec![adress, "ls", remote_tar.to_str().unwrap()];
        let output = Command::new("ssh")
            .args(&args)
            .stdout(Stdio::piped())
            .output()
            .unwrap();
        let str_paths = String::from_utf8(output.stdout).unwrap();
        let paths = str_paths.split_ascii_whitespace().collect::<Vec<&str>>();
        if paths.len() > 0 {
            if paths.get(0).unwrap().len() > 0 {
                Some(PathBuf::from(paths.get(0).unwrap()))
            } else {
                None
            }
        } else {
            None
        }
    }
    
    
    /// Function that checks if a glob file exists and return the first path found
    /// if it's the case
    ///
    /// # Arguments
    /// - `remote_file`: The remote glob located by the remote
    /// - `adress`: The adress of the remote folder
    fn check_glob_file_exists(remote_file: &PathBuf, adress: &str) -> Option<PathBuf> {
        if adress == "file" {
            check_glob_exist_local(remote_file)
        } else {
            check_glob_file_exist_remote(remote_file, adress)
        }
    }
    
    
    /// 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 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 checks if the archive file to push/pull already exists
    ///
    /// # Arguments
    /// - `cmd`: A command name, it should be `push` or `pull`
    /// - `remote_file`: The remote archive file located by the remote
    /// - `adress`: The location of the remote folder
    ///
    /// # Return
    /// Path of the archive to pull/push
    pub(crate) fn handle_existing_remote_file(
        remote_file: &PathBuf,
        adress: &str,
        cmd: &str,
    ) -> PathBuf {
        let archive = remote_file.file_stem().unwrap().to_str().unwrap().split('.').collect::<Vec<_>>()[0];
        let filename_glob = remote_file
            .parent()
            .unwrap()
            .join(format!("{}{}", archive, ".*"));
        let distant_file = check_glob_file_exists(&filename_glob, &adress);
        match cmd {
            "push" => match distant_file {
                None => return remote_file.to_owned(),
                Some(file) => {
                    eprintln!(
                        "{}: A file with the same archive name exists {}, {}",
                        "error".red(),
                        file.display(),
                        "remove it manually if you wish to proceed !"
                    );
                    exit(1);
                }
            },
            _ => match distant_file {
                None => {
                    eprintln!(
                        "{}: The archive {} wasn't found on the remote",
                        "error".red(),
                        archive
                    );
                    exit(1)
                }
                Some(mfile) => return mfile,
            },
        }
    }
    
    /// 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);
        }
    }
    /// set write permission for user and read for other
    ///
    /// # Argument:
    /// - `remote_file`: The file for which we want to change the permissions
    fn set_good_permission(remote_file: &PathBuf) -> () {
        let metadata = remote_file.metadata().unwrap();
        let mut permissions = metadata.permissions();
        permissions.set_mode(0o644);
        fs::set_permissions(remote_file, permissions).unwrap();
    }
    
    /// Function used to export a tar archive in the remote file in the current
    /// filsesystem
    ///
    /// # Arguments
    /// - `borg_folder`: Path to borg folder
    /// - `remote_file`: Path where the archive file will be stored
    /// - `archive`: The archive name
    fn export_tar_locally(borg_folder: &PathBuf, remote_file: &PathBuf, archive: &str) {
        let borg_archive = format!(
            "{}::{}",
            borg_folder.canonicalize().unwrap().to_str().unwrap(),
            archive
        );
        let args = vec!["export-tar", &borg_archive, remote_file.to_str().unwrap()];
        let mut output: std::process::Child = Command::new("borg")
            .args(&args)
            .stdout(Stdio::inherit())
            .stdin(Stdio::inherit())
            .spawn()
            .unwrap();
        let ecode = match output.wait() {
            Err(e) => {
                eprintln!(
                    "{}: borg export-tar returned an error: {} !",
                    "error".red(),
                    e
                );
                exit(1);
            }
            Ok(r) => r,
        };
        match ecode.code().unwrap() {
            0 => (),
            num => {
                if remote_file.is_file() {
                    fs::remove_file(&remote_file).unwrap();
                }
                exit(num);
            }
        };
        set_good_permission(remote_file);
    }
    
    /// Function used to export a tar archive in the remote file in the current
    /// filsesystem
    ///
    /// # Arguments
    /// - `borg_folder`: Path to borg folder
    /// - `remote_file`: Path where the archive file will be stored
    /// - `archive`: The archive name
    /// - `adress`: The server location
    fn export_tar_remotely(borg_folder: &PathBuf, remote_file: &PathBuf, archive: &str, adress: &str) {
        let tmp_folder = mount::file_diff::get_tmp_folder(borg_folder);
        let tmp_file = tmp_folder.join(remote_file.file_name().unwrap());
        export_tar_locally(borg_folder, &tmp_file, archive);
        let remote_args = format!("{}:{}", adress, remote_file.to_str().unwrap());
        let args = vec![tmp_file.to_str().unwrap(), &remote_args];
        let mut output = Command::new("rsync")
            .args(&args)
            .stdout(Stdio::inherit())
            .stdin(Stdio::inherit())
            .spawn()
            .unwrap();
        let ecode = match output.wait() {
            Err(e) => {
                eprintln!("{}: rsync returned an error: {} !", "error".red(), e);
                exit(1);
            }
            Ok(r) => r,
        };
        match ecode.code().unwrap() {
            0 => (),
            num => {
                exit(num);
            }
        };
        fs::remove_file(tmp_file).unwrap();
    }
    
    /// Function used to export a tar archive in the remote file
    /// # Arguments
    /// - `borg_folder`: Path to borg folder
    /// - `remote_file`: Path where the archive file will be stored
    /// - `adress`: to location of the `url` path, 'file' for a local file
    /// - `archive`: The archive name
    fn export_tar(borg_folder: &PathBuf, remote_file: &PathBuf, adress: &str, archive: &str) -> () {
        match adress {
            "file" => export_tar_locally(borg_folder, remote_file, archive),
            _ => export_tar_remotely(borg_folder, remote_file, archive, adress),
        }
    }
    
    /// Stop the program if the compression is not avalable
    ///
    /// # Arguments
    /// - `compression`: The compression to use
    fn compression_available(compression: &str) {
        if !COMPRESSION.contains(&compression) {
            eprintln!(
                "{}: compression not defined ! Available compressions: {}",
                "error".red(),
                COMPRESSION.join(" ").yellow()
            );
            exit(121);
        }
    }
    
    /// get the extention of the archive to recover
    ///
    /// # Arguments
    /// - `compression`: The compression to use
    fn extention_file(compression: &str) -> String {
        match compression {
            "gzip" => String::from(".tar.gz"),
            "bzip2" => String::from(".tar.bz2"),
            "xz" => String::from(".tar.xz"),
            "zstd" => String::from(".tar.zstd"),
            "lz4" => String::from(".tar.lz4"),
            "none" => String::from(".tar"),
            _ => String::from(".tar"),
        }
    }
    
    /// Function that return path of the archive that will be exported/to get
    ///
    /// # Arguments
    /// - `url`: The path where the remote borg archive is or will be stored
    /// - `archive`: The name of the archive to pull/push
    /// - `compression`: The compression used
    /// # 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_file(url: &PathBuf, archive: &str, compression: &str) -> PathBuf {
        compression_available(compression);
        let mut remote_file = url.to_owned();
        let ext = extention_file(compression);
        let filename = format!("{}{}", archive, ext);
        remote_file.push(filename);
        remote_file
    }
    
    /// Create a push the tar achive on the selected remote path
    /// # Arguments
    /// - `remote`: The name of a remote
    /// - `archive`: The name of the archive to push
    /// - `compression`: The compression to use when exporting the archive
    pub fn push(remote: &str, archive: &str, compression: &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_file = get_remote_file(&url, archive, compression);
        let remote_file = handle_existing_remote_file(&remote_file, &adress, "push");
        export_tar(&borg_folder, &remote_file, &adress, &archive);
    }