diff --git a/Cargo.lock b/Cargo.lock index 9977859..bea158f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -152,6 +161,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.38" @@ -222,6 +237,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -266,6 +294,29 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctrlc" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" +dependencies = [ + "nix", + "windows-sys", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -277,6 +328,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "errno" version = "0.3.9" @@ -417,6 +474,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.155" @@ -436,15 +499,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] -name = "lzma-sys" -version = "0.1.20" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" @@ -455,6 +513,18 @@ dependencies = [ "adler", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -483,22 +553,16 @@ dependencies = [ "aes-gcm", "chrono", "clap", + "ctrlc", + "dialoguer", "flate2", "hkdf", - "hmac", + "regex", "sha2", "tar", - "tempfile", - "thiserror", - "xz2", + "walkdir", ] -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - [[package]] name = "polyval" version = "0.6.2" @@ -547,6 +611,35 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rustix" version = "0.38.34" @@ -560,6 +653,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "sha2" version = "0.10.8" @@ -571,6 +673,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "strsim" version = "0.11.1" @@ -649,6 +757,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + [[package]] name = "universal-hash" version = "0.5.1" @@ -671,6 +785,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -731,6 +855,15 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -825,10 +958,7 @@ dependencies = [ ] [[package]] -name = "xz2" -version = "0.1.7" +name = "zeroize" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 4392103..1398430 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,20 @@ edition = "2021" aes-gcm = "0.10.3" chrono = "0.4.38" clap = { version = "4.5.4", features = ["derive"] } +# cron = "0.12.1" +ctrlc = "3.4.4" +dialoguer = "0.11.0" flate2 = "1.0.30" hkdf = "0.12.4" -hmac = "0.12.1" +# hmac = "0.12.1" +regex = { version = "1.10.5", features = ["use_std"] } sha2 = "0.10.8" tar = "0.4.41" -tempfile = "3.10.1" -thiserror = "1.0.61" -xz2 = "0.1.7" +walkdir = "2.5.0" + +[profile.release] +opt-level = 3 # Optimize for speed. +lto = true # Enable Link Time Optimization. +codegen-units = 1 # Fewer codegen units for better optimization. +panic = "abort" # Abort on panic to reduce binary size. +strip = true # Remove symbols from the binary. diff --git a/README.md b/README.md index e8afb47..caa3ba9 100644 --- a/README.md +++ b/README.md @@ -14,55 +14,8 @@ and SSH keys. - Backup GPG and SSH keys - Restore from backups -## Roadmap -### Phase 1: Core Functionality -- [ ] Develop a command-line interface (CLI) for the application -- [ ] Implement the functionality to backup installed applications using Pacman -- [ ] Implement the functionality to backup the user's home directory -- [ ] Implement the functionality to backup GPG and SSH keys -### Phase 2: Flatpak Integration - -- [ ] Add an option to backup Flatpak packages -- [ ] Integrate with the Flatpak package manager to list and backup installed Flatpak packages - -### Phase 3: Restore Functionality - -- [ ] Implement the functionality to restore backed-up applications using Pacman -- [ ] Implement the functionality to restore the user's home directory -- [ ] Implement the functionality to restore GPG and SSH keys -- [ ] Implement the functionality to restore Flatpak packages (if backed up) - -### Phase 4: User Interface - -- [ ] Develop a graphical user interface (GUI) for the application -- [ ] Integrate the CLI functionality into the GUI -- [ ] Provide options to schedule backups and set backup locations - -### Phase 5: Optimization and Testing - -- [ ] Optimize the backup and restore processes for performance and efficiency -- [ ] Conduct thorough testing, including edge cases and error handling -- [ ] Implement error reporting and logging mechanisms - -### Phase 6: Documentation and Release - -- [ ] Write comprehensive documentation for users and developers -- [ ] Package the application for distribution -- [ ] Release the application to the ParchLinux community - -## Dependencies -- Pacman / libalpm -- Flatpak (optional) - -## Potential Challenges -- Handling large home directories and optimizing backup/restore times -- Ensuring compatibility with different versions of Pacman / AUR helpers -- Handling edge cases and error scenarios gracefully -- Providing a user-friendly and intuitive interface - -## Future Plans -- Support for incremental backups -- Integration with cloud storage services for remote backups (Nextcloud/Gdrive and ....) -- Support for encrypted backups -- Backup and restore of system configurations and settings +## TODO +- [ ] Supporting scheduling +- [ ] Writing PKGBUILD +- [ ] Writing README diff --git a/src/backup/backup.rs b/src/backup/backup.rs index 5da4553..53cc038 100644 --- a/src/backup/backup.rs +++ b/src/backup/backup.rs @@ -3,64 +3,72 @@ use crate::cli::BackupArgs; use crate::flatpak::flatpak; use crate::pm::paru; use crate::system::{home, keys}; -use std::path::Path; +use crate::utils::compression::ARCHIVE_EXT; +use dialoguer::{Confirm, Password}; +use regex::Regex; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +/// Clean up backup files matching the pattern *_backup.EXTENSION in the current directory. +fn cleanup_backup_files() { + let pattern = format!(r".*_backup\.{}", ARCHIVE_EXT); + let re = Regex::new(&pattern).unwrap(); + + for entry in fs::read_dir(".").unwrap() { + if let Ok(entry) = entry { + let path = entry.path(); + if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { + if re.is_match(file_name) { + let _ = fs::remove_file(path); + } + } + } + } +} pub fn handle_backup(args: &BackupArgs) { - if !args.apps && !args.home && !args.flatpak && !args.keys { - eprintln!("No backup options specified. Please provide at least one backup option."); - return; - } - let home_dir = std::env::var("HOME").expect("HOME environment variable not set"); let home_path = Path::new(&home_dir); let mut backup_files = Vec::new(); + // Atomic flag to indicate if the process was interrupted. + let interrupted = Arc::new(AtomicBool::new(false)); + let interrupt_clone = Arc::clone(&interrupted); + + ctrlc::set_handler(move || { + interrupt_clone.store(true, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + if args.apps { - println!("Backing up installed apps..."); - match paru::list_installed_apps() { - Ok(file) => { - println!("Installed apps backed up successfully."); - backup_files.push(("appsb", file)); - }, - Err(e) => eprintln!("Failed to backup installed apps: {}", e), - } + backup_apps(&mut backup_files); } if args.home { - println!("Backing up home directory..."); - match home::backup_home(home_path) { - Ok(file) => { - println!("Home directory backed up successfully."); - backup_files.push(("homeb", file)); - }, - Err(e) => eprintln!("Failed to backup home directory: {}", e), - } + backup_home( + &mut backup_files, + home_path, + &args.exclude_dir, + &interrupted, + ); } if args.flatpak { - println!("Backing up Flatpak applications..."); - match flatpak::list_installed_flatpak_apps() { - Ok(file) => { - println!("Installed Flatpak apps backed up successfully."); - backup_files.push(("flatpakb", file)); - }, - Err(e) => eprintln!("Failed to backup Flatpak apps: {}", e), - } + backup_flatpak(&mut backup_files); } if args.keys { - println!("Backing up GPG keys..."); - match keys::backup_gpg_keys(home_path) { - Ok(file) => backup_files.push(("gnupgb", file)), - Err(e) => eprintln!("Failed to backup GPG keys: {}", e), - } + backup_keys(&mut backup_files, home_path, &interrupted); + } - println!("Backing up SSH keys..."); - match keys::backup_ssh_keys(home_path) { - Ok(file) => backup_files.push(("sshb", file)), - Err(e) => eprintln!("Failed to backup SSH keys: {}", e), - } + // Check for interruption after initial backups. + if interrupted.load(Ordering::SeqCst) { + exit_gracefully(); + return; } if !backup_files.is_empty() { @@ -68,7 +76,145 @@ pub fn handle_backup(args: &BackupArgs) { Ok(_) => println!("All backups consolidated successfully."), Err(e) => eprintln!("Failed to consolidate backups: {}", e), } + return; } else { - eprintln!("No backups to consolidate."); + // Prompt the user for additional actions + if Confirm::new() + .with_prompt( + "Do you want to backup with all functionality (apps, home, keys, flatpak)?", + ) + .interact() + .unwrap_or(false) + { + let backup_key = if Confirm::new() + .with_prompt("Do you want to encrypt the backup?") + .interact() + .unwrap_or(false) + { + Some( + Password::new() + .with_prompt("Enter the encryption key") + .with_confirmation("Confirm the encryption key", "Keys mismatch!") + .interact() + .unwrap(), + ) + } else { + None + }; + // Backup with all functionality + backup_apps(&mut backup_files); + backup_home( + &mut backup_files, + home_path, + &args.exclude_dir, + &interrupted, + ); + backup_flatpak(&mut backup_files); + backup_keys(&mut backup_files, home_path, &interrupted); + + if let Some(key) = backup_key { + match consolidate::consolidate_backups( + &backup_files, + &BackupArgs { + archive_path: args.archive_path.clone(), + apps: true, + home: true, + exclude_dir: args.exclude_dir.clone(), + flatpak: true, + keys: true, + encrypt: true, + encrypt_key: Some(key), + ..*args + }, + ) { + Ok(_) => println!("All backups consolidated successfully."), + Err(e) => eprintln!("Failed to consolidate backups: {}", e), + } + } else { + match consolidate::consolidate_backups( + &backup_files, + &BackupArgs { + archive_path: args.archive_path.clone(), + apps: true, + home: true, + exclude_dir: args.exclude_dir.clone(), + flatpak: true, + keys: true, + encrypt: false, + encrypt_key: None, + ..*args + }, + ) { + Ok(_) => println!("All backups consolidated successfully."), + Err(e) => eprintln!("Failed to consolidate backups: {}", e), + } + } + } else { + exit_gracefully(); + } } } +fn backup_apps(backup_files: &mut Vec<(&str, PathBuf)>) { + println!("Backing up installed apps..."); + match paru::list_installed_apps() { + Ok(file) => { + println!("Installed apps backed up successfully."); + backup_files.push(("appsb", file)); + } + Err(e) => eprintln!("Failed to backup installed apps: {}", e), + } +} +fn backup_home( + backup_files: &mut Vec<(&str, PathBuf)>, + home_path: &Path, + exclude_dirs: &[String], + interrupted: &Arc, +) { + println!("Backing up home directory..."); + match home::backup_home(home_path, &exclude_dirs, &interrupted) { + Ok(file) => { + println!("Home directory backed up successfully."); + backup_files.push(("homeb", file)); + } + Err(e) => { + if e.kind() == io::ErrorKind::Interrupted { + cleanup_backup_files(); + return; + } else { + eprintln!("Failed to backup home directory: {}", e); + return; + } + } + } +} +fn backup_flatpak(backup_files: &mut Vec<(&str, PathBuf)>) { + println!("Backing up Flatpak applications..."); + match flatpak::list_installed_flatpak_apps() { + Ok(file) => { + println!("Installed Flatpak apps backed up successfully."); + backup_files.push(("flatpakb", file)); + } + Err(e) => eprintln!("Failed to backup Flatpak apps: {}", e), + } +} +fn backup_keys( + backup_files: &mut Vec<(&str, PathBuf)>, + home_path: &Path, + interrupted: &Arc, +) { + println!("Backing up GPG keys..."); + match keys::backup_gpg_keys(home_path, &interrupted) { + Ok(file) => backup_files.push(("gnupgb", file)), + Err(e) => eprintln!("Failed to backup GPG keys: {}", e), + } + + println!("Backing up SSH keys..."); + match keys::backup_ssh_keys(home_path, &interrupted) { + Ok(file) => backup_files.push(("sshb", file)), + Err(e) => eprintln!("Failed to backup SSH keys: {}", e), + } +} +fn exit_gracefully() { + cleanup_backup_files(); + eprintln!("Operation canceled."); +} diff --git a/src/backup/consolidate.rs b/src/backup/consolidate.rs index 3a5c5df..cfa5027 100644 --- a/src/backup/consolidate.rs +++ b/src/backup/consolidate.rs @@ -1,15 +1,15 @@ +use chrono::Utc; + use crate::cli::BackupArgs; +use crate::utils::compression::create_tar_gz_archive; use crate::utils::security::{self, CryptoError}; -use chrono::Local; -use std::fs::{self, File}; -use std::io; +use std::{fs, io}; use std::path::{Path, PathBuf}; -use tar::Builder; /// Consolidates individual backups into a single archive file. pub fn consolidate_backups(backup_files: &[(&str, PathBuf)], args: &BackupArgs) -> io::Result<()> { // Prepare timestamp and components for archive name - let timestamp = Local::now().format("%Y-%m-%d-%H:%M:%S"); + let timestamp = Utc::now().format("%Y-%m-%d-%H:%M:%S"); let components = { let mut components = String::new(); if args.apps { @@ -31,35 +31,49 @@ pub fn consolidate_backups(backup_files: &[(&str, PathBuf)], args: &BackupArgs) }; // Construct archive name - let archive_name = format!("backup-{}-{}.{}", timestamp, components, crate::utils::compression::TAR_GZ); + let archive_name = format!( + "backup-{}-{}.{}", + timestamp, + components, + crate::utils::compression::ARCHIVE_EXT + ); - // Create the archive file - let archive_file = File::create(&archive_name)?; - let enc = flate2::write::GzEncoder::new(archive_file, flate2::Compression::default()); - let mut tar = Builder::new(enc); + // Determine the full archive path + let archive_path = if let Some(ref path) = args.archive_path { + Path::new(path).join(&archive_name) + } else { + Path::new(&archive_name).to_path_buf() + }; - // Add each backup file to the archive and remove original files - for (subdir, file) in backup_files { - let path_in_archive = Path::new(subdir).join(file.file_name().unwrap()); - tar.append_path_with_name(file, path_in_archive)?; - fs::remove_file(file)?; + // Create directory if it doesn't exist + if let Some(parent_dir) = archive_path.parent() { + if !parent_dir.exists() { + fs::create_dir_all(parent_dir)?; + println!("Created directory: {}", parent_dir.display()); + } } - // Finalize the archive - tar.finish()?; + // Convert &str elements in backup_files to String + let backup_files: Vec<(String, PathBuf)> = backup_files + .iter() + .map(|(subdir, path)| (subdir.to_string(), path.clone())) + .collect(); + + // Create the archive + create_tar_gz_archive(archive_path.to_str().unwrap(), &backup_files)?; // Encrypt the consolidated backup file if encryption is enabled if let Some(key) = &args.encrypt_key { - if let Err(e) = security::encrypt_file(Path::new(&archive_name), key.as_bytes()) { + if let Err(e) = security::encrypt_file(&archive_path, key.as_bytes()) { match e { CryptoError::FileRead(_) => eprintln!("Failed to read the archive file: {:?}", e), - CryptoError::FileWrite(_) => { - eprintln!("Failed to write the encrypted file: {:?}", e) - } - _ => {} + CryptoError::FileWrite(_) => eprintln!("Failed to write the encrypted file: {:?}", e), + _ => {}, } } } + println!("Archive located at: {}", archive_path.display()); + Ok(()) } diff --git a/src/cli.rs b/src/cli.rs index 6215626..10af995 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,7 +5,10 @@ use clap::{Args, Parser, Subcommand}; author = "DanielcoderX", version = "1.0", about = "ParchBackup", - long_about = "Koonam goshade" + long_about = "The ParchLinux Backup Application is a utility designed to simplify the backup +process for ParchLinux users. It provides a comprehensive solution for backing +up installed applications, home directory, Flatpak packages (optional), and GPG +and SSH keys." )] pub struct Cli { #[command(subcommand)] @@ -18,18 +21,23 @@ pub enum Commands { Backup(BackupArgs), /// Restore functionality Restore(RestoreArgs), - /// Schedule backup - Schedule(ScheduleArgs), + // Schedule(ScheduleArgs), } #[derive(Args)] pub struct BackupArgs { + /// Backup archive location + #[arg(long, help = "Backup archive location", default_value = "/home/backup")] + pub archive_path: Option, /// Backup installed apps names #[arg(long, help = "Backup installed apps names")] pub apps: bool, /// Backup home directory #[arg(long, help = "Backup home directory")] pub home: bool, + /// Execluded directories from backup + #[arg(long, help = "Excluded directories from backup", num_args(1..))] + pub exclude_dir: Vec, /// Backup flatpak applications #[arg(long, help = "Backup flatpak applications")] pub flatpak: bool, @@ -65,13 +73,42 @@ pub struct RestoreArgs { pub decrypt_key: Option, } - -#[derive(Args)] -pub struct ScheduleArgs { - /// Cron expression for scheduling - #[arg(short, long, help = "Cron expression for scheduling")] - pub cron: String, -} +// #[derive(Args)] +// pub struct ScheduleArgs { +// /// Cron expression for scheduling +// /// sec min hour day of month month day of week year +// #[arg( +// short, +// long, +// help = "Cron expression for scheduling backup functionality: \nExample: 'sec min hour day of month month day of week year'\n" +// )] +// pub cron: String, +// /// Backup installed apps names +// #[arg(long, help = "Backup installed apps names")] +// pub apps: bool, +// /// Backup home directory +// #[arg(long, help = "Backup home directory")] +// pub home: bool, +// /// Execluded directories from backup +// #[arg(long, help = "Excluded directories from backup", num_args(1..))] +// pub exclude_dir: Vec, +// /// Backup flatpak applications +// #[arg(long, help = "Backup flatpak applications")] +// pub flatpak: bool, +// /// Backup keys +// #[arg(long, help = "Backup keys")] +// pub keys: bool, +// /// Use Encryption +// #[arg( +// long, +// help = "Use Encryption, pass the password", +// requires = "encrypt_key" +// )] +// pub encrypt: bool, +// /// Encryption key +// #[arg(long, help = "Encryption key", requires = "encrypt")] +// pub encrypt_key: Option, +// } pub fn parse_cli() -> Cli { Cli::parse() diff --git a/src/main.rs b/src/main.rs index 3fcb3d1..33104e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,11 +24,12 @@ fn main() { std::process::exit(1); } } - Commands::Schedule(args) => { - // Handle schedule functionality - println!("Schedule called with cron: {}", args.cron); - // Call the appropriate function from the system module - // system::schedule_backup(args.cron); - } + // Commands::Schedule(args) => { + // let result = system::schedule::schedule_backup(&args); + // if let Err(e) = result { + // eprintln!("Error: {}", e); + // std::process::exit(1); + // } + // } } } diff --git a/src/pm/paru.rs b/src/pm/paru.rs index a66ffd9..551c942 100644 --- a/src/pm/paru.rs +++ b/src/pm/paru.rs @@ -13,7 +13,13 @@ pub fn list_installed_apps() -> io::Result { .output()?; if output.status.success() { - let installed_apps = String::from_utf8_lossy(&output.stdout).to_string(); + // Process the output to remove versions + let installed_apps = String::from_utf8_lossy(&output.stdout) + .lines() + .map(|line| line.split_whitespace().next().unwrap_or("")) + .collect::>() + .join("\n"); + let apps_list_path = PathBuf::from(APPS_LIST_FILE); let mut file = File::create(&apps_list_path)?; file.write_all(installed_apps.as_bytes())?; diff --git a/src/restore/restore.rs b/src/restore/restore.rs index 34b0440..edfed56 100644 --- a/src/restore/restore.rs +++ b/src/restore/restore.rs @@ -1,6 +1,7 @@ use crate::cli::RestoreArgs; use crate::flatpak::flatpak; use crate::pm::paru; +use crate::utils::compression::open_and_decode_archive; use crate::utils::security; use crate::utils::security::CryptoError; use flate2::read::GzDecoder; @@ -12,91 +13,128 @@ use tar::Archive; /// Restores files from a backup archive. pub fn handle_restore(args: &RestoreArgs) -> io::Result<()> { - let archive_path_for_restore = PathBuf::from(&args.archive_path); // Start with original path + let archive_path_for_restore = PathBuf::from(&args.archive_path); + if args.archive_path.contains("e") { - // Decrypt the archive if decryption is enabled if args.decrypt { - let key = args - .decrypt_key - .as_ref() - .expect("Decryption key not provided"); - if let Err(e) = security::decrypt_file(&archive_path_for_restore, key.as_bytes()) { - match e { - CryptoError::FileRead(_) => { - eprintln!("Failed to read the archive file: {:?}", e) + let key = args.decrypt_key.as_ref().expect("Decryption key not provided"); + + let decrypted_data = match security::decrypt_file(&archive_path_for_restore, key.as_bytes()) { + Ok(data) => data, + Err(e) => { + match e { + CryptoError::FileRead(_) => eprintln!("Failed to read the archive file: {:?}", e), + CryptoError::FileWrite(_) => eprintln!("Failed to write the decrypted file: {:?}", e), + _ => eprintln!("Incorrect decryption key"), } - CryptoError::FileWrite(_) => { - eprintln!("Failed to write the decrypted file: {:?}", e) + return Ok(()); + } + }; + + let mut tar = Archive::new(GzDecoder::new(&decrypted_data[..])); + + let mut apps_to_install = Vec::new(); + let mut flatpak_apps_to_install = Vec::new(); + + for entry in tar.entries()? { + let mut entry = entry?; + let entry_path = entry.path()?; + println!("Restoring {:?}", entry_path); + + let dest_path = determine_restore_path(entry_path.to_path_buf(), &args)?; + + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent)?; + } + + if let Some(subdir) = entry_path.iter().next().and_then(|s| s.to_str()) { + match subdir { + "appsb" => collect_apps_list_from_entry(&mut entry, &mut apps_to_install)?, + "flatpakb" => collect_apps_list_from_entry(&mut entry, &mut flatpak_apps_to_install)?, + _ => { + if entry_path.extension() == Some(std::ffi::OsStr::new("gz")) { + extract_nested_tarball(&dest_path, &mut entry)?; + } else { + entry.unpack(&dest_path)?; + } + } + } + } + } + + if !apps_to_install.is_empty() { + paru::restore_installed_apps(&apps_to_install)?; + } else { + eprintln!("No Paru apps found in backup."); + } + + if !flatpak_apps_to_install.is_empty() { + flatpak::restore_installed_flatpak_apps(&flatpak_apps_to_install)?; + } else { + eprintln!("No Flatpak apps found in backup."); + } + + println!("Restore completed successfully."); + + if let Some(key) = &args.decrypt_key { + if let Err(e) = security::encrypt_file(&archive_path_for_restore, key.as_bytes()) { + match e { + CryptoError::FileRead(_) => eprintln!("Failed to read the archive file: {:?}", e), + CryptoError::FileWrite(_) => eprintln!("Failed to write the encrypted file: {:?}", e), + _ => {} } - _ => {}, } } } else { eprintln!("Decryption is not enabled. Please enable decryption by passing the --decrypt flag."); return Ok(()); } - } - // Open the archive file - let archive_file = fs::File::open(&archive_path_for_restore)?; - let archive_decoder = GzDecoder::new(archive_file); - let mut tar = Archive::new(archive_decoder); + } else { + let mut tar = open_and_decode_archive(&archive_path_for_restore)?; - // In-memory collections for apps and flatpak apps - let mut apps_to_install = Vec::new(); - let mut flatpak_apps_to_install = Vec::new(); + let mut apps_to_install = Vec::new(); + let mut flatpak_apps_to_install = Vec::new(); - // Iterate over each entry in the archive - for entry in tar.entries()? { - let mut entry = entry?; + for entry in tar.entries()? { + let mut entry = entry?; + let entry_path = entry.path()?; + println!("Restoring {:?}", entry_path); - // Get the path of the entry in the archive - let entry_path = entry.path()?; - println!("Restoring {:?}", entry_path); + let dest_path = determine_restore_path(entry_path.to_path_buf(), &args)?; - // Determine the destination path where the entry will be restored - let dest_path = determine_restore_path(entry_path.to_path_buf(), &args)?; + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent)?; + } - // Create parent directories if they don't exist - if let Some(parent) = dest_path.parent() { - fs::create_dir_all(parent)?; - } - - // Collect applications to install without writing to disk - if let Some(subdir) = entry_path.iter().next().and_then(|s| s.to_str()) { - match subdir { - "appsb" => collect_apps_list_from_entry(&mut entry, &mut apps_to_install)?, - "flatpakb" => { - collect_apps_list_from_entry(&mut entry, &mut flatpak_apps_to_install)? - } - _ => { - // Extract other entries to the destination path - if entry_path.extension() == Some(std::ffi::OsStr::new("gz")) { - // Handle nested tarball - extract_nested_tarball(&dest_path, &mut entry)?; - } else { - // Otherwise, unpack as usual - entry.unpack(&dest_path)?; + if let Some(subdir) = entry_path.iter().next().and_then(|s| s.to_str()) { + match subdir { + "appsb" => collect_apps_list_from_entry(&mut entry, &mut apps_to_install)?, + "flatpakb" => collect_apps_list_from_entry(&mut entry, &mut flatpak_apps_to_install)?, + _ => { + if entry_path.extension() == Some(std::ffi::OsStr::new("gz")) { + extract_nested_tarball(&dest_path, &mut entry)?; + } else { + entry.unpack(&dest_path)?; + } } } } } - } - // Restore installed applications - if !apps_to_install.is_empty() { - paru::restore_installed_apps(&apps_to_install)?; - } else { - eprintln!("No Paru apps found in backup."); - } + if !apps_to_install.is_empty() { + paru::restore_installed_apps(&apps_to_install)?; + } else { + eprintln!("No Paru apps found in backup."); + } - // Restore installed Flatpak applications - if !flatpak_apps_to_install.is_empty() { - flatpak::restore_installed_flatpak_apps(&flatpak_apps_to_install)?; - } else { - eprintln!("No Flatpak apps found in backup."); - } + if !flatpak_apps_to_install.is_empty() { + flatpak::restore_installed_flatpak_apps(&flatpak_apps_to_install)?; + } else { + eprintln!("No Flatpak apps found in backup."); + } - println!("Restore completed successfully."); + println!("Restore completed successfully."); + } Ok(()) } diff --git a/src/system/home.rs b/src/system/home.rs index 7456325..cc88346 100644 --- a/src/system/home.rs +++ b/src/system/home.rs @@ -2,10 +2,16 @@ use crate::utils::compression; use std::fs; use std::io; use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; /// Backs up the user's home directory. -pub fn backup_home(home_dir: &Path) -> io::Result { - let file_extension = compression::TAR_GZ; +pub fn backup_home( + home_dir: &Path, + exclude_dir: &[String], + interrupted: &Arc, +) -> io::Result { + let file_extension = compression::ARCHIVE_EXT; let file_name = format!("home_backup.{}", file_extension); let backup_file = PathBuf::from(file_name); @@ -15,7 +21,11 @@ pub fn backup_home(home_dir: &Path) -> io::Result { } // Compress the home directory - compression::compress_directory(home_dir, &backup_file)?; - - Ok(backup_file) + match compression::compress_directory(home_dir, &backup_file, Some(exclude_dir), interrupted) { + Ok(_) => Ok(backup_file), + Err(e) => { + eprintln!("Failed to backup home directory: {}", e); + Err(e) + } + } } diff --git a/src/system/keys.rs b/src/system/keys.rs index 8015451..87195d3 100644 --- a/src/system/keys.rs +++ b/src/system/keys.rs @@ -1,6 +1,8 @@ use std::fs; use std::io; use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; use crate::utils::compression; @@ -8,9 +10,9 @@ const GPG_DIR: &str = ".gnupg"; const SSH_DIR: &str = ".ssh"; /// Backs up the user's GPG keys. -pub fn backup_gpg_keys(home_dir: &Path) -> io::Result { +pub fn backup_gpg_keys(home_dir: &Path, interrupted: &Arc) -> io::Result { let gpg_path = home_dir.join(GPG_DIR); - let file_extension = compression::TAR_GZ; + let file_extension = compression::ARCHIVE_EXT; let file_name = format!("gnupg_backup.{}", file_extension); let backup_file = PathBuf::from(file_name); if !gpg_path.exists() { @@ -25,15 +27,15 @@ pub fn backup_gpg_keys(home_dir: &Path) -> io::Result { } // Compress the GPG directory - compression::compress_directory(&gpg_path, &backup_file)?; + compression::compress_directory(&gpg_path, &backup_file, None, interrupted)?; Ok(backup_file) } /// Backs up the user's SSH keys. -pub fn backup_ssh_keys(home_dir: &Path) -> io::Result { +pub fn backup_ssh_keys(home_dir: &Path, interrupted: &Arc) -> io::Result { let ssh_path = home_dir.join(SSH_DIR); - let file_extension = compression::TAR_GZ; + let file_extension = compression::ARCHIVE_EXT; let file_name = format!("ssh_backup.{}", file_extension); let backup_file = PathBuf::from(file_name); if !ssh_path.exists() { @@ -48,7 +50,7 @@ pub fn backup_ssh_keys(home_dir: &Path) -> io::Result { } // Compress the SSH directory - compression::compress_directory(&ssh_path, &backup_file)?; + compression::compress_directory(&ssh_path, &backup_file, None, interrupted)?; Ok(backup_file) } diff --git a/src/system/mod.rs b/src/system/mod.rs index 8ec133d..7046caf 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -1,2 +1,3 @@ pub mod home; pub mod keys; +// pub mod schedule; diff --git a/src/system/schedule.rs b/src/system/schedule.rs new file mode 100644 index 0000000..f58d320 --- /dev/null +++ b/src/system/schedule.rs @@ -0,0 +1,7 @@ +// use std::io; + +// use crate::cli::ScheduleArgs; +// pub fn schedule_backup(args: &ScheduleArgs) -> io::Result<()> { +// // develop backup schedule +// return Ok(()); +// } diff --git a/src/utils/compression/mod.rs b/src/utils/compression/mod.rs index 5be38fb..11f75e9 100644 --- a/src/utils/compression/mod.rs +++ b/src/utils/compression/mod.rs @@ -1,17 +1,101 @@ -use std::fs::File; -use std::io; -use std::path::Path; use flate2::write::GzEncoder; use flate2::Compression; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use walkdir::WalkDir; -pub const TAR_GZ: &str = "tar.gz"; -/// Compresses the contents of a directory into a tar.gz file. -pub fn compress_directory>(source_dir: P, target_file: P) -> io::Result<()> { +pub static ARCHIVE_EXT: &str = "tar.gz"; + +pub fn compress_directory>( + source_dir: P, + target_file: P, + exclude_dirs: Option<&[String]>, + should_stop: &AtomicBool, +) -> io::Result<()> { let tar_gz = File::create(target_file)?; let enc = GzEncoder::new(tar_gz, Compression::default()); let mut tar = tar::Builder::new(enc); - tar.append_dir_all(".", source_dir)?; + let source_dir = source_dir.as_ref(); + let exclude_paths: Vec = exclude_dirs + .unwrap_or(&[]) + .iter() + .map(|d| source_dir.join(d)) + .collect(); + + for entry in WalkDir::new(source_dir).into_iter().filter_map(|e| e.ok()) { + if should_stop.load(Ordering::SeqCst) { + return Err(io::Error::new(io::ErrorKind::Interrupted, "Operation canceled")); + } + + let entry_path = entry.path(); + + // Skip directories that need to be excluded + if exclude_paths.iter().any(|d| entry_path.starts_with(d)) { + continue; + } + + let path_in_archive = match entry_path.strip_prefix(source_dir) { + Ok(p) => p, + Err(_) => continue, + }; + + if path_in_archive.components().count() == 0 { + // Skip empty paths + continue; + } + + if entry.file_type().is_dir() { + tar.append_dir(path_in_archive, entry_path)?; + } else { + match File::open(entry_path) { + Ok(mut file) => { + tar.append_file(path_in_archive, &mut file)?; + } + Err(e) if e.kind() == io::ErrorKind::NotFound => { + eprintln!("Failed to backup file: {} - {}", entry_path.display(), e); + continue; // Skip not found files and continue the loop + } + Err(e) => return Err(e), + } + } + } + tar.finish()?; Ok(()) } + +/// Create a compressed tar archive (tar.gz) from specified files. +pub fn create_tar_gz_archive>( + archive_name: P, + backup_files: &[(String, PathBuf)], +) -> io::Result<()> { + // Create the archive file + let archive_file = File::create(&archive_name)?; + let enc = GzEncoder::new(archive_file, Compression::default()); + let mut tar = tar::Builder::new(enc); + + // Add each backup file to the archive and remove original files + for (subdir, file) in backup_files { + let path_in_archive = Path::new(subdir).join(file.file_name().unwrap()); + tar.append_path_with_name(file, path_in_archive)?; + std::fs::remove_file(file)?; + } + + // Finalize the archive + tar.finish()?; + + Ok(()) +} +/// Opens and decodes a tar.gz archive file. +pub fn open_and_decode_archive>( + archive_path: P, +) -> io::Result>> { + // Open the archive file + let archive_file = File::open(archive_path)?; + let archive_decoder = flate2::read::GzDecoder::new(archive_file); + let tar = tar::Archive::new(archive_decoder); + Ok(tar) +} diff --git a/src/utils/security/mod.rs b/src/utils/security/mod.rs index 6fb023f..7debfd7 100644 --- a/src/utils/security/mod.rs +++ b/src/utils/security/mod.rs @@ -46,18 +46,13 @@ pub fn encrypt_file>(file_path: P, key: &[u8]) -> Result<(), Cryp } /// Decrypts a file using AES-GCM. -pub fn decrypt_file>(file_path: P, key: &[u8]) -> Result<(), CryptoError> { - let cipher_text = fs::read(file_path.as_ref()).map_err(CryptoError::FileRead)?; - println!("cipher_text: {:?}", cipher_text); +pub fn decrypt_file>(file_path: P, key: &[u8]) -> Result, CryptoError> { + let data = fs::read(file_path.as_ref()).map_err(CryptoError::FileRead)?; let derived_key = derive_key(key); - println!("{:?}",derived_key); let cipher = Aes256Gcm::new(GenericArray::from_slice(&derived_key)); - let nonce = GenericArray::from_slice(b"unique nonce"); // Must be the same nonce used for encryption - println!("{:?}", nonce); + let nonce = GenericArray::from_slice(b"unique nonce"); let plaintext = cipher - .decrypt(nonce, Payload { msg: &cipher_text, aad: b"" }) + .decrypt(nonce, Payload { msg: &data, aad: b"" }) .map_err(|_| CryptoError::Decryption)?; - - fs::write(file_path.as_ref(), &plaintext).map_err(CryptoError::FileWrite)?; - Ok(()) + Ok(plaintext) }