~ubuntu-branches/ubuntu/saucy/deja-dup/saucy

« back to all changes in this revision

Viewing changes to common/Duplicity.vala

  • Committer: Package Import Robot
  • Author(s): Michael Terry
  • Date: 2012-06-05 13:45:39 UTC
  • mfrom: (1.1.43)
  • Revision ID: package-import@ubuntu.com-20120605134539-l35tewhkjfq4qp6e
Tags: 23.2-0ubuntu1
* New upstream release
* debian/control:
  - Add libpeas-dev to Build-Depends
  - Update valac and libglib2.0-dev versions
  - Bump debhelper version to 9
* debian/compat:
  - Bump to 9
* debian/rules:
  - Don't install new .la and .a files from upstream
* debian/patches/allow-resuming-encrypted-backup.patch:
  - Dropped, included upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 2 -*- */
2
 
/*
3
 
    This file is part of Déjà Dup.
4
 
    For copyright information, see AUTHORS.
5
 
 
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.
10
 
 
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.
15
 
 
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/>.
18
 
*/
19
 
 
20
 
using GLib;
21
 
 
22
 
namespace DejaDup {
23
 
 
24
 
internal class Duplicity : Object
25
 
{
26
 
  /*
27
 
   * Vala implementation of various methods for accessing duplicity
28
 
   *
29
 
   * Vala implementation of various methods for accessing duplicity from
30
 
   * vala withot the need of manually running duplicity command.
31
 
   */
32
 
 
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);
38
 
  /*
39
 
   * Signal emitted when collection dates are retrieved from duplicity
40
 
   */
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();
46
 
  
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;}
51
 
  
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;}
58
 
  
59
 
  private List<File> _restore_files;
60
 
  public List<File> restore_files {
61
 
    get {
62
 
      return this._restore_files;
63
 
    }
64
 
    set {
65
 
      foreach (File f in this._restore_files)
66
 
        f.unref();
67
 
      this._restore_files = value.copy();
68
 
      foreach (File f in this._restore_files)
69
 
        f.ref();
70
 
    }
71
 
  }
72
 
  
73
 
  protected enum State {
74
 
    NORMAL,
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
78
 
    CLEANUP,
79
 
    DELETE,
80
 
  }
81
 
  protected State state {get; set;}
82
 
  
83
 
  DuplicityInstance inst;
84
 
  
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;
93
 
 
94
 
  string last_bad_volume;
95
 
  uint bad_volume_count;
96
 
  
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
100
 
  
101
 
  static File slash;
102
 
  static File slash_root;
103
 
  static File slash_home;
104
 
  static File slash_home_me;
105
 
  static Regex gpg_regex;
106
 
  
107
 
  bool has_checked_contents = false;
108
 
  bool has_non_home_contents = false;
109
 
  List<File> homes = new List<File>();
110
 
 
111
 
  List<File> local_error_files = null;
112
 
  
113
 
  bool checked_collection_info = false;
114
 
  bool got_collection_info = false;
115
 
  struct DateInfo {
116
 
    public bool full;
117
 
    public TimeVal time;
118
 
  }
119
 
  List<DateInfo?> collection_info = null;
120
 
  
121
 
  bool checked_backup_space = false;
122
 
 
123
 
  static const int MINIMUM_FULL = 2;
124
 
  bool deleted_files = false;
125
 
  int delete_age = 0;
126
 
  
127
 
  File last_touched_file = null;
128
 
 
129
 
  void network_changed()
130
 
  {
131
 
    if (Network.get().connected)
132
 
      resume();
133
 
    else
134
 
      pause(_("Paused (no network)"));
135
 
  }
136
 
 
137
 
  public Duplicity(Operation.Mode mode) {
138
 
    Object(original_mode: mode);
139
 
  }
140
 
  
141
 
  construct {
142
 
    if (slash == null) {
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());
147
 
    }
148
 
 
149
 
    if (gpg_regex == null) {
150
 
      try {
151
 
        gpg_regex = new Regex(".*\\[.*\\.(g|gpg)'.*]$");
152
 
      }
153
 
      catch (Error e) {
154
 
        error("%s\n", e.message); // this is a programmer error, so use error()
155
 
      }
156
 
    }
157
 
  }
158
 
 
159
 
  ~Duplicity() {
160
 
    Network.get().notify["connected"].disconnect(network_changed);
161
 
  }
162
 
 
163
 
  public virtual void start(Backend backend,
164
 
                            List<string>? argv, List<string>? envp)
165
 
  {
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);
175
 
    
176
 
    if (mode == Operation.Mode.BACKUP)
177
 
      process_include_excludes();
178
 
    
179
 
    var settings = get_settings();
180
 
    delete_age = settings.get_int(DELETE_AFTER_KEY);
181
 
 
182
 
    if (!restart())
183
 
      done(false, false, null);
184
 
 
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)"));
190
 
      }
191
 
    }
192
 
  }
193
 
 
194
 
  // This will treat a < b iff a is 'lower' in the file tree than b
195
 
  int cmp_prefix(File? a, File? b)
196
 
  {
197
 
    if (a == null && b == null)
198
 
      return 0;
199
 
    else if (b == null || a.has_prefix(b))
200
 
      return -1;
201
 
    else if (a == null || b.has_prefix(a))
202
 
      return 1;
203
 
    else
204
 
      return 0;
205
 
  }
206
 
 
207
 
  string get_remote ()
208
 
  {
209
 
    return backend.get_location(ref needs_root);
210
 
  }
211
 
 
212
 
  void expand_links_in_file(File file, ref List<File> all, bool include, List<File>? seen = null)
213
 
  {
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.
222
 
    //
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));
229
 
      iter = parent;
230
 
    }
231
 
 
232
 
    try {
233
 
      File so_far = slash;
234
 
      foreach (weak string piece in pieces) {
235
 
        parent = so_far;
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, 
240
 
                                     null);
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)
245
 
            return; // stop here
246
 
 
247
 
          if (include)
248
 
            all.prepend(so_far); // back up symlink as a leaf element of its path
249
 
 
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).
253
 
 
254
 
          var symlink_target = info.get_symlink_target();
255
 
          File full_target;
256
 
          if (Path.is_absolute(symlink_target))
257
 
            full_target = File.new_for_path(symlink_target);
258
 
          else
259
 
            full_target = parent.resolve_relative_path(symlink_target);
260
 
 
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);
265
 
 
266
 
          if (include)
267
 
            all.remove(file); // may fail if it's not there, which is fine
268
 
 
269
 
          seen.prepend(so_far);
270
 
 
271
 
          expand_links_in_file(full_target, ref all, include, seen);
272
 
          return;
273
 
        }
274
 
      }
275
 
 
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)
278
 
      if (seen != null)
279
 
        all.prepend(file);
280
 
    }
281
 
    catch (IOError.NOT_FOUND e) {
282
 
      // Don't bother keeping this file in the list
283
 
      all.remove(file);
284
 
    }
285
 
    catch (Error e) {
286
 
      warning("%s\n", e.message);
287
 
    }
288
 
  }
289
 
 
290
 
  void expand_links_in_list(ref List<File> all, bool include)
291
 
  {
292
 
    var all2 = all.copy();
293
 
    foreach (File file in all2)
294
 
      expand_links_in_file(file, ref all, include);
295
 
  }
296
 
 
297
 
  string escape_duplicity_path(string path)
298
 
  {
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.
303
 
    string rv;
304
 
    rv = path.replace("[", "[[]");
305
 
    rv =   rv.replace("?", "[?]");
306
 
    rv =   rv.replace("*", "[*]");
307
 
    return rv;
308
 
  }
309
 
 
310
 
  void process_include_excludes()
311
 
  {
312
 
    expand_links_in_list(ref includes, true);
313
 
    expand_links_in_list(ref excludes, false);
314
 
 
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);
320
 
 
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()));
326
 
          excludes.remove(e);
327
 
        }
328
 
      }
329
 
      saved_argv.append("--include=" + escape_duplicity_path(i.get_path()));
330
 
      //if (!i.has_prefix(slash_home_me))
331
 
      //  needs_root = true;
332
 
    }
333
 
    foreach (File e in excludes) {
334
 
      saved_argv.append("--exclude=" + escape_duplicity_path(e.get_path()));
335
 
    }
336
 
 
337
 
    saved_argv.append("--exclude=**");
338
 
  }
339
 
  
340
 
  public void cancel() {
341
 
    var prev_mode = mode;
342
 
    mode = Operation.Mode.INVALID;
343
 
    
344
 
    if (prev_mode == Operation.Mode.BACKUP && state == State.NORMAL) {
345
 
      if (cleanup())
346
 
        return;
347
 
    }
348
 
    
349
 
    cancel_inst();
350
 
  }
351
 
  
352
 
  public void stop() {
353
 
    // just abruptly stop, without a cleanup, duplicity will resume
354
 
    was_stopped = true;
355
 
    mode = Operation.Mode.INVALID;
356
 
    cancel_inst();
357
 
  }
358
 
 
359
 
  public void pause(string? reason)
360
 
  {
361
 
    if (inst != null) {
362
 
      inst.pause();
363
 
      if (reason != null)
364
 
        set_status(reason, false);
365
 
    }
366
 
  }
367
 
 
368
 
  public void resume()
369
 
  {
370
 
    if (inst != null) {
371
 
      inst.resume();
372
 
      set_saved_status();
373
 
    }
374
 
  }
375
 
 
376
 
  void cancel_inst()
377
 
  {
378
 
    disconnect_inst();
379
 
    handle_done(null, false, true);
380
 
  }
381
 
 
382
 
  bool restart()
383
 
  {
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;
387
 
    
388
 
    if (mode == Operation.Mode.INVALID)
389
 
      return false;
390
 
    
391
 
    var extra_argv = new List<string>();
392
 
    string action_desc = null;
393
 
    File custom_local = null;
394
 
    
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…");
403
 
      }
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");
411
 
      }
412
 
      else if (!checked_backup_space) {
413
 
        check_backup_space();
414
 
        return true;
415
 
      }
416
 
      else {
417
 
        if (has_progress_total)
418
 
          progress(0f);
419
 
      }
420
 
      
421
 
      break;
422
 
    case Operation.Mode.RESTORE:
423
 
      // We need to first check the backup status to see if we should use
424
 
      // encryption.
425
 
      if (!checked_collection_info) {
426
 
        mode = Operation.Mode.STATUS;
427
 
        state = State.STATUS;
428
 
        action_desc = _("Preparing…");
429
 
      }
430
 
      else if (!has_checked_contents) {
431
 
        mode = Operation.Mode.LIST;
432
 
        state = State.CHECK_CONTENTS;
433
 
        action_desc = _("Preparing…");
434
 
      }
435
 
      else {
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));
449
 
          }
450
 
        }
451
 
        
452
 
        if (restore_files != null) {
453
 
          // Just do first one.  Others will come when we're done
454
 
          
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()));
462
 
            return false;
463
 
          }
464
 
 
465
 
          if (!local_file.has_prefix(slash_home_me))
466
 
            needs_root = true;
467
 
          
468
 
          try {
469
 
            // won't have correct permissions...
470
 
            local_file.make_directory_with_parents(null);
471
 
          }
472
 
          catch (IOError.EXISTS e) {
473
 
            // ignore
474
 
          }
475
 
          catch (Error e) {
476
 
            show_error(e.message);
477
 
            return false;
478
 
          }
479
 
          custom_local = local_file;
480
 
          
481
 
          var rel_file_path = slash.get_relative_path(restore_files.data);
482
 
          extra_argv.append("--file-to-restore=%s".printf(rel_file_path));
483
 
        }
484
 
        else {
485
 
          if (has_non_home_contents && !this.local.has_prefix(slash_home_me))
486
 
            needs_root = true;
487
 
        }
488
 
        
489
 
        progress(0f);
490
 
      }
491
 
      break;
492
 
    }
493
 
    
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);
499
 
    
500
 
    connect_and_start(extra_argv, null, null, custom_local);
501
 
    return true;
502
 
  }
503
 
  
504
 
  File? make_local_rel_path(File file)
505
 
  {
506
 
    string rel_file_path = slash.get_relative_path(file);
507
 
    if (rel_file_path == null)
508
 
      return null;
509
 
    return local.resolve_relative_path(rel_file_path);
510
 
  }
511
 
  
512
 
  async void check_backup_space()
513
 
  {
514
 
    checked_backup_space = true;
515
 
 
516
 
    if (!has_progress_total) {
517
 
      if (!restart())
518
 
        done(false, false, null);
519
 
      return;
520
 
    }
521
 
 
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."));
527
 
        return;
528
 
    }
529
 
 
530
 
    if (free < progress_total) {
531
 
      if (got_collection_info) {
532
 
        // Alright, let's look at collection data
533
 
        int full_dates = 0;
534
 
        foreach (DateInfo info in collection_info) {
535
 
          if (info.full)
536
 
            ++full_dates;
537
 
        }
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;
544
 
          return;
545
 
        }
546
 
      }
547
 
      else {
548
 
        show_error(_("Backup location does not have enough free space."));
549
 
        return;
550
 
      }
551
 
    }
552
 
    
553
 
    if (!restart())
554
 
      done(false, false, null);
555
 
  }
556
 
 
557
 
  bool cleanup() {
558
 
    if (state == State.CLEANUP)
559
 
      return false;
560
 
    
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());
566
 
    
567
 
    set_status(_("Cleaning up…"));
568
 
    connect_and_start(null, null, cleanup_argv);
569
 
    
570
 
    return true;
571
 
  }
572
 
  
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());
580
 
    
581
 
    set_status(_("Cleaning up…"));
582
 
    connect_and_start(null, null, argv);
583
 
    
584
 
    return;
585
 
  }
586
 
  
587
 
  bool can_ignore_error()
588
 
  {
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;
595
 
  }
596
 
 
597
 
  void handle_done(DuplicityInstance? inst, bool success, bool cancelled)
598
 
  {
599
 
    string detail = null;
600
 
 
601
 
    if (can_ignore_error())
602
 
      success = true;
603
 
 
604
 
    if (!cancelled && success) {
605
 
      switch (state) {
606
 
      case State.DRY_RUN:
607
 
        has_progress_total = true;
608
 
        progress_total = progress_count; // save max progress for next run
609
 
        if (restart())
610
 
          return;
611
 
        break;
612
 
      
613
 
      case State.DELETE:
614
 
        if (restart()) // In case we were interrupting normal flow
615
 
          return;
616
 
        break;
617
 
      
618
 
      case State.CLEANUP:
619
 
        cleaned_up_once = true;
620
 
        if (restart()) // restart in case cleanup was interrupting normal flow
621
 
          return;
622
 
        
623
 
        // Else, we probably started cleaning up after a cancel.  Just continue
624
 
        // that cancels
625
 
        success = false;
626
 
        cancelled = true;
627
 
        break;
628
 
      
629
 
      case State.STATUS:
630
 
        checked_collection_info = true;
631
 
        var should_restart = mode != original_mode;
632
 
        mode = original_mode;
633
 
 
634
 
        /* Set full backup threshold and determine whether we should trigger
635
 
           a full backup. */
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) {
640
 
            if (info.full)
641
 
              full_backup.set_time_val(info.time);
642
 
          }
643
 
          if (!full_backup.valid() || threshold.compare(full_backup) > 0) {
644
 
            is_full_backup = true;
645
 
            is_full(!full_backup.valid());
646
 
          }
647
 
        }
648
 
 
649
 
        if (should_restart) {
650
 
          if (restart())
651
 
            return;
652
 
        }
653
 
        break;
654
 
      
655
 
      case State.CHECK_CONTENTS:
656
 
        has_checked_contents = true;
657
 
        mode = original_mode;
658
 
        
659
 
        if (restart())
660
 
          return;
661
 
        break;
662
 
      
663
 
      case State.NORMAL:
664
 
        if (mode == Operation.Mode.RESTORE && restore_files != null) {
665
 
          _restore_files.delete_link(_restore_files);
666
 
          if (restore_files != null) {
667
 
            if (restart())
668
 
              return;
669
 
          }
670
 
        }
671
 
 
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.");
678
 
            detail += "\n";
679
 
            foreach (File f in local_error_files) {
680
 
              detail += "\n%s".printf(f.get_parse_name());
681
 
            }
682
 
          }
683
 
 
684
 
          mode = Operation.Mode.INVALID; // mark 'done' so when we delete, we don't restart
685
 
          if (delete_files_if_needed())
686
 
            return;
687
 
        }
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.");
694
 
            detail += "\n";
695
 
            foreach (File f in local_error_files) {
696
 
              detail += "\n%s".printf(f.get_parse_name());
697
 
            }
698
 
          }
699
 
        }
700
 
        break;
701
 
      }
702
 
    }
703
 
    else if (was_stopped)
704
 
      success = true; // we treat stops as success
705
 
    
706
 
    if (error_issued)
707
 
      success = false;
708
 
    
709
 
    if (!success && !cancelled && !error_issued)
710
 
      show_error(_("Failed with an unknown error."));
711
 
 
712
 
    inst = null;
713
 
    done(success, cancelled, detail);
714
 
  }
715
 
  
716
 
  string saved_status;
717
 
  File saved_status_file;
718
 
  bool saved_status_file_action;
719
 
  void set_status(string msg, bool save = true)
720
 
  {
721
 
    if (save) {
722
 
      saved_status = msg;
723
 
      saved_status_file = null;
724
 
    }
725
 
    action_desc_changed(msg);
726
 
  }
727
 
 
728
 
  void set_status_file(File file, bool action, bool save = true)
729
 
  {
730
 
    if (save) {
731
 
      saved_status = null;
732
 
      saved_status_file = file;
733
 
      saved_status_file_action = action;
734
 
    }
735
 
    action_file_changed(file, action);
736
 
  }
737
 
 
738
 
  void set_saved_status()
739
 
  {
740
 
    if (saved_status != null)
741
 
      set_status(saved_status, false);
742
 
    else
743
 
      set_status_file(saved_status_file, saved_status_file_action, false);
744
 
  }
745
 
 
746
 
  // Should only be called *after* a successful backup
747
 
  bool delete_files_if_needed()
748
 
  {
749
 
    if (delete_age == 0) {
750
 
      deleted_files = true;
751
 
      return false;
752
 
    }
753
 
    
754
 
    // Check if we need to delete any backups
755
 
    // If we got collection info, examine it to see if we should delete old
756
 
    // files.
757
 
    if (got_collection_info && !deleted_files) {
758
 
      // Alright, let's look at collection data
759
 
      int full_dates = 0;
760
 
      TimeVal prev_time = TimeVal();
761
 
      Date prev_date = Date();
762
 
      int too_old = 0;
763
 
      TimeVal now = TimeVal();
764
 
      now.get_current_time();
765
 
 
766
 
      Date today = Date();
767
 
      today.set_time_val(now);
768
 
      
769
 
      foreach (DateInfo info in collection_info) {
770
 
        if (info.full) {
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)
774
 
              ++too_old;
775
 
          }
776
 
          ++full_dates;
777
 
        }
778
 
        prev_time = info.time;
779
 
      }
780
 
      prev_date.set_time_val(prev_time); // compare last incremental backup
781
 
      if (prev_date.days_between(today) > delete_age)
782
 
        ++too_old;
783
 
      
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.
787
 
      if (is_full_backup)
788
 
        ++full_dates;
789
 
 
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);
794
 
        return true;
795
 
      }
796
 
      
797
 
      // If we don't need to delete, pretend we did and move on.
798
 
      deleted_files = true;
799
 
      return false;
800
 
    }
801
 
    else
802
 
      return false;
803
 
  }
804
 
 
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;
835
 
 
836
 
  void delete_cache()
837
 
  {
838
 
    string dir = Environment.get_user_cache_dir();
839
 
    if (dir == null)
840
 
      return;
841
 
 
842
 
    var cachedir = Path.build_filename(dir, Config.PACKAGE);
843
 
    var del = new RecursiveDelete(File.new_for_path(cachedir));
844
 
    del.start();
845
 
  }
846
 
 
847
 
  bool restarted_without_cache = false;
848
 
  bool restart_without_cache()
849
 
  {
850
 
    if (restarted_without_cache)
851
 
      return false;
852
 
 
853
 
    restarted_without_cache = true;
854
 
 
855
 
    delete_cache();
856
 
    return restart();
857
 
  }
858
 
 
859
 
  void handle_exit(int code)
860
 
  {
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();
869
 
    }
870
 
  }
871
 
 
872
 
  void handle_message(DuplicityInstance inst, string[] control_line,
873
 
                      List<string>? data_lines, string user_text)
874
 
  {
875
 
    /*
876
 
     * Based on duplicity's output handle message as either process data as error, info or warning
877
 
     */
878
 
    if (control_line.length == 0)
879
 
      return;
880
 
    
881
 
    var keyword = control_line[0];
882
 
    switch (keyword) {
883
 
    case "ERROR":
884
 
      process_error(control_line, data_lines, user_text);
885
 
      break;
886
 
    case "INFO":
887
 
      process_info(control_line, data_lines, user_text);
888
 
      break;
889
 
    case "WARNING":
890
 
      process_warning(control_line, data_lines, user_text);
891
 
      break;
892
 
    case "DEBUG":
893
 
      process_debug(control_line, data_lines, user_text);
894
 
      break;
895
 
    }
896
 
  }
897
 
  
898
 
  bool ask_question(string t, string m)
899
 
  {
900
 
    disconnect_inst();
901
 
    question(t, m);
902
 
    var rv = mode != Operation.Mode.INVALID; // return whether we were canceled
903
 
    if (!rv)
904
 
      handle_done(null, false, true);
905
 
    return rv;
906
 
  }
907
 
 
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)
911
 
  {
912
 
    int next = 0;
913
 
    while (skip_bits-- > 0 && next >= 0)
914
 
      next = file.index_of_char('.', next) + 1;
915
 
    if (next < 0)
916
 
      return "";
917
 
    else
918
 
      return file.substring(next);
919
 
  }
920
 
 
921
 
  protected virtual void process_error(string[] firstline, List<string>? data,
922
 
                                       string text_in)
923
 
  {
924
 
    string text = text_in;
925
 
    
926
 
    if (can_ignore_error())
927
 
      return;
928
 
    
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);
933
 
        return;
934
 
 
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());
942
 
        break;
943
 
 
944
 
      case ERROR_GPG:
945
 
        bad_encryption_password(); // notify upper layers, if they want to do anything
946
 
        text = _("Bad encryption password.");
947
 
        break;
948
 
 
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])))
952
 
            return;
953
 
        }
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");
957
 
        if (restart())
958
 
          return;
959
 
        break;
960
 
 
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
966
 
        // do about it.
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;
973
 
          }
974
 
 
975
 
          if ((bad_volume_count == 0 && restart()) ||
976
 
              (bad_volume_count == 1 && cleanup())) {
977
 
            bad_volume_count += 1;
978
 
            return;
979
 
          }
980
 
        }
981
 
        break;
982
 
 
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());
987
 
        }
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());
991
 
        }
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());
995
 
        }
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());
999
 
        }
1000
 
        break;
1001
 
 
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());
1006
 
        }
1007
 
        break;
1008
 
 
1009
 
      case ERROR_BACKEND_NO_SPACE:
1010
 
        if (firstline.length >= 5) {
1011
 
          text = _("No space left.");
1012
 
        }
1013
 
        break;
1014
 
      }
1015
 
    }
1016
 
    
1017
 
    show_error(text);
1018
 
  }
1019
 
  
1020
 
  void process_exception(string exception, string text)
1021
 
  {
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."));
1030
 
      break;
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
1035
 
            return;
1036
 
        }
1037
 
        
1038
 
        show_error(_("S3 bucket name is not available."));
1039
 
      }
1040
 
      break;
1041
 
    case "EOFError":
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."));
1045
 
      break;
1046
 
    case "IOError":
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()));
1053
 
        else
1054
 
          show_error(_("Error writing file ‘%s’.").printf(last_touched_file.get_parse_name()));
1055
 
      }
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();
1060
 
        else
1061
 
          where = local.get_path();
1062
 
        if (where == null)
1063
 
          show_error(_("No space left."));
1064
 
        else
1065
 
          show_error(_("No space left in ‘%s’.").printf(where));
1066
 
      }
1067
 
      else if (text.contains("CRC check failed")) { // bug 676767
1068
 
        if (restart_without_cache())
1069
 
          return;
1070
 
      }
1071
 
      break;
1072
 
    case "CollectionsError":
1073
 
      show_error(_("No backup files found"));
1074
 
      break;
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
1079
 
      // shot.
1080
 
      if (restart_without_cache())
1081
 
        return;
1082
 
      break;
1083
 
    }
1084
 
    
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);
1093
 
  }
1094
 
  
1095
 
  protected virtual void process_info(string[] firstline, List<string>? data,
1096
 
                                      string text)
1097
 
  {
1098
 
    /*
1099
 
     * Pass message to appropriate function considering the type of output
1100
 
     */
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]);
1108
 
        break;
1109
 
      case INFO_PATCH_FILE_WRITING:
1110
 
      case INFO_PATCH_FILE_PATCHING:
1111
 
        if (firstline.length > 2)
1112
 
          process_patch_file(firstline[2]);
1113
 
        break;
1114
 
      case INFO_PROGRESS:
1115
 
        process_progress(firstline);
1116
 
        break;
1117
 
      case INFO_COLLECTION_STATUS:
1118
 
        process_collection_status(data);
1119
 
        break;
1120
 
      case INFO_SYNCHRONOUS_UPLOAD_BEGIN:
1121
 
      case INFO_ASYNCHRONOUS_UPLOAD_BEGIN:
1122
 
        if (!backend.is_native())
1123
 
          set_status(_("Uploading…"));
1124
 
        break;
1125
 
      case INFO_FILE_STAT:
1126
 
        process_file_stat(firstline[2], firstline[3], data, text);
1127
 
        break;
1128
 
      }
1129
 
    }
1130
 
  }
1131
 
 
1132
 
  protected virtual void process_debug(string[] firstline, List<string>? data,
1133
 
                                       string text)
1134
 
  {
1135
 
    /*
1136
 
     * Pass message to appropriate function considering the type of output
1137
 
     */
1138
 
    if (firstline.length > 1) {
1139
 
      switch (int.parse(firstline[1])) {
1140
 
      case DEBUG_GENERIC:
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;
1147
 
          }
1148
 
        }
1149
 
        break;
1150
 
      }
1151
 
    }
1152
 
  }
1153
 
 
1154
 
  void process_file_stat(string date, string file, List<string> data, string text)
1155
 
  {
1156
 
    if (mode != Operation.Mode.LIST)
1157
 
      return;
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;
1168
 
    }
1169
 
    listed_current_files(date, file);
1170
 
  }
1171
 
  
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);
1177
 
  }
1178
 
  
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);
1184
 
  }
1185
 
  
1186
 
  void process_progress(string[] firstline)
1187
 
  {
1188
 
    double total;
1189
 
    
1190
 
    if (firstline.length > 2)
1191
 
      this.progress_count = uint64.parse(firstline[2]);
1192
 
    else
1193
 
      return;
1194
 
    
1195
 
    if (firstline.length > 3)
1196
 
      total = double.parse(firstline[3]);
1197
 
    else if (this.progress_total > 0)
1198
 
      total = this.progress_total;
1199
 
    else
1200
 
      return; // can't do progress without a total
1201
 
    
1202
 
    double percent = (double)this.progress_count / total;
1203
 
    if (percent > 1)
1204
 
      percent = 1;
1205
 
    if (percent < 0) // ???
1206
 
      percent = 0;
1207
 
    progress(percent);
1208
 
  }
1209
 
  
1210
 
  File make_file_obj(string file)
1211
 
  {
1212
 
    // All files are relative to root.
1213
 
    return slash.resolve_relative_path(file);
1214
 
  }
1215
 
  
1216
 
  void process_collection_status(List<string>? lines)
1217
 
  {
1218
 
    /*
1219
 
     * Collect output of collection status and return list of dates as strings via a signal
1220
 
     *
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.
1225
 
     */
1226
 
    if (mode != Operation.Mode.STATUS || got_collection_info)
1227
 
      return;
1228
 
    
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)
1235
 
        in_chain = true;
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]);
1244
 
          
1245
 
          var info = DateInfo();
1246
 
          info.time = timeval;
1247
 
          info.full = tokens[1] == "full";
1248
 
          infos.append(info);
1249
 
 
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";
1257
 
          }
1258
 
        }
1259
 
      }
1260
 
      else if (in_chain)
1261
 
        in_chain = false;
1262
 
    }
1263
 
 
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
1268
 
 
1269
 
    collection_dates(dates);
1270
 
  }
1271
 
  
1272
 
  protected virtual void process_warning(string[] firstline, List<string>? data,
1273
 
                                         string text)
1274
 
  {
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
1288
 
        break;
1289
 
 
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);
1301
 
          }
1302
 
        }
1303
 
        break;
1304
 
 
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);
1318
 
        }
1319
 
        break;
1320
 
      }
1321
 
    }
1322
 
  }
1323
 
  
1324
 
  void show_error(string errorstr, string? detail = null)
1325
 
  {
1326
 
    if (error_issued == false) {
1327
 
      error_issued = true;
1328
 
      raise_error(errorstr, detail);
1329
 
    }
1330
 
  }
1331
 
 
1332
 
  // Returns volume size in megs
1333
 
  int get_volsize()
1334
 
  {
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
1341
 
    // Downsides:
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
1345
 
    //     file
1346
 
    // * lots of files looks ugly to users
1347
 
    //
1348
 
    // duplicity's default is 25 (used to be 5).
1349
 
    //
1350
 
    // For local filesystems, we'll choose large volsize.
1351
 
    // For remote FSs, we'll go smaller.
1352
 
    if (in_testing_mode())
1353
 
      return 1;
1354
 
    else if (backend.is_native())
1355
 
      return 50;
1356
 
    else
1357
 
      return 25;
1358
 
  }
1359
 
 
1360
 
  void disconnect_inst()
1361
 
  {
1362
 
    /* Disconnect signals and cancel call to duplicity instance */
1363
 
    if (inst != null) {
1364
 
      inst.done.disconnect(handle_done);
1365
 
      inst.message.disconnect(handle_message);
1366
 
      inst.exited.disconnect(handle_exit);
1367
 
      inst.cancel();
1368
 
      inst = null;
1369
 
    }
1370
 
  }
1371
 
 
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)
1376
 
  { 
1377
 
    /*
1378
 
     * For passed arguments start a new duplicity instance, set duplicity in the right mode and execute command
1379
 
     */
1380
 
    /* Disconnect instance */
1381
 
    disconnect_inst();
1382
 
    
1383
 
    /* Start new duplicity instance */
1384
 
    inst = new DuplicityInstance();
1385
 
    inst.done.connect(handle_done);
1386
 
 
1387
 
    /* As duplicity's data is returned via a signal, handle_message begins post-raw stream processing */
1388
 
    inst.message.connect(handle_message);
1389
 
 
1390
 
    /* When duplicity exits, we may be also interested in its return code */
1391
 
    inst.exited.connect(handle_exit);
1392
 
 
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;
1396
 
    
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);
1401
 
 
1402
 
    /* Set duplicity into right mode */
1403
 
    if (argv_entire == null) {
1404
 
      // add operation, local, and remote args
1405
 
      switch (mode) {
1406
 
      case Operation.Mode.BACKUP:
1407
 
        if (is_full_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());
1412
 
        break;
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());
1418
 
        break;
1419
 
      case Operation.Mode.STATUS:
1420
 
        argv.prepend("collection-status");
1421
 
        argv.append(get_remote());
1422
 
        break;
1423
 
      case Operation.Mode.LIST:
1424
 
        argv.prepend("list-current-files");
1425
 
        argv.append(get_remote());
1426
 
        break;
1427
 
      }
1428
 
    }
1429
 
 
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);
1434
 
 
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 != "";
1440
 
 
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.
1448
 
    }
1449
 
    else {
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
1452
 
    }
1453
 
 
1454
 
    /* Start duplicity instance */
1455
 
    try {
1456
 
      inst.start(argv, envp, needs_root);
1457
 
    }
1458
 
    catch (Error e) {
1459
 
      show_error(e.message);
1460
 
      done(false, false, null);
1461
 
    }
1462
 
  }
1463
 
}
1464
 
 
1465
 
} // end namespace
1466