ready to use

This commit is contained in:
DanielcoderX 2024-06-25 22:54:21 +03:30
parent 3e6b0afc7a
commit e626185d13
No known key found for this signature in database
GPG key ID: 567ED2DAC3277531
15 changed files with 683 additions and 250 deletions

178
Cargo.lock generated
View file

@ -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"

View file

@ -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.

View file

@ -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

View file

@ -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<AtomicBool>,
) {
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<AtomicBool>,
) {
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.");
}

View file

@ -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(())
}

View file

@ -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<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<String>,
/// Backup flatpak applications
#[arg(long, help = "Backup flatpak applications")]
pub flatpak: bool,
@ -65,13 +73,42 @@ pub struct RestoreArgs {
pub decrypt_key: Option<String>,
}
#[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<String>,
// /// 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<String>,
// }
pub fn parse_cli() -> Cli {
Cli::parse()

View file

@ -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);
// }
// }
}
}

View file

@ -13,7 +13,13 @@ pub fn list_installed_apps() -> io::Result<PathBuf> {
.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::<Vec<_>>()
.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())?;

View file

@ -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(())
}

View file

@ -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<PathBuf> {
let file_extension = compression::TAR_GZ;
pub fn backup_home(
home_dir: &Path,
exclude_dir: &[String],
interrupted: &Arc<AtomicBool>,
) -> io::Result<PathBuf> {
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<PathBuf> {
}
// 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)
}
}
}

View file

@ -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<PathBuf> {
pub fn backup_gpg_keys(home_dir: &Path, interrupted: &Arc<AtomicBool>) -> io::Result<PathBuf> {
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<PathBuf> {
}
// 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<PathBuf> {
pub fn backup_ssh_keys(home_dir: &Path, interrupted: &Arc<AtomicBool>) -> io::Result<PathBuf> {
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<PathBuf> {
}
// Compress the SSH directory
compression::compress_directory(&ssh_path, &backup_file)?;
compression::compress_directory(&ssh_path, &backup_file, None, interrupted)?;
Ok(backup_file)
}

View file

@ -1,2 +1,3 @@
pub mod home;
pub mod keys;
// pub mod schedule;

7
src/system/schedule.rs Normal file
View file

@ -0,0 +1,7 @@
// use std::io;
// use crate::cli::ScheduleArgs;
// pub fn schedule_backup(args: &ScheduleArgs) -> io::Result<()> {
// // develop backup schedule
// return Ok(());
// }

View file

@ -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<P: AsRef<Path>>(source_dir: P, target_file: P) -> io::Result<()> {
pub static ARCHIVE_EXT: &str = "tar.gz";
pub fn compress_directory<P: AsRef<Path>>(
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<PathBuf> = 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<P: AsRef<Path>>(
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<P: AsRef<Path>>(
archive_path: P,
) -> io::Result<tar::Archive<flate2::read::GzDecoder<File>>> {
// 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)
}

View file

@ -46,18 +46,13 @@ pub fn encrypt_file<P: AsRef<Path>>(file_path: P, key: &[u8]) -> Result<(), Cryp
}
/// Decrypts a file using AES-GCM.
pub fn decrypt_file<P: AsRef<Path>>(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<P: AsRef<Path>>(file_path: P, key: &[u8]) -> Result<Vec<u8>, 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)
}