Class: ActiveStorage::AwsRecord::Blob
- Inherits:
-
Object
- Object
- ActiveStorage::AwsRecord::Blob
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
-
.active_storage_find(id) ⇒ Object
Owner resolution for Active Storage (Blob is itself an attachment owner via preview_image).
-
.build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) ⇒ Object
-
.create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) ⇒ Object
-
.create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) ⇒ Object
-
.create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil) ⇒ Object
-
.current_timestamp ⇒ Object
Microsecond-precision, fixed-width UTC ISO8601 — lexically sortable (so has_many ordering by created_at is correct) and parseable back to a Time.
-
.find(id) ⇒ Object
Contract find(id): GetItem on the blob’s collection-root key.
-
.find_signed(id, record: nil, purpose: :blob_id) ⇒ Object
-
.find_signed!(id, record: nil, purpose: :blob_id) ⇒ Object
-
.generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH) ⇒ Object
-
.logical_keys_for(blob_id) ⇒ Object
Logical keys for a blob id without an instance (used by the attachment refcount transaction).
-
.scope_for_strict_loading ⇒ Object
-
.service ⇒ Object
-
.service=(service) ⇒ Object
-
.services ⇒ Object
-
.services=(registry) ⇒ Object
Instance Method Summary
collapse
-
#analyze ⇒ Object
-
#analyze_later ⇒ Object
-
#analyze_without_saving ⇒ Object
-
#analyzed ⇒ Object
-
#analyzed=(value) ⇒ Object
-
#analyzed? ⇒ Boolean
-
#attachments ⇒ Object
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).
-
#audio? ⇒ Boolean
-
#composed ⇒ Object
-
#composed=(value) ⇒ Object
-
#content_type_for_serving ⇒ Object
-
#created_at ⇒ Object
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.
-
#custom_metadata ⇒ Object
-
#delete ⇒ Object
-
#destroy ⇒ Object
Delete the metadata item and, when variant tracking is on, its variant records.
-
#download(&block) ⇒ Object
-
#download_chunk(range) ⇒ Object
-
#filename ⇒ Object
-
#filename=(value) ⇒ Object
-
#forced_disposition_for_serving ⇒ Object
-
#identified ⇒ Object
-
#identified=(value) ⇒ Object
-
#identified? ⇒ Boolean
-
#identify_without_saving ⇒ Object
-
#image? ⇒ Boolean
-
#initialize(attributes = {}) ⇒ Blob
constructor
-
#logical_keys ⇒ Object
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).
-
#mirror_later ⇒ Object
-
#open(tmpdir: nil, &block) ⇒ Object
-
#preview(transformations) ⇒ Object
-
#previewable? ⇒ Boolean
-
#previously_persisted? ⇒ Boolean
-
#purge ⇒ Object
-
#purge_later ⇒ Object
-
#representable? ⇒ Boolean
-
#representation(transformations) ⇒ Object
-
#service ⇒ Object
-
#service_headers_for_direct_upload ⇒ Object
-
#service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in) ⇒ Object
-
#signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil) ⇒ Object
-
#text? ⇒ Boolean
-
#unfurl(io, identify: true) ⇒ Object
-
#upload(io, identify: true) ⇒ Object
-
#upload_without_unfurling(io) ⇒ Object
-
#url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options) ⇒ Object
-
#variable? ⇒ Boolean
-
#variant(transformations) ⇒ Object
-
#video? ⇒ Boolean
Methods included from Owner
#save, #save!
Methods included from Item
#ns_key, #physical_key, #schema, #stamp_physical_keys!
#==, #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.metadata ||= {}
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_io ⇒ Object
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: 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: 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: 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)
metadata = ActiveStorage.filter_blob_metadata(metadata || {})
new(key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name).tap(&:save!)
end
|
.current_timestamp ⇒ Object
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_loading ⇒ Object
122
123
124
|
# File 'lib/active_storage/aws_record/blob.rb', line 122
def scope_for_strict_loading
self
end
|
.service ⇒ Object
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
|
.services ⇒ Object
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
#analyze ⇒ Object
199
200
201
202
|
# File 'lib/active_storage/aws_record/blob.rb', line 199
def analyze
analyze_without_saving
save!
end
|
#analyze_later ⇒ Object
204
205
206
|
# File 'lib/active_storage/aws_record/blob.rb', line 204
def analyze_later
ActiveStorage::AnalyzeJob.perform_later(self)
end
|
#analyze_without_saving ⇒ Object
195
196
197
|
# File 'lib/active_storage/aws_record/blob.rb', line 195
def analyze_without_saving
metadata_set(:analyzed, true)
end
|
#analyzed ⇒ Object
177
|
# File 'lib/active_storage/aws_record/blob.rb', line 177
def analyzed = !!indifferent_metadata[:analyzed]
|
#analyzed=(value) ⇒ Object
179
180
181
|
# File 'lib/active_storage/aws_record/blob.rb', line 179
def analyzed=(value)
metadata_set(:analyzed, value)
end
|
#analyzed? ⇒ Boolean
178
|
# File 'lib/active_storage/aws_record/blob.rb', line 178
def analyzed?(*) = analyzed
|
#attachments ⇒ Object
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.
#audio? ⇒ Boolean
260
|
# File 'lib/active_storage/aws_record/blob.rb', line 260
def audio? = content_type&.start_with?('audio')
|
#composed ⇒ Object
183
|
# File 'lib/active_storage/aws_record/blob.rb', line 183
def composed = !!indifferent_metadata[:composed]
|
#composed=(value) ⇒ Object
184
185
186
|
# File 'lib/active_storage/aws_record/blob.rb', line 184
def composed=(value)
metadata_set(:composed, value)
end
|
#content_type_for_serving ⇒ Object
256
|
# File 'lib/active_storage/aws_record/blob.rb', line 256
def content_type_for_serving = super
|
#created_at ⇒ Object
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
|
167
168
169
|
# File 'lib/active_storage/aws_record/blob.rb', line 167
def custom_metadata
indifferent_metadata[:custom] || {}
end
|
#delete ⇒ Object
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
|
#destroy ⇒ Object
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
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
|
#filename ⇒ Object
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_serving ⇒ Object
257
|
# File 'lib/active_storage/aws_record/blob.rb', line 257
def forced_disposition_for_serving = super
|
#identified ⇒ Object
171
|
# File 'lib/active_storage/aws_record/blob.rb', line 171
def identified = !!indifferent_metadata[:identified]
|
#identified=(value) ⇒ Object
173
174
175
|
# File 'lib/active_storage/aws_record/blob.rb', line 173
def identified=(value)
metadata_set(:identified, value)
end
|
#identified? ⇒ Boolean
172
|
# File 'lib/active_storage/aws_record/blob.rb', line 172
def identified?(*) = identified
|
#identify_without_saving ⇒ Object
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
259
|
# File 'lib/active_storage/aws_record/blob.rb', line 259
def image? = content_type&.start_with?('image')
|
#logical_keys ⇒ Object
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_later ⇒ Object
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
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
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
350
351
352
|
# File 'lib/active_storage/aws_record/blob.rb', line 350
def previously_persisted?
@previously_persisted
end
|
#purge ⇒ Object
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_later ⇒ Object
346
347
348
|
# File 'lib/active_storage/aws_record/blob.rb', line 346
def purge_later
ActiveStorage::PurgeJob.perform_later(self)
end
|
#representable? ⇒ 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
|
#service ⇒ Object
305
306
307
|
# File 'lib/active_storage/aws_record/blob.rb', line 305
def service
self.class.services.fetch(service_name)
end
|
251
252
253
254
|
# File 'lib/active_storage/aws_record/blob.rb', line 251
def
service.(key, filename: filename, content_type: content_type,
content_length: byte_size, checksum: checksum, custom_metadata: 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: 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
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
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
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
261
|
# File 'lib/active_storage/aws_record/blob.rb', line 261
def video? = content_type&.start_with?('video')
|