forge_backup/
backup.rs

1use crate::error::{AppResult, S3CopyError};
2use crate::{config::Config, error::BackupError};
3use chrono::Local;
4use indicatif::{ProgressBar, ProgressStyle};
5use std::ffi::OsString;
6use std::io::{self, Write};
7use std::path::Path;
8use std::process::Command;
9
10pub fn run(config: &Config) -> AppResult<(String, String)> {
11    let mut homes = get_home_directories(&config.home_dir)?;
12    homes.retain(|dir| !config.exclude_users.contains(dir));
13
14    ensure_directory_exists(&config.temp_folder)?;
15
16    let s3_folder = format!(
17        "s3://{}/{}/{}/{}/",
18        &config.s3_bucket,
19        &config.s3_folder,
20        &config.hostname,
21        Local::now().format("%Y-%m%d")
22    );
23    let pb = ProgressBar::new(homes.len().try_into().unwrap());
24    let tmpl = ProgressStyle::with_template(
25        "{spinner:.green} [{elapsed_precise}] [{bar:.cyan/blue}] {pos:>7}/{len:7} {msg}",
26    )
27    .unwrap()
28    .progress_chars("#>-");
29    pb.set_style(tmpl);
30    let (ok_results, err_results): (Vec<_>, Vec<_>) = homes
31        .iter()
32        .map(|home| {
33            pb.set_message("home");
34            pb.inc(1);
35
36            perform_backup_of_home(home, config, &s3_folder, true)
37        })
38        .partition(Result::is_ok);
39    pb.finish_with_message("done");
40
41    let ok_results = ok_results
42        .into_iter()
43        .map(Result::unwrap)
44        .collect::<Vec<_>>()
45        .join("\n");
46
47    let err_results = err_results
48        .into_iter()
49        .map(|e| e.unwrap_err().to_string())
50        .collect::<Vec<_>>()
51        .join("\n");
52
53    Ok((ok_results, err_results))
54}
55
56fn perform_backup_of_home<P: AsRef<Path>>(
57    home: P,
58    config: &Config,
59    s3_folder: &str,
60    _verbose: bool,
61) -> AppResult<String> {
62    let home_path = Path::new(&config.home_dir).join(&home);
63
64    // 1. make sure directory exists
65    if !home_path.exists() {
66        return Err(BackupError::MissingHomeError(home_path).into());
67    }
68
69    // 2. set backup file name
70    let home_str: &str = home
71        .as_ref()
72        .to_str()
73        .ok_or_else(|| BackupError::InvalidHomeError(home_path.clone()))?;
74    io::stdout().flush().unwrap();
75
76    let file = format!("{}_backup_{}.zip", home_str, Local::now().format("%Y-%m%d"));
77    let backup_file = Path::new(&config.temp_folder).join(file);
78    let backup_file = backup_file.to_str().ok_or_else(|| {
79        BackupError::MissingTempError(Path::new(&config.temp_folder).to_path_buf())
80    })?;
81    let home_folder = &home_path
82        .to_str()
83        .ok_or_else(|| BackupError::MissingHomeError(home_path.clone()))?;
84
85    _ = zip(backup_file, home_folder, &config.exclude_files)?;
86    _ = copy_to_s3(backup_file, s3_folder, config.aws_profile.as_deref())?;
87    delete_backup(backup_file)?;
88
89    Ok(format!("SUCCESS: {backup_file} backed up to: {s3_folder}"))
90}
91
92fn delete_backup<P: AsRef<Path>>(file: P) -> AppResult<()> {
93    std::fs::remove_file(file.as_ref())
94        .map_err(|_| BackupError::DeleteTempError(file.as_ref().to_path_buf()))?;
95    Ok(())
96}
97
98fn copy_to_s3<P: AsRef<Path>>(file: P, folder: &str, profile: Option<&str>) -> AppResult<String> {
99    let mut command = Command::new("aws");
100
101    let file = file
102        .as_ref()
103        .to_str()
104        .ok_or_else(|| BackupError::S3InvalidFile(file.as_ref().to_path_buf()))?;
105
106    if let Some(profile) = profile {
107        command.arg("--profile").arg(profile);
108    }
109
110    command.arg("s3").arg("cp").arg(file).arg(folder);
111
112    let output = command.output().map_err(|_| {
113        BackupError::S3CopyError(S3CopyError {
114            src: file.to_owned(),
115            dest: folder.to_owned(),
116            std_err: "Failed to execute command".to_owned(),
117            std_out: "".to_owned(),
118        })
119    })?;
120
121    if !output.status.success() {
122        let std_err = String::from_utf8_lossy(&output.stderr).to_string();
123        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
124
125        return Err(BackupError::S3CopyError(S3CopyError {
126            src: file.to_owned(),
127            dest: folder.to_owned(),
128            std_err,
129            std_out: stdout,
130        })
131        .into());
132    }
133
134    Ok(format!("Copied file: {file} to {folder}"))
135}
136
137fn zip(file: &str, folder: &str, exclude_list: &[String]) -> AppResult<String> {
138    let mut command = Command::new("zip");
139    command.arg("-q").arg("-r").arg(file).arg(folder);
140
141    for exclude in exclude_list {
142        let mut exclude_arg = OsString::from("--exclude=");
143        exclude_arg.push(exclude);
144        command.arg(exclude_arg);
145    }
146
147    let output = command
148        .output()
149        .map_err(|e| BackupError::ZipError(format!("Failed to execute command: {e}")))?;
150
151    if !output.status.success() {
152        let std_err = String::from_utf8_lossy(&output.stderr).to_string();
153        let std_out = String::from_utf8_lossy(&output.stdout).to_string();
154
155        return Err(BackupError::ZipError(format!(
156            "zip failed with status {:?}\nstdout: \n{}\nstderr:\n{}",
157            output.status.code(),
158            std_out,
159            std_err
160        ))
161        .into());
162    }
163
164    Ok(format!("Compressed folder: {folder} to {file}"))
165}
166
167fn ensure_directory_exists<P: AsRef<Path>>(path: P) -> AppResult<()> {
168    if !path.as_ref().exists() {
169        std::fs::create_dir_all(&path)
170            .map_err(|_| BackupError::MakeDirectoryError(path.as_ref().to_path_buf()))?;
171    }
172
173    Ok(())
174}
175fn get_home_directories<P: AsRef<Path> + Copy>(path: P) -> AppResult<Vec<String>> {
176    Ok(std::fs::read_dir(path)
177        .map_err(|_| BackupError::DirectoryReadError(path.as_ref().to_path_buf()))?
178        .filter_map(|entry| {
179            entry.ok().and_then(|e| {
180                if e.path().is_dir() {
181                    e.path()
182                        .file_name()
183                        .and_then(|name| name.to_str().map(|s| s.to_string()))
184                } else {
185                    None
186                }
187            })
188        })
189        .collect::<Vec<String>>())
190}