1
/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 2 -*- */
3
This file is part of Déjà Dup.
4
For copyright information, see AUTHORS.
6
Déjà Dup is free software: you can redistribute it and/or modify
7
it under the terms of the GNU General Public License as published by
8
the Free Software Foundation, either version 3 of the License, or
9
(at your option) any later version.
11
Déjà Dup is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
GNU General Public License for more details.
16
You should have received a copy of the GNU General Public License
17
along with Déjà Dup. If not, see <http://www.gnu.org/licenses/>.
24
internal class Duplicity : Object
27
* Vala implementation of various methods for accessing duplicity
29
* Vala implementation of various methods for accessing duplicity from
30
* vala withot the need of manually running duplicity command.
33
public signal void done(bool success, bool cancelled, string? detail);
34
public signal void raise_error(string errstr, string? detail);
35
public signal void action_desc_changed(string action);
36
public signal void action_file_changed(File file, bool actual);
37
public signal void progress(double percent);
39
* Signal emitted when collection dates are retrieved from duplicity
41
public signal void collection_dates(List<string>? dates);
42
public signal void listed_current_files(string date, string file);
43
public signal void question(string title, string msg);
44
public signal void is_full(bool first);
45
public signal void bad_encryption_password();
47
public Operation.Mode original_mode {get; construct;}
48
public Operation.Mode mode {get; private set; default = Operation.Mode.INVALID;}
49
public bool error_issued {get; private set; default = false;}
50
public bool was_stopped {get; private set; default = false;}
52
public File local {get; set;}
53
public Backend backend {get; set;}
54
public List<File> includes;
55
public List<File> excludes;
56
public bool use_progress {get; set; default = true;}
57
public string encrypt_password {private get; set; default = null;}
59
private List<File> _restore_files;
60
public List<File> restore_files {
62
return this._restore_files;
65
foreach (File f in this._restore_files)
67
this._restore_files = value.copy();
68
foreach (File f in this._restore_files)
73
protected enum State {
75
DRY_RUN, // used when backing up, and we need to first get time estimate
76
STATUS, // used when backing up, and we need to first get collection info
77
CHECK_CONTENTS, // used when restoring, and we need to list /home
81
protected State state {get; set;}
83
DuplicityInstance inst;
85
List<string> backend_argv;
86
List<string> saved_argv;
87
List<string> saved_envp;
88
bool is_full_backup = false;
89
bool cleaned_up_once = false;
90
bool needs_root = false;
91
bool detected_encryption = false;
92
bool existing_encrypted = false;
94
string last_bad_volume;
95
uint bad_volume_count;
97
bool has_progress_total = false;
98
uint64 progress_total; // zero, unless we already know limit
99
uint64 progress_count; // count of how far we are along in the current instance
102
static File slash_root;
103
static File slash_home;
104
static File slash_home_me;
105
static Regex gpg_regex;
107
bool has_checked_contents = false;
108
bool has_non_home_contents = false;
109
List<File> homes = new List<File>();
111
List<File> local_error_files = null;
113
bool checked_collection_info = false;
114
bool got_collection_info = false;
119
List<DateInfo?> collection_info = null;
121
bool checked_backup_space = false;
123
static const int MINIMUM_FULL = 2;
124
bool deleted_files = false;
127
File last_touched_file = null;
129
void network_changed()
131
if (Network.get().connected)
134
pause(_("Paused (no network)"));
137
public Duplicity(Operation.Mode mode) {
138
Object(original_mode: mode);
143
slash = File.new_for_path("/");
144
slash_root = File.new_for_path("/root");
145
slash_home = File.new_for_path("/home");
146
slash_home_me = File.new_for_path(Environment.get_home_dir());
149
if (gpg_regex == null) {
151
gpg_regex = new Regex(".*\\[.*\\.(g|gpg)'.*]$");
154
error("%s\n", e.message); // this is a programmer error, so use error()
160
Network.get().notify["connected"].disconnect(network_changed);
163
public virtual void start(Backend backend,
164
List<string>? argv, List<string>? envp)
166
// save arguments for calling duplicity again later
167
mode = original_mode;
168
this.backend = backend;
169
saved_argv = new List<string>();
170
saved_envp = new List<string>();
171
backend_argv = new List<string>();
172
foreach (string s in argv) saved_argv.append(s);
173
foreach (string s in envp) saved_envp.append(s);
174
backend.add_argv(Operation.Mode.INVALID, ref backend_argv);
176
if (mode == Operation.Mode.BACKUP)
177
process_include_excludes();
179
var settings = get_settings();
180
delete_age = settings.get_int(DELETE_AFTER_KEY);
183
done(false, false, null);
185
if (!backend.is_native()) {
186
Network.get().notify["connected"].connect(network_changed);
187
if (!Network.get().connected) {
188
debug("No connection found. Postponing the backup.");
189
pause(_("Paused (no network)"));
194
// This will treat a < b iff a is 'lower' in the file tree than b
195
int cmp_prefix(File? a, File? b)
197
if (a == null && b == null)
199
else if (b == null || a.has_prefix(b))
201
else if (a == null || b.has_prefix(a))
209
return backend.get_location(ref needs_root);
212
void expand_links_in_file(File file, ref List<File> all, bool include, List<File>? seen = null)
214
// For symlinks, we want to add the link and its target to the list.
215
// Normally, duplicity ignores targets, and this is fine and expected
216
// behavior. But if the user explicitly requested a directory with a
217
// symlink in it's path, they expect a follow-through.
218
// If a symlink is anywhere above the directory specified by the user,
219
// duplicity will stop at that symlink and only backup the broken link.
220
// So we try to work around that behavior by checking for symlinks and only
221
// passing duplicity symlinks as leaf elements.
223
// This will be much easier if we approach it from the root down. So
224
// walk back towards root, keeping track of each piece as we go.
225
List<string> pieces = new List<string>();
226
File iter = file, parent;
227
while ((parent = iter.get_parent()) != null) {
228
pieces.prepend(parent.get_relative_path(iter));
234
foreach (weak string piece in pieces) {
236
so_far = parent.resolve_relative_path(piece);
237
var info = so_far.query_info(FILE_ATTRIBUTE_STANDARD_IS_SYMLINK + "," +
238
FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET,
239
FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
241
if (info.get_is_symlink()) {
242
// Check if we've seen this before (i.e. are we in a loop?)
243
if (seen.find_custom(so_far, (a, b) => {
244
return (a != null && b != null && a.equal(b)) ? 0 : 1;}) != null)
248
all.prepend(so_far); // back up symlink as a leaf element of its path
250
// Recurse on the new file (since it could point at a completely
251
// new place, which has its own symlinks in its hierarchy, so we need
252
// to check the whole thing over again).
254
var symlink_target = info.get_symlink_target();
256
if (Path.is_absolute(symlink_target))
257
full_target = File.new_for_path(symlink_target);
259
full_target = parent.resolve_relative_path(symlink_target);
261
// Now add the rest of the undone pieces
262
var remaining = so_far.get_relative_path(file);
263
if (remaining != null)
264
full_target = full_target.resolve_relative_path(remaining);
267
all.remove(file); // may fail if it's not there, which is fine
269
seen.prepend(so_far);
271
expand_links_in_file(full_target, ref all, include, seen);
276
// Survived symlink gauntlet, add it to list if this is not the original
277
// request (i.e. if this is the final target of a symlink chain)
281
catch (IOError.NOT_FOUND e) {
282
// Don't bother keeping this file in the list
286
warning("%s\n", e.message);
290
void expand_links_in_list(ref List<File> all, bool include)
292
var all2 = all.copy();
293
foreach (File file in all2)
294
expand_links_in_file(file, ref all, include);
297
string escape_duplicity_path(string path)
299
// Duplicity paths are actually shell globs. So we want to escape anything
300
// that might fool duplicity into thinking this isn't the real path.
301
// Specifically, anything in '[?*'. Duplicity does not have escape
302
// characters, so we surround each with brackets.
304
rv = path.replace("[", "[[]");
305
rv = rv.replace("?", "[?]");
306
rv = rv.replace("*", "[*]");
310
void process_include_excludes()
312
expand_links_in_list(ref includes, true);
313
expand_links_in_list(ref excludes, false);
315
// We need to make sure that the most specific includes/excludes will
316
// be first in the list (duplicity uses only first matched dir). Includes
317
// will be preferred if the same dir is present in both lists.
318
includes.sort((CompareFunc)cmp_prefix);
319
excludes.sort((CompareFunc)cmp_prefix);
321
foreach (File i in includes) {
322
var excludes2 = excludes.copy();
323
foreach (File e in excludes2) {
324
if (e.has_prefix(i)) {
325
saved_argv.append("--exclude=" + escape_duplicity_path(e.get_path()));
329
saved_argv.append("--include=" + escape_duplicity_path(i.get_path()));
330
//if (!i.has_prefix(slash_home_me))
331
// needs_root = true;
333
foreach (File e in excludes) {
334
saved_argv.append("--exclude=" + escape_duplicity_path(e.get_path()));
337
saved_argv.append("--exclude=**");
340
public void cancel() {
341
var prev_mode = mode;
342
mode = Operation.Mode.INVALID;
344
if (prev_mode == Operation.Mode.BACKUP && state == State.NORMAL) {
353
// just abruptly stop, without a cleanup, duplicity will resume
355
mode = Operation.Mode.INVALID;
359
public void pause(string? reason)
364
set_status(reason, false);
379
handle_done(null, false, true);
384
state = State.NORMAL;
385
if (restore_files == null) // only clear if we're not in middle of restore sequence
386
local_error_files = null;
388
if (mode == Operation.Mode.INVALID)
391
var extra_argv = new List<string>();
392
string action_desc = null;
393
File custom_local = null;
395
switch (original_mode) {
396
case Operation.Mode.BACKUP:
397
// We need to first check the backup status to see if we need to start
398
// a full backup and to see if we should use encryption.
399
if (!checked_collection_info) {
400
mode = Operation.Mode.STATUS;
401
state = State.STATUS;
402
action_desc = _("Preparing…");
404
// If we're backing up, and the version of duplicity supports it, we should
405
// first run using --dry-run to get the total size of the backup, to make
406
// accurate progress bars.
407
else if (use_progress && !has_progress_total) {
408
state = State.DRY_RUN;
409
action_desc = _("Preparing…");
410
extra_argv.append("--dry-run");
412
else if (!checked_backup_space) {
413
check_backup_space();
417
if (has_progress_total)
422
case Operation.Mode.RESTORE:
423
// We need to first check the backup status to see if we should use
425
if (!checked_collection_info) {
426
mode = Operation.Mode.STATUS;
427
state = State.STATUS;
428
action_desc = _("Preparing…");
430
else if (!has_checked_contents) {
431
mode = Operation.Mode.LIST;
432
state = State.CHECK_CONTENTS;
433
action_desc = _("Preparing…");
436
// OK, do we have multiple, one, or no home dirs?
437
// Only want to bother doing anything if one. If one, we rename it's
438
// home dir to the current user's home dir (i.e. they backed up on one
439
// machine as 'alice' and restored on a machine as 'bob').
440
if (homes.length() > 1)
441
has_non_home_contents = true;
442
else if (homes.length() == 1) {
443
var old_home = homes.data;
444
var new_home = slash_home_me;
445
if (!old_home.equal(new_home)) {
446
extra_argv.append("--rename");
447
extra_argv.append(slash.get_relative_path(old_home));
448
extra_argv.append(slash.get_relative_path(new_home));
452
if (restore_files != null) {
453
// Just do first one. Others will come when we're done
455
// make path to specific restore file, since duplicity will just
456
// drop the file exactly where you ask it
457
var local_file = make_local_rel_path(restore_files.data);
458
if (local_file == null) {
459
// Was not even a file path (maybe something goofy like computer://)
460
show_error(_("Could not restore ‘%s’: Not a valid file location").printf(
461
(restore_files.data as File).get_parse_name()));
465
if (!local_file.has_prefix(slash_home_me))
469
// won't have correct permissions...
470
local_file.make_directory_with_parents(null);
472
catch (IOError.EXISTS e) {
476
show_error(e.message);
479
custom_local = local_file;
481
var rel_file_path = slash.get_relative_path(restore_files.data);
482
extra_argv.append("--file-to-restore=%s".printf(rel_file_path));
485
if (has_non_home_contents && !this.local.has_prefix(slash_home_me))
494
// Send appropriate description for what we're about to do. Is often
495
// very quickly overridden by a message like "Backing up file X"
496
if (action_desc == null)
497
action_desc = Operation.mode_to_string(mode);
498
set_status(action_desc);
500
connect_and_start(extra_argv, null, null, custom_local);
504
File? make_local_rel_path(File file)
506
string rel_file_path = slash.get_relative_path(file);
507
if (rel_file_path == null)
509
return local.resolve_relative_path(rel_file_path);
512
async void check_backup_space()
514
checked_backup_space = true;
516
if (!has_progress_total) {
518
done(false, false, null);
522
var free = yield backend.get_space();
523
var total = yield backend.get_space(false);
524
if (total < progress_total) {
525
// Tiny backup location. Suggest they get a larger one.
526
show_error(_("Backup location is too small. Try using one with more space."));
530
if (free < progress_total) {
531
if (got_collection_info) {
532
// Alright, let's look at collection data
534
foreach (DateInfo info in collection_info) {
538
if (full_dates > 1) {
539
delete_excess(full_dates - 1);
540
// don't set checked_backup_space, we want to be able to do this again if needed
541
checked_backup_space = false;
542
checked_collection_info = false; // get info again
543
got_collection_info = false;
548
show_error(_("Backup location does not have enough free space."));
554
done(false, false, null);
558
if (state == State.CLEANUP)
561
state = State.CLEANUP;
562
var cleanup_argv = new List<string>();
563
cleanup_argv.append("cleanup");
564
cleanup_argv.append("--force");
565
cleanup_argv.append(get_remote());
567
set_status(_("Cleaning up…"));
568
connect_and_start(null, null, cleanup_argv);
573
void delete_excess(int cutoff) {
574
state = State.DELETE;
575
var argv = new List<string>();
576
argv.append("remove-all-but-n-full");
577
argv.append("%d".printf(cutoff));
578
argv.append("--force");
579
argv.append(get_remote());
581
set_status(_("Cleaning up…"));
582
connect_and_start(null, null, argv);
587
bool can_ignore_error()
589
// Ignore errors during cleanup. If they're real, they'll repeat.
590
// They might be not-so-real, like the errors one gets when restoring
591
// from a backup when not all of the signature files are in your archive
592
// dir (which happens when you start using an archive dir in the middle
593
// of a backup chain).
594
return state == State.CLEANUP;
597
void handle_done(DuplicityInstance? inst, bool success, bool cancelled)
599
string detail = null;
601
if (can_ignore_error())
604
if (!cancelled && success) {
607
has_progress_total = true;
608
progress_total = progress_count; // save max progress for next run
614
if (restart()) // In case we were interrupting normal flow
619
cleaned_up_once = true;
620
if (restart()) // restart in case cleanup was interrupting normal flow
623
// Else, we probably started cleaning up after a cancel. Just continue
630
checked_collection_info = true;
631
var should_restart = mode != original_mode;
632
mode = original_mode;
634
/* Set full backup threshold and determine whether we should trigger
636
if (mode == Operation.Mode.BACKUP && got_collection_info) {
637
Date threshold = DejaDup.get_full_backup_threshold_date();
638
Date full_backup = Date();
639
foreach (DateInfo info in collection_info) {
641
full_backup.set_time_val(info.time);
643
if (!full_backup.valid() || threshold.compare(full_backup) > 0) {
644
is_full_backup = true;
645
is_full(!full_backup.valid());
649
if (should_restart) {
655
case State.CHECK_CONTENTS:
656
has_checked_contents = true;
657
mode = original_mode;
664
if (mode == Operation.Mode.RESTORE && restore_files != null) {
665
_restore_files.delete_link(_restore_files);
666
if (restore_files != null) {
672
if (mode == Operation.Mode.BACKUP) {
673
if (local_error_files != null) {
674
// OK, we succeeded yay! But some files didn't make it into the backup
675
// because we couldn't read them. So tell the user so they don't think
676
// everything is hunky dory.
677
detail = _("Could not back up the following files. Please make sure you are able to open them.");
679
foreach (File f in local_error_files) {
680
detail += "\n%s".printf(f.get_parse_name());
684
mode = Operation.Mode.INVALID; // mark 'done' so when we delete, we don't restart
685
if (delete_files_if_needed())
688
else if (mode == Operation.Mode.RESTORE) {
689
if (local_error_files != null) {
690
// OK, we succeeded yay! But some files didn't actually restore
691
// because we couldn't write to them. So tell the user so they
692
// don't think everything is hunky dory.
693
detail = _("Could not restore the following files. Please make sure you are able to write to them.");
695
foreach (File f in local_error_files) {
696
detail += "\n%s".printf(f.get_parse_name());
703
else if (was_stopped)
704
success = true; // we treat stops as success
709
if (!success && !cancelled && !error_issued)
710
show_error(_("Failed with an unknown error."));
713
done(success, cancelled, detail);
717
File saved_status_file;
718
bool saved_status_file_action;
719
void set_status(string msg, bool save = true)
723
saved_status_file = null;
725
action_desc_changed(msg);
728
void set_status_file(File file, bool action, bool save = true)
732
saved_status_file = file;
733
saved_status_file_action = action;
735
action_file_changed(file, action);
738
void set_saved_status()
740
if (saved_status != null)
741
set_status(saved_status, false);
743
set_status_file(saved_status_file, saved_status_file_action, false);
746
// Should only be called *after* a successful backup
747
bool delete_files_if_needed()
749
if (delete_age == 0) {
750
deleted_files = true;
754
// Check if we need to delete any backups
755
// If we got collection info, examine it to see if we should delete old
757
if (got_collection_info && !deleted_files) {
758
// Alright, let's look at collection data
760
TimeVal prev_time = TimeVal();
761
Date prev_date = Date();
763
TimeVal now = TimeVal();
764
now.get_current_time();
767
today.set_time_val(now);
769
foreach (DateInfo info in collection_info) {
771
if (full_dates > 0) { // Wait until we have a prev_time
772
prev_date.set_time_val(prev_time); // compare last incremental backup
773
if (prev_date.days_between(today) > delete_age)
778
prev_time = info.time;
780
prev_date.set_time_val(prev_time); // compare last incremental backup
781
if (prev_date.days_between(today) > delete_age)
784
// Did we just finished a successful full backup?
785
// Collection info won't have our recent backup, because it is done at
786
// beginning of backup.
790
if (too_old > 0 && full_dates > MINIMUM_FULL) {
791
// Alright, let's delete those ancient files!
792
int cutoff = int.max(MINIMUM_FULL, full_dates - too_old);
793
delete_excess(cutoff);
797
// If we don't need to delete, pretend we did and move on.
798
deleted_files = true;
805
protected static const int ERROR_GENERIC = 1;
806
protected static const int ERROR_HOSTNAME_CHANGED = 3;
807
protected static const int ERROR_RESTORE_DIR_NOT_FOUND = 19;
808
protected static const int ERROR_EXCEPTION = 30;
809
protected static const int ERROR_GPG = 31;
810
protected static const int ERROR_BAD_VOLUME = 44;
811
protected static const int ERROR_BACKEND = 50;
812
protected static const int ERROR_BACKEND_PERMISSION_DENIED = 51;
813
protected static const int ERROR_BACKEND_NOT_FOUND = 52;
814
protected static const int ERROR_BACKEND_NO_SPACE = 53;
815
protected static const int INFO_PROGRESS = 2;
816
protected static const int INFO_COLLECTION_STATUS = 3;
817
protected static const int INFO_DIFF_FILE_NEW = 4;
818
protected static const int INFO_DIFF_FILE_CHANGED = 5;
819
protected static const int INFO_DIFF_FILE_DELETED = 6;
820
protected static const int INFO_PATCH_FILE_WRITING = 7;
821
protected static const int INFO_PATCH_FILE_PATCHING = 8;
822
protected static const int INFO_FILE_STAT = 10;
823
protected static const int INFO_SYNCHRONOUS_UPLOAD_BEGIN = 11;
824
protected static const int INFO_ASYNCHRONOUS_UPLOAD_BEGIN = 12;
825
protected static const int INFO_SYNCHRONOUS_UPLOAD_DONE = 13;
826
protected static const int INFO_ASYNCHRONOUS_UPLOAD_DONE = 14;
827
protected static const int WARNING_ORPHANED_SIG = 2;
828
protected static const int WARNING_UNNECESSARY_SIG = 3;
829
protected static const int WARNING_UNMATCHED_SIG = 4;
830
protected static const int WARNING_INCOMPLETE_BACKUP = 5;
831
protected static const int WARNING_ORPHANED_BACKUP = 6;
832
protected static const int WARNING_CANNOT_READ = 10;
833
protected static const int WARNING_CANNOT_PROCESS = 12; // basically, cannot write or change attrs
834
protected static const int DEBUG_GENERIC = 1;
838
string dir = Environment.get_user_cache_dir();
842
var cachedir = Path.build_filename(dir, Config.PACKAGE);
843
var del = new RecursiveDelete(File.new_for_path(cachedir));
847
bool restarted_without_cache = false;
848
bool restart_without_cache()
850
if (restarted_without_cache)
853
restarted_without_cache = true;
859
void handle_exit(int code)
861
// Duplicity has a habit of dying and returning 1 without sending an error
862
// if there was some unexpected issue with its cached metadata. It often
863
// goes away if you delete ~/.cache/deja-dup and try again. This issue
864
// happens often enough that we do that for the user here. It should be
865
// safe to do this, as the cache is not necessary for operation, only
866
// a performance improvement.
867
if (code == ERROR_GENERIC && !error_issued) {
868
restart_without_cache();
872
void handle_message(DuplicityInstance inst, string[] control_line,
873
List<string>? data_lines, string user_text)
876
* Based on duplicity's output handle message as either process data as error, info or warning
878
if (control_line.length == 0)
881
var keyword = control_line[0];
884
process_error(control_line, data_lines, user_text);
887
process_info(control_line, data_lines, user_text);
890
process_warning(control_line, data_lines, user_text);
893
process_debug(control_line, data_lines, user_text);
898
bool ask_question(string t, string m)
902
var rv = mode != Operation.Mode.INVALID; // return whether we were canceled
904
handle_done(null, false, true);
908
// Hacky function to return later parts of a duplicity filename.
909
// Used to chop off the date bit
910
string parse_duplicity_file(string file, int skip_bits)
913
while (skip_bits-- > 0 && next >= 0)
914
next = file.index_of_char('.', next) + 1;
918
return file.substring(next);
921
protected virtual void process_error(string[] firstline, List<string>? data,
924
string text = text_in;
926
if (can_ignore_error())
929
if (firstline.length > 1) {
930
switch (int.parse(firstline[1])) {
931
case ERROR_EXCEPTION: // exception
932
process_exception(firstline.length > 2 ? firstline[2] : "", text);
935
case ERROR_RESTORE_DIR_NOT_FOUND:
936
// make text a little nicer than duplicity gives
937
// duplicity gives something like "home/blah/blah not found in archive,
938
// no files restored".
939
if (restore_files != null)
940
text = _("Could not restore ‘%s’: File not found in backup").printf(
941
restore_files.data.get_parse_name());
945
bad_encryption_password(); // notify upper layers, if they want to do anything
946
text = _("Bad encryption password.");
949
case ERROR_HOSTNAME_CHANGED:
950
if (firstline.length >= 4) {
951
if (!ask_question(_("Computer name changed"), _("The existing backup is of a computer named %s, but the current computer’s name is %s. If this is unexpected, you should back up to a different location.").printf(firstline[3], firstline[2])))
954
// Else just assume that user wants to allow the mismatch...
955
// A little troubling but better than not letting user proceed
956
saved_argv.append("--allow-source-mismatch");
961
case ERROR_BAD_VOLUME:
962
// A volume was detected to be corrupt/incomplete after uploading.
963
// We'll first try a restart because then duplicity will retry it.
964
// If it's still bad, we'll do a full cleanup and try again.
965
// If it's *still* bad, tell the user, but I'm not sure what they can
967
if (mode == Operation.Mode.BACKUP) {
968
// strip date info from volume (after cleanup below, we'll get new date)
969
var this_volume = parse_duplicity_file(firstline[2], 2);
970
if (last_bad_volume != this_volume) {
971
bad_volume_count = 0;
972
last_bad_volume = this_volume;
975
if ((bad_volume_count == 0 && restart()) ||
976
(bad_volume_count == 1 && cleanup())) {
977
bad_volume_count += 1;
983
case ERROR_BACKEND_PERMISSION_DENIED:
984
if (firstline.length >= 5 && firstline[2] == "put") {
985
var file = make_file_obj(firstline[4]);
986
text = _("Permission denied when trying to create ‘%s’.").printf(file.get_parse_name());
988
if (firstline.length >= 5 && firstline[2] == "get") {
989
var file = make_file_obj(firstline[3]); // assume error is on backend side
990
text = _("Permission denied when trying to read ‘%s’.").printf(file.get_parse_name());
992
else if (firstline.length >= 4 && firstline[2] == "list") {
993
var file = make_file_obj(firstline[3]);
994
text = _("Permission denied when trying to read ‘%s’.").printf(file.get_parse_name());
996
else if (firstline.length >= 4 && firstline[2] == "delete") {
997
var file = make_file_obj(firstline[3]);
998
text = _("Permission denied when trying to delete ‘%s’.").printf(file.get_parse_name());
1002
case ERROR_BACKEND_NOT_FOUND:
1003
if (firstline.length >= 4) {
1004
var file = make_file_obj(firstline[3]);
1005
text = _("Backup location ‘%s’ does not exist.").printf(file.get_parse_name());
1009
case ERROR_BACKEND_NO_SPACE:
1010
if (firstline.length >= 5) {
1011
text = _("No space left.");
1020
void process_exception(string exception, string text)
1022
switch (exception) {
1023
case "S3ResponseError":
1024
if (text.contains("<Code>InvalidAccessKeyId</Code>"))
1025
show_error(_("Invalid ID."));
1026
else if (text.contains("<Code>SignatureDoesNotMatch</Code>"))
1027
show_error(_("Invalid secret key."));
1028
else if (text.contains("<Code>NotSignedUp</Code>"))
1029
show_error(_("Your Amazon Web Services account is not signed up for the S3 service."));
1031
case "S3CreateError":
1032
if (text.contains("<Code>BucketAlreadyExists</Code>")) {
1033
if (((BackendS3)backend).bump_bucket()) {
1034
if (restart()) // get_remote() will eventually grab new bucket name
1038
show_error(_("S3 bucket name is not available."));
1042
// Duplicity tried to ask the user what the encryption password is.
1043
bad_encryption_password(); // notify upper layers, if they want to do anything
1044
show_error(_("Bad encryption password."));
1047
if (text.contains("GnuPG"))
1048
show_error(_("Bad encryption password."));
1049
else if (text.contains("[Errno 5]") && // I/O Error
1050
last_touched_file != null) {
1051
if (mode == Operation.Mode.BACKUP)
1052
show_error(_("Error reading file ‘%s’.").printf(last_touched_file.get_parse_name()));
1054
show_error(_("Error writing file ‘%s’.").printf(last_touched_file.get_parse_name()));
1056
else if (text.contains("[Errno 28]")) { // No space left on device
1057
string where = null;
1058
if (mode == Operation.Mode.BACKUP)
1059
where = backend.get_location_pretty();
1061
where = local.get_path();
1063
show_error(_("No space left."));
1065
show_error(_("No space left in ‘%s’.").printf(where));
1067
else if (text.contains("CRC check failed")) { // bug 676767
1068
if (restart_without_cache())
1072
case "CollectionsError":
1073
show_error(_("No backup files found"));
1075
case "AssertionError":
1076
// This is an internal error. Similar to when duplicity just returns
1077
// 1 with no message. Some of these, like "time not moving forward" or
1078
// bug 877631, can be recovered from by clearing the cache. Worth a
1080
if (restart_without_cache())
1085
// For most, don't do anything special. Show generic 'unknown error'
1086
// message, but provide the exception text for better bug reports.
1087
// Plus, sometimes it may clue the user in to what's wrong.
1088
// But first, try to restart without a cache, since that seems to quite
1089
// frequently fix odd metadata errors with duplicity. If we hit an error
1090
// a second time, we'll show the unknown error message.
1091
if (!error_issued && !restart_without_cache())
1092
show_error(_("Failed with an unknown error."), text);
1095
protected virtual void process_info(string[] firstline, List<string>? data,
1099
* Pass message to appropriate function considering the type of output
1101
if (firstline.length > 1) {
1102
switch (int.parse(firstline[1])) {
1103
case INFO_DIFF_FILE_NEW:
1104
case INFO_DIFF_FILE_CHANGED:
1105
case INFO_DIFF_FILE_DELETED:
1106
if (firstline.length > 2)
1107
process_diff_file(firstline[2]);
1109
case INFO_PATCH_FILE_WRITING:
1110
case INFO_PATCH_FILE_PATCHING:
1111
if (firstline.length > 2)
1112
process_patch_file(firstline[2]);
1115
process_progress(firstline);
1117
case INFO_COLLECTION_STATUS:
1118
process_collection_status(data);
1120
case INFO_SYNCHRONOUS_UPLOAD_BEGIN:
1121
case INFO_ASYNCHRONOUS_UPLOAD_BEGIN:
1122
if (!backend.is_native())
1123
set_status(_("Uploading…"));
1125
case INFO_FILE_STAT:
1126
process_file_stat(firstline[2], firstline[3], data, text);
1132
protected virtual void process_debug(string[] firstline, List<string>? data,
1136
* Pass message to appropriate function considering the type of output
1138
if (firstline.length > 1) {
1139
switch (int.parse(firstline[1])) {
1141
if (mode == Operation.Mode.STATUS &&
1142
!DuplicityInfo.get_default().reports_encryption &&
1143
!detected_encryption) {
1144
if (gpg_regex != null && gpg_regex.match(text)) {
1145
detected_encryption = true;
1146
existing_encrypted = true;
1154
void process_file_stat(string date, string file, List<string> data, string text)
1156
if (mode != Operation.Mode.LIST)
1158
if (state == State.CHECK_CONTENTS) {
1159
var gfile = make_file_obj(file);
1160
if (gfile.equal(slash_root) ||
1161
(gfile.get_parent() != null && gfile.get_parent().equal(slash_home)))
1162
homes.append(gfile);
1163
if (!has_non_home_contents &&
1164
!gfile.equal(slash) &&
1165
!gfile.equal(slash_home) &&
1166
!gfile.has_prefix(slash_home))
1167
has_non_home_contents = true;
1169
listed_current_files(date, file);
1172
void process_diff_file(string file) {
1173
var gfile = make_file_obj(file);
1174
last_touched_file = gfile;
1175
if (gfile.query_file_type(FileQueryInfoFlags.NONE, null) != FileType.DIRECTORY)
1176
set_status_file(gfile, state != State.DRY_RUN);
1179
void process_patch_file(string file) {
1180
var gfile = make_file_obj(file);
1181
last_touched_file = gfile;
1182
if (gfile.query_file_type(FileQueryInfoFlags.NONE, null) != FileType.DIRECTORY)
1183
set_status_file(gfile, state != State.DRY_RUN);
1186
void process_progress(string[] firstline)
1190
if (firstline.length > 2)
1191
this.progress_count = uint64.parse(firstline[2]);
1195
if (firstline.length > 3)
1196
total = double.parse(firstline[3]);
1197
else if (this.progress_total > 0)
1198
total = this.progress_total;
1200
return; // can't do progress without a total
1202
double percent = (double)this.progress_count / total;
1205
if (percent < 0) // ???
1210
File make_file_obj(string file)
1212
// All files are relative to root.
1213
return slash.resolve_relative_path(file);
1216
void process_collection_status(List<string>? lines)
1219
* Collect output of collection status and return list of dates as strings via a signal
1221
* Duplicity returns collection status as a bunch of lines, some of which are
1222
* indented which contain information about specific chains. We gather
1223
* this all up and report back to caller via a signal.
1224
* We're really only interested in the list of entries in the complete chain.
1226
if (mode != Operation.Mode.STATUS || got_collection_info)
1229
var timeval = TimeVal();
1230
var dates = new List<string>();
1231
var infos = new List<DateInfo?>();
1232
bool in_chain = false;
1233
foreach (string line in lines) {
1234
if (line == "chain-complete" || line.index_of("chain-no-sig") == 0)
1236
else if (in_chain && line.length > 0 && line[0] == ' ') {
1237
// OK, appears to be a date line. Try to parse. Should look like:
1238
// ' inc TIMESTR NUMVOLS [ENCRYPTED]'.
1239
// Since there's a space at the beginning, when we tokenize it, we
1240
// should expect an extra token at the front.
1241
string[] tokens = line.split(" ");
1242
if (tokens.length > 2 && timeval.from_iso8601(tokens[2])) {
1243
dates.append(tokens[2]);
1245
var info = DateInfo();
1246
info.time = timeval;
1247
info.full = tokens[1] == "full";
1250
if (DuplicityInfo.get_default().reports_encryption &&
1251
!detected_encryption &&
1252
tokens.length > 4) {
1253
// Just use the encryption status of the first one we see;
1254
// mixed-encryption backups is not supported.
1255
detected_encryption = true;
1256
existing_encrypted = tokens[4] == "enc";
1264
got_collection_info = true;
1265
collection_info = new List<DateInfo?>();
1266
foreach (DateInfo s in infos)
1267
collection_info.append(s); // we want to keep our own copy too
1269
collection_dates(dates);
1272
protected virtual void process_warning(string[] firstline, List<string>? data,
1275
if (firstline.length > 1) {
1276
switch (int.parse(firstline[1])) {
1277
case WARNING_ORPHANED_SIG:
1278
case WARNING_UNNECESSARY_SIG:
1279
case WARNING_UNMATCHED_SIG:
1280
case WARNING_INCOMPLETE_BACKUP:
1281
case WARNING_ORPHANED_BACKUP:
1282
// Random files left on backend from previous run. Should clean them
1283
// up before we continue. We don't want to wait until we finish to
1284
// clean them up, since we may want that space, and if there's a bug
1285
// in ourselves, we may never get to it.
1286
if (mode == Operation.Mode.BACKUP && !this.cleaned_up_once)
1287
cleanup(); // stops current backup, cleans up, then resumes
1290
case WARNING_CANNOT_READ:
1291
// A file couldn't be backed up! We should note the name and present
1292
// the user with a list at the end.
1293
if (firstline.length > 2) {
1294
// Only add it if it's a child of one of our includes. Sometimes
1295
// Duplicity likes to talk to us about folders like /lost+found and
1296
// such that we don't care about.
1297
var error_file = make_file_obj(firstline[2]);
1298
foreach (File f in includes) {
1299
if (error_file.equal(f) || error_file.has_prefix(f))
1300
local_error_files.append(error_file);
1305
case WARNING_CANNOT_PROCESS:
1306
// A file couldn't be restored! We should note the name and present
1307
// the user with a list at the end.
1308
if (firstline.length > 2) {
1309
// Only add it if it's a child of one of our includes. Sometimes
1310
// Duplicity likes to talk to us about folders like /lost+found and
1311
// such that we don't care about.
1312
var error_file = make_file_obj(firstline[2]);
1313
if (!error_file.equal(slash) && // for some reason, duplicity likes to talk about '/'
1314
// Duplicity also likes to whine about files a lot, with errno 1, for no reason.
1315
// We only care about errno 13, which is "couldn't write at all"
1316
text.contains("[Errno 13]"))
1317
local_error_files.append(error_file);
1324
void show_error(string errorstr, string? detail = null)
1326
if (error_issued == false) {
1327
error_issued = true;
1328
raise_error(errorstr, detail);
1332
// Returns volume size in megs
1335
// Advantages of a smaller value:
1336
// * takes less temp space
1337
// * retries of a volume take less time
1338
// * quicker restore of a particular file (less excess baggage to download)
1339
// * we get feedback more frequently (duplicity only gives us a progress
1340
// report at the end of a volume) -- fixed by reporting when we're uploading
1342
// * less throughput:
1343
// * some protocols have large per-file overhead (like sftp)
1344
// * the network doesn't have time to ramp up to max tcp transfer speed per
1346
// * lots of files looks ugly to users
1348
// duplicity's default is 25 (used to be 5).
1350
// For local filesystems, we'll choose large volsize.
1351
// For remote FSs, we'll go smaller.
1352
if (in_testing_mode())
1354
else if (backend.is_native())
1360
void disconnect_inst()
1362
/* Disconnect signals and cancel call to duplicity instance */
1364
inst.done.disconnect(handle_done);
1365
inst.message.disconnect(handle_message);
1366
inst.exited.disconnect(handle_exit);
1372
void connect_and_start(List<string>? argv_extra = null,
1373
List<string>? envp_extra = null,
1374
List<string>? argv_entire = null,
1375
File? custom_local = null)
1378
* For passed arguments start a new duplicity instance, set duplicity in the right mode and execute command
1380
/* Disconnect instance */
1383
/* Start new duplicity instance */
1384
inst = new DuplicityInstance();
1385
inst.done.connect(handle_done);
1387
/* As duplicity's data is returned via a signal, handle_message begins post-raw stream processing */
1388
inst.message.connect(handle_message);
1390
/* When duplicity exits, we may be also interested in its return code */
1391
inst.exited.connect(handle_exit);
1393
/* Set arguments for call to duplicity */
1394
weak List<string> master_argv = argv_entire == null ? saved_argv : argv_entire;
1395
weak File local_arg = custom_local == null ? local : custom_local;
1397
var argv = new List<string>();
1398
foreach (string s in master_argv) argv.append(s);
1399
foreach (string s in argv_extra) argv.append(s);
1400
foreach (string s in this.backend_argv) argv.append(s);
1402
/* Set duplicity into right mode */
1403
if (argv_entire == null) {
1404
// add operation, local, and remote args
1406
case Operation.Mode.BACKUP:
1408
argv.prepend("full");
1409
argv.append("--volsize=%d".printf(get_volsize()));
1410
argv.append(local_arg.get_path());
1411
argv.append(get_remote());
1413
case Operation.Mode.RESTORE:
1414
argv.prepend("restore");
1415
argv.append("--force");
1416
argv.append(get_remote());
1417
argv.append(local_arg.get_path());
1419
case Operation.Mode.STATUS:
1420
argv.prepend("collection-status");
1421
argv.append(get_remote());
1423
case Operation.Mode.LIST:
1424
argv.prepend("list-current-files");
1425
argv.append(get_remote());
1430
/* Set environmental parameters */
1431
var envp = new List<string>();
1432
foreach (string s in saved_envp) envp.append(s);
1433
foreach (string s in envp_extra) envp.append(s);
1435
bool use_encryption = false;
1436
if (detected_encryption)
1437
use_encryption = existing_encrypted;
1438
else if (encrypt_password != null)
1439
use_encryption = encrypt_password != "";
1441
if (use_encryption) {
1442
if (encrypt_password != null && encrypt_password != "")
1443
envp.append("PASSPHRASE=%s".printf(encrypt_password));
1444
// else duplicity will try to prompt user and we'll get an exception,
1445
// which is our cue to ask user for password. We could pass an empty
1446
// passphrase (as we do below), but by not setting it at all, duplicity
1447
// will error out quicker, and notably before it tries to sync metadata.
1450
argv.append("--no-encryption");
1451
envp.append("PASSPHRASE="); // duplicity sometimes asks for a passphrase when it doesn't need it (during cleanup), so this stops it from prompting the user and us getting an exception as a result
1454
/* Start duplicity instance */
1456
inst.start(argv, envp, needs_root);
1459
show_error(e.message);
1460
done(false, false, null);