diff --git a/Cargo.lock b/Cargo.lock index 4f4288f..933d632 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -423,6 +423,7 @@ dependencies = [ "humansize", "indicatif", "itertools 0.10.3", + "libc", "log", "metrics", "serde", diff --git a/datman/Cargo.toml b/datman/Cargo.toml index 6711534..6f243aa 100644 --- a/datman/Cargo.toml +++ b/datman/Cargo.toml @@ -33,4 +33,5 @@ hostname = "0.3.1" yama = { path = "../yama", version = "0.6.0-alpha.1" } metrics = "0.17.1" bare-metrics-recorder = { version = "0.1.0" } -comfy-table = "6.0.0-rc.1" \ No newline at end of file +comfy-table = "6.0.0-rc.1" +libc = "0.2.126" diff --git a/datman/src/bin/datman.rs b/datman/src/bin/datman.rs index 3cf3faa..f3dd5ac 100644 --- a/datman/src/bin/datman.rs +++ b/datman/src/bin/datman.rs @@ -317,9 +317,9 @@ fn main() -> anyhow::Result<()> { let descriptor = load_descriptor(Path::new(".")).unwrap(); let destination = &descriptor.piles[&pile_name]; let report = datman::commands::report::generate_report(destination, &descriptor)?; - // TODO Display report - // TODO E-mail report (Can just pipe through aha and then apprise though!) + datman::commands::report::print_report(&report)?; + datman::commands::report::print_filesystem_space(&destination.path)?; } } Ok(()) diff --git a/datman/src/commands/report.rs b/datman/src/commands/report.rs index b98e1b1..7c3b205 100644 --- a/datman/src/commands/report.rs +++ b/datman/src/commands/report.rs @@ -1,14 +1,19 @@ use crate::commands::backup::split_pointer_name; use crate::descriptor::{Descriptor, DestPileDescriptor}; use anyhow::Context; -use chrono::{DateTime, Utc}; +use chrono::{Date, DateTime, Utc}; use comfy_table::presets::UTF8_FULL; -use comfy_table::{Cell, Color, ContentArrangement, Table}; +use comfy_table::{Attribute, Cell, Color, ContentArrangement, Table}; use humansize::FileSize; +use itertools::Itertools; use log::info; use std::collections::{BTreeMap, BTreeSet}; +use std::ffi::CString; use std::io::Read; +use std::mem; use std::mem::size_of; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; use yama::chunking::RecursiveUnchunker; use yama::commands::{load_pile_descriptor, open_pile, retrieve_tree_node}; use yama::definitions::{ChunkId, RecursiveChunkRef, TreeNode}; @@ -212,10 +217,86 @@ fn collect_chunk_ids_from_chunkref( } pub fn print_report(report: &Report) -> anyhow::Result<()> { + print_time_report(report)?; + print_size_report(report)?; + Ok(()) +} + +pub fn print_time_report(report: &Report) -> anyhow::Result<()> { + println!("\nBackup times"); let mut table = Table::new(); table .load_preset(UTF8_FULL) - .set_content_arrangement(ContentArrangement::DynamicFullWidth); + .set_content_arrangement(ContentArrangement::DynamicFullWidth) + .enforce_styling(); + + table.set_header(vec![ + Cell::new("Source name").fg(Color::Cyan), + Cell::new("Last backed up").fg(Color::Cyan), + ]); + + let today = Utc::today(); + + let sort_by_dates: Vec<(Option>, String)> = report + .last_source_backups + .iter() + .map(|(name, datetime)| (datetime.map(|dt| dt.date()), name.to_owned())) + .sorted() + .collect(); + + for (date, source_name) in sort_by_dates { + match date { + None => { + table.add_row(vec![ + Cell::new(source_name).fg(Color::Magenta), + Cell::new("NEVER").fg(Color::Red).add_attributes(vec![ + Attribute::SlowBlink, + Attribute::RapidBlink, + Attribute::Bold, + ]), + ]); + } + Some(date) => { + let number_of_days = today.signed_duration_since(date).num_days(); + let num_days_human = if number_of_days > 0 { + format!("{number_of_days} days ago") + } else { + format!("today") + }; + + let colour = if number_of_days < 2 { + Color::Green + } else if number_of_days < 14 { + Color::Yellow + } else { + Color::Red + }; + + let formatted_date = date.format("%F"); + + let mut val_cell = + Cell::new(format!("{formatted_date} {num_days_human}")).fg(colour); + if number_of_days > 28 { + val_cell = val_cell.add_attribute(Attribute::SlowBlink); + } + + table.add_row(vec![Cell::new(source_name).fg(Color::Magenta), val_cell]); + } + } + } + + println!("{table}"); + + Ok(()) +} + +pub fn print_size_report(report: &Report) -> anyhow::Result<()> { + println!("\nPile size"); + let mut table = Table::new(); + table + .load_preset(UTF8_FULL) + .set_content_arrangement(ContentArrangement::DynamicFullWidth) + .enforce_styling(); //.set_width(100); table.set_header(vec![ Cell::new("Pointer name").fg(Color::Cyan), @@ -248,13 +329,76 @@ fn format_size(chunks: u32, average_chunk_size: Option) -> String { let est_size_suffix = average_chunk_size .map(|bytes_per_chunk| { let num_bytes = (chunks as f64 * bytes_per_chunk) as u64; - format!( - " ~{}", - num_bytes - .file_size(humansize::file_size_opts::BINARY) - .unwrap() - ) + let mut format = humansize::file_size_opts::BINARY; + format.decimal_places = 1; + format!(" ~{}", num_bytes.file_size(format).unwrap()) }) .unwrap_or_default(); format!("{} c{}", chunks, est_size_suffix) } + +pub fn print_filesystem_space(pile_path: &Path) -> anyhow::Result<()> { + let path_c = CString::new(pile_path.as_os_str().as_bytes()).unwrap(); + let stats = unsafe { + let mut stats: libc::statfs = mem::zeroed(); + match libc::statfs(path_c.as_ptr(), &mut stats) { + 0 => Ok(stats), + other => Err(std::io::Error::from_raw_os_error(other)), + } + }?; + + // On a BTRFS system with 2 disks in RAID1, note (about df -h): + // - 'Size' shows the average size of the two disks. I think of it as 'ideal size'. + // - 'Avail' seems to show the actual number of bytes usable. + // - 'Used' seems to show the actual number of bytes used. + // In short: probably avoid relying on 'size'. + + let block_size = stats.f_bsize as i64; + let used_bytes = (stats.f_blocks - stats.f_bfree) as i64 * block_size; + let avail_bytes = stats.f_bavail as i64 * block_size; + let usable_bytes = used_bytes + avail_bytes; + let theoretical_size = stats.f_blocks as i64 * block_size; + + let mut format = humansize::file_size_opts::BINARY; + format.decimal_places = 1; + format.decimal_zeroes = 1; + + println!("\nFilesystem Information"); + + let mut table = Table::new(); + table + .load_preset(UTF8_FULL) + .set_content_arrangement(ContentArrangement::DynamicFullWidth) + .enforce_styling(); + //.set_width(100); + table.set_header(vec![ + Cell::new("Theoretical Size").fg(Color::Cyan), + Cell::new("Usable Size").fg(Color::Cyan), + Cell::new("Used").fg(Color::Cyan), + Cell::new("Available").fg(Color::Cyan), + ]); + + let available_space_colour = if avail_bytes < 8 * 1024 * 1024 * 1024 { + Color::Red + } else if avail_bytes < 64 * 1024 * 1024 * 1024 { + Color::Yellow + } else { + Color::Green + }; + + table.add_row(vec![ + Cell::new(format!( + "{:>9}", + theoretical_size.file_size(&format).unwrap() + )) + .fg(Color::Blue), + Cell::new(format!("{:>9}", usable_bytes.file_size(&format).unwrap())).fg(Color::Blue), + Cell::new(format!("{:>9}", used_bytes.file_size(&format).unwrap())).fg(Color::Blue), + Cell::new(format!("{:>9}", avail_bytes.file_size(&format).unwrap())) + .fg(available_space_colour), + ]); + + print!("{table}"); + + Ok(()) +}