Class: ActiveStorage::AwsRecord::Blob

Inherits:
Object
  • Object
show all
Includes:
Item, Owner, Servable
Defined in:
lib/active_storage/aws_record/blob.rb

Overview

The blob metadata item. Mirrors the behavior of the default Active Record blob (and the in-memory reference backend) but persists through aws-record. Bytes still live in the configured Active Storage Service; only metadata is here. It is also an attachment owner (for preview_image) and carries a strongly-consistent attachments_count used by #destroy to protect shared blobs.

Single-table layout: the blob and all of its variant records share one partition (+ns#Blob#<id>+); the blob’s own item is the collection root.

Constant Summary collapse

MINIMUM_TOKEN_LENGTH =
28

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Owner

#save, #save!

Methods included from Item

#ns_key, #physical_key, #schema, #stamp_physical_keys!

Methods included from Persistence

#==, #changed?, #dynamodb_client, #hash, #mark_destroyed!, #mark_persisted!, #read_attribute, #write_attribute

Constructor Details

#initialize(attributes = {}) ⇒ Blob

Returns a new instance of Blob.



127
128
129
130
131
132
133
134
135
# File 'lib/active_storage/aws_record/blob.rb', line 127

def initialize(attributes = {})
  super
  self.id ||= generate_uuid
  self.key ||= self.class.generate_unique_secure_token
  self. ||= {}
  self.service_name ||= self.class.service&.name&.to_s
  self.created_at ||= self.class.current_timestamp
  self.attachments_count ||= 0
end

Instance Attribute Details

#local_ioObject

Returns the value of attribute local_io.



38
39
40
# File 'lib/active_storage/aws_record/blob.rb', line 38

def local_io
  @local_io
end

Class Method Details

.active_storage_find(id) ⇒ Object

Owner resolution for Active Storage (Blob is itself an attachment owner via preview_image). Delegate to the composite-key #find: Attachable‘s default find_with_opts(hash_key => id) adapter cannot address the ns#Blob#<id> key.



80
81
82
# File 'lib/active_storage/aws_record/blob.rb', line 80

def active_storage_find(id)
  find(id)
end

.build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) ⇒ Object



91
92
93
94
95
# File 'lib/active_storage/aws_record/blob.rb', line 91

def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
  new(key: key, filename: filename, content_type: content_type, metadata: , service_name: service_name).tap do |blob|
    blob.unfurl(io, identify: identify)
  end
end

.create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) ⇒ Object



97
98
99
# File 'lib/active_storage/aws_record/blob.rb', line 97

def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
  build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: , service_name: service_name, identify: identify, record: record).tap(&:save!)
end

.create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) ⇒ Object



101
102
103
104
105
# File 'lib/active_storage/aws_record/blob.rb', line 101

def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
  create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: , service_name: service_name, identify: identify, record: record).tap do |blob|
    blob.upload_without_unfurling(io)
  end
end

.create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil) ⇒ Object



107
108
109
110
# File 'lib/active_storage/aws_record/blob.rb', line 107

def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
   = ActiveStorage.( || {})
  new(key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: , service_name: service_name).tap(&:save!)
end

.current_timestampObject

Microsecond-precision, fixed-width UTC ISO8601 — lexically sortable (so has_many ordering by created_at is correct) and parseable back to a Time.



59
60
61
# File 'lib/active_storage/aws_record/blob.rb', line 59

def current_timestamp
  Time.now.utc.iso8601(6)
end

.find(id) ⇒ Object

Contract find(id): GetItem on the blob’s collection-root key.



68
69
70
71
72
73
74
# File 'lib/active_storage/aws_record/blob.rb', line 68

def find(id)
  keys = logical_keys_for(id)
  get_item(**keys) ||
    raise(ActiveStorage::RecordNotFound, "Couldn't find #{name} with id=#{id.inspect}")
rescue ArgumentError, Aws::Record::Errors::KeyMissing
  raise ActiveStorage::RecordNotFound, "Couldn't find #{name} with id=#{id.inspect}"
end

.find_signed(id, record: nil, purpose: :blob_id) ⇒ Object



112
113
114
115
116
# File 'lib/active_storage/aws_record/blob.rb', line 112

def find_signed(id, record: nil, purpose: :blob_id)
  find_signed!(id, record: record, purpose: purpose)
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveStorage::RecordNotFound
  nil
end

.find_signed!(id, record: nil, purpose: :blob_id) ⇒ Object



118
119
120
# File 'lib/active_storage/aws_record/blob.rb', line 118

def find_signed!(id, record: nil, purpose: :blob_id)
  find(ActiveStorage.verifier.verify(id, purpose: purpose.to_s))
end

.generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH) ⇒ Object



63
64
65
# File 'lib/active_storage/aws_record/blob.rb', line 63

def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
  SecureRandom.base36(length)
end

.logical_keys_for(blob_id) ⇒ Object

Logical keys for a blob id without an instance (used by the attachment refcount transaction). The blob and its variants share one partition.



86
87
88
89
# File 'lib/active_storage/aws_record/blob.rb', line 86

def logical_keys_for(blob_id)
  root = ns_key('Blob', blob_id)
  { h: root, r: root, item_id: root }
end

.scope_for_strict_loadingObject



122
123
124
# File 'lib/active_storage/aws_record/blob.rb', line 122

def scope_for_strict_loading
  self
end

.serviceObject



49
50
51
# File 'lib/active_storage/aws_record/blob.rb', line 49

def service
  ActiveStorage::Services.default
end

.service=(service) ⇒ Object



53
54
55
# File 'lib/active_storage/aws_record/blob.rb', line 53

def service=(service)
  ActiveStorage::Services.default = service
end

.servicesObject



41
42
43
# File 'lib/active_storage/aws_record/blob.rb', line 41

def services
  ActiveStorage::Services.registry
end

.services=(registry) ⇒ Object



45
46
47
# File 'lib/active_storage/aws_record/blob.rb', line 45

def services=(registry)
  ActiveStorage::Services.registry = registry
end

Instance Method Details

#analyzeObject



199
200
201
202
# File 'lib/active_storage/aws_record/blob.rb', line 199

def analyze
  analyze_without_saving
  save!
end

#analyze_laterObject



204
205
206
# File 'lib/active_storage/aws_record/blob.rb', line 204

def analyze_later
  ActiveStorage::AnalyzeJob.perform_later(self)
end

#analyze_without_savingObject



195
196
197
# File 'lib/active_storage/aws_record/blob.rb', line 195

def analyze_without_saving
  (:analyzed, true)
end

#analyzedObject



177
# File 'lib/active_storage/aws_record/blob.rb', line 177

def analyzed = !![:analyzed]

#analyzed=(value) ⇒ Object



179
180
181
# File 'lib/active_storage/aws_record/blob.rb', line 179

def analyzed=(value)
  (:analyzed, value)
end

#analyzed?Boolean

Returns:

  • (Boolean)


178
# File 'lib/active_storage/aws_record/blob.rb', line 178

def analyzed?(*) = analyzed

#attachmentsObject

The blob→attachment reverse lookup is intentionally unsupported (it is the one access pattern that would force a secondary index; the generic path only ever reaches it for a non-persisted blob, which has no rows). Returns an empty, no-op relation; materializing it on a persisted blob raises.



301
302
303
# File 'lib/active_storage/aws_record/blob.rb', line 301

def attachments
  ActiveStorage::AwsRecord::Attachment.none_for_blob(persisted?)
end

#audio?Boolean

Returns:

  • (Boolean)


260
# File 'lib/active_storage/aws_record/blob.rb', line 260

def audio? = content_type&.start_with?('audio')

#composedObject



183
# File 'lib/active_storage/aws_record/blob.rb', line 183

def composed = !![:composed]

#composed=(value) ⇒ Object



184
185
186
# File 'lib/active_storage/aws_record/blob.rb', line 184

def composed=(value)
  (:composed, value)
end

#content_type_for_servingObject



256
# File 'lib/active_storage/aws_record/blob.rb', line 256

def content_type_for_serving = super

#created_atObject

Returns a Time (not the stored String), because Active Storage’s proxy controller passes blob.created_at to http_cache_forever, which calls .utc on it for the Last-Modified header.



150
151
152
153
154
155
156
157
# File 'lib/active_storage/aws_record/blob.rb', line 150

def created_at
  raw = read_attribute(:created_at)
  return raw unless raw.is_a?(String)

  Time.iso8601(raw)
rescue ArgumentError
  nil
end

#custom_metadataObject



167
168
169
# File 'lib/active_storage/aws_record/blob.rb', line 167

def 
  [:custom] || {}
end

#deleteObject



313
314
315
316
# File 'lib/active_storage/aws_record/blob.rb', line 313

def delete
  service.delete(key)
  service.delete_prefixed("variants/#{key}/") if image?
end

#destroyObject

Delete the metadata item and, when variant tracking is on, its variant records. A still-referenced blob (attachments_count > 0) raises ForeignKeyViolation via a strongly-consistent conditional delete.



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

def destroy
  @previously_persisted = persisted?
  destroyed = false
  run_callbacks(:destroy) do
    # Guard on persisted? so a new/stamped blob object with a colliding id
    # cannot delete the stored blob's metadata.
    if persisted?
      delete_with_foreign_key_guard!
      destroyed = true
    end
  end
  if destroyed
    sweep_variant_records if ActiveStorage.track_variants
    run_callbacks(:commit)
  end
  destroyed
end

#download(&block) ⇒ Object



224
225
226
# File 'lib/active_storage/aws_record/blob.rb', line 224

def download(&block)
  service.download(key, &block)
end

#download_chunk(range) ⇒ Object



228
229
230
# File 'lib/active_storage/aws_record/blob.rb', line 228

def download_chunk(range)
  service.download_chunk(key, range)
end

#filenameObject



159
160
161
# File 'lib/active_storage/aws_record/blob.rb', line 159

def filename
  ActiveStorage::Filename.new(read_attribute(:filename).to_s)
end

#filename=(value) ⇒ Object



163
164
165
# File 'lib/active_storage/aws_record/blob.rb', line 163

def filename=(value)
  write_attribute(:filename, value&.to_s)
end

#forced_disposition_for_servingObject



257
# File 'lib/active_storage/aws_record/blob.rb', line 257

def forced_disposition_for_serving = super

#identifiedObject



171
# File 'lib/active_storage/aws_record/blob.rb', line 171

def identified = !![:identified]

#identified=(value) ⇒ Object



173
174
175
# File 'lib/active_storage/aws_record/blob.rb', line 173

def identified=(value)
  (:identified, value)
end

#identified?Boolean

Returns:

  • (Boolean)


172
# File 'lib/active_storage/aws_record/blob.rb', line 172

def identified?(*) = identified

#identify_without_savingObject



188
189
190
191
192
193
# File 'lib/active_storage/aws_record/blob.rb', line 188

def identify_without_saving
  return if identified?

  self.content_type ||= 'application/octet-stream'
  self.identified = true
end

#image?Boolean

Returns:

  • (Boolean)


259
# File 'lib/active_storage/aws_record/blob.rb', line 259

def image? = content_type&.start_with?('image')

#logical_keysObject

Logical keys: the blob and its variants share the ns#Blob#<id> partition; the blob’s own sort key mirrors the partition (the collection root).



139
140
141
# File 'lib/active_storage/aws_record/blob.rb', line 139

def logical_keys
  self.class.logical_keys_for(id)
end

#mirror_laterObject



309
310
311
# File 'lib/active_storage/aws_record/blob.rb', line 309

def mirror_later
  service.mirror_later(key, checksum: checksum) if service.respond_to?(:mirror_later)
end

#open(tmpdir: nil, &block) ⇒ Object



232
233
234
235
236
237
238
239
# File 'lib/active_storage/aws_record/blob.rb', line 232

def open(tmpdir: nil, &block)
  if local_io
    open_local_io(tmpdir: tmpdir, &block)
  else
    service.open(key, checksum: checksum, verify: !composed,
      name: ["ActiveStorage-#{id}-", filename.extension_with_delimiter], tmpdir: tmpdir, &block)
  end
end

#preview(transformations) ⇒ Object

Raises:

  • (ActiveStorage::UnpreviewableError)


282
283
284
285
286
# File 'lib/active_storage/aws_record/blob.rb', line 282

def preview(transformations)
  raise ActiveStorage::UnpreviewableError unless previewable?

  ActiveStorage::Preview.new(self, transformations)
end

#previewable?Boolean

Returns:

  • (Boolean)


268
269
270
# File 'lib/active_storage/aws_record/blob.rb', line 268

def previewable?
  ActiveStorage.previewers.any? { |klass| klass.accept?(self) }
end

#previously_persisted?Boolean

Returns:

  • (Boolean)


350
351
352
# File 'lib/active_storage/aws_record/blob.rb', line 350

def previously_persisted?
  @previously_persisted
end

#purgeObject



339
340
341
342
343
344
# File 'lib/active_storage/aws_record/blob.rb', line 339

def purge
  destroy
  delete if previously_persisted?
rescue ActiveStorage::ForeignKeyViolation
  nil
end

#purge_laterObject



346
347
348
# File 'lib/active_storage/aws_record/blob.rb', line 346

def purge_later
  ActiveStorage::PurgeJob.perform_later(self)
end

#representable?Boolean

Returns:

  • (Boolean)


272
273
274
# File 'lib/active_storage/aws_record/blob.rb', line 272

def representable?
  variable? || previewable?
end

#representation(transformations) ⇒ Object



288
289
290
291
292
293
294
295
# File 'lib/active_storage/aws_record/blob.rb', line 288

def representation(transformations)
  case
  when previewable? then preview(transformations)
  when variable?    then variant(transformations)
  else
    raise ActiveStorage::UnrepresentableError
  end
end

#serviceObject



305
306
307
# File 'lib/active_storage/aws_record/blob.rb', line 305

def service
  self.class.services.fetch(service_name)
end

#service_headers_for_direct_uploadObject



251
252
253
254
# File 'lib/active_storage/aws_record/blob.rb', line 251

def service_headers_for_direct_upload
  service.headers_for_direct_upload(key, filename: filename, content_type: content_type,
    content_length: byte_size, checksum: checksum, custom_metadata: )
end

#service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in) ⇒ Object



246
247
248
249
# File 'lib/active_storage/aws_record/blob.rb', line 246

def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
  service.url_for_direct_upload(key, expires_in: expires_in, content_type: content_type,
    content_length: byte_size, checksum: checksum, custom_metadata: )
end

#signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil) ⇒ Object



143
144
145
# File 'lib/active_storage/aws_record/blob.rb', line 143

def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
  ActiveStorage.verifier.generate(id, purpose: purpose.to_s, expires_in: expires_in, expires_at: expires_at)
end

#text?Boolean

Returns:

  • (Boolean)


262
# File 'lib/active_storage/aws_record/blob.rb', line 262

def text? = content_type&.start_with?('text')

#unfurl(io, identify: true) ⇒ Object



213
214
215
216
217
218
# File 'lib/active_storage/aws_record/blob.rb', line 213

def unfurl(io, identify: true)
  self.checksum = service.compute_checksum(io)
  self.content_type = Marcel::MimeType.for(io, name: filename.to_s, declared_type: content_type) if content_type.nil? || identify
  self.byte_size = io.size
  self.identified = true
end

#upload(io, identify: true) ⇒ Object



208
209
210
211
# File 'lib/active_storage/aws_record/blob.rb', line 208

def upload(io, identify: true)
  unfurl(io, identify: identify)
  upload_without_unfurling(io)
end

#upload_without_unfurling(io) ⇒ Object



220
221
222
# File 'lib/active_storage/aws_record/blob.rb', line 220

def upload_without_unfurling(io)
  service.upload(key, io, checksum: checksum, content_type: content_type)
end

#url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options) ⇒ Object



241
242
243
244
# File 'lib/active_storage/aws_record/blob.rb', line 241

def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
  service.url(key, expires_in: expires_in, filename: ActiveStorage::Filename.wrap(filename || self.filename),
    content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options)
end

#variable?Boolean

Returns:

  • (Boolean)


264
265
266
# File 'lib/active_storage/aws_record/blob.rb', line 264

def variable?
  ActiveStorage.variable_content_types.include?(content_type)
end

#variant(transformations) ⇒ Object

Raises:

  • (ActiveStorage::InvariableError)


276
277
278
279
280
# File 'lib/active_storage/aws_record/blob.rb', line 276

def variant(transformations)
  raise ActiveStorage::InvariableError unless variable?

  variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
end

#video?Boolean

Returns:

  • (Boolean)


261
# File 'lib/active_storage/aws_record/blob.rb', line 261

def video? = content_type&.start_with?('video')