Select Git revision
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(¤t_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");
}