Select Git revision

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