Commit 4fa06d93 authored by Sophie Herold's avatar Sophie Herold
Browse files

Merge branch 'add_backup_ui' into 'master'

Better UI feedback while adding backups

Closes #1

See merge request sophie-h/pika-backup!3
parents ba9df2a1 93b7b2eb
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 -->
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkDialog" id="dialog">
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="urgency_hint">True</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="width_request">350</property>
......
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 -->
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkDialog" id="new_backup">
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox">
......
#!/usr/bin/python3
import xml.etree.ElementTree as ET
import glob
import os.path as path
import os
UI_FILES = [
["About", "ui/about.ui"],
["Main", "ui/main.ui"],
["NewBackup", "ui/new.ui"],
["Storage", "ui/storage.ui"],
["EncryptionPassword", "ui/password.ui"],
]
os.chdir("data")
UI_PATH = "ui/*.ui"
SRC_PATH = "../src/ui/builder.rs"
def main():
global UI_PATH, SRC_PATH
ui_files = glob.glob(UI_PATH)
ui_files.sort()
with open(SRC_PATH, "w") as src:
first = True
for file in ui_files:
filename, _ = path.splitext(path.basename(file))
rs_type = ''.join(x.title() for x in filename.split('_'))
if first:
first = False
else:
src.write("\n\n")
src.write(struct_code(rs_type, file))
src.write("\n")
SRC_PATH = "src/ui/builder.rs"
class Item:
def __init__(self, id, type):
self.id = id
self.type = type
def objects(path):
objects = []
for item in ET.parse("data/" + path).iter():
for item in ET.parse(path).iter():
if item.tag == "object" and item.get("id"):
objects.append(Item(item.get("id"), item.get("class")[3:]))
objects.sort(key=lambda item: item.id)
return objects
def fn_code(objects):
template = """
pub fn {id}(&self) -> gtk::{type} {{
......@@ -40,9 +63,9 @@ def fn_code(objects):
return code
def struct_code(name, path):
template = \
"""pub struct {name} {{
template = """pub struct {name} {{
builder: gtk::Builder,
}}
......@@ -66,14 +89,7 @@ impl {name} {{
code = fn_code(objects(path))
return template.format(name=name, path=path, fn_code=code)
with open(SRC_PATH, "w") as src:
first = True
for x in UI_FILES:
if first:
first = False
else:
src.write("\n\n")
src.write(struct_code(*x))
if __name__ == "__main__":
main()
src.write("\n")
......@@ -152,9 +152,10 @@ impl BorgCall {
);
if let Some(ref password) = borg.password {
debug!("Using password enforced by explicitly passed password");
pipe_writer.write_all(password)?;
} else if borg.config.encrypted {
trace!("Config says the backup is encrypted");
debug!("Config says the backup is encrypted");
let password: Zeroizing<Vec<u8>> =
secret_service::SecretService::new(secret_service::EncryptionType::Dh)?
.search_items(vec![
......@@ -167,6 +168,7 @@ impl BorgCall {
.into();
pipe_writer.write_all(&password)?;
} else {
trace!("Config says no encryption. Writing empty passsword.");
pipe_writer.write_all(b"")?;
}
......
......@@ -18,6 +18,7 @@ mod builder;
mod detail;
mod encryption_password;
mod globals;
mod main_pending;
mod new_backup;
mod overview;
pub mod prelude;
......
......@@ -22,6 +22,54 @@ impl About {
}
}
pub struct EncryptionPassword {
builder: gtk::Builder,
}
impl EncryptionPassword {
pub fn new() -> Self {
Self {
builder: gtk::Builder::new_from_string(include_str!(concat!(
data_dir!(),
"/ui/encryption_password.ui"
))),
}
}
fn get<T: glib::IsA<glib::object::Object>>(&self, id: &str) -> T {
gtk::prelude::BuilderExtManual::get_object(&self.builder, id).unwrap_or_else(|| {
panic!(
"Object with id '{}' not found in 'ui/encryption_password.ui'",
id
)
})
}
pub fn cancel(&self) -> gtk::Button {
self.get("cancel")
}
pub fn dialog(&self) -> gtk::Dialog {
self.get("dialog")
}
pub fn ok(&self) -> gtk::Button {
self.get("ok")
}
pub fn password(&self) -> gtk::Entry {
self.get("password")
}
pub fn password_forget(&self) -> gtk::RadioButton {
self.get("password_forget")
}
pub fn password_store(&self) -> gtk::RadioButton {
self.get("password_store")
}
}
pub struct Main {
builder: gtk::Builder,
}
......@@ -41,6 +89,10 @@ impl Main {
.unwrap_or_else(|| panic!("Object with id '{}' not found in 'ui/main.ui'", id))
}
pub fn add_pending_label(&self) -> gtk::Label {
self.get("add_pending_label")
}
pub fn archive_comment(&self) -> gtk::Label {
self.get("archive_comment")
}
......@@ -169,6 +221,22 @@ impl Main {
self.get("overview_none_empty")
}
pub fn page_archives(&self) -> gtk::Box {
self.get("page_archives")
}
pub fn page_detail(&self) -> gtk::Box {
self.get("page_detail")
}
pub fn page_overview(&self) -> gtk::ScrolledWindow {
self.get("page_overview")
}
pub fn page_pending(&self) -> gtk::Box {
self.get("page_pending")
}
pub fn pending_menu(&self) -> gtk::MenuButton {
self.get("pending_menu")
}
......@@ -231,14 +299,14 @@ impl NewBackup {
Self {
builder: gtk::Builder::new_from_string(include_str!(concat!(
data_dir!(),
"/ui/new.ui"
"/ui/new_backup.ui"
))),
}
}
fn get<T: glib::IsA<glib::object::Object>>(&self, id: &str) -> T {
gtk::prelude::BuilderExtManual::get_object(&self.builder, id)
.unwrap_or_else(|| panic!("Object with id '{}' not found in 'ui/new.ui'", id))
.unwrap_or_else(|| panic!("Object with id '{}' not found in 'ui/new_backup.ui'", id))
}
pub fn add_button(&self) -> gtk::Button {
......@@ -437,47 +505,3 @@ impl Storage {
self.get("volume")
}
}
pub struct EncryptionPassword {
builder: gtk::Builder,
}
impl EncryptionPassword {
pub fn new() -> Self {
Self {
builder: gtk::Builder::new_from_string(include_str!(concat!(
data_dir!(),
"/ui/password.ui"
))),
}
}
fn get<T: glib::IsA<glib::object::Object>>(&self, id: &str) -> T {
gtk::prelude::BuilderExtManual::get_object(&self.builder, id)
.unwrap_or_else(|| panic!("Object with id '{}' not found in 'ui/password.ui'", id))
}
pub fn cancel(&self) -> gtk::Button {
self.get("cancel")
}
pub fn dialog(&self) -> gtk::Dialog {
self.get("dialog")
}
pub fn ok(&self) -> gtk::Button {
self.get("ok")
}
pub fn password(&self) -> gtk::Entry {
self.get("password")
}
pub fn password_forget(&self) -> gtk::RadioButton {
self.get("password_forget")
}
pub fn password_store(&self) -> gtk::RadioButton {
self.get("password_store")
}
}
......@@ -355,10 +355,7 @@ fn rel_path(path: &std::path::Path) -> std::path::PathBuf {
}
fn add_include() {
if let Some(path) = ui::utils::folder_chooser_dialog(
&gettext("Include directory in backup"),
&main_ui().window(),
) {
if let Some(path) = ui::utils::folder_chooser_dialog(&gettext("Include directory in backup")) {
SETTINGS.update(|settings| {
settings
.backups
......@@ -373,10 +370,8 @@ fn add_include() {
}
fn add_exclude() {
if let Some(path) = ui::utils::folder_chooser_dialog(
&gettext("Exclude directory from backup"),
&main_ui().window(),
) {
if let Some(path) = ui::utils::folder_chooser_dialog(&gettext("Exclude directory from backup"))
{
SETTINGS.update(|settings| {
settings
.backups
......
......@@ -24,8 +24,8 @@ impl Ask {
pub fn run(&self) -> Option<(Zeroizing<Vec<u8>>, bool)> {
let ui = ui::builder::EncryptionPassword::new();
ui.dialog().show_all();
ui.dialog().set_transient_for(Some(&main_ui().window()));
ui.dialog().show_all();
if !self.pre_select_store {
ui.password_forget().set_active(true);
......@@ -45,6 +45,7 @@ impl Ask {
.map(|x| x.as_bytes().to_vec())
.map(Zeroizing::new);
ui.dialog().close();
ui.dialog().hide();
match password {
Some(password) if gtk::ResponseType::Ok == response => {
......
use gtk::prelude::*;
use std::cell::Cell;
use crate::ui::globals::*;
thread_local!(
static LAST_PAGE: Cell<Option<gtk::Widget>> = Default::default();
);
pub fn show(msg: &str) {
LAST_PAGE.with(|last_page| last_page.set(main_ui().main_stack().get_visible_child()));
main_ui()
.main_stack()
.set_visible_child(&main_ui().page_pending());
main_ui().add_pending_label().set_text(msg);
}
pub fn back() {
LAST_PAGE.with(|last_page| {
if let Some(page) = last_page.take() {
main_ui().main_stack().set_visible_child(&page)
}
})
}
......@@ -11,6 +11,7 @@ use crate::ui;
use crate::ui::builder;
use crate::ui::globals::*;
use crate::ui::prelude::*;
use ui::main_pending;
pub fn new_backup() {
let ui_new = Rc::new(ui::builder::NewBackup::new());
......@@ -30,14 +31,15 @@ pub fn new_backup() {
.set_transient_for(Some(&main_ui().window()));
let dialog = ui_new.new_backup();
ui_new
.cancel_button()
.connect_clicked(move |_| dialog.response(gtk::ResponseType::Cancel));
ui_new.cancel_button().connect_clicked(move |_| {
dialog.close();
dialog.hide();
});
let ui = ui_new.clone();
ui_new
.add_repo_list()
.connect_row_activated(move |_, row| add_repo_list_activated(row, &ui));
.connect_row_activated(move |_, row| add_repo_list_activated(row, ui.clone()));
let ui = ui_new.clone();
ui_new
......@@ -52,12 +54,12 @@ pub fn new_backup() {
let ui = ui_new.clone();
ui_new
.add_button()
.connect_clicked(move |_| add_button_clicked(&ui));
.connect_clicked(move |_| add_button_clicked(ui.clone()));
let ui = ui_new.clone();
ui_new
.init_button()
.connect_clicked(move |_| init_button_clicked(&ui));
.connect_clicked(move |_| init_button_clicked(ui.clone()));
// refresh ui on mount events
let monitor = gio::VolumeMonitor::get();
......@@ -69,48 +71,50 @@ pub fn new_backup() {
let ui = ui_new.clone();
monitor.connect_mount_removed(move |_, _| refresh(&ui));
ui_new.new_backup().run();
ui_new.new_backup().close();
ui_new.new_backup().show_all();
}
fn add_repo_list_activated(row: &gtk::ListBoxRow, ui: &builder::NewBackup) {
fn add_repo_list_activated(row: &gtk::ListBoxRow, ui: Rc<builder::NewBackup>) {
if let Some(name) = row.get_widget_name() {
if name == "-add-local" {
if let Some(path) = ui::utils::folder_chooser_dialog(
&gettext("Select existing repository"),
&ui.new_backup(),
) {
if is_backup_repo(&path) {
if add_repo_config_local(&path) {
ui.new_backup().response(gtk::ResponseType::Other(1));
}
} else {
ui::utils::dialog_error(gettext(
"The selected directory is not a valid backup repository.",
));
}
}
add_local(ui);
} else if name == "-add-remote" {
ui.stack().set_visible_child(&ui.add_remote_page());
ui.add_button().show();
ui.add_button().grab_default();
} else {
add_repo_config_local(std::path::Path::new(&name));
ui.new_backup().response(gtk::ResponseType::Other(1));
add_repo_config_local(std::path::Path::new(&name), ui);
}
}
}
fn add_button_clicked(ui: &builder::NewBackup) {
if let Some(uri) = ui.add_remote_uri().get_text() {
if add_repo_config_remote(uri.to_string()) {
ui.new_backup().response(gtk::ResponseType::Other(1));
fn add_local(ui: Rc<builder::NewBackup>) {
ui.new_backup().hide();
if let Some(path) = ui::utils::folder_chooser_dialog(&gettext("Select existing repository")) {
ui::main_pending::show(&gettext("Adding existing repository …"));
if is_backup_repo(&path) {
add_repo_config_local(&path, ui);
} else {
ui::utils::dialog_error(gettext(
"The selected directory is not a valid backup repository.",
));
ui::main_pending::back();
ui.new_backup().show();
}
} else {
error!("get_text() returned 'None'");
ui.new_backup().show();
}
}
fn add_button_clicked(ui: Rc<builder::NewBackup>) {
main_pending::show(&gettext("Initializing new backup respository …"));
ui.new_backup().hide();
let uri = ui.add_remote_uri().get_text().unwrap();
add_repo_config_remote(uri.to_string(), ui);
}
fn init_repo_list_activated(row: &gtk::ListBoxRow, ui: &builder::NewBackup) {
if let Some(name) = row.get_widget_name() {
ui.init_dir().set_text(&format!(
......@@ -137,7 +141,7 @@ fn init_repo_list_activated(row: &gtk::ListBoxRow, ui: &builder::NewBackup) {
}
}
fn init_button_clicked(ui: &builder::NewBackup) {
fn init_button_clicked(ui: Rc<builder::NewBackup>) {
let encrypted =
ui.encryption().get_visible_child() != Some(ui.unencrypted().upcast::<gtk::Widget>());
......@@ -186,17 +190,27 @@ fn init_button_clicked(ui: &builder::NewBackup) {
borg.set_password(password);
}
if ui::utils::dialog_catch_err(borg.init(), gettext("Failed to initialize repository")) {
return;
}
main_pending::show(&gettext("Initializing new backup respository …"));
ui.new_backup().hide();
ui::utils::async_react(
"borg::init",
move || borg.init(),
enclose!((config, ui) move |result| {
if ui::utils::dialog_catch_err(result, gettext("Failed to initialize repository")) {
main_pending::back();
ui.new_backup().show();
return;
}
insert_backup_config(config);
insert_backup_config(config.clone());
ui.new_backup().response(gtk::ResponseType::Other(1));
ui.new_backup().close();
}),
);
}
fn init_repo_password_changed(ui: &builder::NewBackup) {
trace!("Password entered");
let password = ui.password().get_text().unwrap_or_else(|| "".into());
let score = if let Ok(pw_check) = zxcvbn::zxcvbn(&password, &[]) {
if pw_check.score() > 3 {
......@@ -293,54 +307,53 @@ fn add_mount(list: &gtk::ListBox, mount: &gio::Mount, repo: Option<&std::path::P
horizontal_box.add(&vertical_box);
}
fn add_repo_config_local(repo: &std::path::Path) -> bool {
fn add_repo_config_local(repo: &std::path::Path, ui: Rc<builder::NewBackup>) {
let config = BackupConfig::new_from_path(repo);
insert_backup_config_encryption_unknown(config)
insert_backup_config_encryption_unknown(config, ui);
}
fn add_repo_config_remote(uri: String) -> bool {
fn add_repo_config_remote(uri: String, ui: Rc<builder::NewBackup>) {
let config = BackupConfig::new_from_uri(uri);
insert_backup_config_encryption_unknown(config)
insert_backup_config_encryption_unknown(config, ui);
}
fn insert_backup_config_encryption_unknown(mut config: shared::BackupConfig) -> bool {
fn insert_backup_config_encryption_unknown(
mut config: shared::BackupConfig,
ui: Rc<builder::NewBackup>,
) {
let mut borg = borg::Borg::new(config.clone());
while let Err(borg_err) = borg.peak() {
if borg_err.has_borg_msgid(&MsgId::PassphraseWrong) {
debug!("New backup is encrypted");
config.encrypted = true;
borg = borg::Borg::new(config.clone());
match ui::utils::get_and_store_password(&config, true) {
Ok(None) => {
borg.unset_password();
}
Ok(Some(password)) => {
borg.set_password(password);
}
Err(()) => {
return false;
}
}
} else {
debug!("This repo config is not working");
ui::utils::dialog_error(gettext!(
"There was an error with the specified repository:\n\n{}",
borg_err
));
ui::utils::dialog_catch_err(
ui::utils::secret_service_delete_passwords(&config),
"Failed to remove potentially remaining passwords from key storage.",
);
return false;
}
}
insert_backup_config(config);
borg.set_password(Zeroizing::new(vec![]));
config.encrypted = borg.peak().is_err();
insert_backup_config_password_unknown(config, ui);
}
true
fn insert_backup_config_password_unknown(config: shared::BackupConfig, ui: Rc<builder::NewBackup>) {
ui.new_backup().hide();
let x = config.clone();
ui::utils::Async::borg(
"borg::peak",
borg::Borg::new(x),
|borg| borg.peak(),
move |result| match result {
Ok(()) => {
insert_backup_config(config.clone());
ui.new_backup().close();
}
Err(borg_err) => {
debug!("This repo config is not working");
ui::utils::dialog_error(gettext!(
"There was an error with the specified repository:\n\n{}",
borg_err
));
ui::utils::dialog_catch_err(
ui::utils::secret_service_delete_passwords(&config),
"Failed to remove potentially remaining passwords from key storage.",