mirror of
https://github.com/rustic-rs/rustic.git
synced 2025-10-26 11:18:51 +00:00
feat: Add interactive restore (#1123)
Currently only available from the interactive ls mode, see #1117
This commit is contained in:
parent
b6e9c1b924
commit
de93aa390d
@ -1,6 +1,7 @@
|
||||
//! `tui` subcommand
|
||||
mod ls;
|
||||
mod progress;
|
||||
mod restore;
|
||||
mod snapshots;
|
||||
mod widgets;
|
||||
|
||||
|
||||
@ -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
157
src/commands/tui/restore.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user