Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 3 additions & 39 deletions src/cli/commands/backup/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,12 @@ pub fn inspect(name: String) {
};

let backup_dir_prefix = format!("backup_{name}_");
let backups = match &backup_job.datastore {
Datastore::FileSystem(store) => store
.list_backups()
.unwrap_or_default()
.into_iter()
.filter_map(|store| {
let dir_name = store.base_path.file_name()?.to_str()?.to_string();

if dir_name.starts_with(&backup_dir_prefix) {
Some((dir_name, Datastore::FileSystem(store)))
} else {
None
}
})
.collect::<Vec<_>>(),

Datastore::S3(store) => store
.list_backups()
.unwrap_or_default()
.into_iter()
.filter_map(|store| {
let dir_name = store.base_path.file_name()?.to_str()?.to_string();

if dir_name.starts_with(&backup_dir_prefix) {
Some((dir_name, Datastore::S3(store)))
} else {
None
}
})
.collect::<Vec<_>>(),
};

let datastore_type = backup_job.datastore.as_str();
let datastore_base_path = match &backup_job.datastore {
Datastore::FileSystem(store) => store.base_path.display().to_string(),
Datastore::S3(store) => store.base_path.display().to_string(),
};
let backups = backup_job.datastore.get_backups(&name);

println!("-- {} --", backup_job.display_name);
println!("Datastore:");
println!("\tType: {}", datastore_type);
println!("\tPath: {}", datastore_base_path);
println!("\tType: {}", backup_job.datastore.as_str());
println!("\tPath: {}", backup_job.datastore.get_base_path());
println!(
"Schedule: {:?}",
if let Some(schedule) = backup_job.clone().raw_schedule {
Expand Down
43 changes: 43 additions & 0 deletions src/datastores/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ impl Datastore {
}
}

pub fn get_base_path(&self) -> String {
match self {
Datastore::FileSystem(store) => store.base_path.display().to_string(),
Datastore::S3(store) => store.base_path.display().to_string(),
}
}

pub fn check_backup_integrity(&self) -> Result<bool, String> {
delegate_to_datastore!(self, check_backup_integrity())
}
Expand Down Expand Up @@ -101,4 +108,40 @@ impl Datastore {
Datastore::S3(store) => store.open_read_stream(object_name).await,
}
}

pub fn get_backups(&self, backup_identifier: &str) -> Vec<(String, Datastore)> {
let backup_dir_prefix = format!("backup_{backup_identifier}_");

match self {
Datastore::FileSystem(store) => store
.list_backups()
.unwrap_or_default()
.into_iter()
.filter_map(|store| {
let dir_name = store.base_path.file_name()?.to_str()?.to_string();

if dir_name.starts_with(&backup_dir_prefix) {
Some((dir_name, Datastore::FileSystem(store)))
} else {
None
}
})
.collect::<Vec<_>>(),

Datastore::S3(store) => store
.list_backups()
.unwrap_or_default()
.into_iter()
.filter_map(|store| {
let dir_name = store.base_path.file_name()?.to_str()?.to_string();

if dir_name.starts_with(&backup_dir_prefix) {
Some((dir_name, Datastore::S3(store)))
} else {
None
}
})
.collect::<Vec<_>>(),
}
}
}
65 changes: 49 additions & 16 deletions src/ui/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::io;

use chrono::DateTime;
use ratatui::{
Terminal,
crossterm::{
Expand All @@ -10,42 +9,53 @@ use ratatui::{
prelude::CrosstermBackend,
widgets::ListState,
};
use std::io;

use crate::datastores::Datastore;
use crate::ui::app::CurrentScreen::BackupInspect;
use crate::ui::screens::BackupInspectScreen;
use crate::utils::backup_manager::BackupJob;
use crate::{
db::DatabaseConnection,
ui::screens::{DatabasesScreen, HomeItem, HomeScreen, SettingsScreen},
ui::screens::{BackupsScreen, HomeItem, HomeScreen, SettingsScreen},
utils::config::Config,
};

#[derive(PartialEq)]
pub enum CurrentScreen {
Main,
Databases,
Backups,
BackupInspect { backup: BackupJob },
Settings,
}

#[allow(unused)]
pub struct App {
should_quit: bool,
current_screen: CurrentScreen,
pub current_screen: CurrentScreen,
pub list_state: ListState,
pub config: Config,
pub database_connection: DatabaseConnection,
}

impl App {
pub fn new() -> Self {
let config = Config::new();
let mut list_state = ListState::default();
list_state.select_first();
Self {
should_quit: false,
current_screen: CurrentScreen::Main,
list_state,
config,
database_connection: DatabaseConnection::new(),
}
}

pub async fn run(&mut self) -> io::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
execute!(stdout, EnterAlternateScreen)?;

let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
Expand All @@ -57,11 +67,16 @@ impl App {
eprintln!("Draw error: {}", e);
}
}
CurrentScreen::Databases => {
if let Err(e) = DatabasesScreen::draw(self, frame) {
CurrentScreen::Backups => {
if let Err(e) = BackupsScreen::draw(self, frame) {
eprintln!("Draw error: {}", e);
}
}
CurrentScreen::BackupInspect { ref backup } => {
if let Err(e) = BackupInspectScreen::draw(self, backup.clone(), frame) {
eprintln!("Draw error: {}", e)
}
}
CurrentScreen::Settings => {
if let Err(e) = SettingsScreen::draw(self, frame) {
eprintln!("Draw error: {}", e);
Expand All @@ -75,11 +90,7 @@ impl App {
}

disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;

Ok(())
Expand All @@ -96,18 +107,40 @@ impl App {
}

match (&self.current_screen, key.code) {
(CurrentScreen::Main, KeyCode::Down) => self.list_state.select_next(),
(CurrentScreen::Main, KeyCode::Up) => self.list_state.select_previous(),
(
CurrentScreen::Main | CurrentScreen::Backups | CurrentScreen::BackupInspect { backup: _ },
KeyCode::Down,
) => self.list_state.select_next(),
(
CurrentScreen::Main | CurrentScreen::Backups | CurrentScreen::BackupInspect { backup: _ },
KeyCode::Up,
) => self.list_state.select_previous(),
(CurrentScreen::Main, KeyCode::Enter) => {
let items = HomeScreen::list_items();
if let Some(idx) = self.list_state.selected() {
match items[idx] {
HomeItem::Databases => self.set_screen(CurrentScreen::Databases),
HomeItem::Backups => self.set_screen(CurrentScreen::Backups),
HomeItem::Settings => self.set_screen(CurrentScreen::Settings),
HomeItem::Exit => self.should_quit = true,
}
}
}
(CurrentScreen::Backups, KeyCode::Enter) => {
let items = BackupsScreen::list_items(self);
if let Some(idx) = self.list_state.selected() {
let backup = self
.config
.backups
.get(&format!("backup.{}", items[idx]))
.expect("Backup not found");
self.set_screen(CurrentScreen::BackupInspect {
backup: backup.clone(),
})
}
}
(CurrentScreen::Backups | CurrentScreen::Settings, KeyCode::Backspace) => {
self.set_screen(CurrentScreen::Main)
}
(_, KeyCode::Char('q') | KeyCode::Esc) => self.should_quit = true,
_ => {}
}
Expand Down
105 changes: 105 additions & 0 deletions src/ui/screens/backup_inspect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::datastores::Datastore;
use crate::ui::{app::App, screens::ScreenLayout};
use crate::utils::backup_manager::BackupJob;
use chrono::DateTime;
use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::prelude::{Style, Stylize};
use ratatui::text::Line;
use ratatui::widgets::{Borders, List, ListDirection, Paragraph, Wrap};
use ratatui::{
Frame,
widgets::{Block, BorderType},
};
use std::io::Result;

pub struct BackupInspectScreen;

impl BackupInspectScreen {
pub fn draw(app: &mut App, backup: BackupJob, frame: &mut Frame) -> Result<()> {
ScreenLayout::draw(app, frame, Some(&backup.display_name));

let area = frame.area();
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Min(25),
Constraint::Length(1),
Constraint::Fill(1),
])
.split(area.centered(
Constraint::Length(area.width - 2),
Constraint::Length(area.height - 2),
));

let mut lines = vec![
Line::raw("Datastore:"),
Line::raw(format!(" Type: {}", backup.datastore.as_str())),
Line::raw(format!(" Path: {}", backup.datastore.get_base_path())),
Line::raw(format!(
"Schedule: {}",
if let Some(schedule) = backup.clone().raw_schedule {
schedule
} else {
"disabled".to_string()
}
)),
Line::raw(format!(
"Encryption: {}",
if backup.is_encryption_enabled() {
"enabled"
} else {
"disabled"
}
)),
];

if let Some(next) = backup.clone().get_next_run() {
lines.insert(4, Line::raw(format!("Next run: {:?}", next)))
}

let paragraph = Paragraph::new(lines).block(
Block::new()
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
);

frame.render_widget(paragraph, layout[0]);

let mut backups_labels = backup
.datastore
.get_backups(&backup.identifier)
.iter()
.map(|(dir_name, datastore)| {
let health_state = datastore.check_backup_integrity().is_ok_and(|res| res);
let timestamp = dir_name
.rsplit("_")
.next()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
let date = match DateTime::from_timestamp_secs(timestamp) {
Some(value) => value.to_rfc3339(),
None => "Unknown date".to_string(),
};
format!(
"\t{} - {} - {}",
dir_name,
date,
if health_state { "Healthy" } else { "Unhealthy" }
)
})
.collect::<Vec<String>>();
backups_labels.push("Trigger manual backup".to_string());

let list_block = Block::bordered().border_type(BorderType::Rounded);
let list = List::new(backups_labels)
.block(list_block)
.highlight_style(Style::new().reversed())
.highlight_symbol("▶")
.repeat_highlight_symbol(true)
.direction(ListDirection::TopToBottom);

frame.render_stateful_widget(list, layout[2], &mut app.list_state);

Ok(())
}
}
Loading