~ubuntu-branches/ubuntu/precise/deja-dup/precise-proposed

« back to all changes in this revision

Viewing changes to .pc/fix_encryption_switch.patch/common/Duplicity.vala

  • Committer: Package Import Robot
  • Author(s): Michael Terry
  • Date: 2011-11-03 17:33:20 UTC
  • Revision ID: package-import@ubuntu.com-20111103173320-fekl17yx2fu4nq25
Tags: 21.1-0ubuntu2
* debian/patches/fix_encryption_switch.patch:
  - Backport patch from trunk to fix a bad bug that breaks backups after
    an interruption for some locales.  LP: #877631

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
public 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);
 
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
  string remote;
 
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;
 
94
 
 
95
  string last_bad_volume;
 
96
  uint bad_volume_count;
 
97
  
 
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
 
101
  
 
102
  static File slash;
 
103
  static File slash_root;
 
104
  static File slash_home;
 
105
  static File slash_home_me;
 
106
  
 
107
  bool has_checked_contents = false;
 
108
  bool has_non_home_contents = false;
 
109
  List<File> homes = new List<File>();
 
110
  
 
111
  bool checked_collection_info = false;
 
112
  bool got_collection_info = false;
 
113
  struct DateInfo {
 
114
    public bool full;
 
115
    public TimeVal time;
 
116
  }
 
117
  List<DateInfo?> collection_info = null;
 
118
  
 
119
  bool checked_backup_space = false;
 
120
 
 
121
  static const int MINIMUM_FULL = 2;
 
122
  bool deleted_files = false;
 
123
  int delete_age = 0;
 
124
  
 
125
  File last_touched_file = null;
 
126
 
 
127
  void network_changed()
 
128
  {
 
129
    if (Network.get().connected)
 
130
      resume();
 
131
    else
 
132
      pause(_("Paused (no network)"));
 
133
  }
 
134
 
 
135
  public Duplicity(Operation.Mode mode) {
 
136
    Object(original_mode: mode);
 
137
  }
 
138
  
 
139
  construct {
 
140
    if (slash == null) {
 
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());
 
145
    }
 
146
  }
 
147
 
 
148
  ~Duplicity() {
 
149
    Network.get().notify["connected"].disconnect(network_changed);
 
150
  }
 
151
 
 
152
  public virtual void start(Backend backend,
 
153
                            List<string>? argv, List<string>? envp)
 
154
  {
 
155
    // save arguments for calling duplicity again later
 
156
    mode = original_mode;
 
157
    try {
 
158
      this.remote = backend.get_location();
 
159
    }
 
160
    catch (Error e) {
 
161
      raise_error(e.message, null);
 
162
      done(false, false);
 
163
      return;
 
164
    }
 
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);
 
172
    
 
173
    if (mode == Operation.Mode.BACKUP)
 
174
      process_include_excludes();
 
175
    
 
176
    var settings = get_settings();
 
177
    delete_age = settings.get_int(DELETE_AFTER_KEY);
 
178
 
 
179
    if (!restart())
 
180
      done(false, false);
 
181
 
 
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)"));
 
187
      }
 
188
    }
 
189
  }
 
190
 
 
191
  // This will treat a < b iff a is 'lower' in the file tree than b
 
192
  int cmp_prefix(File? a, File? b)
 
193
  {
 
194
    if (a == null && b == null)
 
195
      return 0;
 
196
    else if (b == null || a.has_prefix(b))
 
197
      return -1;
 
198
    else if (a == null || b.has_prefix(a))
 
199
      return 1;
 
200
    else
 
201
      return 0;
 
202
  }
 
203
 
 
204
  void expand_links_in_file(File file, ref List<File> all, bool include, List<File>? seen = null)
 
205
  {
 
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.
 
214
    //
 
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));
 
221
      iter = parent;
 
222
    }
 
223
 
 
224
    try {
 
225
      File so_far = slash;
 
226
      foreach (weak string piece in pieces) {
 
227
        parent = so_far;
 
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, 
 
232
                                     null);
 
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)
 
237
            return; // stop here
 
238
 
 
239
          if (include)
 
240
            all.prepend(so_far); // back up symlink as a leaf element of its path
 
241
 
 
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).
 
245
 
 
246
          var symlink_target = info.get_symlink_target();
 
247
          File full_target;
 
248
          if (Path.is_absolute(symlink_target))
 
249
            full_target = File.new_for_path(symlink_target);
 
250
          else
 
251
            full_target = parent.resolve_relative_path(symlink_target);
 
252
 
 
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);
 
257
 
 
258
          if (include)
 
259
            all.remove(file); // may fail if it's not there, which is fine
 
260
 
 
261
          seen.prepend(so_far);
 
262
 
 
263
          expand_links_in_file(full_target, ref all, include, seen);
 
264
          return;
 
265
        }
 
266
      }
 
267
 
 
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)
 
270
      if (seen != null)
 
271
        all.prepend(file);
 
272
    }
 
273
    catch (IOError.NOT_FOUND e) {
 
274
      // Don't bother keeping this file in the list
 
275
      all.remove(file);
 
276
    }
 
277
    catch (Error e) {
 
278
      warning("%s\n", e.message);
 
279
    }
 
280
  }
 
281
 
 
282
  void expand_links_in_list(ref List<File> all, bool include)
 
283
  {
 
284
    var all2 = all.copy();
 
285
    foreach (File file in all2)
 
286
      expand_links_in_file(file, ref all, include);
 
287
  }
 
288
 
 
289
  string escape_duplicity_path(string path)
 
290
  {
 
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.
 
295
    string rv;
 
296
    rv = path.replace("[", "[[]");
 
297
    rv =   rv.replace("?", "[?]");
 
298
    rv =   rv.replace("*", "[*]");
 
299
    return rv;
 
300
  }
 
301
 
 
302
  void process_include_excludes()
 
303
  {
 
304
    expand_links_in_list(ref includes, true);
 
305
    expand_links_in_list(ref excludes, false);
 
306
 
 
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);
 
312
 
 
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()));
 
318
          excludes.remove(e);
 
319
        }
 
320
      }
 
321
      saved_argv.append("--include=" + escape_duplicity_path(i.get_path()));
 
322
      //if (!i.has_prefix(slash_home_me))
 
323
      //  needs_root = true;
 
324
    }
 
325
    foreach (File e in excludes) {
 
326
      saved_argv.append("--exclude=" + escape_duplicity_path(e.get_path()));
 
327
    }
 
328
 
 
329
    saved_argv.append("--exclude=**");
 
330
  }
 
331
  
 
332
  public void cancel() {
 
333
    var prev_mode = mode;
 
334
    mode = Operation.Mode.INVALID;
 
335
    
 
336
    if (prev_mode == Operation.Mode.BACKUP && state == State.NORMAL) {
 
337
      if (cleanup())
 
338
        return;
 
339
    }
 
340
    
 
341
    cancel_inst();
 
342
  }
 
343
  
 
344
  public void stop() {
 
345
    // just abruptly stop, without a cleanup, duplicity will resume
 
346
    was_stopped = true;
 
347
    mode = Operation.Mode.INVALID;
 
348
    cancel_inst();
 
349
  }
 
350
 
 
351
  public void pause(string? reason)
 
352
  {
 
353
    if (inst != null) {
 
354
      inst.pause();
 
355
      if (reason != null)
 
356
        set_status(reason, false);
 
357
    }
 
358
  }
 
359
 
 
360
  public void resume()
 
361
  {
 
362
    if (inst != null) {
 
363
      inst.resume();
 
364
      set_saved_status();
 
365
    }
 
366
  }
 
367
 
 
368
  void cancel_inst()
 
369
  {
 
370
    disconnect_inst();
 
371
    handle_done(null, false, true);
 
372
  }
 
373
 
 
374
  bool restart()
 
375
  {
 
376
    state = State.NORMAL;
 
377
    
 
378
    if (mode == Operation.Mode.INVALID)
 
379
      return false;
 
380
    
 
381
    var extra_argv = new List<string>();
 
382
    string action_desc = null;
 
383
    File custom_local = null;
 
384
    
 
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…");
 
393
      }
 
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");
 
401
      }
 
402
      else if (!checked_backup_space) {
 
403
        check_backup_space();
 
404
        return true;
 
405
      }
 
406
      else {
 
407
        if (has_progress_total)
 
408
          progress(0f);
 
409
      }
 
410
      
 
411
      break;
 
412
    case Operation.Mode.RESTORE:
 
413
      // We need to first check the backup status to see if we should use
 
414
      // encryption.
 
415
      if (!checked_collection_info) {
 
416
        mode = Operation.Mode.STATUS;
 
417
        state = State.STATUS;
 
418
        action_desc = _("Preparing…");
 
419
      }
 
420
      else if (!has_checked_contents) {
 
421
        mode = Operation.Mode.LIST;
 
422
        state = State.CHECK_CONTENTS;
 
423
        action_desc = _("Preparing…");
 
424
      }
 
425
      else {
 
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));
 
439
          }
 
440
        }
 
441
        
 
442
        if (restore_files != null) {
 
443
          // Just do first one.  Others will come when we're done
 
444
          
 
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()));
 
452
            return false;
 
453
          }
 
454
 
 
455
          if (!local_file.has_prefix(slash_home_me))
 
456
            needs_root = true;
 
457
          
 
458
          try {
 
459
            // won't have correct permissions...
 
460
            local_file.make_directory_with_parents(null);
 
461
          }
 
462
          catch (IOError.EXISTS e) {
 
463
            // ignore
 
464
          }
 
465
          catch (Error e) {
 
466
            show_error(e.message);
 
467
            return false;
 
468
          }
 
469
          custom_local = local_file;
 
470
          
 
471
          var rel_file_path = slash.get_relative_path(restore_files.data);
 
472
          extra_argv.append("--file-to-restore=%s".printf(rel_file_path));
 
473
        }
 
474
        else {
 
475
          if (has_non_home_contents && !this.local.has_prefix(slash_home_me))
 
476
            needs_root = true;
 
477
        }
 
478
        
 
479
        progress(0f);
 
480
      }
 
481
      break;
 
482
    }
 
483
    
 
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);
 
489
    
 
490
    connect_and_start(extra_argv, null, null, custom_local);
 
491
    return true;
 
492
  }
 
493
  
 
494
  File? make_local_rel_path(File file)
 
495
  {
 
496
    string rel_file_path = slash.get_relative_path(file);
 
497
    if (rel_file_path == null)
 
498
      return null;
 
499
    return local.resolve_relative_path(rel_file_path);
 
500
  }
 
501
  
 
502
  async void check_backup_space()
 
503
  {
 
504
    checked_backup_space = true;
 
505
 
 
506
    if (!has_progress_total) {
 
507
      if (!restart())
 
508
        done(false, false);
 
509
      return;
 
510
    }
 
511
 
 
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."));
 
517
        return;
 
518
    }
 
519
 
 
520
    if (free < progress_total) {
 
521
      if (got_collection_info) {
 
522
        // Alright, let's look at collection data
 
523
        int full_dates = 0;
 
524
        foreach (DateInfo info in collection_info) {
 
525
          if (info.full)
 
526
            ++full_dates;
 
527
        }
 
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;
 
534
          return;
 
535
        }
 
536
      }
 
537
      else {
 
538
        show_error(_("Backup location does not have enough free space."));
 
539
        return;
 
540
      }
 
541
    }
 
542
    
 
543
    if (!restart())
 
544
      done(false, false);
 
545
  }
 
546
 
 
547
  bool cleanup() {
 
548
    if (state == State.CLEANUP)
 
549
      return false;
 
550
    
 
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);
 
556
    
 
557
    set_status(_("Cleaning up…"));
 
558
    connect_and_start(null, null, cleanup_argv);
 
559
    
 
560
    return true;
 
561
  }
 
562
  
 
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);
 
570
    
 
571
    set_status(_("Cleaning up…"));
 
572
    connect_and_start(null, null, argv);
 
573
    
 
574
    return;
 
575
  }
 
576
  
 
577
  bool can_ignore_error()
 
578
  {
 
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;
 
585
  }
 
586
 
 
587
  void handle_done(DuplicityInstance? inst, bool success, bool cancelled)
 
588
  {
 
589
    if (can_ignore_error())
 
590
      success = true;
 
591
 
 
592
    if (!cancelled && success) {
 
593
      switch (state) {
 
594
      case State.DRY_RUN:
 
595
        has_progress_total = true;
 
596
        progress_total = progress_count; // save max progress for next run
 
597
        if (restart())
 
598
          return;
 
599
        break;
 
600
      
 
601
      case State.DELETE:
 
602
        if (restart()) // In case we were interrupting normal flow
 
603
          return;
 
604
        break;
 
605
      
 
606
      case State.CLEANUP:
 
607
        cleaned_up_once = true;
 
608
        if (restart()) // restart in case cleanup was interrupting normal flow
 
609
          return;
 
610
        
 
611
        // Else, we probably started cleaning up after a cancel.  Just continue
 
612
        // that cancels
 
613
        cancelled = true;
 
614
        break;
 
615
      
 
616
      case State.STATUS:
 
617
        checked_collection_info = true;
 
618
        var should_restart = mode != original_mode;
 
619
        mode = original_mode;
 
620
 
 
621
        /* Set full backup threshold and determine whether we should trigger
 
622
           a full backup. */
 
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) {
 
627
            if (info.full)
 
628
              full_backup.set_time_val(info.time);
 
629
          }
 
630
          if (!full_backup.valid() || threshold.compare(full_backup) > 0) {
 
631
            is_full_backup = true;
 
632
            is_full(!full_backup.valid());
 
633
          }
 
634
        }
 
635
 
 
636
        if (should_restart) {
 
637
          if (restart())
 
638
            return;
 
639
        }
 
640
        break;
 
641
      
 
642
      case State.CHECK_CONTENTS:
 
643
        has_checked_contents = true;
 
644
        mode = original_mode;
 
645
        
 
646
        if (restart())
 
647
          return;
 
648
        break;
 
649
      
 
650
      case State.NORMAL:
 
651
        if (mode == Operation.Mode.RESTORE && restore_files != null) {
 
652
          _restore_files.delete_link(_restore_files);
 
653
          if (restore_files != null) {
 
654
            if (restart())
 
655
              return;
 
656
          }
 
657
        }
 
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())
 
661
            return;
 
662
        }
 
663
        break;
 
664
      }
 
665
    }
 
666
    else if (was_stopped)
 
667
      success = true; // we treat stops as success
 
668
    
 
669
    if (error_issued)
 
670
      success = false;
 
671
    
 
672
    if (!success && !cancelled && !error_issued)
 
673
      show_error(_("Failed with an unknown error."));
 
674
    
 
675
    inst = null;
 
676
    done(success, cancelled);
 
677
  }
 
678
  
 
679
  string saved_status;
 
680
  File saved_status_file;
 
681
  bool saved_status_file_action;
 
682
  void set_status(string msg, bool save = true)
 
683
  {
 
684
    if (save) {
 
685
      saved_status = msg;
 
686
      saved_status_file = null;
 
687
    }
 
688
    action_desc_changed(msg);
 
689
  }
 
690
 
 
691
  void set_status_file(File file, bool action, bool save = true)
 
692
  {
 
693
    if (save) {
 
694
      saved_status = null;
 
695
      saved_status_file = file;
 
696
      saved_status_file_action = action;
 
697
    }
 
698
    action_file_changed(file, action);
 
699
  }
 
700
 
 
701
  void set_saved_status()
 
702
  {
 
703
    if (saved_status != null)
 
704
      set_status(saved_status, false);
 
705
    else
 
706
      set_status_file(saved_status_file, saved_status_file_action, false);
 
707
  }
 
708
 
 
709
  // Should only be called *after* a successful backup
 
710
  bool delete_files_if_needed()
 
711
  {
 
712
    if (delete_age == 0) {
 
713
      deleted_files = true;
 
714
      return false;
 
715
    }
 
716
    
 
717
    // Check if we need to delete any backups
 
718
    // If we got collection info, examine it to see if we should delete old
 
719
    // files.
 
720
    if (got_collection_info && !deleted_files) {
 
721
      // Alright, let's look at collection data
 
722
      int full_dates = 0;
 
723
      TimeVal prev_time = TimeVal();
 
724
      Date prev_date = Date();
 
725
      int too_old = 0;
 
726
      TimeVal now = TimeVal();
 
727
      now.get_current_time();
 
728
 
 
729
      Date today = Date();
 
730
      today.set_time_val(now);
 
731
      
 
732
      foreach (DateInfo info in collection_info) {
 
733
        if (info.full) {
 
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)
 
737
              ++too_old;
 
738
          }
 
739
          ++full_dates;
 
740
        }
 
741
        prev_time = info.time;
 
742
      }
 
743
      prev_date.set_time_val(prev_time); // compare last incremental backup
 
744
      if (prev_date.days_between(today) > delete_age)
 
745
        ++too_old;
 
746
      
 
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.
 
750
      if (is_full_backup)
 
751
        ++full_dates;
 
752
 
 
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);
 
757
        return true;
 
758
      }
 
759
      
 
760
      // If we don't need to delete, pretend we did and move on.
 
761
      deleted_files = true;
 
762
      return false;
 
763
    }
 
764
    else
 
765
      return false;
 
766
  }
 
767
 
 
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;
 
796
 
 
797
  void delete_cache()
 
798
  {
 
799
    string dir = Environment.get_user_cache_dir();
 
800
    if (dir == null)
 
801
      return;
 
802
 
 
803
    var cachedir = Path.build_filename(dir, Config.PACKAGE);
 
804
    var del = new RecursiveDelete(File.new_for_path(cachedir));
 
805
    del.start();
 
806
  }
 
807
 
 
808
  bool restarted_without_cache = false;
 
809
  bool restart_without_cache()
 
810
  {
 
811
    if (restarted_without_cache)
 
812
      return false;
 
813
 
 
814
    restarted_without_cache = true;
 
815
 
 
816
    delete_cache();
 
817
    return restart();
 
818
  }
 
819
 
 
820
  void handle_exit(int code)
 
821
  {
 
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();
 
830
    }
 
831
  }
 
832
 
 
833
  void handle_message(DuplicityInstance inst, string[] control_line,
 
834
                      List<string>? data_lines, string user_text)
 
835
  {
 
836
    /*
 
837
     * Based on duplicity's output handle message as either process data as error, info or warning
 
838
     */
 
839
    if (control_line.length == 0)
 
840
      return;
 
841
    
 
842
    var keyword = control_line[0];
 
843
    switch (keyword) {
 
844
    case "ERROR":
 
845
      process_error(control_line, data_lines, user_text);
 
846
      break;
 
847
    case "INFO":
 
848
      process_info(control_line, data_lines, user_text);
 
849
      break;
 
850
    case "WARNING":
 
851
      process_warning(control_line, data_lines, user_text);
 
852
      break;
 
853
    case "DEBUG":
 
854
      process_debug(control_line, data_lines, user_text);
 
855
      break;
 
856
    }
 
857
  }
 
858
  
 
859
  bool ask_question(string t, string m)
 
860
  {
 
861
    disconnect_inst();
 
862
    question(t, m);
 
863
    var rv = mode != Operation.Mode.INVALID; // return whether we were canceled
 
864
    if (!rv)
 
865
      handle_done(null, false, true);
 
866
    return rv;
 
867
  }
 
868
 
 
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)
 
872
  {
 
873
    int next = 0;
 
874
    while (skip_bits-- > 0 && next >= 0)
 
875
      next = file.index_of_char('.', next) + 1;
 
876
    if (next < 0)
 
877
      return "";
 
878
    else
 
879
      return file.substring(next);
 
880
  }
 
881
 
 
882
  protected virtual void process_error(string[] firstline, List<string>? data,
 
883
                                       string text_in)
 
884
  {
 
885
    string text = text_in;
 
886
    
 
887
    if (can_ignore_error())
 
888
      return;
 
889
    
 
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);
 
894
        return;
 
895
 
 
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());
 
903
        break;
 
904
 
 
905
      case ERROR_GPG:
 
906
        bad_encryption_password(); // notify upper layers, if they want to do anything
 
907
        text = _("Bad encryption password.");
 
908
        break;
 
909
 
 
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])))
 
913
            return;
 
914
        }
 
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");
 
918
        if (restart())
 
919
          return;
 
920
        break;
 
921
 
 
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
 
927
        // do about it.
 
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;
 
934
          }
 
935
 
 
936
          if ((bad_volume_count == 0 && restart()) ||
 
937
              (bad_volume_count == 1 && cleanup())) {
 
938
            bad_volume_count += 1;
 
939
            return;
 
940
          }
 
941
        }
 
942
        break;
 
943
 
 
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());
 
948
        }
 
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());
 
952
        }
 
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());
 
956
        }
 
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());
 
960
        }
 
961
        break;
 
962
 
 
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());
 
967
        }
 
968
        break;
 
969
 
 
970
      case ERROR_BACKEND_NO_SPACE:
 
971
        if (firstline.length >= 5) {
 
972
          text = _("No space left.");
 
973
        }
 
974
        break;
 
975
      }
 
976
    }
 
977
    
 
978
    show_error(text);
 
979
  }
 
980
  
 
981
  void process_exception(string exception, string text)
 
982
  {
 
983
    switch (exception) {
 
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."));
 
991
      break;
 
992
    case "S3CreateError":
 
993
      if (text.contains("<Code>BucketAlreadyExists</Code>")) {
 
994
        if (((BackendS3)backend).bump_bucket()) {
 
995
          try {
 
996
            remote = backend.get_location();
 
997
            if (restart())
 
998
              return;
 
999
          }
 
1000
          catch (Error e) {warning("%s\n", e.message);}
 
1001
        }
 
1002
        
 
1003
        show_error(_("S3 bucket name is not available."));
 
1004
      }
 
1005
      break;
 
1006
    case "EOFError":
 
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."));
 
1010
      break;
 
1011
    case "IOError":
 
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()));
 
1018
        else
 
1019
          show_error(_("Error writing file ‘%s’.").printf(last_touched_file.get_parse_name()));
 
1020
      }
 
1021
      else if (text.contains("[Errno 28]")) { // No space left on device
 
1022
        string where = null;
 
1023
        if (mode == Operation.Mode.BACKUP) {
 
1024
          try {
 
1025
            where = backend.get_location_pretty();
 
1026
          }
 
1027
          catch (Error e) {warning("%s\n", e.message);}
 
1028
        }
 
1029
        else
 
1030
          where = local.get_path();
 
1031
        if (where == null)
 
1032
          show_error(_("No space left."));
 
1033
        else
 
1034
          show_error(_("No space left in ‘%s’.").printf(where));
 
1035
      }
 
1036
      else if (text.contains("CRC check failed")) { // bug 676767
 
1037
        if (restart_without_cache())
 
1038
          return;
 
1039
      }
 
1040
      break;
 
1041
    case "CollectionsError":
 
1042
      show_error(_("No backup files found"));
 
1043
      break;
 
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())
 
1050
          return;
 
1051
      }
 
1052
      break;
 
1053
    }
 
1054
    
 
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);
 
1063
  }
 
1064
  
 
1065
  protected virtual void process_info(string[] firstline, List<string>? data,
 
1066
                                      string text)
 
1067
  {
 
1068
    /*
 
1069
     * Pass message to appropriate function considering the type of output
 
1070
     */
 
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]);
 
1078
        break;
 
1079
      case INFO_PATCH_FILE_WRITING:
 
1080
      case INFO_PATCH_FILE_PATCHING:
 
1081
        if (firstline.length > 2)
 
1082
          process_patch_file(firstline[2]);
 
1083
        break;
 
1084
      case INFO_PROGRESS:
 
1085
        process_progress(firstline);
 
1086
        break;
 
1087
      case INFO_COLLECTION_STATUS:
 
1088
        process_collection_status(data);
 
1089
        break;
 
1090
      case INFO_SYNCHRONOUS_UPLOAD_BEGIN:
 
1091
      case INFO_ASYNCHRONOUS_UPLOAD_BEGIN:
 
1092
        if (!backend.is_native())
 
1093
          set_status(_("Uploading…"));
 
1094
        break;
 
1095
      case INFO_FILE_STAT:
 
1096
        process_file_stat(firstline[2], firstline[3], data, text);
 
1097
        break;
 
1098
      }
 
1099
    }
 
1100
  }
 
1101
 
 
1102
  protected virtual void process_debug(string[] firstline, List<string>? data,
 
1103
                                       string text)
 
1104
  {
 
1105
    /*
 
1106
     * Pass message to appropriate function considering the type of output
 
1107
     */
 
1108
    if (firstline.length > 1) {
 
1109
      switch (int.parse(firstline[1])) {
 
1110
      case DEBUG_GENERIC:
 
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'");
 
1121
        }
 
1122
        break;
 
1123
      }
 
1124
    }
 
1125
  }
 
1126
 
 
1127
  void process_file_stat(string date, string file, List<string> data, string text)
 
1128
  {
 
1129
    if (mode != Operation.Mode.LIST)
 
1130
      return;
 
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;
 
1141
    }
 
1142
    listed_current_files(date, file);
 
1143
  }
 
1144
  
 
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);
 
1150
  }
 
1151
  
 
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);
 
1157
  }
 
1158
  
 
1159
  void process_progress(string[] firstline)
 
1160
  {
 
1161
    double total;
 
1162
    
 
1163
    if (firstline.length > 2)
 
1164
      this.progress_count = uint64.parse(firstline[2]);
 
1165
    else
 
1166
      return;
 
1167
    
 
1168
    if (firstline.length > 3)
 
1169
      total = double.parse(firstline[3]);
 
1170
    else if (this.progress_total > 0)
 
1171
      total = this.progress_total;
 
1172
    else
 
1173
      return; // can't do progress without a total
 
1174
    
 
1175
    double percent = (double)this.progress_count / total;
 
1176
    if (percent > 1)
 
1177
      percent = 1;
 
1178
    if (percent < 0) // ???
 
1179
      percent = 0;
 
1180
    progress(percent);
 
1181
  }
 
1182
  
 
1183
  File make_file_obj(string file)
 
1184
  {
 
1185
    // All files are relative to root.
 
1186
    return slash.resolve_relative_path(file);
 
1187
  }
 
1188
  
 
1189
  void process_collection_status(List<string>? lines)
 
1190
  {
 
1191
    /*
 
1192
     * Collect output of collection status and return list of dates as strings via a signal
 
1193
     *
 
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.
 
1198
     */
 
1199
    if (mode != Operation.Mode.STATUS || got_collection_info)
 
1200
      return;
 
1201
    
 
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)
 
1208
        in_chain = true;
 
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]);
 
1217
          
 
1218
          var info = DateInfo();
 
1219
          info.time = timeval;
 
1220
          info.full = tokens[1] == "full";
 
1221
          infos.append(info);
 
1222
 
 
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";
 
1230
          }
 
1231
        }
 
1232
      }
 
1233
      else if (in_chain)
 
1234
        in_chain = false;
 
1235
    }
 
1236
 
 
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
 
1241
 
 
1242
    collection_dates(dates);
 
1243
  }
 
1244
  
 
1245
  protected virtual void process_warning(string[] firstline, List<string>? data,
 
1246
                                         string text)
 
1247
  {
 
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
 
1261
      break;
 
1262
      }
 
1263
    }
 
1264
  }
 
1265
  
 
1266
  void show_error(string errorstr, string? detail = null)
 
1267
  {
 
1268
    if (error_issued == false) {
 
1269
      error_issued = true;
 
1270
      raise_error(errorstr, detail);
 
1271
    }
 
1272
  }
 
1273
 
 
1274
  // Returns volume size in megs
 
1275
  int get_volsize()
 
1276
  {
 
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
 
1283
    // Downsides:
 
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
 
1287
    //     file
 
1288
    // * lots of files looks ugly to users
 
1289
    //
 
1290
    // duplicity's default is 25 (used to be 5).
 
1291
    //
 
1292
    // For local filesystems, we'll choose large volsize.
 
1293
    // For remote FSs, we'll go smaller.
 
1294
    if (in_testing_mode())
 
1295
      return 1;
 
1296
    else if (backend.is_native())
 
1297
      return 50;
 
1298
    else
 
1299
      return 25;
 
1300
  }
 
1301
 
 
1302
  void disconnect_inst()
 
1303
  {
 
1304
    /* Disconnect signals and cancel call to duplicity instance */
 
1305
    if (inst != null) {
 
1306
      inst.done.disconnect(handle_done);
 
1307
      inst.message.disconnect(handle_message);
 
1308
      inst.exited.disconnect(handle_exit);
 
1309
      inst.cancel();
 
1310
      inst = null;
 
1311
    }
 
1312
  }
 
1313
 
 
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)
 
1318
  { 
 
1319
    /*
 
1320
     * For passed arguments start a new duplicity instance, set duplicity in the right mode and execute command
 
1321
     */
 
1322
    /* Disconnect instance */
 
1323
    disconnect_inst();
 
1324
    
 
1325
    /* Start new duplicity instance */
 
1326
    inst = new DuplicityInstance();
 
1327
    inst.done.connect(handle_done);
 
1328
 
 
1329
    /* As duplicity's data is returned via a signal, handle_message begins post-raw stream processing */
 
1330
    inst.message.connect(handle_message);
 
1331
 
 
1332
    /* When duplicity exits, we may be also interested in its return code */
 
1333
    inst.exited.connect(handle_exit);
 
1334
 
 
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;
 
1338
    
 
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);
 
1343
 
 
1344
    /* Set duplicity into right mode */
 
1345
    if (argv_entire == null) {
 
1346
      // add operation, local, and remote args
 
1347
      switch (mode) {
 
1348
      case Operation.Mode.BACKUP:
 
1349
        if (is_full_backup)
 
1350
          argv.prepend("full");
 
1351
        argv.append("--volsize=%d".printf(get_volsize()));
 
1352
        argv.append(local_arg.get_path());
 
1353
        argv.append(remote);
 
1354
        break;
 
1355
      case Operation.Mode.RESTORE:
 
1356
        argv.prepend("restore");
 
1357
        argv.append("--force");
 
1358
        argv.append(remote);
 
1359
        argv.append(local_arg.get_path());
 
1360
        break;
 
1361
      case Operation.Mode.STATUS:
 
1362
        argv.prepend("collection-status");
 
1363
        argv.append(remote);
 
1364
        break;
 
1365
      case Operation.Mode.LIST:
 
1366
        argv.prepend("list-current-files");
 
1367
        argv.append(remote);
 
1368
        break;
 
1369
      }
 
1370
    }
 
1371
 
 
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);
 
1376
 
 
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 != "";
 
1382
 
 
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.
 
1390
    }
 
1391
    else {
 
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
 
1394
    }
 
1395
 
 
1396
    /* Start duplicity instance */
 
1397
    try {
 
1398
      inst.start(argv, envp, needs_root);
 
1399
    }
 
1400
    catch (Error e) {
 
1401
      show_error(e.message);
 
1402
      done(false, false);
 
1403
    }
 
1404
  }
 
1405
}
 
1406
 
 
1407
} // end namespace
 
1408