use crate::config::Config;
use crate::db::pool::DbPool;
use crate::errors::{AppError, AppResult};
use crate::ui::messages::{info, success as ok, warning as warn};
use rusqlite::Connection;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use zip::ZipWriter;
use zip::write::FileOptions;
pub struct BackupLogic;
impl BackupLogic {
pub fn backup(
_pool: &mut DbPool,
cfg: &Config,
dest_file: &str,
compress: bool,
) -> AppResult<()> {
let src = Path::new(&cfg.database);
let dest = Path::new(dest_file);
if !src.exists() {
return Err(AppError::Io(io::Error::new(
io::ErrorKind::NotFound,
format!("Database not found: {}", src.display()),
)));
}
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).map_err(AppError::Io)?;
}
if dest.exists() {
warn(format!(
"The backup file '{}' already exists.",
dest.display()
));
if !ask_overwrite_confirmation()? {
info("Backup cancelled by user.".to_string());
return Ok(());
}
}
fs::copy(src, dest).map_err(AppError::Io)?;
ok(format!("Backup created: {}", dest.display()));
let final_path = if compress {
let zipped = compress_backup(dest)?;
if zipped != dest {
if let Err(e) = fs::remove_file(dest) {
warn(format!(
"Failed to delete temporary uncompressed backup: {}",
e
));
} else {
info(format!("Removed uncompressed backup: {}", dest.display()));
}
}
zipped
} else {
dest.to_path_buf()
};
if let Ok(conn) = Connection::open(src) {
let _ = crate::db::log::ttlog(
&conn,
"backup",
&final_path.to_string_lossy(),
if compress {
"Backup created and compressed"
} else {
"Backup created"
},
);
}
Ok(())
}
}
fn ask_overwrite_confirmation() -> AppResult<bool> {
use std::io::{stdin, stdout};
println!("Do you want to overwrite it? [y/N]: ");
print!("> ");
stdout().flush().ok();
let mut input = String::new();
stdin().read_line(&mut input).map_err(AppError::Io)?;
let ans = input.trim().to_lowercase();
Ok(ans == "y" || ans == "yes")
}
fn compress_backup(path: &Path) -> AppResult<PathBuf> {
let zip_path = path.with_extension("zip");
let file = fs::File::create(&zip_path).map_err(AppError::Io)?;
let mut zip = ZipWriter::new(file);
let options: FileOptions<'_, ()> =
FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let filename = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.ok_or_else(|| AppError::InvalidOperation("Invalid file name for backup".into()))?;
zip.start_file(filename, options)
.map_err(|e| AppError::Io(io::Error::other(e)))?;
let mut f = fs::File::open(path).map_err(AppError::Io)?;
io::copy(&mut f, &mut zip).map_err(AppError::Io)?;
zip.finish()
.map_err(|e| AppError::Io(io::Error::other(e)))?;
ok(format!("Compressed backup: {}", zip_path.display()));
Ok(zip_path)
}