Class: ActiveStorage::AwsRecord::Attachment
- Inherits:
-
Object
- Object
- ActiveStorage::AwsRecord::Attachment
- Includes:
- Item
- Defined in:
- lib/active_storage/aws_record/attachment.rb
Overview
The join item between an owner record and a blob. Stored under the owner’s partition (+ns#Owner#<record_type>#<record_id>+) with a sort key of ns#Attachment#<name>#<id>, so loading an owner’s attachments (the contract’s hot path) is a single base-table query. Creating/destroying an attachment also adjusts a strongly-consistent reference count on its blob, so a shared blob is only purged once no attachment references it.
Defined Under Namespace
Classes: NullBlobRelation
Instance Attribute Summary collapse
-
#immediate_variants_processed ⇒ Object
Transient flags Active Storage’s change objects set on attachments; not persisted to DynamoDB.
-
#pending_upload ⇒ Object
Transient flags Active Storage’s change objects set on attachments; not persisted to DynamoDB.
Class Method Summary collapse
-
.attachment_prefix(name = nil) ⇒ Object
The sort-key prefix for an owner’s attachments, optionally narrowed to a single attachment
name, with a trailing separator sobegins_withcannot bleed across names. -
.blob_count_update(blob_id, delta) ⇒ Object
Atomic
ADDon a blob’s reference count byblob_id, guarded on the blob item still existing (so a stale/purged blob is never resurrected by the ADD). - .find_by(attributes) ⇒ Object
-
.none_for_blob(persisted) ⇒ Object
Empty relation used by Blob#attachments (see Blob): returns nothing for a non-persisted blob, and refuses to materialize on a persisted one (no blob→attachment index exists by design).
-
.owner_partition(record_type, record_id) ⇒ Object
The owner-adjacency partition key for a (record_type, record_id) pair.
-
.transaction(&block) ⇒ Object
Open a real, fiber-local DynamoDB transaction (see Transaction).
- .where(attributes = nil) ⇒ Object
Instance Method Summary collapse
- #as_json(options = nil) ⇒ Object
- #assign_attributes(attributes) ⇒ Object
- #blob ⇒ Object
- #blob=(blob) ⇒ Object
-
#commit_destroy! ⇒ Object
Flush a single buffered destroy: identical to a non-transactional destroy, so its idempotent duplicate-purge / orphaned-blob recovery is preserved (the batched ≥2 path cannot offer per-row recovery).
-
#delete ⇒ Object
Like #destroy (it also decrements the blob count, so the count never drifts), but used by replace/detach paths that manage the blob themselves; skips
touch/blob cleanup. -
#delete_transact_item ⇒ Object
The
Deleteaction (without the { delete: } wrapper) for this attachment’s row, guarded so a row that already vanished cancels the transaction rather than silently masking a concurrent change. -
#destroy ⇒ Object
Delete the join item and decrement its blob’s reference count atomically.
-
#initialize(attributes = {}) ⇒ Attachment
constructor
A new instance of Attachment.
- #logical_keys ⇒ Object
- #preview(transformations) ⇒ Object
- #previously_persisted? ⇒ Boolean
-
#purge ⇒ Object
Purge the attachment and (if unreferenced) its blob.
-
#purge_later ⇒ Object
See #purge: assumes no ambient
Attachment.transaction. -
#record ⇒ Object
Resolve the owner from the stored (record_type, record_id).
- #record=(record) ⇒ Object
- #representation(transformations) ⇒ Object
- #save(opts = {}) ⇒ Object
-
#save!(opts = {}) ⇒ Object
Persist the join item.
- #signed_id ⇒ Object
-
#uploaded(io:) ⇒ Object
Upload the io to the service and (optionally) analyze, mirroring the reference backend’s attachment upload lifecycle.
- #variant(transformations) ⇒ Object
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 = {}) ⇒ Attachment
Returns a new instance of Attachment.
85 86 87 88 89 90 91 92 93 94 |
# File 'lib/active_storage/aws_record/attachment.rb', line 85 def initialize(attributes = {}) super() attributes = attributes.dup record = attributes.delete(:record) || attributes.delete('record') blob = attributes.delete(:blob) || attributes.delete('blob') self.id ||= generate_uuid assign_attributes(attributes) if attributes.any? self.record = record if record self.blob = blob if blob end |
Instance Attribute Details
#immediate_variants_processed ⇒ Object
Transient flags Active Storage’s change objects set on attachments; not persisted to DynamoDB.
26 27 28 |
# File 'lib/active_storage/aws_record/attachment.rb', line 26 def immediate_variants_processed @immediate_variants_processed end |
#pending_upload ⇒ Object
Transient flags Active Storage’s change objects set on attachments; not persisted to DynamoDB.
26 27 28 |
# File 'lib/active_storage/aws_record/attachment.rb', line 26 def pending_upload @pending_upload end |
Class Method Details
.attachment_prefix(name = nil) ⇒ Object
The sort-key prefix for an owner’s attachments, optionally narrowed to a single attachment name, with a trailing separator so begins_with cannot bleed across names.
72 73 74 75 |
# File 'lib/active_storage/aws_record/attachment.rb', line 72 def (name = nil) base = name ? ns_key('Attachment', name) : ns_key('Attachment') "#{base}#{ActiveStorage::AwsRecord.config.separator}" end |
.blob_count_update(blob_id, delta) ⇒ Object
Atomic ADD on a blob’s reference count by blob_id, guarded on the blob item still existing (so a stale/purged blob is never resurrected by the ADD). Shared by the per-row path and the batched commit (which coalesces one update per distinct blob).
44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/active_storage/aws_record/attachment.rb', line 44 def blob_count_update(blob_id, delta) blob_keys = ActiveStorage::AwsRecord::Blob.logical_keys_for(blob_id) { table_name: ActiveStorage::AwsRecord::Blob.table_name, key: ActiveStorage::AwsRecord::Blob.physical_key(**blob_keys), update_expression: 'ADD #c :delta', condition_expression: 'attribute_exists(#h)', expression_attribute_names: { '#c' => 'as_attachments_count', '#h' => schema.partition_attr }, expression_attribute_values: { ':delta' => delta }, } end |
.find_by(attributes) ⇒ Object
56 57 58 |
# File 'lib/active_storage/aws_record/attachment.rb', line 56 def find_by(attributes) ActiveStorage::AwsRecord::Relation.new(self).find_by(attributes) end |
.none_for_blob(persisted) ⇒ Object
Empty relation used by Blob#attachments (see Blob): returns nothing for a non-persisted blob, and refuses to materialize on a persisted one (no blob→attachment index exists by design).
80 81 82 |
# File 'lib/active_storage/aws_record/attachment.rb', line 80 def none_for_blob(persisted) NullBlobRelation.new(persisted) end |
.owner_partition(record_type, record_id) ⇒ Object
The owner-adjacency partition key for a (record_type, record_id) pair.
65 66 67 |
# File 'lib/active_storage/aws_record/attachment.rb', line 65 def owner_partition(record_type, record_id) ns_key('Owner', record_type, record_id) end |
.transaction(&block) ⇒ Object
Open a real, fiber-local DynamoDB transaction (see Transaction). The generic has_many clear/replace/detach paths wrap their per-row destroys in this; buffering them into one transact_write_items makes a multi-attachment change atomic, instead of deleting some rows before a later one fails. Nested calls join the enclosing transaction. *Only destroys are buffered* — creates stay synchronous so Active Storage’s own failed-save cleanup still fires.
36 37 38 |
# File 'lib/active_storage/aws_record/attachment.rb', line 36 def transaction(&block) ActiveStorage::AwsRecord::Transaction.run(self, &block) end |
.where(attributes = nil) ⇒ Object
60 61 62 |
# File 'lib/active_storage/aws_record/attachment.rb', line 60 def where(attributes = nil) ActiveStorage::AwsRecord::Relation.new(self).where(attributes) end |
Instance Method Details
#as_json(options = nil) ⇒ Object
273 274 275 |
# File 'lib/active_storage/aws_record/attachment.rb', line 273 def as_json( = nil) { id: id, name: name, record_type: record_type, record_id: record_id, blob_id: blob_id }.as_json() end |
#assign_attributes(attributes) ⇒ Object
96 97 98 99 100 101 102 103 |
# File 'lib/active_storage/aws_record/attachment.rb', line 96 def assign_attributes(attributes) attributes = attributes.dup record = attributes.delete(:record) || attributes.delete('record') blob = attributes.delete(:blob) || attributes.delete('blob') attributes.each { |name, value| public_send("#{name}=", value) } self.record = record if record self.blob = blob if blob end |
#blob ⇒ Object
140 141 142 |
# File 'lib/active_storage/aws_record/attachment.rb', line 140 def blob @blob ||= ActiveStorage::AwsRecord::Blob.find(blob_id) end |
#blob=(blob) ⇒ Object
135 136 137 138 |
# File 'lib/active_storage/aws_record/attachment.rb', line 135 def blob=(blob) @blob = blob self.blob_id = blob&.id end |
#commit_destroy! ⇒ Object
Flush a single buffered destroy: identical to a non-transactional destroy, so its idempotent duplicate-purge / orphaned-blob recovery is preserved (the batched ≥2 path cannot offer per-row recovery).
208 209 210 |
# File 'lib/active_storage/aws_record/attachment.rb', line 208 def commit_destroy! transactional_destroy! end |
#delete ⇒ Object
Like #destroy (it also decrements the blob count, so the count never drifts), but used by replace/detach paths that manage the blob themselves; skips touch/blob cleanup.
193 194 195 196 197 |
# File 'lib/active_storage/aws_record/attachment.rb', line 193 def delete @previously_persisted = persisted? enqueue_or_destroy if @previously_persisted true end |
#delete_transact_item ⇒ Object
The Delete action (without the { delete: } wrapper) for this attachment’s row, guarded so a row that already vanished cancels the transaction rather than silently masking a concurrent change.
215 216 217 218 219 220 221 222 |
# File 'lib/active_storage/aws_record/attachment.rb', line 215 def delete_transact_item { table_name: self.class.table_name, key: physical_key, condition_expression: 'attribute_exists(#h)', expression_attribute_names: { '#h' => schema.partition_attr }, } end |
#destroy ⇒ Object
Delete the join item and decrement its blob’s reference count atomically. Must complete or raise (never return false) for the generic dependent-destroy path; a failure maps to RecordNotDestroyed.
179 180 181 182 183 184 185 186 187 188 |
# File 'lib/active_storage/aws_record/attachment.rb', line 179 def destroy @previously_persisted = persisted? enqueue_or_destroy if @previously_persisted true rescue Aws::DynamoDB::Errors::ServiceError => e # ServiceError, not Errors::Error: every DynamoDB error (throttling, # conditional failure, a re-raised cancellation) subclasses ServiceError, # while Errors::Error has no subclasses — rescuing it would catch nothing. raise ActiveStorage::RecordNotDestroyed.new("Failed to destroy attachment: #{e.}", self) end |
#logical_keys ⇒ Object
105 106 107 108 109 110 111 |
# File 'lib/active_storage/aws_record/attachment.rb', line 105 def logical_keys { h: self.class.owner_partition(record_type, record_id), r: ns_key('Attachment', name, id), item_id: ns_key('Attachment', id), } end |
#preview(transformations) ⇒ Object
265 266 267 |
# File 'lib/active_storage/aws_record/attachment.rb', line 265 def preview(transformations) blob.preview(transformations_by_name(transformations)) end |
#previously_persisted? ⇒ Boolean
199 200 201 |
# File 'lib/active_storage/aws_record/attachment.rb', line 199 def previously_persisted? @previously_persisted end |
#purge ⇒ Object
Purge the attachment and (if unreferenced) its blob. Assumes no ambient Attachment.transaction is open: the destroy here commits at the end of its own block, so the refcount is decremented before blob.purge checks it. Nested inside an outer transaction the decrement would not have committed yet, so the blob’s foreign-key guard would (harmlessly) refuse the purge — the contract never calls purge from inside a transaction.
230 231 232 233 234 235 236 |
# File 'lib/active_storage/aws_record/attachment.rb', line 230 def purge self.class.transaction do destroy touch_record end blob&.purge end |
#purge_later ⇒ Object
See #purge: assumes no ambient Attachment.transaction.
239 240 241 242 243 244 245 |
# File 'lib/active_storage/aws_record/attachment.rb', line 239 def purge_later self.class.transaction do destroy touch_record end blob&.purge_later end |
#record ⇒ Object
Resolve the owner from the stored (record_type, record_id). Prefer the active_storage_find hook (ActiveStorage::AwsRecord::Attachable/Owner owners define it so an aws-record model with a key-hash #find resolves correctly), and fall back to the generic contract’s bare-id find(record_id) (the gem’s own Blob/VariantRecord owners override that).
124 125 126 127 128 129 130 131 132 133 |
# File 'lib/active_storage/aws_record/attachment.rb', line 124 def record @record ||= begin owner_class = record_type.constantize if owner_class.respond_to?(:active_storage_find) owner_class.active_storage_find(record_id) else owner_class.find(record_id) end end end |
#record=(record) ⇒ Object
113 114 115 116 117 |
# File 'lib/active_storage/aws_record/attachment.rb', line 113 def record=(record) @record = record self.record_type = ActiveStorage::Attached::Changes.polymorphic_name(record) self.record_id = record.id.to_s end |
#representation(transformations) ⇒ Object
269 270 271 |
# File 'lib/active_storage/aws_record/attachment.rb', line 269 def representation(transformations) blob.representation(transformations_by_name(transformations)) end |
#save(opts = {}) ⇒ Object
169 170 171 172 173 174 |
# File 'lib/active_storage/aws_record/attachment.rb', line 169 def save(opts = {}) save!(opts) true rescue ActiveStorage::RecordNotSaved false end |
#save!(opts = {}) ⇒ Object
Persist the join item. On create, atomically (DynamoDB transaction) put the attachment and increment its blob’s reference count — guarding the increment on the blob still existing so a purged blob is never resurrected.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/active_storage/aws_record/attachment.rb', line 151 def save!(opts = {}) self.created_at ||= ActiveStorage::AwsRecord::Blob. if new_record? stamp_physical_keys! transactional_create! end # A persisted attachment join row is immutable (record/blob/name are # fixed at creation), so a re-save is a deliberate no-op. Calling super # here would invoke aws-record's #save!, which delegates to #save — and # because we override #save to call #save!, that recurses infinitely # (SystemStackError). Active Storage re-saves already-persisted # attachments during its attach flow, so this path is hot. self rescue Aws::DynamoDB::Errors::TransactionCanceledException, Aws::DynamoDB::Errors::ConditionalCheckFailedException, Aws::Record::Errors::ConditionalWriteFailed, Aws::Record::Errors::ValidationError => e raise ActiveStorage::RecordNotSaved.new(e., self) end |
#signed_id ⇒ Object
144 145 146 |
# File 'lib/active_storage/aws_record/attachment.rb', line 144 def signed_id blob.signed_id end |
#uploaded(io:) ⇒ Object
Upload the io to the service and (optionally) analyze, mirroring the reference backend’s attachment upload lifecycle.
249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/active_storage/aws_record/attachment.rb', line 249 def uploaded(io:) blob.local_io = io blob.analyze_without_saving unless blob.analyzed? || skip_later_analysis? io.rewind if io.respond_to?(:rewind) blob.upload_without_unfurling(io) blob.save! if blob.persisted? blob.mirror_later blob.analyze_later unless blob.analyzed? || skip_later_analysis? ensure blob.local_io = nil end |
#variant(transformations) ⇒ Object
261 262 263 |
# File 'lib/active_storage/aws_record/attachment.rb', line 261 def variant(transformations) blob.variant(transformations_by_name(transformations)) end |