Class: Store::Digest::Object

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

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



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/store/digest/object.rb', line 152

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

  # size and flags should be non-negative integers
  %i[size flags].each do |k|
    x = b.local_variable_get k
    v = case x
        when nil then 0
        when Integer
          raise ArgumentError, "#{k} must be non-negative" if x < 0
          x
        else
          raise ArgumentError, "#{k} must be nil or an Integer"
        end
    instance_variable_set "@#{k}", v
  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.



243
244
245
# File 'lib/store/digest/object.rb', line 243

def charset
  @charset
end

#ctimeObject

Returns the value of attribute ctime.



243
244
245
# File 'lib/store/digest/object.rb', line 243

def ctime
  @ctime
end

#digestsObject (readonly)

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



242
243
244
# File 'lib/store/digest/object.rb', line 242

def digests
  @digests
end

#dtimeObject

Returns the value of attribute dtime.



243
244
245
# File 'lib/store/digest/object.rb', line 243

def dtime
  @dtime
end

#encodingObject

Returns the value of attribute encoding.



243
244
245
# File 'lib/store/digest/object.rb', line 243

def encoding
  @encoding
end

#flagsObject

Returns the value of attribute flags.



243
244
245
# File 'lib/store/digest/object.rb', line 243

def flags
  @flags
end

#languageObject

Returns the value of attribute language.



243
244
245
# File 'lib/store/digest/object.rb', line 243

def language
  @language
end

#mtimeObject

Returns the value of attribute mtime.



243
244
245
# File 'lib/store/digest/object.rb', line 243

def mtime
  @mtime
end

#ptimeObject

Returns the value of attribute ptime.



243
244
245
# File 'lib/store/digest/object.rb', line 243

def ptime
  @ptime
end

#sizeObject (readonly)

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



242
243
244
# File 'lib/store/digest/object.rb', line 242

def size
  @size
end

#typeObject

Returns the value of attribute type.



243
244
245
# File 'lib/store/digest/object.rb', line 243

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



247
248
249
250
251
252
253
# File 'lib/store/digest/object.rb', line 247

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)


347
348
349
# File 'lib/store/digest/object.rb', line 347

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

#charset_checked?false, true

Returns true if the character set has been checked.

Returns:

  • (false, true)


402
403
404
# File 'lib/store/digest/object.rb', line 402

def charset_checked?
  0 != @flags & CHARSET_CHECKED
end

#charset_valid?false, true

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

Returns:

  • (false, true)


408
409
410
# File 'lib/store/digest/object.rb', line 408

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

#contentIO

Returns the content stored in the object.

Returns:

  • (IO)


364
365
366
# File 'lib/store/digest/object.rb', line 364

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)


370
371
372
# File 'lib/store/digest/object.rb', line 370

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)


457
458
459
# File 'lib/store/digest/object.rb', line 457

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)


354
355
356
357
358
# File 'lib/store/digest/object.rb', line 354

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)


415
416
417
# File 'lib/store/digest/object.rb', line 415

def encoding_checked?
  0 != @flags & ENCODING_CHECKED
end

#encoding_valid?false, true

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

Returns:

  • (false, true)


421
422
423
# File 'lib/store/digest/object.rb', line 421

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

#fresh?(state = nil) ⇒ Boolean

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

Parameters:

  • state (true, false) (defaults to: nil)

Returns:

  • (Boolean)


341
342
343
# File 'lib/store/digest/object.rb', line 341

def fresh? state = nil
  state.nil? ? @fresh : @fresh = !!state
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)


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
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/store/digest/object.rb', line 255

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), :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

  # 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)
    # XXX the need to do ts.to_s is a bug in mimemagic
    ts = ps if ps and ps.child_of?(ts.to_s)
  end
  @type = !type || ts.child_of?(type) ? ts.to_s : type

  self
end

#scanned?false, true

Determines if the object has been scanned.

Returns:

  • (false, true)


384
385
386
# File 'lib/store/digest/object.rb', line 384

def scanned?
  !@digests.empty?
end

#syntax_checked?false, true

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

Returns:

  • (false, true)


427
428
429
# File 'lib/store/digest/object.rb', line 427

def syntax_checked?
  0 != @flags & SYNTAX_CHECKED
end

#syntax_valid?false, true

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

Returns:

  • (false, true)


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

def syntax_valid?
  0 != @flags & (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



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

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.



473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'lib/store/digest/object.rb', line 473

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 >> (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)


376
377
378
379
380
# File 'lib/store/digest/object.rb', line 376

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)


390
391
392
# File 'lib/store/digest/object.rb', line 390

def type_checked?
  0 != @flags & TYPE_CHECKED
end

#type_valid?false, true

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

Returns:

  • (false, true)


396
397
398
# File 'lib/store/digest/object.rb', line 396

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