mclone 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +8 -4
  3. data/README.md +409 -369
  4. data/bin/mclone +23 -8
  5. data/lib/mclone.rb +223 -126
  6. metadata +3 -3
data/bin/mclone CHANGED
@@ -13,7 +13,9 @@ Clamp do
13
13
 
14
14
  using Refinements
15
15
 
16
- option ['-f', '--force'], :flag, 'Insist on potentially dangerous activities', default: false
16
+ self.default_subcommand = 'info'
17
+
18
+ option ['-f', '--force'], :flag, 'Insist on potentially dangerous actions', default: false
17
19
  option ['-n', '--dry-run'], :flag, 'Simulation mode with no on-disk modifications', default: false
18
20
  option ['-v', '--verbose'], :flag, 'Verbose operation', default: false
19
21
 
@@ -46,9 +48,7 @@ Clamp do
46
48
  $stdout.puts
47
49
  $stdout.puts '## Volumes'
48
50
  $stdout.puts
49
- s.volumes.each do |volume|
50
- $stdout.puts "* [#{volume.id}] :: (#{volume.root})"
51
- end
51
+ s.volumes.each { |volume| $stdout.puts "* [#{volume.id}] :: (#{volume.root})" }
52
52
  stales = []
53
53
  intacts = []
54
54
  intact_tasks = s.intact_tasks
@@ -56,7 +56,8 @@ Clamp do
56
56
  ts = (t = intact_tasks[task]).nil? ? "<#{task.id}>" : "[#{task.id}]"
57
57
  svs = s.volumes.volume(task.source_id).nil? ? "<#{task.source_id}>" : "[#{task.source_id}]"
58
58
  dvs = s.volumes.volume(task.destination_id).nil? ? "<#{task.destination_id}>" : "[#{task.destination_id}]"
59
- xs = ["* #{ts} :: #{task.mode} #{svs}(#{task.source_root}) -> #{dvs}(#{task.destination_root})"]
59
+ crypter_mode = task.crypter_mode.nil? ? nil : "#{task.crypter_mode}+"
60
+ xs = ["* #{ts} :: #{crypter_mode}#{task.mode} #{svs}(#{task.source_root}) -> #{dvs}(#{task.destination_root})"]
60
61
  xs << "include #{task.include}" unless task.include.nil? || task.include.empty?
61
62
  xs << "exclude #{task.exclude}" unless task.exclude.nil? || task.exclude.empty?
62
63
  (t.nil? ? stales : intacts) << xs.join(' :: ')
@@ -94,21 +95,35 @@ Clamp do
94
95
 
95
96
  end
96
97
 
97
- subcommand ['task'], 'Task operations' do
98
+ subcommand 'task', 'Task operations' do
98
99
 
99
100
  def self.set_task_opts
100
101
  modes = Task::MODES.collect(&:to_s).join(' | ')
101
102
  option ['-m', '--mode'], 'MODE', "Operation mode (#{modes})", default: Task::MODES.first.to_s
102
- option ['-i', '--include'], 'PATTERN', 'Include paths pattern', default: '**'
103
+ option ['-i', '--include'], 'PATTERN', 'Include paths pattern'
103
104
  option ['-x', '--exclude'], 'PATTERN', 'Exclude paths pattern'
104
105
  end
105
106
 
106
107
  subcommand ['new', 'create'], 'Create new SOURCE -> DESTINATION task' do
107
108
  set_task_opts
109
+ option ['-d', '--decrypt'], :flag, 'Decrypt source'
110
+ option ['-e', '--encrypt'], :flag, 'Encrypt destination'
111
+ option ['-p', '--password'], 'PASSWORD', 'Plain text password'
112
+ option ['-t', '--token'], 'TOKEN', 'Rclone crypt token (obscured password)'
108
113
  parameter 'SOURCE', 'Source path'
109
114
  parameter 'DESTINATION', 'Destination path'
110
115
  def execute
111
- session.create_task!(source, destination, mode: resolve_mode(mode), include: include, exclude: exclude).commit!
116
+ crypter_mode = nil
117
+ signal_usage_error 'choose either encryption or decryption mode, not both' if decrypt? && encrypt?
118
+ signal_usage_error 'specify either plain text password or Rclone crypt token, not both' if !password.nil? && !token.nil?
119
+ crypter_mode = :encrypt if encrypt?
120
+ crypter_mode = :decrypt if decrypt?
121
+ session.create_task!(
122
+ resolve_mode(mode),
123
+ source,
124
+ destination,
125
+ include: include, exclude: exclude, crypter_mode: crypter_mode, crypter_password: password, crypter_token: token
126
+ ).commit!
112
127
  end
113
128
  end
114
129
 
data/lib/mclone.rb CHANGED
@@ -3,7 +3,9 @@
3
3
 
4
4
  require 'date'
5
5
  require 'json'
6
+ require 'open3'
6
7
  require 'fileutils'
8
+ require 'shellwords'
7
9
  require 'securerandom'
8
10
 
9
11
 
@@ -11,13 +13,11 @@ require 'securerandom'
11
13
  module Mclone
12
14
 
13
15
 
14
- VERSION = '0.1.1'
16
+ VERSION = '0.2.0'
15
17
 
16
18
 
17
19
  #
18
- class Error < StandardError
19
-
20
- end
20
+ class Error < StandardError; end
21
21
 
22
22
  #
23
23
  module Refinements
@@ -41,6 +41,11 @@ module Mclone
41
41
  end
42
42
  end
43
43
 
44
+ refine ::String do
45
+ def escape
46
+ Mclone.windows? && %r![^\w\-\=\\\/:]!.match?(self) ? %("#{self}") : shellescape
47
+ end
48
+ end
44
49
  end
45
50
 
46
51
 
@@ -67,12 +72,31 @@ module Mclone
67
72
  @objects.empty?
68
73
  end
69
74
 
75
+ #
76
+ def size
77
+ @objects.size
78
+ end
79
+
70
80
  def initialize
71
81
  @ids = {} # { id => object }
72
82
  @objects = {} # { object => object }
73
83
  @modified = false
74
84
  end
75
85
 
86
+ protected attr_reader :objects
87
+
88
+ #
89
+ def hash
90
+ @objects.hash
91
+ end
92
+
93
+ #
94
+ def eql?(other)
95
+ equal?(other) || objects == other.objects
96
+ end
97
+
98
+ alias == eql?
99
+
76
100
  # Return ID of the object considered equal to the specified obj or nil
77
101
  def id(obj)
78
102
  @objects[obj]&.id
@@ -156,6 +180,9 @@ module Mclone
156
180
  #
157
181
  attr_reader :include, :exclude
158
182
 
183
+ #
184
+ attr_reader :crypter_mode
185
+
159
186
  def hash
160
187
  @hash ||= source_id.hash ^ destination_id.hash ^ source_root.hash ^ destination_root.hash
161
188
  end
@@ -172,85 +199,139 @@ module Mclone
172
199
  alias == eql?
173
200
 
174
201
  #
175
- def initialize(source_id, source_root, destination_id, destination_root)
202
+ def initialize(session, mode, source_id, source_root, destination_id, destination_root, include: nil, exclude: nil, crypter_mode: nil, crypter_token: nil, crypter_password: nil)
176
203
  @touch = false # Indicates that the time stamp should be updated whenever state of self is altered
204
+ @session = session
177
205
  @id = SecureRandom.hex(4)
178
206
  @source_id = source_id
179
207
  @destination_id = destination_id
180
208
  @source_root = source_root
181
209
  @destination_root = destination_root
182
- self.mode = :update
183
- self.include = '**'
184
- self.exclude = ''
210
+ self.mode = mode
211
+ self.include = include
212
+ self.exclude = exclude
213
+ set_crypter_mode crypter_mode
214
+ unless crypter_mode.nil?
215
+ raise(Task::Error, %(either Rclone crypt token or plain text password is expected, not both)) if !crypter_token.nil? && !crypter_password.nil?
216
+ @assigned_token = register_crypter_token crypter_token
217
+ @assigned_password = crypter_password
218
+ end
185
219
  ensure
186
220
  @touch = true
187
221
  touch!
188
222
  end
189
223
 
224
+ CRYPTER_MODES = %i[encrypt decrypt].freeze
225
+
226
+ @@crypter_tokens = {}
227
+
228
+ private def set_crypter_mode(mode)
229
+ @crypter_mode = mode.nil? ? nil : (CRYPTER_MODES.include?(mode = mode.intern) ? mode : raise(Task::Error, %(unknown crypt mode "#{mode}")))
230
+ end
231
+
232
+ private def register_crypter_token(token)
233
+ unless token.nil?
234
+ @@crypter_tokens[id] = @@crypter_tokens[id].nil? ? token : raise(Task::Error, %(attempt to re-register token for task "#{id}"))
235
+ end
236
+ token
237
+ end
238
+
239
+ # Lazily determine the crypt token from either assigned values or the token repository
240
+ def crypter_token
241
+ # Locally assigned token takes precedence over the repository's
242
+ unless @assigned_token.nil?
243
+ @@crypter_tokens[id] = @assigned_token unless @@crypter_tokens[id].nil? # Assign repository entry with this local token if not yet assigned
244
+ @assigned_token
245
+ else
246
+ if @@crypter_tokens[id].nil?
247
+ # If token is neither locally assigned nor in repository, try to construct it from the user-supplied password
248
+ # If a user-supplied password is omitted, create a new randomly generated password
249
+ args = [Mclone.rclone, 'obscure', @assigned_password.nil? ? SecureRandom.alphanumeric(16) : @assigned_password]
250
+ $stdout << args.collect(&:escape).join(' ') << "\n" if @session.verbose?
251
+ stdout, status = Open3.capture2(*args)
252
+ raise(Task::Error, %(Rclone execution failure)) unless status.success?
253
+ @@crypter_tokens[id] = stdout.strip
254
+ else
255
+ @@crypter_tokens[id]
256
+ end
257
+ end
258
+ end
259
+
190
260
  #
191
- MODES = [:update, :synchronize, :copy, :move].freeze
261
+ MODES = %i[update synchronize copy move].freeze
192
262
 
193
263
  #
194
264
  def mode=(mode)
195
- unless mode.nil?
196
- @mode = MODES.include?(mode = mode.intern) ? mode : raise(Task::Error, %(unknown mode "#{mode}"))
197
- touch!
198
- end
265
+ @mode = MODES.include?(mode = mode.intern) ? mode : raise(Task::Error, %(unknown mode "#{mode}"))
266
+ touch!
267
+ mode
199
268
  end
200
269
 
201
270
  #
202
271
  def include=(mask)
203
- unless mask.nil?
204
- @include = mask # TODO extensive verification
205
- touch!
206
- end
272
+ @include = mask.nil? || mask == '-' ? nil : mask # TODO: verify mask
273
+ touch!
274
+ mask
207
275
  end
208
276
 
209
277
  #
210
278
  def exclude=(mask)
211
- unless mask.nil?
212
- @exclude = mask # TODO extensive verification
213
- touch!
214
- end
279
+ @exclude = mask.nil? || mask == '-' ? nil : mask # TODO: verify mask
280
+ touch!
281
+ mask
215
282
  end
216
283
 
217
284
  #
218
- def self.restore(hash)
285
+ def self.restore(session, hash)
219
286
  obj = allocate
220
- obj.send(:from_h, hash)
287
+ obj.send(:from_h, session, hash)
221
288
  obj
222
289
  end
223
290
 
224
291
  #
225
- private def from_h(hash)
226
- initialize(hash.extract(:source, :volume), hash.extract(:source, :root), hash.extract(:destination, :volume), hash.extract(:destination, :root))
292
+ private def from_h(session, hash)
293
+ @session = session
227
294
  @touch = false
228
295
  @id = hash.extract(:task)
296
+ @mtime = DateTime.parse(hash.extract(:mtime)) rescue DateTime.now # Deleting mtime entry from json can be used to modify data out of mclone
297
+ @source_id = hash.extract(:source, :volume)
298
+ @destination_id = hash.extract(:destination, :volume)
299
+ @source_root = hash.dig(:source, :root)
300
+ @destination_root = hash.dig(:destination, :root)
229
301
  self.mode = hash.extract(:mode)
230
- # Deleting the mtime key from json
231
- @mtime = DateTime.parse(hash.extract(:mtime)) rescue @mtime
232
- self.include = hash.extract(:include) rescue nil
233
- self.exclude = hash.extract(:exclude) rescue nil
302
+ self.include = hash.dig(:include)
303
+ self.exclude = hash.dig(:exclude)
304
+ set_crypter_mode hash.dig(:crypter, :mode)
305
+ @assigned_token = register_crypter_token(hash.dig(:crypter, :token)) unless crypter_mode.nil?
234
306
  ensure
235
307
  @touch = true
236
308
  end
237
309
 
238
310
  #
239
- def to_h
240
- {
241
- mode: mode,
242
- include: include,
243
- exclude: exclude,
311
+ def to_h(volume)
312
+ hash = {
244
313
  task: id,
245
- source: {volume: source_id, root: source_root},
246
- destination: {volume: destination_id, root: destination_root},
247
- mtime: mtime
314
+ mode: mode,
315
+ mtime: mtime,
316
+ source: { volume: source_id },
317
+ destination: { volume: destination_id }
248
318
  }
319
+ hash[:source][:root] = source_root unless source_root.nil? || source_root.empty?
320
+ hash[:destination][:root] = destination_root unless destination_root.nil? || destination_root.empty?
321
+ hash[:include] = include unless include.nil?
322
+ hash[:exclude] = exclude unless exclude.nil?
323
+ unless crypter_mode.nil?
324
+ crypter = hash[:crypter] = { mode: crypter_mode }
325
+ # Make sure the token won't get into the encrypted volume's task
326
+ crypter[:token] = crypter_token if (crypter_mode == :encrypt && source_id == volume.id) || (crypter_mode == :decrypt && destination_id == volume.id)
327
+ end
328
+ hash
249
329
  end
250
330
 
251
331
  #
252
332
  def touch!
253
333
  @mtime = DateTime.now if @touch
334
+ self
254
335
  end
255
336
  end
256
337
 
@@ -270,12 +351,9 @@ module Mclone
270
351
  #
271
352
  def resolve(id)
272
353
  case (ids = super).size
273
- when 0
274
- raise(Task::Error, %(no task matching "#{id}" pattern found))
275
- when 1
276
- ids.first
277
- else
278
- raise(Task::Error, %(ambiguous "#{id}" pattern: two or more tasks match))
354
+ when 0 then raise(Task::Error, %(no task matching "#{id}" pattern found))
355
+ when 1 then ids.first
356
+ else raise(Task::Error, %(ambiguous "#{id}" pattern: two or more tasks match))
279
357
  end
280
358
  end
281
359
 
@@ -302,8 +380,6 @@ module Mclone
302
380
  #
303
381
  attr_reader :file
304
382
 
305
- #
306
- attr_reader :tasks
307
383
 
308
384
  #
309
385
  def root
@@ -311,31 +387,30 @@ module Mclone
311
387
  end
312
388
 
313
389
  #
314
- def initialize(file)
315
- @file = file
390
+ def initialize(session, file)
391
+ @loaded_tasks = ObjectSet.new
316
392
  @id = SecureRandom.hex(4)
317
- @tasks = VolumeTaskSet.new(self)
393
+ @session = session
394
+ @file = file
318
395
  end
319
396
 
320
397
  #
321
- def self.restore(file)
398
+ def self.restore(session, file)
322
399
  obj = allocate
323
- obj.send(:from_file, file)
400
+ obj.send(:from_file, session, file)
324
401
  obj
325
402
  end
326
403
 
327
404
  #
328
- private def from_file(file)
329
- initialize(file)
405
+ private def from_file(session, file)
330
406
  hash = JSON.parse(IO.read(file), symbolize_names: true)
407
+ @loaded_tasks = ObjectSet.new
408
+ @id = hash.extract(:volume)
409
+ @session = session
410
+ @file = file
331
411
  raise(Volume::Error, %(unsupported Mclone volume format version "#{version}")) unless hash.extract(:mclone) == VERSION
332
- @id = hash.extract(:volume).to_s
333
- hash.extract(:tasks).each { |t| @tasks << Task.restore(t) }
334
- end
335
-
336
- #
337
- def dirty?
338
- tasks.modified?
412
+ hash.dig(:tasks)&.each { |h| session.tasks << (@loaded_tasks << Task.restore(@session, h)) }
413
+ self
339
414
  end
340
415
 
341
416
  #
@@ -348,23 +423,43 @@ module Mclone
348
423
  equal?(other) || id == other.id
349
424
  end
350
425
 
426
+ #
427
+ def modified?
428
+ # Comparison against the original loaded tasks set allows to account for task removals
429
+ (ts = tasks).modified? || ts != @loaded_tasks
430
+ end
431
+
351
432
  #
352
433
  def commit!(force = false)
353
- if force || dirty?
354
- open(file, 'w') do |stream|
355
- stream << JSON.pretty_generate(to_h)
356
- tasks.commit!
434
+ if force || @session.force? || modified?
435
+ # As a safeguard against malformed volume files generation, first write to a new file
436
+ # and rename it to a real volume file only in case of normal turn of events
437
+ _file = "#{file}~"
438
+ begin
439
+ open(_file, 'w') do |stream|
440
+ stream << JSON.pretty_generate(to_h)
441
+ tasks.commit!
442
+ end
443
+ FileUtils.mv(_file, file, force: true)
444
+ ensure
445
+ FileUtils.rm_f(_file)
357
446
  end
358
447
  end
448
+ self
449
+ end
450
+
451
+ #
452
+ def tasks
453
+ TaskSet.new(self).merge!(@session.tasks)
359
454
  end
360
455
 
361
456
  #
362
457
  def to_h
363
- {mclone: VERSION, volume: id, tasks: tasks.collect(&:to_h)}
458
+ { mclone: VERSION, volume: id, tasks: tasks.collect { |task| task.to_h(self) } }
364
459
  end
365
460
 
366
461
  # Volume-bound set of tasks belonging to the specific volume
367
- class VolumeTaskSet < Mclone::TaskSet
462
+ class TaskSet < Mclone::TaskSet
368
463
 
369
464
  def initialize(volume)
370
465
  @volume = volume
@@ -388,12 +483,9 @@ module Mclone
388
483
  #
389
484
  def resolve(id)
390
485
  case (ids = super).size
391
- when 0
392
- raise(Volume::Error, %(no volume matching "#{id}" pattern found))
393
- when 1
394
- ids.first
395
- else
396
- raise(Volume::Error, %(ambiguous "#{id}" pattern: two or more volumes match))
486
+ when 0 then raise(Volume::Error, %(no volume matching "#{id}" pattern found))
487
+ when 1 then ids.first
488
+ else raise(Volume::Error, %(ambiguous "#{id}" pattern: two or more volumes match))
397
489
  end
398
490
  end
399
491
 
@@ -429,31 +521,33 @@ module Mclone
429
521
  #
430
522
  attr_writer :simulate, :verbose, :force
431
523
 
524
+ #
525
+ attr_reader :tasks
526
+
432
527
  #
433
528
  def initialize
434
529
  @volumes = VolumeSet.new
530
+ @tasks = SessionTaskSet.new(self)
435
531
  end
436
532
 
437
533
  #
438
534
  def format_volume!(dir)
439
535
  mclone = File.join(dir, Volume::FILE)
440
536
  raise(Session::Error, %(refuse to overwrite existing Mclone volume file "#{mclone}")) if File.exist?(mclone) && !force?
441
- volume = Volume.new(mclone)
442
- @volumes << volume
443
- volume.commit!(true) unless simulate? # Force creating a new (empty) volume
537
+ volumes << (volume = Volume.new(self, mclone))
538
+ volume.commit!(true) unless simulate? # Force creation of a new (empty) volume
444
539
  self
445
540
  end
446
541
 
447
542
  #
448
543
  def restore_volume!(dir)
449
- volume = Volume.restore(File.join(dir, Volume::FILE))
450
- @volumes << volume
544
+ volumes << Volume.restore(self, File.join(dir, Volume::FILE))
451
545
  self
452
546
  end
453
547
 
454
548
  #
455
549
  def restore_volumes!
456
- (Mclone.environment_mounts + Mclone.system_mounts).each { |dir| restore_volume!(dir) rescue Errno::ENOENT }
550
+ (Mclone.environment_mounts + Mclone.system_mounts + [ENV['HOME']]).each { |dir| restore_volume!(dir) rescue Errno::ENOENT }
457
551
  self
458
552
  end
459
553
 
@@ -467,84 +561,83 @@ module Mclone
467
561
  end
468
562
 
469
563
  #
470
- def create_task!(source, destination, mode: :update, include: '**', exclude: '')
471
- task = Task.new(*locate(source), *locate(destination))
472
- task.mode = mode
473
- task.include = include
474
- task.exclude = exclude
475
- volumes.each do |volume|
476
- t = volume.tasks[task]
477
- raise(Session::Error, %(refuse to overwrite existing task "#{t.id}")) unless t.nil? || force?
478
- volume.tasks << task # It is a volume's responsibility to collect appropriate tasks, see Volume::TaskSet#<<
479
- end
564
+ def create_task!(mode, source, destination, **kws)
565
+ task = Task.new(self, mode, *locate(source), *locate(destination), **kws)
566
+ _task = tasks[task]
567
+ raise(Session::Error, %(refuse to overwrite existing task "#{_task.id}")) unless _task.nil? || force?
568
+ tasks << task
480
569
  self
481
570
  end
482
571
 
483
572
  #
484
- def modify_task!(id, mode:, include:, exclude:)
573
+ def modify_task!(id, mode: nil, include: nil, exclude: nil)
485
574
  ts = tasks
486
575
  task = ts.task(ts.resolve(id)).clone
487
- task.mode = mode
488
- task.include = include
489
- task.exclude = exclude
490
- volumes.each { |volume| volume.tasks << task }
576
+ task.mode = mode unless mode.nil?
577
+ task.include = include unless include.nil?
578
+ task.exclude = exclude unless exclude.nil?
579
+ tasks << task
491
580
  self
492
581
  end
493
582
 
494
583
  #
495
584
  def delete_task!(id)
496
- ts = tasks
497
- task = ts.task(ts.resolve(id))
498
- volumes.each { |volume| volume.tasks >> task }
585
+ tasks >> tasks.task(tasks.resolve(id))
499
586
  self
500
587
  end
501
588
 
502
589
  #
503
590
  def process_tasks!(*ids)
504
- ts = intact_tasks
505
- ids = ts.collect(&:id) if ids.empty?
506
- rclone = 'rclone' if (rclone = ENV['RCLONE']).nil?
507
- ids.collect { |id| ts.task(ts.resolve(id)) }.each do |task|
508
- args = [rclone]
591
+ failed = false
592
+ intacts = intact_tasks
593
+ ids = intacts.collect(&:id) if ids.empty?
594
+ ids.collect { |id| intacts.task(intacts.resolve(id)) }.each do |task|
595
+ source_path = File.join(volumes.volume(task.source_id).root, task.source_root.nil? || task.source_root.empty? ? '' : task.source_root)
596
+ destination_path = File.join(volumes.volume(task.destination_id).root, task.destination_root.nil? || task.destination_root.empty? ? '' : task.destination_root)
597
+ args = [Mclone.rclone]
509
598
  opts = [
599
+ '--config', Mclone.windows? ? 'NUL' : '/dev/null',
510
600
  simulate? ? '--dry-run' : nil,
511
- verbose? ? '--verbose' : nil
601
+ verbose? ? '--verbose' : nil,
602
+ verbose? ? '--progress' : nil
512
603
  ].compact
604
+ opts.append('--crypt-password', task.crypter_token) unless task.crypter_mode.nil?
605
+ case task.crypter_mode
606
+ when :encrypt then opts.append('--crypt-remote', destination_path)
607
+ when :decrypt then opts.append('--crypt-remote', source_path)
608
+ end
513
609
  case task.mode
514
- when :update
515
- args << 'copy'
516
- opts << '--update'
517
- when :synchronize
518
- args << 'sync'
519
- when :copy
520
- args << 'copy'
521
- when :move
522
- args << 'move'
610
+ when :update then args.push('copy', '--update')
611
+ when :synchronize then args << 'sync'
612
+ when :copy then args << 'copy'
613
+ when :move then args << 'move'
523
614
  end
524
615
  opts.append('--filter', "- /#{Volume::FILE}")
525
616
  opts.append('--filter', "- #{task.exclude}") unless task.exclude.nil? || task.exclude.empty?
526
617
  opts.append('--filter', "+ #{task.include}") unless task.include.nil? || task.include.empty?
527
618
  args.concat(opts)
528
- args.append(File.join(volumes.volume(task.source_id).root, task.source_root), File.join(volumes.volume(task.destination_id).root, task.destination_root))
619
+ case task.crypter_mode
620
+ when nil then args.append(source_path, destination_path)
621
+ when :encrypt then args.append(source_path, ':crypt:')
622
+ when :decrypt then args.append(':crypt:', destination_path)
623
+ end
624
+ $stdout << args.collect(&:escape).join(' ') << "\n" if verbose?
529
625
  case system(*args)
530
- when nil then raise(Session::Error, %(failed to execute "#{args.first}"))
531
- when false then exit($?)
626
+ when nil
627
+ $stderr << %(failed to execute "#{args.first}") << "\n" if verbose?
628
+ failed = true
629
+ when false
630
+ $stderr << %(Rclone exited with status #{$?.to_i}) << "\n" if verbose?
631
+ failed = true
532
632
  end
533
633
  end
534
- end
535
-
536
- # Collect all tasks from all loaded volumes
537
- def tasks
538
- tasks = SessionTaskSet.new(self)
539
- volumes.each { |volume| tasks.merge!(volume.tasks) }
540
- tasks
634
+ raise(Session::Error, "Rclone execution failure(s)") if failed
635
+ self
541
636
  end
542
637
 
543
638
  # Collect all tasks from all loaded volumes which are ready to be executed
544
639
  def intact_tasks
545
- tasks = IntactTaskSet.new(self)
546
- volumes.each { |volume| tasks.merge!(volume.tasks) }
547
- tasks
640
+ IntactTaskSet.new(self).merge!(tasks)
548
641
  end
549
642
 
550
643
  #
@@ -587,6 +680,11 @@ module Mclone
587
680
 
588
681
  end
589
682
 
683
+ #
684
+ def self.rclone
685
+ @@rclone ||= (rclone = ENV['RCLONE']).nil? ? 'rclone' : rclone
686
+ end
687
+
590
688
  # Return true if run in the real Windows environment (e.g. not in real *NIX or various emulation layers such as MSYS, Cygwin etc.)
591
689
  def self.windows?
592
690
  @@windows ||= /^(mingw)/.match?(RbConfig::CONFIG['target_os']) # RubyInstaller's MRI, other MinGW-build MRI
@@ -600,17 +698,16 @@ module Mclone
600
698
  PATH_LIST_SEPARATOR = windows? ? ';' : ':'
601
699
 
602
700
  # Return list of live user-provided mounts (mount points on *NIX and disk drives on Windows) which may contain Mclone volumes
603
- # Look for the $mclone_PATH environment variable
701
+ # Look for the $MCLONE_PATH environment variable
604
702
  def self.environment_mounts
605
703
  ENV['MCLONE_PATH'].split(PATH_LIST_SEPARATOR).collect { |path| File.directory?(path) ? path : nil }.compact rescue []
606
704
  end
607
-
608
705
  # Return list of live system-managed mounts (mount points on *NIX and disk drives on Windows) which may contain Mclone volumes
609
706
  case RbConfig::CONFIG['target_os']
610
707
  when 'linux'
611
708
  # Linux OS
612
709
  def self.system_mounts
613
- # Query on /proc for currently mounted file systems
710
+ # Query /proc for currently mounted file systems
614
711
  IO.readlines('/proc/self/mountstats').collect do |line|
615
712
  mount = line.split[4]
616
713
  UNIX_SYSTEM_MOUNTS.match?(mount) || !File.directory?(mount) ? nil : mount