feat: Add interactive restore (#1123)

Currently only available from the interactive ls mode, see #1117
This commit is contained in:
aawsome 2024-04-18 13:41:50 +02:00 committed by GitHub
parent b6e9c1b924
commit de93aa390d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 221 additions and 26 deletions

View File

@ -1,6 +1,7 @@
//! `tui` subcommand
mod ls;
mod progress;
mod restore;
mod snapshots;
mod widgets;

View File

@ -11,17 +11,21 @@ use style::palette::tailwind;
use crate::commands::{
ls::{NodeLs, Summary},
tui::widgets::{popup_text, Draw, PopUpText, ProcessEvent, SelectTable, WithBlock},
tui::{
restore::Restore,
widgets::{popup_text, Draw, PopUpText, ProcessEvent, SelectTable, WithBlock},
},
};
// the states this screen can be in
enum CurrentScreen {
enum CurrentScreen<'a, P, S> {
Snapshot,
ShowHelp(PopUpText),
Restore(Restore<'a, P, S>),
}
const INFO_TEXT: &str =
"(Esc) quit | (Enter) enter dir | (Backspace) return to parent | (?) show all commands";
"(Esc) quit | (Enter) enter dir | (Backspace) return to parent | (r) restore | (?) show all commands";
const HELP_TEXT: &str = r#"
General Commands:
@ -29,19 +33,21 @@ General Commands:
q,Esc : exit
Enter : enter dir
Backspace : return to parent dir
r : restore selected item
n : toggle numeric IDs
? : show this help page
"#;
pub(crate) struct Snapshot<'a, P, S> {
current_screen: CurrentScreen,
current_screen: CurrentScreen<'a, P, S>,
numeric: bool,
table: WithBlock<SelectTable>,
repo: &'a Repository<P, S>,
snapshot: SnapshotFile,
path: PathBuf,
trees: Vec<Tree>,
tree: Tree,
}
impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
@ -59,7 +65,8 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
repo,
snapshot,
path: PathBuf::new(),
trees: vec![tree],
trees: Vec::new(),
tree,
};
app.update_table();
Ok(app)
@ -94,16 +101,19 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
.collect()
}
pub fn selected_node(&self) -> Option<&Node> {
self.table.widget.selected().map(|i| &self.tree.nodes[i])
}
pub fn update_table(&mut self) {
let tree = self.trees.last().unwrap();
let old_selection = if tree.nodes.is_empty() {
let old_selection = if self.tree.nodes.is_empty() {
None
} else {
Some(self.table.widget.selected().unwrap_or_default())
};
let mut rows = Vec::new();
let mut summary = Summary::default();
for node in &tree.nodes {
for node in &self.tree.nodes {
summary.update(node);
let row = self.ls_row(node);
rows.push(row);
@ -116,7 +126,7 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
.title(format!("{}:{}", self.snapshot.id, self.path.display()))
.title_bottom(format!(
"total: {}, files: {}, dirs: {}, size: {} - {}",
tree.nodes.len(),
self.tree.nodes.len(),
summary.files,
summary.dirs,
summary.size,
@ -132,10 +142,12 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
pub fn enter(&mut self) -> Result<()> {
if let Some(idx) = self.table.widget.selected() {
let node = &self.trees.last().unwrap().nodes[idx];
let node = &self.tree.nodes[idx];
if node.is_dir() {
self.path.push(node.name());
self.trees.push(self.repo.get_tree(&node.subtree.unwrap())?);
let tree = self.tree.clone();
self.tree = self.repo.get_tree(&node.subtree.unwrap())?;
self.trees.push(tree);
}
}
self.table.widget.set_to(0);
@ -145,12 +157,14 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
pub fn goback(&mut self) -> bool {
_ = self.path.pop();
_ = self.trees.pop();
if !self.trees.is_empty() {
if let Some(tree) = self.trees.pop() {
self.tree = tree;
self.table.widget.set_to(0);
self.update_table();
false
} else {
true
}
self.trees.is_empty()
}
pub fn toggle_numeric(&mut self) {
@ -174,6 +188,17 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
CurrentScreen::ShowHelp(popup_text("help", HELP_TEXT.into()));
}
Char('n') => self.toggle_numeric(),
Char('r') => {
if let Some(node) = self.selected_node() {
let path = self.path.join(node.name());
let restore = Restore::new(
self.repo,
node.clone(),
format!("{}:{}", self.snapshot.id, path.display()),
);
self.current_screen = CurrentScreen::Restore(restore);
}
}
_ => self.table.input(event),
},
_ => {}
@ -186,6 +211,11 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
}
_ => {}
},
CurrentScreen::Restore(restore) => {
if restore.input(event)? {
self.current_screen = CurrentScreen::Snapshot;
}
}
}
Ok(false)
}
@ -193,20 +223,24 @@ impl<'a, P: ProgressBars, S: IndexedFull> Snapshot<'a, P, S> {
pub fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
let rects = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).split(area);
// draw the table
self.table.draw(rects[0], f);
if let CurrentScreen::Restore(restore) = &mut self.current_screen {
restore.draw(area, f);
} else {
// draw the table
self.table.draw(rects[0], f);
// draw the footer
let buffer_bg = tailwind::SLATE.c950;
let row_fg = tailwind::SLATE.c200;
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
.style(Style::new().fg(row_fg).bg(buffer_bg))
.centered();
f.render_widget(info_footer, rects[1]);
// draw the footer
let buffer_bg = tailwind::SLATE.c950;
let row_fg = tailwind::SLATE.c200;
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
.style(Style::new().fg(row_fg).bg(buffer_bg))
.centered();
f.render_widget(info_footer, rects[1]);
}
// draw popups
match &mut self.current_screen {
CurrentScreen::Snapshot => {}
CurrentScreen::Snapshot | CurrentScreen::Restore(_) => {}
CurrentScreen::ShowHelp(popup) => popup.draw(area, f),
}
}

157
src/commands/tui/restore.rs Normal file
View File

@ -0,0 +1,157 @@
use anyhow::Result;
use crossterm::event::{Event, KeyCode, KeyEventKind};
use ratatui::prelude::*;
use rustic_core::{
repofile::Node, IndexedFull, LocalDestination, LsOptions, ProgressBars, Repository,
RestoreOptions, RestorePlan,
};
use crate::{
commands::tui::widgets::{
popup_input, popup_prompt, Draw, PopUpInput, PopUpPrompt, PopUpText, ProcessEvent,
PromptResult, TextInputResult,
},
helpers::bytes_size_to_string,
};
use super::widgets::popup_text;
// the states this screen can be in
enum CurrentScreen {
GetDestination(PopUpInput),
PromptRestore(PopUpPrompt, Option<RestorePlan>),
RestoreDone(PopUpText),
}
pub(crate) struct Restore<'a, P, S> {
current_screen: CurrentScreen,
repo: &'a Repository<P, S>,
opts: RestoreOptions,
node: Node,
source: String,
dest: String,
}
impl<'a, P: ProgressBars, S: IndexedFull> Restore<'a, P, S> {
pub fn new(repo: &'a Repository<P, S>, node: Node, source: String) -> Self {
let opts = RestoreOptions::default();
let title = format!("restore {} to:", source);
let popup = popup_input(title, "enter restore destination", "");
Self {
current_screen: CurrentScreen::GetDestination(popup),
node,
repo,
opts,
source,
dest: String::new(),
}
}
pub fn compute_plan(&mut self, dest: String, dry_run: bool) -> Result<RestorePlan> {
self.dest = dest;
let dest = LocalDestination::new(&self.dest, true, !self.node.is_dir())?;
// for restore, always recurse into tree
let ls_opts = LsOptions {
recursive: true,
..Default::default()
};
let ls = self.repo.ls(&self.node, &ls_opts)?;
let plan = self.repo.prepare_restore(&self.opts, ls, &dest, dry_run)?;
Ok(plan)
}
// restore using the plan
//
// Note: This currently runs `prepare_restore` again and doesn't use `plan`
// TODO: Fix when restore is changed such that `prepare_restore` is always dry_run and all modification is don in `restore`
fn restore(&self, _plan: RestorePlan) -> Result<()> {
let dest = LocalDestination::new(&self.dest, true, !self.node.is_dir())?;
// for restore, always recurse into tree
let ls_opts = LsOptions {
recursive: true,
..Default::default()
};
let ls = self.repo.ls(&self.node, &ls_opts)?;
let plan = self
.repo
.prepare_restore(&self.opts, ls.clone(), &dest, false)?;
// the actual restore
self.repo.restore(plan, &self.opts, ls, &dest)?;
Ok(())
}
pub fn input(&mut self, event: Event) -> Result<bool> {
use KeyCode::*;
match &mut self.current_screen {
CurrentScreen::GetDestination(prompt) => match prompt.input(event) {
TextInputResult::Cancel => return Ok(true),
TextInputResult::Input(input) => {
let plan = self.compute_plan(input, true)?;
let fs = plan.stats.files;
let ds = plan.stats.dirs;
let popup = popup_prompt(
"restore information",
Text::from(format!(
r#"
restoring from: {}
restoring to: {}
Files: {} to restore, {} unchanged, {} verified, {} to modify, {} additional
Dirs: {} to restore, {} to modify, {} additional
Total restore size: {}
Do you want to proceed (y/n)?
"#,
self.source,
self.dest,
fs.restore,
fs.unchanged,
fs.verified,
fs.modify,
fs.additional,
ds.restore,
ds.modify,
ds.additional,
bytes_size_to_string(plan.restore_size)
)),
);
self.current_screen = CurrentScreen::PromptRestore(popup, Some(plan));
}
TextInputResult::None => {}
},
CurrentScreen::PromptRestore(prompt, plan) => match prompt.input(event) {
PromptResult::Ok => {
let plan = plan.take().unwrap();
self.restore(plan)?;
self.current_screen = CurrentScreen::RestoreDone(popup_text(
"restore done",
format!("restored {} successfully to {}", self.source, self.dest).into(),
));
}
PromptResult::Cancel => return Ok(true),
PromptResult::None => {}
},
CurrentScreen::RestoreDone(_) => match event {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if matches!(key.code, Char('q') | Esc | Enter | Char(' ')) {
return Ok(true);
}
}
_ => {}
},
}
Ok(false)
}
pub fn draw(&mut self, area: Rect, f: &mut Frame<'_>) {
// draw popups
match &mut self.current_screen {
CurrentScreen::GetDestination(popup) => popup.draw(area, f),
CurrentScreen::PromptRestore(popup, _) => popup.draw(area, f),
CurrentScreen::RestoreDone(popup) => popup.draw(area, f),
}
}
}

View File

@ -40,7 +40,7 @@ pub trait Draw {
// the widgets we are using and convenience builders
pub type PopUpInput = PopUp<WithBlock<TextInput>>;
pub fn popup_input(title: &'static str, text: &str, initial: &str) -> PopUpInput {
pub fn popup_input(title: impl Into<Title<'static>>, text: &str, initial: &str) -> PopUpInput {
PopUp(WithBlock::new(
TextInput::new(text, initial),
Block::bordered().title(title),
@ -56,7 +56,10 @@ pub fn popup_text(title: impl Into<Title<'static>>, text: Text<'static>) -> PopU
}
pub type PopUpTable = PopUp<WithBlock<SizedTable>>;
pub fn popup_table(title: &'static str, content: Vec<Vec<Text<'static>>>) -> PopUpTable {
pub fn popup_table(
title: impl Into<Title<'static>>,
content: Vec<Vec<Text<'static>>>,
) -> PopUpTable {
PopUp(WithBlock::new(
SizedTable::new(content),
Block::bordered().title(title),