Add times and disk space to the report
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/push/release Pipeline was successful Details

This commit is contained in:
Olivier 'reivilibre' 2022-05-29 17:24:46 +01:00
parent 948ca3f2b5
commit ec8c5ff42d
4 changed files with 158 additions and 12 deletions

1
Cargo.lock generated
View File

@ -423,6 +423,7 @@ dependencies = [
"humansize", "humansize",
"indicatif", "indicatif",
"itertools 0.10.3", "itertools 0.10.3",
"libc",
"log", "log",
"metrics", "metrics",
"serde", "serde",

View File

@ -34,3 +34,4 @@ yama = { path = "../yama", version = "0.6.0-alpha.1" }
metrics = "0.17.1" metrics = "0.17.1"
bare-metrics-recorder = { version = "0.1.0" } bare-metrics-recorder = { version = "0.1.0" }
comfy-table = "6.0.0-rc.1" comfy-table = "6.0.0-rc.1"
libc = "0.2.126"

View File

@ -317,9 +317,9 @@ fn main() -> anyhow::Result<()> {
let descriptor = load_descriptor(Path::new(".")).unwrap(); let descriptor = load_descriptor(Path::new(".")).unwrap();
let destination = &descriptor.piles[&pile_name]; let destination = &descriptor.piles[&pile_name];
let report = datman::commands::report::generate_report(destination, &descriptor)?; 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_report(&report)?;
datman::commands::report::print_filesystem_space(&destination.path)?;
} }
} }
Ok(()) Ok(())

View File

@ -1,14 +1,19 @@
use crate::commands::backup::split_pointer_name; use crate::commands::backup::split_pointer_name;
use crate::descriptor::{Descriptor, DestPileDescriptor}; use crate::descriptor::{Descriptor, DestPileDescriptor};
use anyhow::Context; use anyhow::Context;
use chrono::{DateTime, Utc}; use chrono::{Date, DateTime, Utc};
use comfy_table::presets::UTF8_FULL; 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 humansize::FileSize;
use itertools::Itertools;
use log::info; use log::info;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::ffi::CString;
use std::io::Read; use std::io::Read;
use std::mem;
use std::mem::size_of; use std::mem::size_of;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use yama::chunking::RecursiveUnchunker; use yama::chunking::RecursiveUnchunker;
use yama::commands::{load_pile_descriptor, open_pile, retrieve_tree_node}; use yama::commands::{load_pile_descriptor, open_pile, retrieve_tree_node};
use yama::definitions::{ChunkId, RecursiveChunkRef, TreeNode}; use yama::definitions::{ChunkId, RecursiveChunkRef, TreeNode};
@ -212,10 +217,86 @@ fn collect_chunk_ids_from_chunkref<RP: RawPile>(
} }
pub fn print_report(report: &Report) -> anyhow::Result<()> { 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(); let mut table = Table::new();
table table
.load_preset(UTF8_FULL) .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<Date<Utc>>, 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); //.set_width(100);
table.set_header(vec![ table.set_header(vec![
Cell::new("Pointer name").fg(Color::Cyan), Cell::new("Pointer name").fg(Color::Cyan),
@ -248,13 +329,76 @@ fn format_size(chunks: u32, average_chunk_size: Option<f64>) -> String {
let est_size_suffix = average_chunk_size let est_size_suffix = average_chunk_size
.map(|bytes_per_chunk| { .map(|bytes_per_chunk| {
let num_bytes = (chunks as f64 * bytes_per_chunk) as u64; let num_bytes = (chunks as f64 * bytes_per_chunk) as u64;
format!( let mut format = humansize::file_size_opts::BINARY;
" ~{}", format.decimal_places = 1;
num_bytes format!(" ~{}", num_bytes.file_size(format).unwrap())
.file_size(humansize::file_size_opts::BINARY)
.unwrap()
)
}) })
.unwrap_or_default(); .unwrap_or_default();
format!("{} c{}", chunks, est_size_suffix) 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(())
}