mclone 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +8 -4
- data/README.md +409 -369
- data/bin/mclone +23 -8
- data/lib/mclone.rb +223 -126
- metadata +3 -3
data/bin/mclone
CHANGED
@@ -13,7 +13,9 @@ Clamp do
|
|
13
13
|
|
14
14
|
using Refinements
|
15
15
|
|
16
|
-
|
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
|
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
|
-
|
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
|
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'
|
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
|
-
|
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.
|
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 =
|
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 = [
|
261
|
+
MODES = %i[update synchronize copy move].freeze
|
192
262
|
|
193
263
|
#
|
194
264
|
def mode=(mode)
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
275
|
-
|
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
|
-
@
|
390
|
+
def initialize(session, file)
|
391
|
+
@loaded_tasks = ObjectSet.new
|
316
392
|
@id = SecureRandom.hex(4)
|
317
|
-
@
|
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
|
-
@
|
333
|
-
|
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 ||
|
354
|
-
|
355
|
-
|
356
|
-
|
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(
|
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
|
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
|
-
|
393
|
-
|
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
|
-
|
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
|
-
|
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,
|
471
|
-
task = Task.new(*locate(source), *locate(destination))
|
472
|
-
|
473
|
-
task.
|
474
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
505
|
-
|
506
|
-
|
507
|
-
ids.collect { |id|
|
508
|
-
|
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
|
-
|
516
|
-
|
517
|
-
when :
|
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
|
-
|
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
|
531
|
-
|
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
|
-
|
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
|
-
|
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 $
|
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
|
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
|