Class: Store::Digest::Object

Inherits:
Object
  • Object
show all
Defined in:
lib/store/digest/object.rb

Overview

Store entry object class.

Defined Under Namespace

Classes: Flags, IOWrapper

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(content = nil, digests: {}, size: 0, type: 'application/octet-stream', charset: nil, language: nil, encoding: nil, ctime: nil, mtime: nil, ptime: nil, dtime: nil, flags: 0, strict: true, fresh: false) ⇒ Store::Digest::Object

Note:

use scan or #scan to populate

Create a new object, naively recording whatever is handed

Parameters:

  • content (IO, String, Proc, File, Pathname, ...) (defaults to: nil)

    some content

  • digests (Hash) (defaults to: {})

    the digests ascribed to the content

  • size (Integer) (defaults to: 0)

    assert the object’s size

  • type (String) (defaults to: 'application/octet-stream')

    assert the object’s MIME type

  • charset (String) (defaults to: nil)

    the character set, if applicable

  • language (String) (defaults to: nil)

    the (RFC5646) language tag, if applicable

  • encoding (String) (defaults to: nil)

    the content-encoding (e.g. compression)

  • ctime (Time) (defaults to: nil)

    assert object creation time

  • mtime (Time) (defaults to: nil)

    assert object modification time

  • ptime (Time) (defaults to: nil)

    assert object metadata parameter modification time

  • dtime (Time) (defaults to: nil)

    assert object deletion time

  • flags (Integer) (defaults to: 0)

    validation state flags

  • strict (true, false) (defaults to: true)

    raise an error on bad input

  • fresh (true, false) (defaults to: false)

    assert “freshness” of object vis-a-vis the store



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/store/digest/object.rb', line 256

def initialize content = nil, digests: {}, size: 0,
    type: 'application/octet-stream', charset: nil, language: nil,
    encoding: nil, ctime: nil, mtime: nil, ptime: nil, dtime: nil,
    flags: 0, strict: true, fresh: false

  # snag this immediately
  @fresh = !!fresh

  # check input on content
  @content = case content
             when nil then nil
             when IO, StringIO, Proc then content
             when String then StringIO.new content
             when Pathname then -> { content.expand_path.open('rb') }
             when -> x { %i[read seek pos].all? { |m| x.respond_to? m } }
               content
             else
               raise ArgumentError,
                 "Cannot accept content given as #{content.class}"
             end

  # check input on digests
  @digests = case digests
             when Hash
               # hash must be clean
               digests.map do |k, v|
                 raise ArgumentError,
                   'Digest keys must be symbol-able' unless
                   k.respond_to? :to_sym
                 k = k.to_sym
                 raise ArgumentError,
                   'Digest values must be URI::NI' unless
                   v.is_a? URI::NI
                 raise ArgumentError,
                   'Digest key must match value algorithm' unless
                   k == v.algorithm
                 [k.to_sym, v.dup.freeze]
               end.to_h
             when nil then {} # empty hash
             when Array
               # only accepts array of URI::NI
               digests.map do |x|
                 raise ArgumentError,
                   "Digests given as array can only be URI::NI, not #{x}" \
                   unless x.is_a? URI::NI
                 [x.algorithm, x.dup.freeze]
               end.to_h
             when URI::NI then { digests.algorithm => digests.dup.freeze }
             else
               # everything else is invalid
               raise ArgumentError,
                 "Cannot coerce digests given as #{digests.inspect}"
             end

  # ctime, mtime, ptime, dtime should be all nil or nonnegative
  # integers or Time or DateTime
  b = binding
  %i[ctime mtime ptime dtime].each do |k|
    v = coerce_time(b.local_variable_get(k), k)
    instance_variable_set "@#{k}", v
  end

  # set the flags
  @flags = Flags.from(flags || 0)

  @size = case size
          when nil then 0
          when Numeric
            raise ArgumentError, 'size must be non-negative' if size < 0
            size.to_i
          else
            raise ArgumentError, 'size must be nil or Numeric'
          end

  # the following can be strings or symbols:
  TOKENS.keys.each do |k|
    if x = b.local_variable_get(k)
      x = if strict
            coerce_token(x, k)
          else
            coerce_token(x, k) rescue nil
          end
      instance_variable_set "@#{k}", x.freeze if x
    end
  end
end

Instance Attribute Details

#charsetObject

Returns the value of attribute charset.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def charset
  @charset
end

#ctimeObject

Returns the value of attribute ctime.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def ctime
  @ctime
end

#digestsObject (readonly)

XXX come up with a policy for these that isn’t stupid, plus input sanitation



344
345
346
# File 'lib/store/digest/object.rb', line 344

def digests
  @digests
end

#dtimeObject

Returns the value of attribute dtime.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def dtime
  @dtime
end

#encodingObject

Returns the value of attribute encoding.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def encoding
  @encoding
end

#flagsObject

Returns the value of attribute flags.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def flags
  @flags
end

#languageObject

Returns the value of attribute language.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def language
  @language
end

#mtimeObject

Returns the value of attribute mtime.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def mtime
  @mtime
end

#ptimeObject

Returns the value of attribute ptime.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def ptime
  @ptime
end

#sizeObject (readonly)

XXX come up with a policy for these that isn’t stupid, plus input sanitation



344
345
346
# File 'lib/store/digest/object.rb', line 344

def size
  @size
end

#typeObject

Returns the value of attribute type.



345
346
347
# File 'lib/store/digest/object.rb', line 345

def type
  @type
end

Class Method Details

.scan(content, digests: URI::NI.algorithms, mtime: nil, type: nil, language: nil, charset: nil, encoding: nil, blocksize: BLOCKSIZE, strict: true, fresh: false, &block) ⇒ Object



349
350
351
352
353
354
355
# File 'lib/store/digest/object.rb', line 349

def self.scan content, digests: URI::NI.algorithms, mtime: nil,
    type: nil, language: nil, charset: nil, encoding: nil,
    blocksize: BLOCKSIZE, strict: true, fresh: false, &block
  self.new.scan content, digests: digests, mtime: mtime, type: type,
    language: language, charset: charset, encoding: encoding,
    blocksize: blocksize, strict: strict, fresh: fresh, &block
end

Instance Method Details

#algorithmsArray

Return the algorithms used in the object.

Returns:

  • (Array)


460
461
462
# File 'lib/store/digest/object.rb', line 460

def algorithms
  (@digests || {}).keys.sort
end

#cache?false, true

Returns whether the object is cache.

Returns:

  • (false, true)


508
509
510
# File 'lib/store/digest/object.rb', line 508

def cache?
  !!@flags.cache
end

#charset_checked?false, true

Returns true if the character set has been checked.

Returns:

  • (false, true)


528
529
530
# File 'lib/store/digest/object.rb', line 528

def charset_checked?
  0 != @flags.to_i & CHARSET_CHECKED
end

#charset_valid?false, true

Returns true if the character set has been checked and is valid.

Returns:

  • (false, true)


534
535
536
# File 'lib/store/digest/object.rb', line 534

def charset_valid?
  0 != @flags.to_i & (CHARSET_CHECKED|CHARSET_VALID)
end

#content#read

Returns the content stored in the object.

Returns:

  • (#read)


479
480
481
482
# File 'lib/store/digest/object.rb', line 479

def content
  io = @content.is_a?(Proc) ? @content.call : @content
  io = io ? IOWrapper.new(self, io) : io
end

#content?false, true

Determines if there is content embedded in the object.

Returns:

  • (false, true)


486
487
488
# File 'lib/store/digest/object.rb', line 486

def content?
  !!@content
end

#deleted?false, true

Just a plain old predicate to determine whether the blob has been deleted from the store (but implicitly the metadata record remains).

Returns:

  • (false, true)


583
584
585
# File 'lib/store/digest/object.rb', line 583

def deleted?
  !!@dtime
end

#digest(symbol) ⇒ Symbol? Also known as: []

Return a particular digest. Returns nil if there is no match.

Parameters:

  • symbol (Symbol, #to_s, #to_sym)

    the digest

Returns:

  • (Symbol, nil)

Raises:

  • (ArgumentError)


467
468
469
470
471
# File 'lib/store/digest/object.rb', line 467

def digest symbol
  raise ArgumentError, "This method takes a symbol" unless
    symbol.respond_to? :to_sym
  digests[symbol.to_sym]
end

#encoding_checked?false, true

Returns true if the content encoding (e.g. gzip, deflate) has been checked.

Returns:

  • (false, true)


541
542
543
# File 'lib/store/digest/object.rb', line 541

def encoding_checked?
  0 != @flags.to_i & ENCODING_CHECKED
end

#encoding_valid?false, true

Returns true if the content encoding has been checked and is valid.

Returns:

  • (false, true)


547
548
549
# File 'lib/store/digest/object.rb', line 547

def encoding_valid?
  0 != @flags.to_i & (ENCODING_CHECKED|ENCODING_VALID)
end

#fresh=(state) ⇒ Object



454
455
456
# File 'lib/store/digest/object.rb', line 454

def fresh= state
  @fresh = !!state
end

#fresh?true, false

Determine (or set) whether the object is “fresh”, i.e. whether it is new (or restored), or had been previously been in the store.

Returns:

  • (true, false)


450
451
452
# File 'lib/store/digest/object.rb', line 450

def fresh?
  !!@fresh
end

#scan(content = nil, digests: URI::NI.algorithms, mtime: nil, type: nil, charset: nil, language: nil, encoding: nil, blocksize: BLOCKSIZE, strict: true, fresh: nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/store/digest/object.rb', line 357

def scan content = nil, digests: URI::NI.algorithms, mtime: nil,
    type: nil, charset: nil, language: nil, encoding: nil,
    blocksize: BLOCKSIZE, strict: true, fresh: nil, &block
  # update freshness if there is something to update
  @fresh = !!fresh unless fresh.nil?
  # we put all the scanning stuff in here
  content = case content
            when nil          then self.content
            when IO, StringIO then content
            when String       then StringIO.new content
            when Pathname     then content.open('rb')
            when Proc         then content.call
            when -> x { %i[read seek pos].all? { |m| x.respond_to? m } }
              content
            else
              raise ArgumentError,
                "Cannot scan content of type #{content.class}"
            end
  content.binmode if content.respond_to? :binmode

  # sane default for mtime
  @mtime = coerce_time(mtime || @mtime ||
    (content.respond_to?(:mtime) ? content.mtime : Time.now(in: ?Z)), :mtime)

  # eh, *some* code reuse
  b = binding
  TOKENS.keys.each do |k|
    if x = b.local_variable_get(k)
      x = if strict
            coerce_token(x, k)
          else
            coerce_token(x, k) rescue nil
          end
      instance_variable_set "@#{k}", x.freeze if x
    end
  end

  digests = case digests
            when Array  then digests
            when Symbol then [digests]
            else
              raise ArgumentError, 'Digests must be one or more symbol'
            end
  raise ArgumentError,
    "Invalid digest list #{digests - URI::NI.algorithms}" unless
    (digests - URI::NI.algorithms).empty?

  # set up the contexts
  digests = digests.map { |d| [d, URI::NI.context(d)] }.to_h

  # sample for mime type checking
  sample = StringIO.new ''
  @size  = 0
  while buf = content.read(blocksize)
    @size += buf.size
    sample << buf if sample.pos < SAMPLE
    digests.values.each { |ctx| ctx << buf }
    block.call buf if block_given?
  end

  # seek the content back to the front and store it
  content.seek 0, 0
  @content = content

  # set up the digests
  @digests = digests.map do |k, v|
    [k, URI::NI.compute(v, algorithm: k).freeze]
  end.to_h.freeze

  # ensure there is the most generic of possible types
  type ||= 'application/octet-stream'.freeze

  # obtain the sampled content type
  ts = MimeMagic.by_magic(sample) || MimeMagic.default_type(sample)
  if content.respond_to? :path
    # may as well use the path if it's available and more specific
    ps = MimeMagic.by_path(content.path.to_s)
    # XXX the need to do ts.to_s is a bug in mimemagic
    ts = ps if ps and ps.descendant_of?(ts.to_s)
  end

  # set the type to ts if it is more specific
  @type = ts.descendant_of?(type.to_s) ? ts.to_s.freeze :
    type.to_s.dup.downcase.freeze

  self
end

#scanned?false, true

Determines if the object has been scanned.

Returns:

  • (false, true)


500
501
502
# File 'lib/store/digest/object.rb', line 500

def scanned?
  !@digests.empty?
end

#syntax_checked?false, true

Returns true if the blob’s syntax has been checked.

Returns:

  • (false, true)


553
554
555
# File 'lib/store/digest/object.rb', line 553

def syntax_checked?
  0 != @flags.to_i & SYNTAX_CHECKED
end

#syntax_valid?false, true

Returns true if the blob’s syntax has been checked and is valid.

Returns:

  • (false, true)


559
560
561
# File 'lib/store/digest/object.rb', line 559

def syntax_valid?
  0 != @flags.to_i & (SYNTAX_CHECKED|SYNTAX_VALID)
end

#to_h(content: false) ⇒ Hash

Return the object as a hash. Omits the content by default.

Parameters:

  • content (false, true) (defaults to: false)

    include the content if true

Returns:

  • (Hash)

    the object as a hash



590
591
592
593
594
595
596
# File 'lib/store/digest/object.rb', line 590

def to_h content: false
  main = %i[content digests]
  main.shift unless content
  (main + MANDATORY + OPTIONAL + [:flags]).map do |k|
    [k, send(k).dup]
  end.to_h
end

#to_sObject

Outputs a human-readable string representation of the object.



599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'lib/store/digest/object.rb', line 599

def to_s
  out = "#{self.class}\n  Digests:\n"

  # disgorge the digests
  digests.values.sort { |a, b| a.to_s <=> b.to_s }.each do |d|
    out << "    #{d}\n"
  end

  # now the fields
  MANDATORY.each { |m| out << "  #{LABELS[m]}: #{send m}\n" }
  OPTIONAL.each do |o|
    val = send o
    out << "  #{LABELS[o]}: #{val}\n" if val
  end

  # now the validation statuses
  out << "Validation:\n"
  FLAG.each_index do |i|
    x = flags.to_i >> (3 - i) & 3
    out << ("  %-16s: %s\n" % [FLAG[i], STATE[x]])
  end

  out
end

#type_charsetString

Returns the type and charset, suitable for an HTTP header.

Returns:

  • (String)


492
493
494
495
496
# File 'lib/store/digest/object.rb', line 492

def type_charset
  out = type.to_s
  out += ";charset=#{charset}" if charset
  out
end

#type_checked?false, true

Returns true if the content type has been checked.

Returns:

  • (false, true)


516
517
518
# File 'lib/store/digest/object.rb', line 516

def type_checked?
  0 != @flags.to_i & TYPE_CHECKED
end

#type_valid?false, true

Returns true if the content type has been checked and is valid.

Returns:

  • (false, true)


522
523
524
# File 'lib/store/digest/object.rb', line 522

def type_valid?
  0 != @flags.to_i & (TYPE_CHECKED|TYPE_VALID)
end