Commit 8c36c3e0 authored by Michael Terry's avatar Michael Terry

Make dependency installation more customizable and robust

Move dependency installation into the Assistants instead of the Overview
page. And let packagers specify which packages are needed for which
backends.
parent 8aab00e5
......@@ -35,18 +35,17 @@ public abstract class Assistant : Gtk.Window
public signal void prepare(Gtk.Widget page);
public signal void forward();
public signal void backward();
public string apply_text {get; set; default = _("_OK");}
public bool last_op_was_back {get; private set; default = false;}
public enum Type {
NORMAL, INTERRUPT, CHECK, SUMMARY, PROGRESS, FINISH
NORMAL, INTERRUPT, CHECK, PROGRESS, FINISH
}
Gtk.Label header_title;
protected Gtk.Image header_icon;
Gtk.HeaderBar header_bar;
Gtk.Widget back_button;
Gtk.Widget forward_button;
protected Gtk.Widget forward_button;
Gtk.Widget cancel_button;
Gtk.Widget close_button;
Gtk.Widget resume_button;
......@@ -55,8 +54,9 @@ public abstract class Assistant : Gtk.Window
public class PageInfo {
public Gtk.Widget page;
public string title;
public string title = "";
public Type type;
public string forward_text = "";
}
bool interrupt_can_continue = true;
......@@ -68,12 +68,13 @@ public abstract class Assistant : Gtk.Window
public weak List<PageInfo> current;
List<PageInfo> infos;
const int APPLY = 1;
const int BACK = 2;
const int FORWARD = 3;
const int CANCEL = 4;
const int CLOSE = 5;
const int RESUME = 6;
protected const int CUSTOM_RESPONSE = -1;
protected const int APPLY = 1;
protected const int BACK = 2;
protected const int FORWARD = 3;
protected const int CANCEL = 4;
protected const int CLOSE = 5;
protected const int RESUME = 6;
construct
{
......@@ -142,6 +143,7 @@ public abstract class Assistant : Gtk.Window
case CANCEL: canceled(); break;
case CLOSE: closed(); break;
case RESUME: resumed(); break;
case CUSTOM_RESPONSE: break;
}
}
......@@ -274,7 +276,7 @@ public abstract class Assistant : Gtk.Window
}
}
Gtk.Button add_button(string label, int response_id)
protected Gtk.Button add_button(string label, int response_id)
{
var btn = new Gtk.Button.with_mnemonic(label);
btn.can_default = true;
......@@ -294,8 +296,8 @@ public abstract class Assistant : Gtk.Window
weak PageInfo info = current.data;
bool show_cancel = false, show_back = false, show_forward = false,
show_apply = false, show_close = false, show_resume = false;
string forward_text = _("_Forward");
show_close = false, show_resume = false;
string forward_text = info.forward_text;
switch (info.type) {
default:
......@@ -304,11 +306,6 @@ public abstract class Assistant : Gtk.Window
show_back = current.prev != null;
show_forward = true;
break;
case Type.SUMMARY:
show_cancel = true;
show_back = current.prev != null;
show_apply = true;
break;
case Type.INTERRUPT:
show_cancel = true;
if (interrupt_can_continue) {
......@@ -348,12 +345,8 @@ public abstract class Assistant : Gtk.Window
if (apply_button != null) {
area.remove(apply_button); DejaDup.destroy_widget(apply_button); apply_button = null;}
if (show_apply) {
apply_button = add_button(apply_text, APPLY);
apply_button.grab_default();
}
if (show_forward) {
forward_button = add_button(forward_text, FORWARD);
forward_button = add_button(info.forward_text, FORWARD);
forward_button.grab_default();
}
if (show_resume) {
......@@ -378,14 +371,14 @@ public abstract class Assistant : Gtk.Window
}
Gtk.Requisition page_box_req;
public void append_page(Gtk.Widget page, Type type = Type.NORMAL)
public void append_page(Gtk.Widget page, Type type = Type.NORMAL, string forward_text = _("_Forward"))
{
var was_empty = infos == null;
var info = new PageInfo();
info.page = page;
info.type = type;
info.title = "";
info.forward_text = forward_text;
infos.append(info);
page.show_all();
......
......@@ -45,6 +45,11 @@ public abstract class AssistantOperation : Assistant
protected StatusIcon status_icon;
protected bool succeeded = false;
protected Gtk.Widget backend_install_page {get; private set;}
Gtk.Label backend_install_desc;
Gtk.Label backend_install_packages;
Gtk.ProgressBar backend_install_progress;
Gtk.Entry nag_entry;
Gtk.Entry encrypt_entry;
Gtk.Entry encrypt_confirm_entry;
......@@ -72,6 +77,7 @@ public abstract class AssistantOperation : Assistant
protected Gtk.Widget detail_widget;
Gtk.TextView detail_text_view;
protected Gtk.Widget summary_page {get; private set;}
protected string apply_text {get; set; default = _("_OK");}
protected DejaDup.Operation op;
uint timeout_id;
......@@ -87,6 +93,7 @@ public abstract class AssistantOperation : Assistant
construct
{
add_custom_config_pages();
add_backend_install_page();
add_setup_pages();
add_confirm_page();
add_password_page();
......@@ -274,6 +281,40 @@ public abstract class AssistantOperation : Assistant
page_box.queue_resize();
}
protected Gtk.Widget make_backend_install_page()
{
int rows = 0;
Gtk.Label l;
var page = new Gtk.Grid();
page.row_spacing = 6;
page.border_width = 12;
l = new Gtk.Label(_("In order to continue, the following packages need to be installed:"));
l.xalign = 0.0f;
l.max_width_chars = 50;
l.wrap = true;
page.attach(l, 0, rows++, 1, 1);
backend_install_desc = l;
l = new Gtk.Label("");
l.halign = Gtk.Align.START;
l.max_width_chars = 50;
l.wrap = true;
l.margin_left = 12;
l.use_markup = true;
page.attach(l, 0, rows++, 1, 1);
backend_install_packages = l;
backend_install_progress = new Gtk.ProgressBar();
backend_install_progress.no_show_all = true;
backend_install_progress.hexpand = true;
backend_install_progress.hide();
page.attach(backend_install_progress, 0, rows++, 1, 1);
return page;
}
protected Gtk.Widget make_password_page()
{
int rows = 0;
......@@ -464,6 +505,14 @@ public abstract class AssistantOperation : Assistant
return page;
}
void add_backend_install_page()
{
var page = make_backend_install_page();
append_page(page, Type.INTERRUPT);
set_page_title(page, _("Install Packages"));
backend_install_page = page;
}
void add_confirm_page()
{
/*
......@@ -474,7 +523,7 @@ public abstract class AssistantOperation : Assistant
var page = make_confirm_page();
if (page == null)
return;
append_page(page, Type.SUMMARY);
append_page(page, Type.NORMAL, apply_text);
set_page_title(page, _("Summary"));
confirm_page = page;
}
......@@ -524,10 +573,7 @@ public abstract class AssistantOperation : Assistant
this.op = null;
if (cancelled) {
if (success) // stop (resume later) vs cancel
Gtk.main_quit();
else
do_close();
do_close();
}
else {
if (success) {
......@@ -583,6 +629,7 @@ public abstract class AssistantOperation : Assistant
op.action_file_changed.connect(set_progress_label_file);
op.progress.connect(show_progress);
op.question.connect(show_question);
op.install.connect(show_install);
op.backend.mount_op = new MountOperationAssistant(this);
op.backend.pause_op.connect(pause_op);
......@@ -880,6 +927,48 @@ public abstract class AssistantOperation : Assistant
Gtk.main();
}
async void start_install(string[] package_ids, MainLoop loop)
{
backend_install_desc.hide();
backend_install_packages.hide();
backend_install_progress.show();
try {
var client = new Pk.Client();
yield client.install_packages_async(0, package_ids, null, (p, t) => {
backend_install_progress.fraction = (p.percentage / 100.0).clamp(0, 100);
});
}
catch (Error e) {
show_error("%s".printf(e.message), null);
return;
}
go_forward();
loop.quit();
}
void show_install(DejaDup.Operation op, string[] names, string[] ids)
{
var text = "";
foreach (string s in names) {
if (text != "")
text += ", ";
text += "<b>%s</b>".printf(s);
}
backend_install_packages.label = text;
interrupt(backend_install_page, false);
set_header_icon("system-software-install");
var install_button = add_button(C_("verb", "_Install"), CUSTOM_RESPONSE);
var loop = new MainLoop(null);
install_button.clicked.connect(() => {start_install.begin(ids, loop);});
forward_button = install_button;
force_visible(false);
loop.run();
}
protected void pause_op(DejaDup.Backend back, string? header, string? msg)
{
// Basically a question without a response expected
......
......@@ -33,9 +33,6 @@ public class PreferencesPeriodicSwitch : Gtk.Switch
public class Preferences : Gtk.Grid
{
public DejaDup.PreferencesPeriodicSwitch external_auto_switch {get; set; default = null;}
public bool duplicity_installed {get; private set; default = false;}
DejaDupApp _app;
public DejaDupApp app {
get { return _app; }
......@@ -52,89 +49,11 @@ public class Preferences : Gtk.Grid
DejaDup.ConfigLabelDescription backup_desc;
Gtk.Button backup_button;
Gtk.ProgressBar backup_progress;
DejaDup.ConfigLabelDescription restore_desc;
Gtk.Button restore_button;
Gtk.ProgressBar restore_progress;
DejaDup.PreferencesPeriodicSwitch auto_switch;
const int PAGE_HMARGIN = 24;
const int PAGE_VMARGIN = 12;
public Preferences(DejaDup.PreferencesPeriodicSwitch? auto_switch)
{
Object(external_auto_switch: auto_switch);
// Set initial switch sensitivity, but for some odd reason we can't set
// this earlier. Even if at the end of the constructor, it gets reset...
external_auto_switch.sensitive = duplicity_installed;
}
async void install_duplicity()
{
backup_button.sensitive = false;
restore_button.sensitive = false;
try {
var task = new Pk.Task();
var results = yield task.resolve_async(Pk.Filter.NOT_INSTALLED, {"duplicity"}, null, () => {});
if (results != null && results.get_error_code () == null)
{
// Convert from List to array (I don't know why the API couldn't be friendlier...)
var package_array = results.get_package_array();
var package_ids = new string[0];
var package_names = new GenericSet<string>(str_hash, str_equal);
for (var i = 0; i < package_array.length; i++) {
// First make sure we haven't added packages with this name already, which can happen
// if the user has multiple arch repositories enabled (like amd64 and i386). We could
// instead simply take the first result, but we want to make it easy for distros to
// patch the above resolve_async line to have multiple packages if they want.
if (!package_names.contains(package_array.data[i].get_name())) {
package_names.add(package_array.data[i].get_name());
package_ids += package_array.data[i].get_id();
}
}
yield task.install_packages_async(package_ids, null, (p, t) => {
backup_progress.fraction = p.percentage / 100.0;
restore_progress.fraction = p.percentage / 100.0;
});
duplicity_installed = Environment.find_program_in_path("duplicity") != null;
if (duplicity_installed) {
backup_desc.everything_installed = true;
backup_button.label = _("_Back Up Now…");
restore_desc.everything_installed = true;
restore_button.label = _("_Restore…");
auto_switch.sensitive = true;
external_auto_switch.sensitive = true;
}
}
}
catch (Error e) {
// We don't want to show authorization errors -- either the user clicked
// cancel or already entered password several times. Don't need to warn them.
// Oddly enough, I couldn't get error matching to work for this. Maybe the
// policykit bindings I copied are incomplete.
if (e.message.contains("org.freedesktop.PolicyKit.Error.NotAuthorized")) {
warning("%s\n", e.message);
} else {
Gtk.MessageDialog dlg = new Gtk.MessageDialog (get_toplevel() as Gtk.Window,
Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.OK,
"%s", _("Could not install"));
dlg.format_secondary_text("%s", e.message);
dlg.run();
destroy_widget(dlg);
}
}
backup_progress.visible = false;
restore_progress.visible = false;
backup_button.sensitive = true;
restore_button.sensitive = true;
}
Gtk.Widget make_settings_page()
{
var settings_page = new Gtk.Grid();
......@@ -202,35 +121,21 @@ public class Preferences : Gtk.Grid
table.attach(w, 1, row, 2, 1);
++row;
w = new DejaDup.ConfigLabelDescription(DejaDup.ConfigLabelDescription.Kind.RESTORE, duplicity_installed);
w = new DejaDup.ConfigLabelDescription(DejaDup.ConfigLabelDescription.Kind.RESTORE);
w.halign = Gtk.Align.START;
w.valign = Gtk.Align.START;
restore_desc = w as DejaDup.ConfigLabelDescription;
table.attach(w, 1, row, 2, 1);
++row;
w = new Gtk.Button.with_mnemonic(duplicity_installed ? _("_Restore…") : _("_Install…"));
w = new Gtk.Button.with_mnemonic(_("_Restore…"));
w.margin_top = 6;
w.halign = Gtk.Align.START;
w.expand = false;
(w as Gtk.Button).clicked.connect((b) => {
if (duplicity_installed) {
app.restore();
} else {
restore_progress.visible = true;
install_duplicity.begin();
}
});
(w as Gtk.Button).clicked.connect((b) => {app.restore();});
restore_button = w as Gtk.Button;
label_sizes.add_widget(w);
table.attach(w, 1, row, 1, 1);
w = new Gtk.ProgressBar();
w.halign = Gtk.Align.START;
w.valign = Gtk.Align.CENTER;
w.hexpand = true;
w.no_show_all = true;
restore_progress = w as Gtk.ProgressBar;
table.attach(w, 2, row, 1, 1);
++row;
w = new Gtk.Grid(); // spacer
......@@ -248,35 +153,21 @@ public class Preferences : Gtk.Grid
table.attach(w, 1, row, 2, 1);
++row;
w = new DejaDup.ConfigLabelDescription(DejaDup.ConfigLabelDescription.Kind.BACKUP, duplicity_installed);
w = new DejaDup.ConfigLabelDescription(DejaDup.ConfigLabelDescription.Kind.BACKUP);
w.halign = Gtk.Align.START;
w.valign = Gtk.Align.START;
backup_desc = w as DejaDup.ConfigLabelDescription;
table.attach(w, 1, row, 2, 1);
++row;
w = new Gtk.Button.with_mnemonic(duplicity_installed ? _("_Back Up Now…") : _("Install…"));
w = new Gtk.Button.with_mnemonic(_("_Back Up Now…"));
w.margin_top = 6;
w.halign = Gtk.Align.START;
w.expand = false;
(w as Gtk.Button).clicked.connect((b) => {
if (duplicity_installed) {
app.backup();
} else {
backup_progress.visible = true;
install_duplicity.begin();
}
});
(w as Gtk.Button).clicked.connect((b) => {app.backup();});
backup_button = w as Gtk.Button;
label_sizes.add_widget(w);
table.attach(w, 1, row, 1, 1);
w = new Gtk.ProgressBar();
w.halign = Gtk.Align.START;
w.valign = Gtk.Align.CENTER;
w.hexpand = true;
w.no_show_all = true;
backup_progress = w as Gtk.ProgressBar;
table.attach(w, 2, row, 1, 1);
++row;
notebook.append_page(table, null);
......@@ -352,11 +243,10 @@ public class Preferences : Gtk.Grid
row = 0;
var align = new Gtk.Alignment(0.0f, 0.5f, 0.0f, 0.0f);
auto_switch = new DejaDup.PreferencesPeriodicSwitch();
auto_switch.sensitive = duplicity_installed;
align.add(auto_switch);
w = new DejaDup.PreferencesPeriodicSwitch();
align.add(w);
label = new Gtk.Label.with_mnemonic(_("_Automatic backup"));
label.mnemonic_widget = auto_switch;
label.mnemonic_widget = w;
label.xalign = 1.0f;
table.attach(label, 0, row, 1, 1);
table.attach(align, 1, row, 1, 1);
......@@ -422,7 +312,6 @@ public class Preferences : Gtk.Grid
}
construct {
duplicity_installed = Environment.find_program_in_path("duplicity") != null;
add(make_settings_page());
set_size_request(-1, 400);
}
......
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2017-08-14 17:05-0400\n"
"POT-Creation-Date: 2017-08-17 00:27-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -168,7 +168,7 @@ public class DejaDupApp : Gtk.Application
auto_switch.valign = Gtk.Align.CENTER;
header.pack_end(auto_switch);
var prefs = new DejaDup.Preferences(auto_switch);
var prefs = new DejaDup.Preferences();
prefs.app = this;
prefs.border_width = 12;
main_window.add(prefs);
......
......@@ -29,11 +29,10 @@ public class ConfigLabelDescription : ConfigLabel
{
public enum Kind {BACKUP, RESTORE}
public Kind kind {get; construct;}
public bool everything_installed {get; set;}
public ConfigLabelDescription(Kind kind, bool everything_installed)
public ConfigLabelDescription(Kind kind)
{
Object(kind: kind, everything_installed: everything_installed);
Object(kind: kind);
}
construct {
......@@ -59,12 +58,6 @@ public class ConfigLabelDescription : ConfigLabel
void set_from_config_restore()
{
if (!everything_installed) {
var button_name = "<b>%s</b>".printf(_("Install…"));
label.label = _("You can restore existing backups after you first install some necessary software by clicking the %s button.").printf(button_name);
return;
}
var val = DejaDup.last_run_date(DejaDup.TimestampType.BACKUP);
// This here encodes a lot of outside GUI information in this widget,
......@@ -83,12 +76,6 @@ public class ConfigLabelDescription : ConfigLabel
void set_from_config_backup()
{
if (!everything_installed) {
var button_name = "<b>%s</b>".printf(_("Install…"));
label.label = _("You can create a backup after you first install some necessary software by clicking the %s button.").printf(button_name);
return;
}
var next = DejaDup.next_run_date();
if (next == null) {
var button_name = "<b>%s</b>".printf(_("Back Up Now…"));
......
......@@ -34,6 +34,9 @@ public abstract class Backend : Object
public abstract string get_location(ref bool as_root); // URI for duplicity
public abstract string get_location_pretty(); // short description for user
// list of what-provides hints
public virtual string[] get_dependencies() {return {};}
public virtual async bool is_ready(out string when) {when = null; return true;} // must be callable when nothing is mounted, nothing is prepared
public virtual async void get_envp() throws Error {
......
......@@ -23,6 +23,11 @@ namespace DejaDup {
public abstract class BackendFile : Backend
{
public override string[] get_dependencies()
{
return Config.GVFS_PACKAGES.split(",");
}
// Get mountable root
protected abstract File? get_root_from_settings();
......
......@@ -33,7 +33,12 @@ public class BackendGCS : Backend
public override Backend clone() {
return new BackendGCS();
}
public override string[] get_dependencies()
{
return Config.BOTO_PACKAGES.split(",");
}
public override bool is_native() {
return false;
}
......
......@@ -33,6 +33,11 @@ public class BackendOpenstack : Backend
return new BackendOpenstack();
}
public override string[] get_dependencies()
{
return Config.SWIFTCLIENT_PACKAGES.split(",");
}
public override bool is_native() {
return false;
}
......
......@@ -33,6 +33,11 @@ public class BackendRackspace : Backend
return new BackendRackspace();
}
public override string[] get_dependencies()
{
return Config.CLOUDFILES_PACKAGES.split(",");
}
public override bool is_native() {
return false;
}
......
......@@ -33,7 +33,12 @@ public class BackendS3 : Backend
public override Backend clone() {
return new BackendS3();
}
public override string[] get_dependencies()
{
return Config.BOTO_PACKAGES.split(",");
}
public override void add_argv(ToolJob.Mode mode, ref List<string> argv) {
if (mode == ToolJob.Mode.INVALID)
argv.append("--s3-use-new-style");
......
......@@ -429,6 +429,12 @@ public FilteredSettings get_settings(string? subdir = null, string? path = null)
}
ToolPlugin tool = null;
public ToolPlugin get_tool()
{
assert(tool != null);
return tool;
}
void initialize_tool_plugin() throws Error
{
var engine = new Peas.Engine ();
......@@ -454,13 +460,6 @@ void initialize_tool_plugin() throws Error
tool.activate();
}
public ToolJob make_tool_job() throws Error
{
if (tool == null)
initialize_tool_plugin();
return tool.create_job();
}
public bool initialize(out string header, out string msg)
{
header = null;
......
......@@ -39,6 +39,7 @@ public abstract class Operation : Object
public signal void progress(double percent);
public signal void passphrase_required();
public signal void question(string title, string msg);
public signal void install(string[] names, string[] ids);
public signal void is_full(bool first);
public bool use_cached_password {get; protected set; default = true;}
......@@ -102,12 +103,8 @@ public abstract class Operation : Object
{
action_desc_changed(_("Preparing…"));
if (backend is BackendAuto) {
// OK, we're not ready yet. Let's hold off until we are
settings = get_settings();
settings.notify["backend"].connect(restart);
}
else
yield check_dependencies();
if (!finished) // might have been cancelled during check above
restart();
}
......@@ -126,7 +123,7 @@ public abstract class Operation : Object
}
try {
job = make_tool_job();
job = DejaDup.get_tool().create_job();
}
catch (Error e) {
raise_error(e.message, null);
......@@ -160,16 +157,20 @@ public abstract class Operation : Object
{
if (chained_op != null)
chained_op.cancel();
else
else if (job != null)
job.cancel();
else
operation_finished.begin(false, true, null);
}
public void stop()
{
if (chained_op != null)
chained_op.stop();
else
else if (job != null)
job.stop();
else
operation_finished.begin(true, true, null);
}
protected virtual void connect_to_job()
......@@ -177,7 +178,7 @@ public abstract class Operation : Object
/*
* Connect Deja Dup to signals
*/
job.done.connect((d, o, c, detail) => {operation_finished.begin(d, o, c, detail);});
job.done.connect((d, o, c, detail) => {operation_finished.begin(o, c, detail);});
job.raise_error.connect((d, s, detail) => {raise_error(s, detail);});
job.action_desc_changed.connect((d, s) => {action_desc_changed(s);});
job.action_file_changed.connect((d, f, b) => {send_action_file_changed(f, b);});
......@@ -206,7 +207,7 @@ public abstract class Operation : Object
job.encrypt_password = passphrase;
}
internal async virtual void operation_finished(ToolJob job, bool success, bool cancelled, string? detail)
internal async virtual void operation_finished(bool success, bool cancelled, string? detail)
{
finished = true;
......@@ -258,6 +259,7 @@ public abstract class Operation : Object
subop.set_passphrase(passphrase);
});
subop.question.connect((t, m) => {question(t, m);});
subop.install.connect((p, i) => {install(p, i);});
use_cached_password = subop.use_cached_password;
saved_detail = combine_details(saved_detail, detail);
......@@ -268,6 +270,39 @@ public abstract class Operation : Object
yield subop.start();