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

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



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
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
# File 'lib/store/digest/object.rb', line 238

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.



327
328
329
# File 'lib/store/digest/object.rb', line 327

def charset
  @charset
end

#ctimeObject

Returns the value of attribute ctime.



327
328
329
# File 'lib/store/digest/object.rb', line 327

def ctime
  @ctime
end

#digestsObject (readonly)

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



326
327
328
# File 'lib/store/digest/object.rb', line 326

def digests
  @digests
end

#dtimeObject

Returns the value of attribute dtime.



327
328
329
# File 'lib/store/digest/object.rb', line 327

def dtime
  @dtime
end

#encodingObject

Returns the value of attribute encoding.



327
328
329
# File 'lib/store/digest/object.rb', line 327

def encoding
  @encoding
end

#flagsObject

Returns the value of attribute flags.



327
328
329
# File 'lib/store/digest/object.rb', line 327

def flags
  @flags
end

#languageObject

Returns the value of attribute language.



327
328
329
# File 'lib/store/digest/object.rb', line 327

def language
  @language
end

#mtimeObject

Returns the value of attribute mtime.



327
328
329
# File 'lib/store/digest/object.rb', line 327

def mtime
  @mtime
end

#ptimeObject

Returns the value of attribute ptime.



327
328
329
# File 'lib/store/digest/object.rb', line 327

def ptime
  @ptime
end

#sizeObject (readonly)

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



326
327
328
# File 'lib/store/digest/object.rb', line 326

def size
  @size
end

#typeObject

Returns the value of attribute type.



327
328
329
# File 'lib/store/digest/object.rb', line 327

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



331
332
333
334
335
336
337
# File 'lib/store/digest/object.rb', line 331

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)


442
443
444
# File 'lib/store/digest/object.rb', line 442

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

#cache?false, true

Returns whether the object is cache.

Returns:

  • (false, true)


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

def cache?
  !!@flags.cache
end

#charset_checked?false, true

Returns true if the character set has been checked.

Returns:

  • (false, true)


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

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)


513
514
515
# File 'lib/store/digest/object.rb', line 513

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

#contentIO

Returns the content stored in the object.

Returns:

  • (IO)


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

def content
  @content.is_a?(Proc) ? @content.call : @content
end

#content?false, true

Determines if there is content embedded in the object.

Returns:

  • (false, true)


465
466
467
# File 'lib/store/digest/object.rb', line 465

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)


562
563
564
# File 'lib/store/digest/object.rb', line 562

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)


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

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)


520
521
522
# File 'lib/store/digest/object.rb', line 520

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)


526
527
528
# File 'lib/store/digest/object.rb', line 526

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

#fresh=(state) ⇒ Object



436
437
438
# File 'lib/store/digest/object.rb', line 436

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)


432
433
434
# File 'lib/store/digest/object.rb', line 432

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)


339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
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
# File 'lib/store/digest/object.rb', line 339

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)


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

def scanned?
  !@digests.empty?
end

#syntax_checked?false, true

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

Returns:

  • (false, true)


532
533
534
# File 'lib/store/digest/object.rb', line 532

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)


538
539
540
# File 'lib/store/digest/object.rb', line 538

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



569
570
571
572
573
574
575
# File 'lib/store/digest/object.rb', line 569

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.



578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
# File 'lib/store/digest/object.rb', line 578

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)


471
472
473
474
475
# File 'lib/store/digest/object.rb', line 471

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)


495
496
497
# File 'lib/store/digest/object.rb', line 495

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)


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

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