show sizes in list of changes; sort; progress bar

in the list of changes, "add file" and "add dir"
type changes now show the size of the file to be
copied or the sum of the directory content sizes.

changes are now sorted from largest to smallest

progress bar now shows the smaller and larger
progress of change-count or transferred size,
where the larger progress is before the '>'
and the smaller one is on the last '=' before
the '-'s start (which indicate larger-not-smaller)

[========---------->               ]
This commit is contained in:
Mark 2024-12-24 01:37:18 +01:00
parent 20f37de854
commit 6172ffe248
4 changed files with 121 additions and 42 deletions

View File

@ -37,7 +37,7 @@ fn eprint_status(
changes_len_width: usize, changes_len_width: usize,
gib_len_width: usize, gib_len_width: usize,
) { ) {
let leftpad = prog_width.min( let leftpad_min = prog_width.min(
(prog_width as f64 (prog_width as f64
* f64::min( * f64::min(
changes_applied as f64 / changes_total as f64, changes_applied as f64 / changes_total as f64,
@ -45,15 +45,24 @@ fn eprint_status(
)) ))
.round() as usize, .round() as usize,
); );
let leftpad_max = prog_width.min(
(prog_width as f64
* f64::max(
changes_applied as f64 / changes_total as f64,
gib_transferred / gib_total,
))
.round() as usize,
);
let changes_applied = changes_applied.to_string(); let changes_applied = changes_applied.to_string();
let changes_pad = " ".repeat(changes_len_width - changes_applied.len()); let changes_pad = " ".repeat(changes_len_width - changes_applied.len());
let gib_transferred = format!("{gib_transferred:.1}"); let gib_transferred = format!("{gib_transferred:.1}");
let gib_pad = " ".repeat(gib_len_width - gib_transferred.len()); let gib_pad = " ".repeat(gib_len_width - gib_transferred.len());
let rightpad = prog_width - leftpad; let rightpad = prog_width - leftpad_max;
let completed_prog = "-".repeat(leftpad); let completed_prog_min = "=".repeat(leftpad_min);
let completed_prog_max = "-".repeat(leftpad_max - leftpad_min);
let pending_prog = " ".repeat(rightpad); let pending_prog = " ".repeat(rightpad);
eprint!( eprint!(
"\r{changes_pad}{changes_applied}/{changes_total} | {gib_pad}{gib_transferred}/{gib_total:.1}GiB [{completed_prog}>{pending_prog}]", "\r{changes_pad}{changes_applied}/{changes_total} | {gib_pad}{gib_transferred}/{gib_total:.1}GiB [{completed_prog_min}{completed_prog_max}>{pending_prog}]",
); );
} }
@ -91,16 +100,10 @@ pub fn apply_indexchanges_int(
); );
for (i, change) in changes.iter().enumerate() { for (i, change) in changes.iter().enumerate() {
match change { match change {
IndexChange::AddDir(dir) => { IndexChange::AddDir(dir, _) => {
let ok = if let Some(target) = target { let ok = if let Some(target) = target {
let t = target.join(dir); let t = target.join(dir);
if let Some(e) = fs::create_dir(&t).err().and_then(|e| { if let Err(e) = fs::create_dir_all(&t) {
if matches!(e.kind(), io::ErrorKind::AlreadyExists) {
None
} else {
Some(e)
}
}) {
eprintln!("\n[warn] couldn't create directory {t:?}: {e}"); eprintln!("\n[warn] couldn't create directory {t:?}: {e}");
false false
} else { } else {
@ -110,7 +113,7 @@ pub fn apply_indexchanges_int(
true true
}; };
if ok { if ok {
fs::create_dir(&index.join(dir))?; fs::create_dir_all(&index.join(dir))?;
} }
} }
IndexChange::AddFile(file, index_file) => { IndexChange::AddFile(file, index_file) => {

View File

@ -5,7 +5,7 @@ use crate::indexfile::IndexFile;
#[derive(Debug)] #[derive(Debug)]
pub enum IndexChange { pub enum IndexChange {
/// Ensure a directory with this path exists (at least if all its parent directories exist). /// Ensure a directory with this path exists (at least if all its parent directories exist).
AddDir(PathBuf), AddDir(PathBuf, u64),
/// Add or update a file /// Add or update a file
AddFile(PathBuf, IndexFile), AddFile(PathBuf, IndexFile),
/// Remove a file /// Remove a file

View File

@ -71,12 +71,17 @@ fn main() {
} else { } else {
Ignore(vec![]) Ignore(vec![])
}; };
let changes = match perform_index_diff( let (total_size, changes) = match perform_index_diff(
&source, &source,
&index, &index,
target.as_ref().map(|v| v.as_path()), target.as_ref().map(|v| v.as_path()),
ignore, ignore,
&args.settings, &args.settings,
if args.settings.dont_sort {
None
} else {
Some(!args.settings.smallest_first)
},
) { ) {
Ok(c) => c, Ok(c) => c,
Err((what, path, err)) => { Err((what, path, err)) => {
@ -92,12 +97,42 @@ fn main() {
} else { } else {
eprintln!("done! found {} changes:", changes.len()); eprintln!("done! found {} changes:", changes.len());
// display the changes // display the changes
if args.settings.dont_reverse_output {
for change in &changes { for change in &changes {
show_change(change, false);
}
} else {
for change in changes.iter().rev() {
show_change(change, true);
}
}
fn show_change(change: &IndexChange, rev: bool) {
match change { match change {
IndexChange::AddDir(v) => eprintln!(" >> {}", v.display()), IndexChange::AddDir(v, s) => {
IndexChange::AddFile(v, _) => eprintln!(" + {}", v.display()), let mut path_str = v.display().to_string();
if !path_str.ends_with(['/', '\\']) {
path_str.push('/');
}
eprintln!(
"{}>> {} [{:.2} GiB]",
if rev { "^" } else { "v" },
path_str,
*s as f64 / (1024 * 1024 * 1024) as f64
);
}
IndexChange::AddFile(v, f) => eprintln!(
" + {} ({:.3} GiB)",
v.display(),
f.size as f64 / (1024 * 1024 * 1024) as f64
),
IndexChange::RemoveFile(v) => eprintln!(" - {}", v.display()), IndexChange::RemoveFile(v) => eprintln!(" - {}", v.display()),
IndexChange::RemoveDir(v) => eprintln!(" [-] {}", v.display()), IndexChange::RemoveDir(v) => {
let mut path_str = v.display().to_string();
if !path_str.ends_with(['/', '\\']) {
path_str.push('/');
}
eprintln!(" [-] {}", path_str);
}
} }
} }
eprintln!(" - - - - -"); eprintln!(" - - - - -");
@ -105,8 +140,15 @@ fn main() {
.iter() .iter()
.filter(|c| matches!(c, IndexChange::AddDir(..))) .filter(|c| matches!(c, IndexChange::AddDir(..)))
.count(); .count();
eprintln!(" >> add directory | {add_dir_count}x"); eprintln!(
let (add_file_count, add_file_total_size_gib) = changes " {}>> add directory | {add_dir_count}x",
if args.settings.dont_reverse_output {
"v"
} else {
"^"
}
);
let add_file_count = changes
.iter() .iter()
.filter_map(|c| { .filter_map(|c| {
if let IndexChange::AddFile(_, f) = c { if let IndexChange::AddFile(_, f) = c {
@ -115,9 +157,8 @@ fn main() {
None None
} }
}) })
.fold((0, 0.0f64), |(c, s), f| { .count();
(c + 1, s + f.size as f64 / (1024 * 1024 * 1024) as f64) let add_file_total_size_gib = total_size as f64 / (1024 * 1024 * 1024) as f64;
});
eprintln!(" + add/update file | {add_file_count}x ({add_file_total_size_gib:.1} GiB)"); eprintln!(" + add/update file | {add_file_count}x ({add_file_total_size_gib:.1} GiB)");
let remove_file_count = changes let remove_file_count = changes
.iter() .iter()

View File

@ -14,6 +14,16 @@ use crate::{
#[derive(Clone, Default, Args)] #[derive(Clone, Default, Args)]
pub struct Settings { pub struct Settings {
/// don't sort the changes that form the backup. disables sort options.
#[arg(long)]
pub dont_sort: bool,
/// start with smaller directories rather than larger ones
#[arg(long)]
pub smallest_first: bool,
/// show changes in the order in which they will be applied, not reversed
#[arg(long)]
pub dont_reverse_output: bool,
/// don't update files just because their timestamp is different /// don't update files just because their timestamp is different
#[arg(long)] #[arg(long)]
pub ignore_timestamp: bool, pub ignore_timestamp: bool,
@ -37,8 +47,8 @@ pub fn perform_index_diff<'a>(
target: Option<&'a Path>, target: Option<&'a Path>,
mut ignore: Ignore, mut ignore: Ignore,
settings: &Settings, settings: &Settings,
) -> Result<Vec<IndexChange>, (String, PathBuf, io::Error)> { sort_by_size_largest: Option<bool>,
let mut changes = Vec::new(); ) -> Result<(u64, Vec<IndexChange>), (String, PathBuf, io::Error)> {
if let Ok(inner_index) = index.strip_prefix(source) { if let Ok(inner_index) = index.strip_prefix(source) {
eprintln!("[info] source contains index at {inner_index:?}, but index will not be part of the backup."); eprintln!("[info] source contains index at {inner_index:?}, but index will not be part of the backup.");
ignore.0.push(Specifier::InDir { ignore.0.push(Specifier::InDir {
@ -55,15 +65,15 @@ pub fn perform_index_diff<'a>(
}); });
} }
} }
rec( let (total_size, changes) = rec(
source.as_ref(), source.as_ref(),
Path::new(""), Path::new(""),
index, index,
&mut changes,
&ignore, &ignore,
settings, settings,
sort_by_size_largest,
)?; )?;
Ok(changes) Ok((total_size, changes))
} }
fn rec( fn rec(
// location of source files // location of source files
@ -72,18 +82,17 @@ fn rec(
rel_path: &Path, rel_path: &Path,
// location of the index // location of the index
index_files: &Path, index_files: &Path,
// list of changes to be made
changes: &mut Vec<IndexChange>,
ignore: &Ignore, ignore: &Ignore,
settings: &Settings, settings: &Settings,
) -> Result<(), (String, PathBuf, io::Error)> { sort_by_size_largest: Option<bool>,
) -> Result<(u64, Vec<IndexChange>), (String, PathBuf, io::Error)> {
let mut removals = vec![];
let mut ichanges = vec![];
let mut total_size = 0;
// used to find removals // used to find removals
let index_rel_path = index_files.join(rel_path); let index_rel_path = index_files.join(rel_path);
let mut index_entries = match fs::read_dir(&index_rel_path) { let mut index_entries = match fs::read_dir(&index_rel_path) {
Err(_) => { Err(_) => HashMap::new(),
changes.push(IndexChange::AddDir(rel_path.to_path_buf()));
HashMap::new()
}
Ok(e) => e Ok(e) => e
.into_iter() .into_iter()
.filter_map(|v| v.ok()) .filter_map(|v| v.ok())
@ -128,29 +137,55 @@ fn rec(
if metadata.is_dir() { if metadata.is_dir() {
if let Some(false) = in_index_and_is_dir { if let Some(false) = in_index_and_is_dir {
// is dir, but was file -> remove file // is dir, but was file -> remove file
changes.push(IndexChange::RemoveFile(rel_path.clone())); removals.push(IndexChange::RemoveFile(rel_path.clone()));
} }
rec(source, &rel_path, index_files, changes, ignore, settings)?; let (rec_size, rec_changes) = rec(
source,
&rel_path,
index_files,
ignore,
settings,
sort_by_size_largest,
)?;
total_size += rec_size;
ichanges.push((rec_size, rec_changes));
} else { } else {
if let Some(true) = in_index_and_is_dir { if let Some(true) = in_index_and_is_dir {
// is file, but was dir -> remove dir // is file, but was dir -> remove dir
changes.push(IndexChange::RemoveDir(rel_path.clone())); removals.push(IndexChange::RemoveDir(rel_path.clone()));
} }
let newif = IndexFile::new_from_metadata(&metadata); let newif = IndexFile::new_from_metadata(&metadata);
let oldif = IndexFile::from_path(&index_files.join(&rel_path)); let oldif = IndexFile::from_path(&index_files.join(&rel_path));
match oldif { match oldif {
Ok(Ok(oldif)) if !newif.should_be_updated(&oldif, settings) => {} Ok(Ok(oldif)) if !newif.should_be_updated(&oldif, settings) => {}
_ => changes.push(IndexChange::AddFile(rel_path, newif)), _ => {
total_size += newif.size;
ichanges.push((newif.size, vec![IndexChange::AddFile(rel_path, newif)]));
}
} }
} }
} }
// removals // removals
for (removed_file, is_dir) in index_entries { for (removed_file, is_dir) in index_entries {
changes.push(if is_dir { removals.push(if is_dir {
IndexChange::RemoveDir(rel_path.join(removed_file)) IndexChange::RemoveDir(rel_path.join(removed_file))
} else { } else {
IndexChange::RemoveFile(rel_path.join(removed_file)) IndexChange::RemoveFile(rel_path.join(removed_file))
}); });
} }
Ok(()) // sorting
if let Some(sort_largest_first) = sort_by_size_largest {
if sort_largest_first {
ichanges.sort_by(|a, b| b.0.cmp(&a.0));
} else {
ichanges.sort_by_key(|v| v.0);
}
}
// combine everything
let changes = [IndexChange::AddDir(rel_path.to_path_buf(), total_size)]
.into_iter()
.chain(removals.into_iter())
.chain(ichanges.into_iter().flat_map(|(_, v)| v))
.collect();
Ok((total_size, changes))
} }