use super::progress::{REFRESH_LABEL, ZIP_LABEL};
use super::*;
use unicode_width::UnicodeWidthStr;
use std::path::{Path, PathBuf};
use std::sync::mpsc::TryRecvError;
use chrono::{DateTime, Local};
use crate::domain::backup;
use crate::domain::repo::Repo;
use crate::service::zip_service::{ZipJob, spawn_zip};
use crate::util::paths::expand_tilde;
pub(super) fn backup_dest(
folder: &Path,
repo: &Repo,
repos: &[Repo],
) -> PathBuf {
folder.join(backup::backup_filename(repo, repos))
}
impl App {
pub(super) fn zip_targets(&mut self) {
let targets = self.targets();
if targets.is_empty() {
self.set_status("nothing to zip");
return;
}
self.start_zip(&targets);
self.clear_selection();
}
pub(super) fn zip_all(&mut self) {
let indices: Vec<usize> = self
.service
.repos()
.iter()
.enumerate()
.filter(|(_, repo)| repo.include_in_backup)
.map(|(index, _)| index)
.collect();
if indices.is_empty() {
self.set_status("nothing to zip");
return;
}
self.start_zip(&indices);
}
pub(super) fn start_zip(&mut self, indices: &[usize]) {
if self.loading.is_some() {
self.set_status("busy: a refresh or backup is running");
return;
}
let Some(folder) = self.config.zip_backup_folder.as_deref() else {
self.set_status("no zip_backup_folder configured");
return;
};
let folder = expand_tilde(folder);
if let Err(error) = std::fs::create_dir_all(&folder) {
self.set_status(format!("could not create backup folder: {error}"));
return;
}
let repos = self.service.repos();
let jobs: Vec<ZipJob> = indices
.iter()
.filter_map(|&i| repos.get(i))
.filter(|repo| repo.path.is_dir())
.map(|repo| ZipJob {
src: repo.path.clone(),
dest: backup_dest(&folder, repo, repos),
name: repo.display_name(),
})
.collect();
if jobs.is_empty() {
self.set_status("nothing to zip (paths missing?)");
return;
}
let count = jobs.len();
self.set_status(if count == 1 {
"creating backup…".to_string()
} else {
format!("creating {count} backups…")
});
self.loading_name_width = jobs
.iter()
.map(|job| UnicodeWidthStr::width(job.name.as_str()))
.max()
.unwrap_or(0);
self.loading = Some((0, 0));
self.loading_label = ZIP_LABEL;
self.loading_detail = None;
self.zip_rx = Some(spawn_zip(
jobs,
self.config.zip_exclude_dirs.clone(),
self.zip_cache_path.clone(),
));
}
pub(super) fn drain_zip(&mut self) {
let Some(rx) = self.zip_rx.take() else {
return;
};
let mut summary = None;
let mut disconnected = false;
loop {
match rx.try_recv() {
Ok(update) => {
self.loading = Some((update.done, update.total));
if update.label.is_some() {
self.loading_detail = update.label;
}
if update.finished {
summary = Some((
update.archives,
update.unchanged,
update.errors,
));
}
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
disconnected = true;
break;
}
}
}
if disconnected || summary.is_some() {
self.loading = None;
self.loading_label = REFRESH_LABEL;
self.loading_detail = None;
self.reload_zip_backups();
if let Some((archives, unchanged, errors)) = summary {
self.report_zip_done(archives, unchanged, errors);
}
} else {
self.zip_rx = Some(rx);
}
}
pub(super) fn report_zip_done(
&mut self,
archives: usize,
unchanged: usize,
errors: usize,
) {
if archives == 0 && errors == 0 && unchanged > 0 {
self.set_status("backup up to date (no changes)");
return;
}
let folder =
self.config.zip_backup_folder.as_deref().unwrap_or_default();
let mut message = if archives == 1 {
format!("backed up 1 archive to {folder}")
} else {
format!("backed up {archives} archives to {folder}")
};
if unchanged > 0 {
message.push_str(&format!(" ({unchanged} unchanged)"));
}
if errors > 0 {
message.push_str(&format!(" ({errors} failed)"));
}
self.set_status(message);
}
pub(super) fn is_zipping(&self) -> bool {
self.zip_rx.is_some()
}
pub(super) fn reload_zip_backups(&mut self) {
self.zip_backups.clear();
let Some(folder) = self.config.zip_backup_folder.as_deref() else {
return;
};
let folder = expand_tilde(folder);
let repos = self.service.repos();
for repo in repos.iter() {
let dest = backup_dest(&folder, repo, repos);
if let Ok(meta) = std::fs::metadata(&dest)
&& let Ok(modified) = meta.modified()
{
self.zip_backups.insert(
repo.path.clone(),
DateTime::<Local>::from(modified),
);
}
}
}
}