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
public 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);
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;
86
List<string> backend_argv;
87
List<string> saved_argv;
88
List<string> saved_envp;
89
bool is_full_backup = false;
90
bool cleaned_up_once = false;
91
bool needs_root = false;
92
bool detected_encryption = false;
93
bool existing_encrypted = false;
95
string last_bad_volume;
96
uint bad_volume_count;
98
bool has_progress_total = false;
99
uint64 progress_total; // zero, unless we already know limit
100
uint64 progress_count; // count of how far we are along in the current instance
103
static File slash_root;
104
static File slash_home;
105
static File slash_home_me;
107
bool has_checked_contents = false;
108
bool has_non_home_contents = false;
109
List<File> homes = new List<File>();
111
bool checked_collection_info = false;
112
bool got_collection_info = false;
117
List<DateInfo?> collection_info = null;
119
bool checked_backup_space = false;
121
static const int MINIMUM_FULL = 2;
122
bool deleted_files = false;
125
File last_touched_file = null;
127
void network_changed()
129
if (Network.get().connected)
132
pause(_("Paused (no network)"));
135
public Duplicity(Operation.Mode mode) {
136
Object(original_mode: mode);
141
slash = File.new_for_path("/");
142
slash_root = File.new_for_path("/root");
143
slash_home = File.new_for_path("/home");
144
slash_home_me = File.new_for_path(Environment.get_home_dir());
149
Network.get().notify["connected"].disconnect(network_changed);
152
public virtual void start(Backend backend,
153
List<string>? argv, List<string>? envp)
155
// save arguments for calling duplicity again later
156
mode = original_mode;
158
this.remote = backend.get_location();
161
raise_error(e.message, null);
165
this.backend = backend;
166
saved_argv = new List<string>();
167
saved_envp = new List<string>();
168
backend_argv = new List<string>();
169
foreach (string s in argv) saved_argv.append(s);
170
foreach (string s in envp) saved_envp.append(s);
171
backend.add_argv(Operation.Mode.INVALID, ref backend_argv);
173
if (mode == Operation.Mode.BACKUP)
174
process_include_excludes();
176
var settings = get_settings();
177
delete_age = settings.get_int(DELETE_AFTER_KEY);
182
if (!backend.is_native()) {
183
Network.get().notify["connected"].connect(network_changed);
184
if (!Network.get().connected) {
185
debug("No connection found. Postponing the backup.");
186
pause(_("Paused (no network)"));
191
// This will treat a < b iff a is 'lower' in the file tree than b
192
int cmp_prefix(File? a, File? b)
194
if (a == null && b == null)
196
else if (b == null || a.has_prefix(b))
198
else if (a == null || b.has_prefix(a))
204
void expand_links_in_file(File file, ref List<File> all, bool include, List<File>? seen = null)
206
// For symlinks, we want to add the link and its target to the list.
207
// Normally, duplicity ignores targets, and this is fine and expected
208
// behavior. But if the user explicitly requested a directory with a
209
// symlink in it's path, they expect a follow-through.
210
// If a symlink is anywhere above the directory specified by the user,
211
// duplicity will stop at that symlink and only backup the broken link.
212
// So we try to work around that behavior by checking for symlinks and only
213
// passing duplicity symlinks as leaf elements.
215
// This will be much easier if we approach it from the root down. So
216
// walk back towards root, keeping track of each piece as we go.
217
List<string> pieces = new List<string>();
218
File iter = file, parent;
219
while ((parent = iter.get_parent()) != null) {
220
pieces.prepend(parent.get_relative_path(iter));
226
foreach (weak string piece in pieces) {
228
so_far = parent.resolve_relative_path(piece);
229
var info = so_far.query_info(FILE_ATTRIBUTE_STANDARD_IS_SYMLINK + "," +
230
FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET,
231
FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
233
if (info.get_is_symlink()) {
234
// Check if we've seen this before (i.e. are we in a loop?)
235
if (seen.find_custom(so_far, (a, b) => {
236
return (a != null && b != null && a.equal(b)) ? 0 : 1;}) != null)
240
all.prepend(so_far); // back up symlink as a leaf element of its path
242
// Recurse on the new file (since it could point at a completely
243
// new place, which has its own symlinks in its hierarchy, so we need
244
// to check the whole thing over again).
246
var symlink_target = info.get_symlink_target();
248
if (Path.is_absolute(symlink_target))
249
full_target = File.new_for_path(symlink_target);
251
full_target = parent.resolve_relative_path(symlink_target);
253
// Now add the rest of the undone pieces
254
var remaining = so_far.get_relative_path(file);
255
if (remaining != null)
256
full_target = full_target.resolve_relative_path(remaining);
259
all.remove(file); // may fail if it's not there, which is fine
261
seen.prepend(so_far);
263
expand_links_in_file(full_target, ref all, include, seen);
268
// Survived symlink gauntlet, add it to list if this is not the original
269
// request (i.e. if this is the final target of a symlink chain)
273
catch (IOError.NOT_FOUND e) {
274
// Don't bother keeping this file in the list
278
warning("%s\n", e.message);
282
void expand_links_in_list(ref List<File> all, bool include)
284
var all2 = all.copy();
285
foreach (File file in all2)
286
expand_links_in_file(file, ref all, include);
289
string escape_duplicity_path(string path)
291
// Duplicity paths are actually shell globs. So we want to escape anything
292
// that might fool duplicity into thinking this isn't the real path.
293
// Specifically, anything in '[?*'. Duplicity does not have escape
294
// characters, so we surround each with brackets.
296
rv = path.replace("[", "[[]");
297
rv = rv.replace("?", "[?]");
298
rv = rv.replace("*", "[*]");
302
void process_include_excludes()
304
expand_links_in_list(ref includes, true);
305
expand_links_in_list(ref excludes, false);
307
// We need to make sure that the most specific includes/excludes will
308
// be first in the list (duplicity uses only first matched dir). Includes
309
// will be preferred if the same dir is present in both lists.
310
includes.sort((CompareFunc)cmp_prefix);
311
excludes.sort((CompareFunc)cmp_prefix);
313
foreach (File i in includes) {
314
var excludes2 = excludes.copy();
315
foreach (File e in excludes2) {
316
if (e.has_prefix(i)) {
317
saved_argv.append("--exclude=" + escape_duplicity_path(e.get_path()));
321
saved_argv.append("--include=" + escape_duplicity_path(i.get_path()));
322
//if (!i.has_prefix(slash_home_me))
323
// needs_root = true;
325
foreach (File e in excludes) {
326
saved_argv.append("--exclude=" + escape_duplicity_path(e.get_path()));
329
saved_argv.append("--exclude=**");
332
public void cancel() {
333
var prev_mode = mode;
334
mode = Operation.Mode.INVALID;
336
if (prev_mode == Operation.Mode.BACKUP && state == State.NORMAL) {
345
// just abruptly stop, without a cleanup, duplicity will resume
347
mode = Operation.Mode.INVALID;
351
public void pause(string? reason)
356
set_status(reason, false);
371
handle_done(null, false, true);
376
state = State.NORMAL;
378
if (mode == Operation.Mode.INVALID)
381
var extra_argv = new List<string>();
382
string action_desc = null;
383
File custom_local = null;
385
switch (original_mode) {
386
case Operation.Mode.BACKUP:
387
// We need to first check the backup status to see if we need to start
388
// a full backup and to see if we should use encryption.
389
if (!checked_collection_info) {
390
mode = Operation.Mode.STATUS;
391
state = State.STATUS;
392
action_desc = _("Preparing…");
394
// If we're backing up, and the version of duplicity supports it, we should
395
// first run using --dry-run to get the total size of the backup, to make
396
// accurate progress bars.
397
else if (use_progress && !has_progress_total) {
398
state = State.DRY_RUN;
399
action_desc = _("Preparing…");
400
extra_argv.append("--dry-run");
402
else if (!checked_backup_space) {
403
check_backup_space();
407
if (has_progress_total)
412
case Operation.Mode.RESTORE:
413
// We need to first check the backup status to see if we should use
415
if (!checked_collection_info) {
416
mode = Operation.Mode.STATUS;
417
state = State.STATUS;
418
action_desc = _("Preparing…");
420
else if (!has_checked_contents) {
421
mode = Operation.Mode.LIST;
422
state = State.CHECK_CONTENTS;
423
action_desc = _("Preparing…");
426
// OK, do we have multiple, one, or no home dirs?
427
// Only want to bother doing anything if one. If one, we rename it's
428
// home dir to the current user's home dir (i.e. they backed up on one
429
// machine as 'alice' and restored on a machine as 'bob').
430
if (homes.length() > 1)
431
has_non_home_contents = true;
432
else if (homes.length() == 1) {
433
var old_home = homes.data;
434
var new_home = slash_home_me;
435
if (!old_home.equal(new_home)) {
436
extra_argv.append("--rename");
437
extra_argv.append(slash.get_relative_path(old_home));
438
extra_argv.append(slash.get_relative_path(new_home));
442
if (restore_files != null) {
443
// Just do first one. Others will come when we're done
445
// make path to specific restore file, since duplicity will just
446
// drop the file exactly where you ask it
447
var local_file = make_local_rel_path(restore_files.data);
448
if (local_file == null) {
449
// Was not even a file path (maybe something goofy like computer://)
450
show_error(_("Could not restore ‘%s’: Not a valid file location").printf(
451
(restore_files.data as File).get_parse_name()));
455
if (!local_file.has_prefix(slash_home_me))
459
// won't have correct permissions...
460
local_file.make_directory_with_parents(null);
462
catch (IOError.EXISTS e) {
466
show_error(e.message);
469
custom_local = local_file;
471
var rel_file_path = slash.get_relative_path(restore_files.data);
472
extra_argv.append("--file-to-restore=%s".printf(rel_file_path));
475
if (has_non_home_contents && !this.local.has_prefix(slash_home_me))
484
// Send appropriate description for what we're about to do. Is often
485
// very quickly overridden by a message like "Backing up file X"
486
if (action_desc == null)
487
action_desc = Operation.mode_to_string(mode);
488
set_status(action_desc);
490
connect_and_start(extra_argv, null, null, custom_local);
494
File? make_local_rel_path(File file)
496
string rel_file_path = slash.get_relative_path(file);
497
if (rel_file_path == null)
499
return local.resolve_relative_path(rel_file_path);
502
async void check_backup_space()
504
checked_backup_space = true;
506
if (!has_progress_total) {
512
var free = yield backend.get_space();
513
var total = yield backend.get_space(false);
514
if (total < progress_total) {
515
// Tiny backup location. Suggest they get a larger one.
516
show_error(_("Backup location is too small. Try using one with more space."));
520
if (free < progress_total) {
521
if (got_collection_info) {
522
// Alright, let's look at collection data
524
foreach (DateInfo info in collection_info) {
528
if (full_dates > 1) {
529
delete_excess(full_dates - 1);
530
// don't set checked_backup_space, we want to be able to do this again if needed
531
checked_backup_space = false;
532
checked_collection_info = false; // get info again
533
got_collection_info = false;
538
show_error(_("Backup location does not have enough free space."));
548
if (state == State.CLEANUP)
551
state = State.CLEANUP;
552
var cleanup_argv = new List<string>();
553
cleanup_argv.append("cleanup");
554
cleanup_argv.append("--force");
555
cleanup_argv.append(this.remote);
557
set_status(_("Cleaning up…"));
558
connect_and_start(null, null, cleanup_argv);
563
void delete_excess(int cutoff) {
564
state = State.DELETE;
565
var argv = new List<string>();
566
argv.append("remove-all-but-n-full");
567
argv.append("%d".printf(cutoff));
568
argv.append("--force");
569
argv.append(this.remote);
571
set_status(_("Cleaning up…"));
572
connect_and_start(null, null, argv);
577
bool can_ignore_error()
579
// Ignore errors during cleanup. If they're real, they'll repeat.
580
// They might be not-so-real, like the errors one gets when restoring
581
// from a backup when not all of the signature files are in your archive
582
// dir (which happens when you start using an archive dir in the middle
583
// of a backup chain).
584
return state == State.CLEANUP;
587
void handle_done(DuplicityInstance? inst, bool success, bool cancelled)
589
if (can_ignore_error())
592
if (!cancelled && success) {
595
has_progress_total = true;
596
progress_total = progress_count; // save max progress for next run
602
if (restart()) // In case we were interrupting normal flow
607
cleaned_up_once = true;
608
if (restart()) // restart in case cleanup was interrupting normal flow
611
// Else, we probably started cleaning up after a cancel. Just continue
617
checked_collection_info = true;
618
var should_restart = mode != original_mode;
619
mode = original_mode;
621
/* Set full backup threshold and determine whether we should trigger
623
if (mode == Operation.Mode.BACKUP && got_collection_info) {
624
Date threshold = DejaDup.get_full_backup_threshold_date();
625
Date full_backup = Date();
626
foreach (DateInfo info in collection_info) {
628
full_backup.set_time_val(info.time);
630
if (!full_backup.valid() || threshold.compare(full_backup) > 0) {
631
is_full_backup = true;
632
is_full(!full_backup.valid());
636
if (should_restart) {
642
case State.CHECK_CONTENTS:
643
has_checked_contents = true;
644
mode = original_mode;
651
if (mode == Operation.Mode.RESTORE && restore_files != null) {
652
_restore_files.delete_link(_restore_files);
653
if (restore_files != null) {
658
else if (mode == Operation.Mode.BACKUP) {
659
mode = Operation.Mode.INVALID; // mark 'done' so when we delete, we don't restart
660
if (delete_files_if_needed())
666
else if (was_stopped)
667
success = true; // we treat stops as success
672
if (!success && !cancelled && !error_issued)
673
show_error(_("Failed with an unknown error."));
676
done(success, cancelled);
680
File saved_status_file;
681
bool saved_status_file_action;
682
void set_status(string msg, bool save = true)
686
saved_status_file = null;
688
action_desc_changed(msg);
691
void set_status_file(File file, bool action, bool save = true)
695
saved_status_file = file;
696
saved_status_file_action = action;
698
action_file_changed(file, action);
701
void set_saved_status()
703
if (saved_status != null)
704
set_status(saved_status, false);
706
set_status_file(saved_status_file, saved_status_file_action, false);
709
// Should only be called *after* a successful backup
710
bool delete_files_if_needed()
712
if (delete_age == 0) {
713
deleted_files = true;
717
// Check if we need to delete any backups
718
// If we got collection info, examine it to see if we should delete old
720
if (got_collection_info && !deleted_files) {
721
// Alright, let's look at collection data
723
TimeVal prev_time = TimeVal();
724
Date prev_date = Date();
726
TimeVal now = TimeVal();
727
now.get_current_time();
730
today.set_time_val(now);
732
foreach (DateInfo info in collection_info) {
734
if (full_dates > 0) { // Wait until we have a prev_time
735
prev_date.set_time_val(prev_time); // compare last incremental backup
736
if (prev_date.days_between(today) > delete_age)
741
prev_time = info.time;
743
prev_date.set_time_val(prev_time); // compare last incremental backup
744
if (prev_date.days_between(today) > delete_age)
747
// Did we just finished a successful full backup?
748
// Collection info won't have our recent backup, because it is done at
749
// beginning of backup.
753
if (too_old > 0 && full_dates > MINIMUM_FULL) {
754
// Alright, let's delete those ancient files!
755
int cutoff = int.max(MINIMUM_FULL, full_dates - too_old);
756
delete_excess(cutoff);
760
// If we don't need to delete, pretend we did and move on.
761
deleted_files = true;
768
protected static const int ERROR_GENERIC = 1;
769
protected static const int ERROR_HOSTNAME_CHANGED = 3;
770
protected static const int ERROR_RESTORE_DIR_NOT_FOUND = 19;
771
protected static const int ERROR_EXCEPTION = 30;
772
protected static const int ERROR_GPG = 31;
773
protected static const int ERROR_BAD_VOLUME = 44;
774
protected static const int ERROR_BACKEND = 50;
775
protected static const int ERROR_BACKEND_PERMISSION_DENIED = 51;
776
protected static const int ERROR_BACKEND_NOT_FOUND = 52;
777
protected static const int ERROR_BACKEND_NO_SPACE = 53;
778
protected static const int INFO_PROGRESS = 2;
779
protected static const int INFO_COLLECTION_STATUS = 3;
780
protected static const int INFO_DIFF_FILE_NEW = 4;
781
protected static const int INFO_DIFF_FILE_CHANGED = 5;
782
protected static const int INFO_DIFF_FILE_DELETED = 6;
783
protected static const int INFO_PATCH_FILE_WRITING = 7;
784
protected static const int INFO_PATCH_FILE_PATCHING = 8;
785
protected static const int INFO_FILE_STAT = 10;
786
protected static const int INFO_SYNCHRONOUS_UPLOAD_BEGIN = 11;
787
protected static const int INFO_ASYNCHRONOUS_UPLOAD_BEGIN = 12;
788
protected static const int INFO_SYNCHRONOUS_UPLOAD_DONE = 13;
789
protected static const int INFO_ASYNCHRONOUS_UPLOAD_DONE = 14;
790
protected static const int WARNING_ORPHANED_SIG = 2;
791
protected static const int WARNING_UNNECESSARY_SIG = 3;
792
protected static const int WARNING_UNMATCHED_SIG = 4;
793
protected static const int WARNING_INCOMPLETE_BACKUP = 5;
794
protected static const int WARNING_ORPHANED_BACKUP = 6;
795
protected static const int DEBUG_GENERIC = 1;
799
string dir = Environment.get_user_cache_dir();
803
var cachedir = Path.build_filename(dir, Config.PACKAGE);
804
var del = new RecursiveDelete(File.new_for_path(cachedir));
808
bool restarted_without_cache = false;
809
bool restart_without_cache()
811
if (restarted_without_cache)
814
restarted_without_cache = true;
820
void handle_exit(int code)
822
// Duplicity has a habit of dying and returning 1 without sending an error
823
// if there was some unexpected issue with its cached metadata. It often
824
// goes away if you delete ~/.cache/deja-dup and try again. This issue
825
// happens often enough that we do that for the user here. It should be
826
// safe to do this, as the cache is not necessary for operation, only
827
// a performance improvement.
828
if (code == ERROR_GENERIC && !error_issued) {
829
restart_without_cache();
833
void handle_message(DuplicityInstance inst, string[] control_line,
834
List<string>? data_lines, string user_text)
837
* Based on duplicity's output handle message as either process data as error, info or warning
839
if (control_line.length == 0)
842
var keyword = control_line[0];
845
process_error(control_line, data_lines, user_text);
848
process_info(control_line, data_lines, user_text);
851
process_warning(control_line, data_lines, user_text);
854
process_debug(control_line, data_lines, user_text);
859
bool ask_question(string t, string m)
863
var rv = mode != Operation.Mode.INVALID; // return whether we were canceled
865
handle_done(null, false, true);
869
// Hacky function to return later parts of a duplicity filename.
870
// Used to chop off the date bit
871
string parse_duplicity_file(string file, int skip_bits)
874
while (skip_bits-- > 0 && next >= 0)
875
next = file.index_of_char('.', next) + 1;
879
return file.substring(next);
882
protected virtual void process_error(string[] firstline, List<string>? data,
885
string text = text_in;
887
if (can_ignore_error())
890
if (firstline.length > 1) {
891
switch (int.parse(firstline[1])) {
892
case ERROR_EXCEPTION: // exception
893
process_exception(firstline.length > 2 ? firstline[2] : "", text);
896
case ERROR_RESTORE_DIR_NOT_FOUND:
897
// make text a little nicer than duplicity gives
898
// duplicity gives something like "home/blah/blah not found in archive,
899
// no files restored".
900
if (restore_files != null)
901
text = _("Could not restore ‘%s’: File not found in backup").printf(
902
restore_files.data.get_parse_name());
906
bad_encryption_password(); // notify upper layers, if they want to do anything
907
text = _("Bad encryption password.");
910
case ERROR_HOSTNAME_CHANGED:
911
if (firstline.length >= 4) {
912
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])))
915
// Else just assume that user wants to allow the mismatch...
916
// A little troubling but better than not letting user proceed
917
saved_argv.append("--allow-source-mismatch");
922
case ERROR_BAD_VOLUME:
923
// A volume was detected to be corrupt/incomplete after uploading.
924
// We'll first try a restart because then duplicity will retry it.
925
// If it's still bad, we'll do a full cleanup and try again.
926
// If it's *still* bad, tell the user, but I'm not sure what they can
928
if (mode == Operation.Mode.BACKUP) {
929
// strip date info from volume (after cleanup below, we'll get new date)
930
var this_volume = parse_duplicity_file(firstline[2], 2);
931
if (last_bad_volume != this_volume) {
932
bad_volume_count = 0;
933
last_bad_volume = this_volume;
936
if ((bad_volume_count == 0 && restart()) ||
937
(bad_volume_count == 1 && cleanup())) {
938
bad_volume_count += 1;
944
case ERROR_BACKEND_PERMISSION_DENIED:
945
if (firstline.length >= 5 && firstline[2] == "put") {
946
var file = make_file_obj(firstline[4]);
947
text = _("Permission denied when trying to create ‘%s’.").printf(file.get_parse_name());
949
if (firstline.length >= 5 && firstline[2] == "get") {
950
var file = make_file_obj(firstline[3]); // assume error is on backend side
951
text = _("Permission denied when trying to read ‘%s’.").printf(file.get_parse_name());
953
else if (firstline.length >= 4 && firstline[2] == "list") {
954
var file = make_file_obj(firstline[3]);
955
text = _("Permission denied when trying to read ‘%s’.").printf(file.get_parse_name());
957
else if (firstline.length >= 4 && firstline[2] == "delete") {
958
var file = make_file_obj(firstline[3]);
959
text = _("Permission denied when trying to delete ‘%s’.").printf(file.get_parse_name());
963
case ERROR_BACKEND_NOT_FOUND:
964
if (firstline.length >= 4) {
965
var file = make_file_obj(firstline[3]);
966
text = _("Backup location ‘%s’ does not exist.").printf(file.get_parse_name());
970
case ERROR_BACKEND_NO_SPACE:
971
if (firstline.length >= 5) {
972
text = _("No space left.");
981
void process_exception(string exception, string text)
984
case "S3ResponseError":
985
if (text.contains("<Code>InvalidAccessKeyId</Code>"))
986
show_error(_("Invalid ID."));
987
else if (text.contains("<Code>SignatureDoesNotMatch</Code>"))
988
show_error(_("Invalid secret key."));
989
else if (text.contains("<Code>NotSignedUp</Code>"))
990
show_error(_("Your Amazon Web Services account is not signed up for the S3 service."));
992
case "S3CreateError":
993
if (text.contains("<Code>BucketAlreadyExists</Code>")) {
994
if (((BackendS3)backend).bump_bucket()) {
996
remote = backend.get_location();
1000
catch (Error e) {warning("%s\n", e.message);}
1003
show_error(_("S3 bucket name is not available."));
1007
// Duplicity tried to ask the user what the encryption password is.
1008
bad_encryption_password(); // notify upper layers, if they want to do anything
1009
show_error(_("Bad encryption password."));
1012
if (text.contains("GnuPG"))
1013
show_error(_("Bad encryption password."));
1014
else if (text.contains("[Errno 5]") && // I/O Error
1015
last_touched_file != null) {
1016
if (mode == Operation.Mode.BACKUP)
1017
show_error(_("Error reading file ‘%s’.").printf(last_touched_file.get_parse_name()));
1019
show_error(_("Error writing file ‘%s’.").printf(last_touched_file.get_parse_name()));
1021
else if (text.contains("[Errno 28]")) { // No space left on device
1022
string where = null;
1023
if (mode == Operation.Mode.BACKUP) {
1025
where = backend.get_location_pretty();
1027
catch (Error e) {warning("%s\n", e.message);}
1030
where = local.get_path();
1032
show_error(_("No space left."));
1034
show_error(_("No space left in ‘%s’.").printf(where));
1036
else if (text.contains("CRC check failed")) { // bug 676767
1037
if (restart_without_cache())
1041
case "CollectionsError":
1042
show_error(_("No backup files found"));
1044
case "AssertionError":
1045
// Sometimes if an incremental backup is cancelled then tried again,
1046
// duplicity will emit an "time not moving forward" assertion. Clearing
1047
// the cache will solve it. This message is not localized in duplicity.
1048
if (text.contains("time not moving forward at appropriate pace")) {
1049
if (restart_without_cache())
1055
// For most, don't do anything special. Show generic 'unknown error'
1056
// message, but provide the exception text for better bug reports.
1057
// Plus, sometimes it may clue the user in to what's wrong.
1058
// But first, try to restart without a cache, since that seems to quite
1059
// frequently fix odd metadata errors with duplicity. If we hit an error
1060
// a second time, we'll show the unknown error message.
1061
if (!error_issued && !restart_without_cache())
1062
show_error(_("Failed with an unknown error."), text);
1065
protected virtual void process_info(string[] firstline, List<string>? data,
1069
* Pass message to appropriate function considering the type of output
1071
if (firstline.length > 1) {
1072
switch (int.parse(firstline[1])) {
1073
case INFO_DIFF_FILE_NEW:
1074
case INFO_DIFF_FILE_CHANGED:
1075
case INFO_DIFF_FILE_DELETED:
1076
if (firstline.length > 2)
1077
process_diff_file(firstline[2]);
1079
case INFO_PATCH_FILE_WRITING:
1080
case INFO_PATCH_FILE_PATCHING:
1081
if (firstline.length > 2)
1082
process_patch_file(firstline[2]);
1085
process_progress(firstline);
1087
case INFO_COLLECTION_STATUS:
1088
process_collection_status(data);
1090
case INFO_SYNCHRONOUS_UPLOAD_BEGIN:
1091
case INFO_ASYNCHRONOUS_UPLOAD_BEGIN:
1092
if (!backend.is_native())
1093
set_status(_("Uploading…"));
1095
case INFO_FILE_STAT:
1096
process_file_stat(firstline[2], firstline[3], data, text);
1102
protected virtual void process_debug(string[] firstline, List<string>? data,
1106
* Pass message to appropriate function considering the type of output
1108
if (firstline.length > 1) {
1109
switch (int.parse(firstline[1])) {
1111
// In non-modern versions of duplicity, this list of files is the only
1112
// way to tell whether the backup is encrypted or not. This message
1113
// was not translated in duplicity before switching to a better method
1114
// of detecting, so we can safely check for it.
1115
if (mode == Operation.Mode.STATUS &&
1116
!DuplicityInfo.get_default().reports_encryption &&
1117
!detected_encryption &&
1118
text.has_prefix("Extracting backup chains from list of files:")) {
1119
detected_encryption = true;
1120
existing_encrypted = text.contains(".gpg'") || text.contains(".g'");
1127
void process_file_stat(string date, string file, List<string> data, string text)
1129
if (mode != Operation.Mode.LIST)
1131
if (state == State.CHECK_CONTENTS) {
1132
var gfile = make_file_obj(file);
1133
if (gfile.equal(slash_root) ||
1134
(gfile.get_parent() != null && gfile.get_parent().equal(slash_home)))
1135
homes.append(gfile);
1136
if (!has_non_home_contents &&
1137
!gfile.equal(slash) &&
1138
!gfile.equal(slash_home) &&
1139
!gfile.has_prefix(slash_home))
1140
has_non_home_contents = true;
1142
listed_current_files(date, file);
1145
void process_diff_file(string file) {
1146
var gfile = make_file_obj(file);
1147
last_touched_file = gfile;
1148
if (gfile.query_file_type(FileQueryInfoFlags.NONE, null) != FileType.DIRECTORY)
1149
set_status_file(gfile, state != State.DRY_RUN);
1152
void process_patch_file(string file) {
1153
var gfile = make_file_obj(file);
1154
last_touched_file = gfile;
1155
if (gfile.query_file_type(FileQueryInfoFlags.NONE, null) != FileType.DIRECTORY)
1156
set_status_file(gfile, state != State.DRY_RUN);
1159
void process_progress(string[] firstline)
1163
if (firstline.length > 2)
1164
this.progress_count = uint64.parse(firstline[2]);
1168
if (firstline.length > 3)
1169
total = double.parse(firstline[3]);
1170
else if (this.progress_total > 0)
1171
total = this.progress_total;
1173
return; // can't do progress without a total
1175
double percent = (double)this.progress_count / total;
1178
if (percent < 0) // ???
1183
File make_file_obj(string file)
1185
// All files are relative to root.
1186
return slash.resolve_relative_path(file);
1189
void process_collection_status(List<string>? lines)
1192
* Collect output of collection status and return list of dates as strings via a signal
1194
* Duplicity returns collection status as a bunch of lines, some of which are
1195
* indented which contain information about specific chains. We gather
1196
* this all up and report back to caller via a signal.
1197
* We're really only interested in the list of entries in the complete chain.
1199
if (mode != Operation.Mode.STATUS || got_collection_info)
1202
var timeval = TimeVal();
1203
var dates = new List<string>();
1204
var infos = new List<DateInfo?>();
1205
bool in_chain = false;
1206
foreach (string line in lines) {
1207
if (line == "chain-complete" || line.index_of("chain-no-sig") == 0)
1209
else if (in_chain && line.length > 0 && line[0] == ' ') {
1210
// OK, appears to be a date line. Try to parse. Should look like:
1211
// ' inc TIMESTR NUMVOLS [ENCRYPTED]'.
1212
// Since there's a space at the beginning, when we tokenize it, we
1213
// should expect an extra token at the front.
1214
string[] tokens = line.split(" ");
1215
if (tokens.length > 2 && timeval.from_iso8601(tokens[2])) {
1216
dates.append(tokens[2]);
1218
var info = DateInfo();
1219
info.time = timeval;
1220
info.full = tokens[1] == "full";
1223
if (DuplicityInfo.get_default().reports_encryption &&
1224
!detected_encryption &&
1225
tokens.length > 4) {
1226
// Just use the encryption status of the first one we see;
1227
// mixed-encryption backups is not supported.
1228
detected_encryption = true;
1229
existing_encrypted = tokens[4] == "enc";
1237
got_collection_info = true;
1238
collection_info = new List<DateInfo?>();
1239
foreach (DateInfo s in infos)
1240
collection_info.append(s); // we want to keep our own copy too
1242
collection_dates(dates);
1245
protected virtual void process_warning(string[] firstline, List<string>? data,
1248
if (firstline.length > 1) {
1249
switch (int.parse(firstline[1])) {
1250
case WARNING_ORPHANED_SIG:
1251
case WARNING_UNNECESSARY_SIG:
1252
case WARNING_UNMATCHED_SIG:
1253
case WARNING_INCOMPLETE_BACKUP:
1254
case WARNING_ORPHANED_BACKUP:
1255
// Random files left on backend from previous run. Should clean them
1256
// up before we continue. We don't want to wait until we finish to
1257
// clean them up, since we may want that space, and if there's a bug
1258
// in ourselves, we may never get to it.
1259
if (mode == Operation.Mode.BACKUP && !this.cleaned_up_once)
1260
cleanup(); // stops current backup, cleans up, then resumes
1266
void show_error(string errorstr, string? detail = null)
1268
if (error_issued == false) {
1269
error_issued = true;
1270
raise_error(errorstr, detail);
1274
// Returns volume size in megs
1277
// Advantages of a smaller value:
1278
// * takes less temp space
1279
// * retries of a volume take less time
1280
// * quicker restore of a particular file (less excess baggage to download)
1281
// * we get feedback more frequently (duplicity only gives us a progress
1282
// report at the end of a volume) -- fixed by reporting when we're uploading
1284
// * less throughput:
1285
// * some protocols have large per-file overhead (like sftp)
1286
// * the network doesn't have time to ramp up to max tcp transfer speed per
1288
// * lots of files looks ugly to users
1290
// duplicity's default is 25 (used to be 5).
1292
// For local filesystems, we'll choose large volsize.
1293
// For remote FSs, we'll go smaller.
1294
if (in_testing_mode())
1296
else if (backend.is_native())
1302
void disconnect_inst()
1304
/* Disconnect signals and cancel call to duplicity instance */
1306
inst.done.disconnect(handle_done);
1307
inst.message.disconnect(handle_message);
1308
inst.exited.disconnect(handle_exit);
1314
void connect_and_start(List<string>? argv_extra = null,
1315
List<string>? envp_extra = null,
1316
List<string>? argv_entire = null,
1317
File? custom_local = null)
1320
* For passed arguments start a new duplicity instance, set duplicity in the right mode and execute command
1322
/* Disconnect instance */
1325
/* Start new duplicity instance */
1326
inst = new DuplicityInstance();
1327
inst.done.connect(handle_done);
1329
/* As duplicity's data is returned via a signal, handle_message begins post-raw stream processing */
1330
inst.message.connect(handle_message);
1332
/* When duplicity exits, we may be also interested in its return code */
1333
inst.exited.connect(handle_exit);
1335
/* Set arguments for call to duplicity */
1336
weak List<string> master_argv = argv_entire == null ? saved_argv : argv_entire;
1337
weak File local_arg = custom_local == null ? local : custom_local;
1339
var argv = new List<string>();
1340
foreach (string s in master_argv) argv.append(s);
1341
foreach (string s in argv_extra) argv.append(s);
1342
foreach (string s in this.backend_argv) argv.append(s);
1344
/* Set duplicity into right mode */
1345
if (argv_entire == null) {
1346
// add operation, local, and remote args
1348
case Operation.Mode.BACKUP:
1350
argv.prepend("full");
1351
argv.append("--volsize=%d".printf(get_volsize()));
1352
argv.append(local_arg.get_path());
1353
argv.append(remote);
1355
case Operation.Mode.RESTORE:
1356
argv.prepend("restore");
1357
argv.append("--force");
1358
argv.append(remote);
1359
argv.append(local_arg.get_path());
1361
case Operation.Mode.STATUS:
1362
argv.prepend("collection-status");
1363
argv.append(remote);
1365
case Operation.Mode.LIST:
1366
argv.prepend("list-current-files");
1367
argv.append(remote);
1372
/* Set environmental parameters */
1373
var envp = new List<string>();
1374
foreach (string s in saved_envp) envp.append(s);
1375
foreach (string s in envp_extra) envp.append(s);
1377
bool use_encryption = false;
1378
if (detected_encryption)
1379
use_encryption = existing_encrypted;
1380
else if (encrypt_password != null)
1381
use_encryption = encrypt_password != "";
1383
if (use_encryption) {
1384
if (encrypt_password != null && encrypt_password != "")
1385
envp.append("PASSPHRASE=%s".printf(encrypt_password));
1386
// else duplicity will try to prompt user and we'll get an exception,
1387
// which is our cue to ask user for password. We could pass an empty
1388
// passphrase (as we do below), but by not setting it at all, duplicity
1389
// will error out quicker, and notably before it tries to sync metadata.
1392
argv.append("--no-encryption");
1393
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
1396
/* Start duplicity instance */
1398
inst.start(argv, envp, needs_root);
1401
show_error(e.message);