Class: Bitferry::Volume

Inherits:
Object
  • Object
show all
Extended by:
Logging
Includes:
Logging
Defined in:
lib/bitferry.rb

Constant Summary collapse

STORAGE =
'.bitferry'
STORAGE_ =
'.bitferry~'
STORAGE_MASK =
'.bitferry*'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

log, log, log=

Constructor Details

#initialize(root, tag: Bitferry.tag, modified: DateTime.now, overwrite: false) ⇒ Volume

Returns a new instance of Volume.



308
309
310
311
312
313
314
315
# File 'lib/bitferry.rb', line 308

def initialize(root, tag: Bitferry.tag, modified: DateTime.now, overwrite: false)
  @tag = tag
  @generation = 0
  @vault = {}
  @modified = modified
  @overwrite = overwrite
  @root = Pathname.new(root).realdirpath
end

Instance Attribute Details

#generationObject (readonly)

Returns the value of attribute generation.



244
245
246
# File 'lib/bitferry.rb', line 244

def generation
  @generation
end

#rootObject (readonly)

Returns the value of attribute root.



247
248
249
# File 'lib/bitferry.rb', line 247

def root
  @root
end

#tagObject (readonly)

Returns the value of attribute tag.



241
242
243
# File 'lib/bitferry.rb', line 241

def tag
  @tag
end

#vaultObject (readonly)

Returns the value of attribute vault.



250
251
252
# File 'lib/bitferry.rb', line 250

def vault
  @vault
end

Class Method Details

.[](tag) ⇒ Object



253
254
255
256
# File 'lib/bitferry.rb', line 253

def self.[](tag)
  @@registry.each_value { |volume| return volume if volume.tag == tag }
  nil
end

.delete(*tags, wipe: false) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/bitferry.rb', line 293

def self.delete(*tags, wipe: false)
  process = []
  tags.each do |tag|
    case (volumes = Volume.lookup(tag)).size
      when 0 then log.warn("no volumes matching (partial) tag #{tag}")
      when 1 then process += volumes
      else
        tags = volumes.collect { |v| v.tag }.join(', ')
        raise ArgumentError, "multiple volumes matching (partial) tag #{tag}: #{tags}"
    end
  end
  process.each { |volume| volume.delete(wipe: wipe) }
end

.endpoint(root) ⇒ Object

Raises:

  • (ArgumentError)


361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/bitferry.rb', line 361

def self.endpoint(root)
  path = Pathname.new(root).realdirpath
  intact.sort { |v1, v2| v2.root.to_s.size <=> v1.root.to_s.size }.each do |volume|
    begin
      stem = path.relative_path_from(volume.root).to_s #.chomp('/')
      case stem
        when '.' then return volume.endpoint
        when /^[^\.].*/ then return volume.endpoint(stem)
      end
    rescue ArgumentError
      # Catch different prefix error on Windows
    end
  end
  raise ArgumentError, "no intact volume encompasses path #{root}"
end

.intactObject



487
# File 'lib/bitferry.rb', line 487

def self.intact = registered.filter { |volume| volume.intact? }

.lookup(*tags) ⇒ Object

Return list of registered volumes whose tags match at least one specified partial



260
# File 'lib/bitferry.rb', line 260

def self.lookup(*tags) = match(tags, registered)

.match(tags, volumes) ⇒ Object



263
264
265
266
267
268
# File 'lib/bitferry.rb', line 263

def self.match(tags, volumes)
  rxs = tags.collect { |x| Regexp.new(x) }
  volumes.filter do |volume|
    rxs.any? { |rx| !(rx =~ volume.tag).nil? }
  end
end

.new(root, **opts) ⇒ Object



271
272
273
274
275
# File 'lib/bitferry.rb', line 271

def self.new(root, **opts)
  volume = allocate
  volume.send(:create, root, **opts)
  register(volume)
end

.register(volume) ⇒ Object



481
# File 'lib/bitferry.rb', line 481

def self.register(volume) = @@registry[volume.root] = volume

.registeredObject



484
# File 'lib/bitferry.rb', line 484

def self.registered = @@registry.values

.resetObject



478
# File 'lib/bitferry.rb', line 478

def self.reset = @@registry = {}

.restore(root) ⇒ Object



278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/bitferry.rb', line 278

def self.restore(root)
  begin
    volume = allocate
    volume.send(:restore, root)
    volume = register(volume)
    log.info("restored volume #{volume.tag} from #{root}")
    volume
  rescue => e
    log.error("failed to restore volume from #{root}")
    log.error(e.message) if $DEBUG
    raise
  end
end

Instance Method Details

#commitObject



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/bitferry.rb', line 340

def commit
  if modified?
    log.info("commit volume #{tag} (modified)")
    case @state
    when :pristine
      format
      store
    when :intact
      store
    when :removing
      remove
    else
      raise
    end
    committed
  else
    log.info("skipped committing volume #{tag} (unmodified)")
  end
end

#committedObject



402
403
404
405
406
# File 'lib/bitferry.rb', line 402

def committed
  x = tasks.collect { |t| t.generation }.min
  @generation = x ? x : 0
  @modified = false
end

#create(*args, **opts) ⇒ Object



318
319
320
321
322
# File 'lib/bitferry.rb', line 318

def create(*args, **opts)
  initialize(*args, **opts)
  @state = :pristine
  @modified = true
end

#delete(wipe: false) ⇒ Object



394
395
396
397
398
399
# File 'lib/bitferry.rb', line 394

def delete(wipe: false)
  touch
  @wipe = wipe
  @state = :removing
  log.info("marked volume #{tag} for deletion")
end

#endpoint(path = String.new) ⇒ Object



378
# File 'lib/bitferry.rb', line 378

def endpoint(path = String.new) = Endpoint::Bitferry.new(self, path)

#externalizeObject



456
457
458
459
460
461
462
463
464
465
466
# File 'lib/bitferry.rb', line 456

def externalize
  tasks = live_tasks
  v = vault.filter { |t| !Task[t].nil? && Task[t].live? } # Purge entries from non-existing (deleted) tasks
  {
    bitferry: "0",
    volume: tag,
    modified: (@modified = DateTime.now),
    tasks: tasks.empty? ? nil : tasks.collect(&:externalize),
    vault: v.empty? ? nil : v
  }.compact
end

#formatObject

Raises:

  • (IOError)


428
429
430
431
432
433
434
435
436
437
438
# File 'lib/bitferry.rb', line 428

def format
  raise IOError, "refuse to overwrite existing volume storage #{storage}" if !@overwrite && File.exist?(storage)
  if Bitferry.simulate?
    log.info("skipped storage formatting (simulation)")
  else
    FileUtils.mkdir_p(root)
    FileUtils.rm_f [storage, storage_]
    log.info("formatted volume #{tag} in #{root}")
  end
  @state = nil
end

#intact?Boolean

Returns:

  • (Boolean)


384
# File 'lib/bitferry.rb', line 384

def intact? = @state != :removing

#intact_tasksObject



475
# File 'lib/bitferry.rb', line 475

def intact_tasks = live_tasks.filter { |task| task.intact? }

#live_tasksObject



472
# File 'lib/bitferry.rb', line 472

def live_tasks = Task.live.filter { |task| task.refers?(self) }

#modified?Boolean

Returns:

  • (Boolean)


381
# File 'lib/bitferry.rb', line 381

def modified? = @modified || tasks.any? { |t| t.generation > generation }

#removeObject



441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/bitferry.rb', line 441

def remove
  unless Bitferry.simulate?
    if @wipe
      FileUtils.rm_rf(Dir[File.join(root, '*'), File.join(root, '.*')])
      log.info("wiped entire volume directory #{root}")
    else
      FileUtils.rm_f [storage, storage_]
      log.info("deleted volume #{tag} storage files #{File.join(root, STORAGE_MASK)}")
    end
  end
  @@registry.delete(root)
  @state = nil
end

#restore(root) ⇒ Object

Raises:

  • (IOError)


325
326
327
328
329
330
331
332
333
# File 'lib/bitferry.rb', line 325

def restore(root)
  hash = JSON.load_file(storage = Pathname(root).join(STORAGE), { symbolize_names: true })
  raise IOError, "bad volume storage #{storage}" unless hash.fetch(:bitferry) == "0"
  initialize(root, tag: hash.fetch(:volume), modified: DateTime.parse(hash.fetch(:modified)))
  hash.fetch(:tasks, []).each { |hash| Task::ROUTE.fetch(hash.fetch(:operation).intern).restore(hash) }
  @vault = hash.fetch(:vault, {}).transform_keys { |key| key.to_s }
  @state = :intact
  @modified = false
end

#storageObject



336
# File 'lib/bitferry.rb', line 336

def storage  = @storage  ||= root.join(STORAGE)

#storage_Object



337
# File 'lib/bitferry.rb', line 337

def storage_ = @storage_ ||= root.join(STORAGE_)

#storeObject



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/bitferry.rb', line 409

def store
  require 'neatjson'
  tasks.each(&:commit)
  hash = JSON.neat_generate(externalize, short: false, wrap: 200, afterColon: 1, afterComma: 1)
  if Bitferry.simulate?
    log.info("skipped volume #{tag} storage modification (simulation)")
  else
    begin
      File.write(storage_, hash)
      FileUtils.mv(storage_, storage)
      log.info("written volume #{tag} storage #{storage}")
    ensure
      FileUtils.rm_f(storage_)
    end
  end
  @state = :intact
end

#tasksObject



469
# File 'lib/bitferry.rb', line 469

def tasks = Task.registered.filter { |task| task.refers?(self) }

#touchObject



387
388
389
390
391
# File 'lib/bitferry.rb', line 387

def touch
  x = tasks.collect { |t| t.generation }.max
  @generation = x ? x + 1 : 0
  @modified = true
end