Commit feb05f31 authored by Michael Terry's avatar Michael Terry

Support an off-by-default borg backend

parent b567e753
Pipeline #78169 failed with stages
in 7 minutes and 54 seconds
......@@ -291,11 +291,11 @@ public class AssistantRestore : AssistantOperation
protected override DejaDup.Operation? create_op()
{
string date = null;
string tag = null;
if (got_dates) {
Gtk.TreeIter iter;
if (date_combo.get_active_iter(out iter))
date_store.get(iter, 1, out date);
date_store.get(iter, 1, out tag);
}
realize();
......@@ -309,7 +309,7 @@ public class AssistantRestore : AssistantOperation
ensure_config_location();
var rest_op = new DejaDup.OperationRestore(config_location.get_backend(),
restore_location, date,
restore_location, tag,
resolved_files);
if (this.op_state != null)
rest_op.set_state(this.op_state);
......@@ -324,55 +324,42 @@ public class AssistantRestore : AssistantOperation
return _("Restoring:");
}
bool is_same_day(TimeVal one, TimeVal two)
bool is_same_day(DateTime one, DateTime two)
{
Date day1 = Date(), day2 = Date();
day1.set_time_val(one);
day2.set_time_val(two);
return day1.compare(day2) == 0;
int y1, m1, d1, y2, m2, d2;
one.get_ymd(out y1, out m1, out d1);
two.get_ymd(out y2, out m2, out d2);
return y1 == y2 && m1 == m2 && d1 == d2;
}
protected virtual void handle_collection_dates(DejaDup.OperationStatus op, List<string>? dates)
protected virtual void handle_collection_dates(DejaDup.OperationStatus op, Tree<DateTime, string> dates)
{
/*
* Receives list of dates of backups and shows them to user
*
* After receiving list of dates at which backups were performed function
* converts dates to TimeVal structures and later converts them to Time to
* time to show them in nicely formate local form.
*/
var timevals = new List<TimeVal?>();
TimeVal tv = TimeVal();
got_dates = true;
date_store.clear();
foreach (string date in dates) {
if (tv.from_iso8601(date)) {
timevals.append(tv);
}
}
for (unowned List<TimeVal?>? i = timevals; i != null; i = i.next) {
tv = i.data;
dates.foreach((time, tag) => {
string format = "%x";
if ((i.prev != null && is_same_day(i.prev.data, tv)) ||
(i.next != null && is_same_day(i.next.data, tv))) {
// FIXME
/*if ((prev != null && is_same_day(prev, time)) ||
(next != null && is_same_day(next, time))) {
// Translators: %x is the current date, %X is the current time.
// This will be in a list with other strings that just have %x (the
// current date). So make sure if you change this, it still makes
// sense in that context.
format = _("%x %X");
}
}*/
Time t = Time.local(tv.tv_sec);
string user_str = t.format(format);
string user_str = time.format(format);
Gtk.TreeIter iter;
date_store.prepend(out iter);
date_store.@set(iter, 0, user_str, 1, tv.to_iso8601());
date_store.@set(iter, 0, user_str, 1, tag);
date_combo.set_active_iter(iter);
}
return false;
});
// If we didn't see any dates... Must not be any backups on the backend
if (date_store.iter_n_children(null) == 0)
......
......@@ -27,27 +27,21 @@ public class DeletedFile {
* can access pretty file name and mark file for restore.
*
* @param //string// ''name'' Full path name of file
* @param //Time// ''deleted'' Information when was file deleted
* @param //string// ''tag'' Information when was file deleted
*/
public string name {get; set;}
public Time deleted {get; set;}
public string tag {get; set;}
public bool restore {get; set; default = false;}
public DeletedFile(string name, Time deleted) {
public DeletedFile(string name, string tag) {
this.name = name;
this.deleted = deleted;
this.tag = tag;
}
public string filename() {
var splited_fn = this.name.split("/");
return splited_fn[splited_fn.length-1];
}
public string queue_format() {
var file = this.name;
var time = this.deleted.format("%s");
return @"$file $time";
}
}
public class AssistantRestoreMissing : AssistantRestore {
......@@ -70,25 +64,6 @@ public class AssistantRestoreMissing : AssistantRestore {
private File display_directory;
private bool backups_queue_filled = false;
private static int compare_time(Time a, Time b) {
/*
* Compare function for backups queue
*
* Default comparing for queue goes from oldest to newest so we use our own
* compare function to reverse that ordering and proceed from newest to oldest
* since it is far more likely that user deleted file in recent history.
*
* @param //Time// ''a'', ''b'' Time objects
*/
var a_epoch = int.parse(a.format("%s"));
var b_epoch = int.parse(b.format("%s"));
if (a_epoch < b_epoch)
return 1;
else if (a_epoch == b_epoch)
return 0;
else
return -1;
}
/*
If user moves forward while OperationFiles is runing, code cleanup stops the current operation
......@@ -97,7 +72,7 @@ public class AssistantRestoreMissing : AssistantRestore {
*/
private bool scan_queue = true;
private bool cancel_assistant = false;
private Sequence<Time?> backups_queue = new Sequence<Time?>();
private Tree<DateTime, string> backups_queue;
private HashTable<string, DeletedFile> allfiles_prev;
private List<File> restore_files_remaining;
......@@ -235,7 +210,7 @@ public class AssistantRestoreMissing : AssistantRestore {
base.do_prepare(assist, page);
}
protected void handle_listed_files(DejaDup.OperationFiles op, string date, string file)
protected void handle_listed_files(DejaDup.OperationFiles op, string date, string filestr)
{
/*
* Handler for each line returned by duplicity individually
......@@ -246,18 +221,17 @@ public class AssistantRestoreMissing : AssistantRestore {
*
* @param //DejaDup.OperationFiles// ''op'' Operation that is currently running
* @param //string// ''date'' Time of last change of file
* @param //string// ''file'' Full path of file
* @param //string// ''filestr'' Full path of file
*/
string filestr = @"/$file";
if (this.list_directory.get_path() in filestr && this.list_directory.get_path() != filestr) {
var fileobj = File.new_for_path(filestr);
if (!fileobj.query_exists(null) && !this.allfiles_prev.lookup_extended(filestr, null, null)) {
if(fileobj.has_parent(this.list_directory)) {
var fs = new DeletedFile(filestr, op.get_time());
var fs = new DeletedFile(filestr, op.tag);
this.listmodel.append (out this.deleted_iter);
this.listmodel.set (this.deleted_iter, 0, false, 1, fs.filename(), 2, op.get_time().format("%c"));
this.listmodel.set (this.deleted_iter, 0, false, 1, fs.filename(), 2, op.tag); // FIXME should be time instead of tag
this.allfiles_prev.insert(fileobj.get_path(), fs);
}
......@@ -265,29 +239,14 @@ public class AssistantRestoreMissing : AssistantRestore {
}
}
protected override void handle_collection_dates(DejaDup.OperationStatus op, GLib.List<string>? dates)
protected override void handle_collection_dates(DejaDup.OperationStatus op, GLib.Tree<DateTime, string> dates)
{
/*
* Handle collection dates
*
* Collection dates are returned as a single list of strings file timestamps of backup.
* Timestamps are in ISO 8601 format and are first read and converted to //Time// objects and then
* added to backups_queue.
*
* @param //DejaDup.OperationStatus// ''op'' Operation currently being run
* @param //GLib.List<string>?// ''dates'' ISO 8601 dates of backups.
*/
TimeVal tv = TimeVal();
if (!this.backups_queue_filled) {
foreach(var date in dates) {
if (tv.from_iso8601(date)) {
Time t = Time.local(tv.tv_sec);
this.backups_queue.insert_sorted(t, (CompareDataFunc)compare_time);
}
}
this.allfiles_prev = new HashTable<string, DeletedFile>(str_hash, str_equal);
this.backups_queue = dates;
this.backups_queue_filled = true;
this.spinner.start();
......@@ -308,23 +267,31 @@ public class AssistantRestoreMissing : AssistantRestore {
}
// Don't start if queue is empty.
if (backups_queue.get_length() == 0) {
if (backups_queue.nnodes() == 0) {
query_files_finished(query_op_files, true, false, null);
return;
}
var begin = backups_queue.get_begin_iter();
var etime = begin.get();
begin.remove();
// Grab first sorted time (the most recent time left in backups_queue),
// and then remove it, so that the next time we are called, we'll look
// at the next most recent time.
DateTime first_time = null;
string first_label = null;
backups_queue.foreach((k, v) => {
first_time = k;
first_label = v;
return true; // immediately stops
});
backups_queue.remove(first_time);
/* Update progress */
int tepoch = int.parse(etime.format("%s"));
var tepoch = first_time.to_unix();
TimeVal ttoday = TimeVal();
ttoday.get_current_time();
int ttodayi = (int) ttoday.tv_sec;
string worddiff;
int tdiff = (ttodayi - tepoch)/60/60; // Hours
int64 tdiff = (ttodayi - tepoch)/60/60; // Hours
if (tdiff / 24 == 0 ) {
worddiff = _("Scanning for files from up to a day ago…");
}
......@@ -335,14 +302,14 @@ public class AssistantRestoreMissing : AssistantRestore {
worddiff = _("Scanning for files from up to a month ago…");
}
else if (tdiff / 24 / 30 >= 1 && tdiff / 24 / 30 <= 12) {
int n = tdiff / 24 / 30;
int n = (int)(tdiff / 24 / 30);
worddiff = dngettext(Config.GETTEXT_PACKAGE,
"Scanning for files from about a month ago…",
"Scanning for files from about %d months ago…",
n).printf(n);
}
else {
int n = tdiff / 24 / 30 / 12;
int n = (int)(tdiff / 24 / 30 / 12);
worddiff = dngettext(Config.GETTEXT_PACKAGE,
"Scanning for files from about a year ago…",
"Scanning for files from about %d years ago…",
......@@ -354,7 +321,7 @@ public class AssistantRestoreMissing : AssistantRestore {
realize();
/* Time object does not support GObject-style construction */
query_op_files = new DejaDup.OperationFiles(backend, etime, list_directory);
query_op_files = new DejaDup.OperationFiles(backend, first_label, list_directory);
query_op_files.listed_current_files.connect(handle_listed_files);
query_op_files.done.connect(query_files_finished);
......@@ -434,7 +401,7 @@ public class AssistantRestoreMissing : AssistantRestore {
query_op_files = null;
this.op = null;
if (backups_queue.get_length() == 0) {
if (backups_queue.nnodes() == 0) {
this.spinner.stop();
DejaDup.destroy_widget(this.spinner);
this.current_scan_date.set_text(_("Scanning finished"));
......@@ -469,8 +436,7 @@ public class AssistantRestoreMissing : AssistantRestore {
var file_list = new GLib.List<File>();
file_list.append(File.new_for_path(restore_file.name));
var rest_op = new DejaDup.OperationRestore(backend, "/",
restore_file.deleted.format("%s"),
var rest_op = new DejaDup.OperationRestore(backend, "/", restore_file.tag,
file_list);
rest_op.set_state(op_state);
return rest_op;
......
......@@ -37,7 +37,7 @@ public abstract class Backend : Object
public abstract bool is_native(); // must be callable when nothing is mounted, nothing is prepared
public virtual Icon? get_icon() {return null;}
public abstract string get_location(ref bool as_root); // URI for duplicity
public abstract string get_location(bool as_borg, ref bool as_root); // URI for tool
public abstract string get_location_pretty(); // short description for user
// list of what-provides hints
......
......@@ -36,7 +36,7 @@ public class BackendAuto : Backend
return false;
}
public override string get_location(ref bool as_root) {
public override string get_location(bool as_borg, ref bool as_root) {
return "invalid://";
}
......
......@@ -53,7 +53,7 @@ public abstract class BackendFile : Backend
protected abstract File? get_file_from_settings();
// Location will be mounted by this time
public override string get_location(ref bool as_root)
public override string get_location(bool as_borg, ref bool as_root)
{
var file = get_file_from_settings();
if (file == null)
......@@ -103,7 +103,10 @@ public abstract class BackendFile : Backend
as_root = false;
}
return "gio+" + file.get_uri();
if (as_borg)
return file.get_uri();
else
return "gio+" + file.get_uri();
}
public override string get_location_pretty()
......
......@@ -52,7 +52,7 @@ public class BackendGCS : Backend
return yield Network.get().can_reach ("http://%s/".printf(GCS_SERVER));
}
public override string get_location(ref bool as_root)
public override string get_location(bool as_borg, ref bool as_root)
{
var bucket = settings.get_string(GCS_BUCKET_KEY);
var folder = get_folder_key(settings, GCS_FOLDER_KEY);
......
......@@ -90,7 +90,7 @@ public class BackendGoogle : Backend
return yield Network.get().can_reach("https://%s/".printf(GOOGLE_SERVER));
}
public override string get_location(ref bool as_root)
public override string get_location(bool as_borg, ref bool as_root)
{
var folder = get_folder_key(settings, GOOGLE_FOLDER_KEY);
......
......@@ -52,7 +52,7 @@ public class BackendOpenstack : Backend
return yield Network.get().can_reach (authurl);
}
public override string get_location(ref bool as_root)
public override string get_location(bool as_borg, ref bool as_root)
{
var container = get_folder_key(settings, OPENSTACK_CONTAINER_KEY);
if (container == "") {
......
......@@ -51,7 +51,7 @@ public class BackendRackspace : Backend
return yield Network.get().can_reach ("http://%s/".printf(RACKSPACE_SERVER));
}
public override string get_location(ref bool as_root)
public override string get_location(bool as_borg, ref bool as_root)
{
var container = get_folder_key(settings, RACKSPACE_CONTAINER_KEY);
if (container == "") {
......
......@@ -61,7 +61,7 @@ public class BackendS3 : Backend
return yield Network.get().can_reach ("http://%s/".printf(S3_SERVER));
}
public override string get_location(ref bool as_root)
public override string get_location(bool as_borg, ref bool as_root)
{
var bucket = settings.get_string(S3_BUCKET_KEY);
var default_bucket = get_default_bucket();
......
......@@ -398,6 +398,11 @@ public FilteredSettings get_settings(string? subdir = null)
return new FilteredSettings(subdir);
}
int date_time_reverse_compare(DateTime a, DateTime b)
{
return b.compare(a);
}
ToolPlugin tool = null;
public ToolPlugin get_tool()
{
......@@ -410,7 +415,11 @@ public bool initialize(out string header, out string msg)
header = null;
msg = null;
tool = new DuplicityPlugin();
var use_borg = Environment.get_variable("DEJA_DUP_USE_BORG");
if (use_borg == "1")
tool = new BorgPlugin();
else
tool = new DuplicityPlugin();
migrate_file_settings();
migrate_goa_settings();
......
......@@ -134,7 +134,7 @@ public abstract class Operation : Object
job.encrypt_password = passphrase;
if (!finished)
job.start();
job.start.begin();
unref();
}
......
......@@ -38,10 +38,12 @@ public class OperationBackup : Operation
if (metadir != null)
new RecursiveDelete(metadir).start();
if (success && !cancelled)
yield chain_op(new OperationVerify(backend), _("Verifying backup…"), detail);
else
if (success && !cancelled) {
var verify = new OperationVerify(backend, job.tag);
yield chain_op(verify, _("Verifying backup…"), detail);
} else {
yield base.operation_finished(success, cancelled, detail);
}
}
protected override void send_action_file_changed(File file, bool actual)
......
......@@ -20,23 +20,15 @@
using GLib;
namespace DejaDup {
public class OperationFiles : Operation {
public class OperationFiles : Operation
{
public signal void listed_current_files(string date, string file);
public File source {get; construct;}
private Time time; // Default value is 1900-01-00 00:00:00; since epoch hasn't happened yet, its default %s value is -1
public string tag {get; construct;}
public OperationFiles(Backend backend,
Time? time_in,
File source) {
Object(mode: ToolJob.Mode.LIST, source: source, backend: backend);
if (time_in != null)
time = time_in;
}
public Time get_time()
public OperationFiles(Backend backend, string tag, File source)
{
return time;
Object(mode: ToolJob.Mode.LIST, source: source, backend: backend, tag: tag);
}
protected override void connect_to_job()
......@@ -47,12 +39,9 @@ public class OperationFiles : Operation {
protected override List<string>? make_argv()
{
if (time.format("%s") != "-1")
job.time = time.format("%s");
else
job.time = null;
job.tag = tag;
job.local = source;
return null;
}
}
}
}
\ No newline at end of file
......@@ -24,7 +24,7 @@ namespace DejaDup {
public class OperationRestore : Operation
{
public string dest {get; construct;} // Directory user wants to put files in
public string time {get; construct;} // Date user wants to restore to
public string tag {get; construct;} // Date user wants to restore to
private List<File> _restore_files;
public List<File> restore_files {
get {
......@@ -37,9 +37,9 @@ public class OperationRestore : Operation
public OperationRestore(Backend backend,
string dest_in,
string? time_in = null,
string tag,
List<File>? files_in = null) {
Object(dest: dest_in, time: time_in, restore_files: files_in,
Object(dest: dest_in, tag: tag, restore_files: files_in,
mode: ToolJob.Mode.RESTORE, backend: backend);
}
......@@ -52,7 +52,7 @@ public class OperationRestore : Operation
protected override List<string>? make_argv()
{
job.restore_files = restore_files;
job.time = time;
job.tag = tag;
job.local = File.new_for_path(dest);
return null;
}
......@@ -67,4 +67,3 @@ public class OperationRestore : Operation
}
} // end namespace
......@@ -23,7 +23,7 @@ namespace DejaDup {
public class OperationStatus : Operation
{
public signal void collection_dates(List<string>? dates);
public signal void collection_dates(Tree<DateTime, string> dates);
public OperationStatus(Backend backend) {
Object(mode: ToolJob.Mode.STATUS, backend: backend);
......
......@@ -26,12 +26,13 @@ namespace DejaDup {
public class OperationVerify : Operation
{
public string tag {get; construct;}
File metadir;
File destdir;
bool nag;
public OperationVerify(Backend backend) {
Object(mode: ToolJob.Mode.RESTORE, backend: backend);
public OperationVerify(Backend backend, string tag) {
Object(mode: ToolJob.Mode.RESTORE, backend: backend, tag: tag);
}
construct {
......@@ -69,6 +70,8 @@ public class OperationVerify : Operation
destdir = File.new_for_path("/");
job.local = destdir;
job.tag = tag;
base.connect_to_job();
}
......
......@@ -38,18 +38,18 @@ public abstract class ToolJob : Object
public signal void question(string title, string msg);
// type-specific signals
public signal void collection_dates(List<string>? dates); // HISTORY
public signal void collection_dates(Tree<DateTime, string> dates); // STATUS (date, label) in reverse time order
public signal void listed_current_files(string date, string file); // LIST
// life cycle control
public abstract void start ();
public abstract async void start ();
public abstract void cancel (); // destroy progress so far
public abstract void stop (); // just abruptly stop
public abstract void pause (string? reason);
public abstract void resume ();
public enum Mode {
INVALID, BACKUP, RESTORE, STATUS, LIST, HISTORY,
INVALID, BACKUP, RESTORE, STATUS, LIST,
}
public Mode mode {get; set; default = Mode.INVALID;}
......@@ -77,7 +77,7 @@ public abstract class ToolJob : Object
}
}
public string time {get; set;} // RESTORE
public string tag {get; set;} // RESTORE
}
public abstract class ToolPlugin : Object
......
......@@ -43,6 +43,9 @@ libdeja = shared_library('deja',
'RecursiveMove.vala',
'RecursiveOp.vala',
'ToolPlugin.vala',
'tools/borg/BorgInstance.vala',
'tools/borg/BorgJob.vala',
'tools/borg/BorgPlugin.vala',
'tools/duplicity/DuplicityInstance.vala',
'tools/duplicity/DuplicityJob.vala',
'tools/duplicity/DuplicityPlugin.vala',
......
/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 2 -*- */
/*
This file is part of Déjà Dup.
For copyright information, see AUTHORS.
Déjà Dup is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Déjà Dup is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Déjà Dup. If not, see <http://www.gnu.org/licenses/>.
*/
using GLib;
internal class ToolInstance : Object
{
public string name { get; construct; }
public signal void done(bool success, bool cancelled);
public signal void exited(int code);
public async void start(List<string> argv_in, List<string>? envp_in,
bool as_root = false)
{
try {
/* Make deep copies of the lists, so if our caller doesn't yield, the
lists won't be invalidated. */
var argv = argv_in.copy_deep((CopyFunc) Object.ref);
var envp = envp_in.copy_deep((CopyFunc) Object.ref);
if (!yield start_internal(argv, envp, as_root))
done(false, false);
}
catch (Error e) {
handle_start_error(e);
done(false, false);
}
}
public bool is_started()
{
return (int)child_pid > 0;
}
public void cancel()
{
if (is_started())
kill_child();
else
done(false, true);
}
public void pause()
{
if (is_started())
stop_child();
}
public void resume()
{
if (is_started())
cont_child();
}
protected List<string> add_public_args(List<string> argv)
{
return argv;
}
protected List<string> add_private_args(List<string> argv)
{
return argv;
}
protected void handle_start_error(Error e)
{
}
uint watch_id;
Pid child_pid;
int[] pipes;
DataInputStream reader;
File logfile;
IOStream logstream;
File scriptfile;
bool process_done;
int status;
bool processed_a_message;
bool verbose;
construct {
pipes = new int[2];
pipes[0] = pipes[1] = -1;
}
~ToolInstance()
{
if (watch_id != 0)
Source.remove(watch_id);
if (is_started()) {
debug("%s (%i) process killed\n", name, (int)child_pid);
kill_child();
}
try {
if (