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

Constructor Details

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

Returns a new instance of Volume.



289
290
291
292
293
294
295
296
# File 'lib/bitferry.rb', line 289

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.



225
226
227
# File 'lib/bitferry.rb', line 225

def generation
  @generation
end

#rootObject (readonly)

Returns the value of attribute root.



228
229
230
# File 'lib/bitferry.rb', line 228

def root
  @root
end

#tagObject (readonly)

Returns the value of attribute tag.



222
223
224
# File 'lib/bitferry.rb', line 222

def tag
  @tag
end

#vaultObject (readonly)

Returns the value of attribute vault.



231
232
233
# File 'lib/bitferry.rb', line 231

def vault
  @vault
end

Class Method Details

.[](tag) ⇒ Object



234
235
236
237
# File 'lib/bitferry.rb', line 234

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

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



274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/bitferry.rb', line 274

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)


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

def self.endpoint(root)
  path = Pathname.new(root).realdirpath
  # FIXME select innermost or outermost volume in case of nested volumes?
  intact.sort { |v1, v2| v2.root.size <=> v1.root.size }.each do |volume|
    begin
      # FIXME chop trailing slashes
      stem = path.relative_path_from(volume.root)
      case stem.to_s
        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



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

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

.lookup(*tags) ⇒ Object

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



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

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

.match(tags, volumes) ⇒ Object



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

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



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

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

.register(volume) ⇒ Object



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

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

.registeredObject



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

def self.registered = @@registry.values

.resetObject



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

def self.reset = @@registry = {}

.restore(root) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/bitferry.rb', line 259

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



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/bitferry.rb', line 321

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



385
386
387
388
389
# File 'lib/bitferry.rb', line 385

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

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



299
300
301
302
303
# File 'lib/bitferry.rb', line 299

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

#delete(wipe: false) ⇒ Object



377
378
379
380
381
382
# File 'lib/bitferry.rb', line 377

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

#endpoint(path = String.new) ⇒ Object



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

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

#externalizeObject



438
439
440
441
442
443
444
445
446
447
448
# File 'lib/bitferry.rb', line 438

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)


410
411
412
413
414
415
416
417
418
419
420
# File 'lib/bitferry.rb', line 410

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)


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

def intact? = @state != :removing

#intact_tasksObject



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

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

#live_tasksObject



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

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

#modified?Boolean

Returns:

  • (Boolean)


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

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

#removeObject



423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/bitferry.rb', line 423

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)


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

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



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

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

#storage_Object



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

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

#storeObject



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/bitferry.rb', line 392

def store
  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



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

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

#touchObject



370
371
372
373
374
# File 'lib/bitferry.rb', line 370

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