activestorage-aws-record 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +55 -0
- data/LICENSE +21 -0
- data/PLAN.md +413 -0
- data/README.md +287 -0
- data/lib/active_storage/aws_record/attachable.rb +104 -0
- data/lib/active_storage/aws_record/attachment.rb +426 -0
- data/lib/active_storage/aws_record/blob.rb +417 -0
- data/lib/active_storage/aws_record/configuration.rb +62 -0
- data/lib/active_storage/aws_record/item.rb +86 -0
- data/lib/active_storage/aws_record/owner.rb +82 -0
- data/lib/active_storage/aws_record/persistence.rb +89 -0
- data/lib/active_storage/aws_record/railtie.rb +53 -0
- data/lib/active_storage/aws_record/relation.rb +163 -0
- data/lib/active_storage/aws_record/schema.rb +148 -0
- data/lib/active_storage/aws_record/tables.rb +82 -0
- data/lib/active_storage/aws_record/tasks.rake +15 -0
- data/lib/active_storage/aws_record/transaction.rb +132 -0
- data/lib/active_storage/aws_record/variant_record.rb +208 -0
- data/lib/active_storage/aws_record/version.rb +7 -0
- data/lib/active_storage/aws_record.rb +148 -0
- data/lib/activestorage-aws-record.rb +4 -0
- metadata +166 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'active_storage/aws_record/item'
|
|
3
|
+
require 'active_storage/aws_record/owner'
|
|
4
|
+
|
|
5
|
+
module ActiveStorage
|
|
6
|
+
module AwsRecord
|
|
7
|
+
# Tracks a processed variant of a blob when
|
|
8
|
+
# +config.active_storage.track_variants+ is on. It is co-located in its
|
|
9
|
+
# blob's partition (+ns#Blob#<blob_id>+) with a sort key of
|
|
10
|
+
# +ns#VariantRecord#<variation_digest>+, so the (blob_id, variation_digest)
|
|
11
|
+
# pair is unique by construction and the blob and all its variants form one
|
|
12
|
+
# item collection. It is itself an attachment owner (+has_one_attached
|
|
13
|
+
# :image+); its owner +id+ is a reversible, +#+-free encoding of the natural
|
|
14
|
+
# key so +find(id)+ resolves it.
|
|
15
|
+
class VariantRecord
|
|
16
|
+
include ActiveStorage::AwsRecord::Item
|
|
17
|
+
include ActiveStorage::AwsRecord::Owner
|
|
18
|
+
|
|
19
|
+
ID_DELIMITER = ' '
|
|
20
|
+
|
|
21
|
+
# Raised internally when the variant row already exists (the conditional
|
|
22
|
+
# +Put+ lost the race), so create_or_find_by! can fall back to +find+ —
|
|
23
|
+
# while genuine post-write failures (e.g. the image attachment) propagate.
|
|
24
|
+
class CreateConflict < StandardError; end
|
|
25
|
+
|
|
26
|
+
string_attr :blob_id, database_attribute_name: 'as_blob_id'
|
|
27
|
+
string_attr :variation_digest, database_attribute_name: 'as_variation_digest'
|
|
28
|
+
string_attr :entity, database_attribute_name: 'as_entity', default_value: 'VariantRecord'
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Contract +find(id)+: decode the reversible id back to the natural key.
|
|
32
|
+
def find(id)
|
|
33
|
+
blob_id, variation_digest = decode_id(id)
|
|
34
|
+
find_by(blob_id: blob_id, variation_digest: variation_digest) ||
|
|
35
|
+
raise(ActiveStorage::RecordNotFound, "Couldn't find #{name} with id=#{id.inspect}")
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
raise ActiveStorage::RecordNotFound, "Couldn't find #{name} with id=#{id.inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_by(blob_id:, variation_digest:)
|
|
41
|
+
keys = logical_keys_for(blob_id.to_s, variation_digest)
|
|
42
|
+
get_item(**keys)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Owner resolution for Active Storage (VariantRecord is an attachment owner
|
|
46
|
+
# via +image+). Delegate to the reversible-id +#find+: {Attachable}'s default
|
|
47
|
+
# +find_with_opts(hash_key => id)+ adapter cannot address the composite key.
|
|
48
|
+
def active_storage_find(id)
|
|
49
|
+
find(id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Race-safe creation: a conditional put on the variant's own key, paired
|
|
53
|
+
# with a check that the source blob still exists, so a variant can never
|
|
54
|
+
# be created against a just-purged blob. A condition failure (the variant
|
|
55
|
+
# already exists, or the blob is gone) falls back to find. The block (used
|
|
56
|
+
# by VariantWithRecord to +image.attach+ the processed file) runs before
|
|
57
|
+
# save so the queued image attachment is flushed by the save callbacks.
|
|
58
|
+
def create_or_find_by!(blob_id:, variation_digest:)
|
|
59
|
+
record = new(blob_id: blob_id.to_s, variation_digest: variation_digest)
|
|
60
|
+
yield record if block_given?
|
|
61
|
+
record.save!
|
|
62
|
+
record
|
|
63
|
+
rescue CreateConflict
|
|
64
|
+
# Only a genuine create conflict (the row already exists) falls back to
|
|
65
|
+
# find — a post-write failure (image attachment, blob gone) propagates.
|
|
66
|
+
# NOTE: the marker row commits just before its image attachment is
|
|
67
|
+
# flushed, so a *concurrent* processor that loses the race here can
|
|
68
|
+
# briefly observe the record before its image is attached (a transient
|
|
69
|
+
# that resolves once the winner finishes; like Active Storage's
|
|
70
|
+
# create-then-upload, but without AR's single enclosing transaction).
|
|
71
|
+
find_by(blob_id: blob_id, variation_digest: variation_digest) ||
|
|
72
|
+
raise(ActiveStorage::RecordNotSaved.new('Failed to create or find variant record', record))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# All variant records for a blob (used by Blob#destroy's variant sweep).
|
|
76
|
+
# Mode A: strong base-table query under the blob partition. Mode B: GSI.
|
|
77
|
+
def where_blob(blob_id)
|
|
78
|
+
schema = ActiveStorage::AwsRecord.schema
|
|
79
|
+
partition = ns_key('Blob', blob_id)
|
|
80
|
+
prefix = "#{ns_key('VariantRecord')}#{ActiveStorage::AwsRecord.config.separator}"
|
|
81
|
+
opts = {
|
|
82
|
+
key_condition_expression: '#h = :h AND begins_with(#r, :r)',
|
|
83
|
+
expression_attribute_values: { ':h' => partition, ':r' => prefix },
|
|
84
|
+
}
|
|
85
|
+
if schema.range_mode?
|
|
86
|
+
opts[:expression_attribute_names] = { '#h' => schema.partition_attr, '#r' => schema.sort_attr }
|
|
87
|
+
opts[:consistent_read] = true
|
|
88
|
+
else
|
|
89
|
+
opts[:index_name] = schema.index_name
|
|
90
|
+
opts[:expression_attribute_names] = { '#h' => schema.index_partition_attr, '#r' => schema.index_sort_attr }
|
|
91
|
+
end
|
|
92
|
+
query(opts).to_a
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Logical keys for a (blob_id, variation_digest) pair without an instance.
|
|
96
|
+
def logical_keys_for(blob_id, variation_digest)
|
|
97
|
+
{
|
|
98
|
+
h: ns_key('Blob', blob_id),
|
|
99
|
+
r: ns_key('VariantRecord', variation_digest),
|
|
100
|
+
item_id: ns_key('Blob', blob_id, 'VariantRecord', variation_digest),
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def encode_id(blob_id, variation_digest)
|
|
105
|
+
Base64.urlsafe_encode64("#{blob_id}#{ID_DELIMITER}#{variation_digest}", padding: false)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def decode_id(id)
|
|
109
|
+
Base64.urlsafe_decode64(id).split(ID_DELIMITER, 2)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def logical_keys
|
|
114
|
+
self.class.logical_keys_for(blob_id, variation_digest)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Persist the variant row transactionally *and* run the owner save/commit
|
|
118
|
+
# callbacks, so a queued +image+ attachment (from create_or_find_by!'s
|
|
119
|
+
# block) is saved and uploaded. Overrides Owner#save! to substitute the
|
|
120
|
+
# transactional create for aws-record's plain put. If a step *after* the row
|
|
121
|
+
# write fails (e.g. the image attachment), the half-written row is removed
|
|
122
|
+
# so it does not become a markerless "poison" record.
|
|
123
|
+
def save!(opts = {})
|
|
124
|
+
marker_written = false
|
|
125
|
+
completed = run_callbacks(:save) do
|
|
126
|
+
stamp_physical_keys!
|
|
127
|
+
if new_record?
|
|
128
|
+
transactional_create!
|
|
129
|
+
marker_written = true
|
|
130
|
+
end
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
raise ActiveStorage::RecordNotSaved.new('Save halted by a before_save callback', self) unless completed
|
|
134
|
+
|
|
135
|
+
run_callbacks(:commit)
|
|
136
|
+
self
|
|
137
|
+
rescue CreateConflict
|
|
138
|
+
raise
|
|
139
|
+
rescue StandardError
|
|
140
|
+
destroy_marker! if marker_written
|
|
141
|
+
raise
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def save(opts = {})
|
|
145
|
+
save!(opts)
|
|
146
|
+
true
|
|
147
|
+
rescue ActiveStorage::RecordNotSaved, CreateConflict
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The owner id used as +record_id+ for the +image+ attachment; reversible
|
|
152
|
+
# so VariantRecord.find(id) resolves the natural key.
|
|
153
|
+
def id
|
|
154
|
+
return nil if blob_id.nil? || variation_digest.nil?
|
|
155
|
+
|
|
156
|
+
self.class.encode_id(blob_id, variation_digest)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def transactional_create!
|
|
162
|
+
blob_keys = ActiveStorage::AwsRecord::Blob.logical_keys_for(blob_id)
|
|
163
|
+
dynamodb_client.transact_write_items(
|
|
164
|
+
transact_items: [
|
|
165
|
+
{ put: {
|
|
166
|
+
table_name: self.class.table_name,
|
|
167
|
+
item: save_values,
|
|
168
|
+
condition_expression: 'attribute_not_exists(#h)',
|
|
169
|
+
expression_attribute_names: { '#h' => schema.partition_attr },
|
|
170
|
+
} },
|
|
171
|
+
{ condition_check: {
|
|
172
|
+
table_name: ActiveStorage::AwsRecord::Blob.table_name,
|
|
173
|
+
key: ActiveStorage::AwsRecord::Blob.physical_key(**blob_keys),
|
|
174
|
+
condition_expression: 'attribute_exists(#h)',
|
|
175
|
+
expression_attribute_names: { '#h' => schema.partition_attr },
|
|
176
|
+
} },
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
mark_persisted!
|
|
180
|
+
rescue Aws::DynamoDB::Errors::TransactionCanceledException => e
|
|
181
|
+
# reasons[0] = the variant Put, reasons[1] = the blob existence check.
|
|
182
|
+
reasons = e.cancellation_reasons || []
|
|
183
|
+
put_failed = reasons[0]&.code == 'ConditionalCheckFailed'
|
|
184
|
+
blob_failed = reasons[1]&.code == 'ConditionalCheckFailed'
|
|
185
|
+
# Only a pure put-conflict (variant exists, source blob still present) is
|
|
186
|
+
# a CreateConflict; if the blob is gone, surface a genuine failure.
|
|
187
|
+
raise CreateConflict if put_failed && !blob_failed
|
|
188
|
+
|
|
189
|
+
raise ActiveStorage::RecordNotSaved.new(e.message, self)
|
|
190
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException, Aws::Record::Errors::ConditionalWriteFailed,
|
|
191
|
+
Aws::Record::Errors::ValidationError => e
|
|
192
|
+
raise ActiveStorage::RecordNotSaved.new(e.message, self)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Remove a half-written variant row (best effort) so a post-write failure
|
|
196
|
+
# does not leave a markerless record that blocks regeneration. Also purge
|
|
197
|
+
# the image attachment/blob if it was already created during +after_save+,
|
|
198
|
+
# so it is not orphaned.
|
|
199
|
+
def destroy_marker!
|
|
200
|
+
image.purge if image.attached?
|
|
201
|
+
dynamodb_client.delete_item(table_name: self.class.table_name, key: physical_key)
|
|
202
|
+
mark_destroyed!
|
|
203
|
+
rescue StandardError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require 'aws-record'
|
|
3
|
+
require 'globalid'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'active_support/core_ext/object/blank'
|
|
6
|
+
require 'active_model'
|
|
7
|
+
require 'active_storage'
|
|
8
|
+
|
|
9
|
+
require 'active_storage/aws_record/version'
|
|
10
|
+
require 'active_storage/aws_record/configuration'
|
|
11
|
+
require 'active_storage/aws_record/schema'
|
|
12
|
+
|
|
13
|
+
module ActiveStorage
|
|
14
|
+
# Active Storage *metadata* backend backed by Amazon DynamoDB through the
|
|
15
|
+
# +aws-record+ gem. Blob bytes flow through a normal Active Storage Service
|
|
16
|
+
# (Disk/S3); only blob/attachment/variant-record metadata lives in DynamoDB,
|
|
17
|
+
# in a single application-provided table (Single Table Design). See PLAN.md /
|
|
18
|
+
# README.md for the full design.
|
|
19
|
+
module AwsRecord
|
|
20
|
+
autoload :Persistence, 'active_storage/aws_record/persistence'
|
|
21
|
+
autoload :Item, 'active_storage/aws_record/item'
|
|
22
|
+
autoload :Attachable, 'active_storage/aws_record/attachable'
|
|
23
|
+
autoload :Owner, 'active_storage/aws_record/owner'
|
|
24
|
+
autoload :Relation, 'active_storage/aws_record/relation'
|
|
25
|
+
autoload :Transaction, 'active_storage/aws_record/transaction'
|
|
26
|
+
autoload :TransactionTooLarge, 'active_storage/aws_record/transaction'
|
|
27
|
+
autoload :Blob, 'active_storage/aws_record/blob'
|
|
28
|
+
autoload :Attachment, 'active_storage/aws_record/attachment'
|
|
29
|
+
autoload :VariantRecord, 'active_storage/aws_record/variant_record'
|
|
30
|
+
autoload :Tables, 'active_storage/aws_record/tables'
|
|
31
|
+
|
|
32
|
+
# Eager, load-time initialization keeps the shared mutable state fiber-safe
|
|
33
|
+
# under Falcon: the mutex exists before any fiber can race on the client, and
|
|
34
|
+
# the config/schema objects are created up front (never via +||=+).
|
|
35
|
+
@client_mutex = Mutex.new
|
|
36
|
+
@config = Configuration.new
|
|
37
|
+
@schema = nil
|
|
38
|
+
@dynamodb_client = nil
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
# @return [Configuration] the memoized gem configuration.
|
|
42
|
+
attr_reader :config
|
|
43
|
+
|
|
44
|
+
# @return [Schema, nil] the resolved table layout (set by {.install!}).
|
|
45
|
+
attr_reader :schema
|
|
46
|
+
|
|
47
|
+
# Yields {#config} for block-style configuration.
|
|
48
|
+
def configure
|
|
49
|
+
yield @config
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The shared +Aws::DynamoDB::Client+ every model uses. Built lazily but
|
|
53
|
+
# mutex-guarded so concurrent fibers cannot race two clients into existence.
|
|
54
|
+
#
|
|
55
|
+
# @return [Aws::DynamoDB::Client]
|
|
56
|
+
def dynamodb_client
|
|
57
|
+
@client_mutex.synchronize do
|
|
58
|
+
@dynamodb_client ||= @config.client || Aws::DynamoDB::Client.new(@config.client_options)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Inject a client (tests / Railtie) and rewire the models to it.
|
|
63
|
+
def dynamodb_client=(client)
|
|
64
|
+
@client_mutex.synchronize { @dynamodb_client = client }
|
|
65
|
+
[Blob, Attachment, VariantRecord].each do |model|
|
|
66
|
+
model.configure_client(client: client) if model.respond_to?(:configure_client)
|
|
67
|
+
end
|
|
68
|
+
client
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Drop the memoized client/schema (tests, and the Railtie before re-boot).
|
|
72
|
+
def reset!
|
|
73
|
+
@client_mutex.synchronize do
|
|
74
|
+
@dynamodb_client = nil
|
|
75
|
+
@schema = nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Build a +#+-separated composite key, validating every segment is present
|
|
80
|
+
# (a blank segment would silently corrupt the key space). This is the gem's
|
|
81
|
+
# lightweight stand-in for an app-specific +compose_key+.
|
|
82
|
+
#
|
|
83
|
+
# @param parts [Array<#to_s>]
|
|
84
|
+
# @return [String]
|
|
85
|
+
def key(*parts)
|
|
86
|
+
separator = @config.separator
|
|
87
|
+
parts.each do |part|
|
|
88
|
+
raise ArgumentError, "key segment cannot be blank (parts: #{parts.inspect})" if part.nil? || part.to_s.empty?
|
|
89
|
+
|
|
90
|
+
if part.to_s.include?(separator)
|
|
91
|
+
raise ArgumentError, "key segment #{part.inspect} may not contain the separator #{separator.inspect} " \
|
|
92
|
+
"(parts: #{parts.inspect})"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
parts.join(separator)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Discover the table layout, declare the key attributes on the three models
|
|
99
|
+
# (now that their DB names are known), point them at the shared client, and
|
|
100
|
+
# set the table name. Idempotent; safe to call at every boot.
|
|
101
|
+
def install!
|
|
102
|
+
client = dynamodb_client
|
|
103
|
+
Tables.ensure! if @config.manage_table
|
|
104
|
+
@client_mutex.synchronize { @schema ||= Schema.discover(client, @config) }
|
|
105
|
+
|
|
106
|
+
[Blob, Attachment, VariantRecord].each do |model|
|
|
107
|
+
model.set_table_name(@config.table_name)
|
|
108
|
+
define_key_attributes!(model)
|
|
109
|
+
model.configure_client(client: client)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Declare +has_one_attached+ on the gem's owner entities. Must run after
|
|
114
|
+
# Active Storage class indirection so the generic (non-AR) builder is used.
|
|
115
|
+
# Idempotent.
|
|
116
|
+
def install_attachments!
|
|
117
|
+
unless Blob.respond_to?(:reflect_on_attachment) && Blob.reflect_on_attachment(:preview_image)
|
|
118
|
+
Blob.has_one_attached :preview_image
|
|
119
|
+
end
|
|
120
|
+
unless VariantRecord.respond_to?(:reflect_on_attachment) && VariantRecord.reflect_on_attachment(:image)
|
|
121
|
+
VariantRecord.has_one_attached :image
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Declare the table's key attributes on a model using the discovered DB
|
|
128
|
+
# names. Ruby accessor names are gem-private (+dynamo_*+) so they never
|
|
129
|
+
# collide with entity attributes. In Mode B the numeric range key holds a
|
|
130
|
+
# constant and two extra string attributes carry the adjacency keys for the
|
|
131
|
+
# GSI.
|
|
132
|
+
def define_key_attributes!(model)
|
|
133
|
+
return if model.hash_key # already installed in this process
|
|
134
|
+
|
|
135
|
+
model.string_attr :dynamo_partition_key, hash_key: true, database_attribute_name: @schema.partition_attr
|
|
136
|
+
if @schema.range_mode?
|
|
137
|
+
model.string_attr :dynamo_range_key, range_key: true, database_attribute_name: @schema.sort_attr
|
|
138
|
+
else
|
|
139
|
+
model.integer_attr :dynamo_range_key, range_key: true, database_attribute_name: @schema.sort_attr
|
|
140
|
+
model.string_attr :dynamo_index_partition, database_attribute_name: @schema.index_partition_attr
|
|
141
|
+
model.string_attr :dynamo_index_sort, database_attribute_name: @schema.index_sort_attr
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
require 'active_storage/aws_record/railtie' if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activestorage-aws-record
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Thomas Witt
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activestorage
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activejob
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '8.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: activemodel
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '8.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '8.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: activesupport
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '8.1'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '8.1'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: aws-record
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '2.15'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.15'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: aws-sdk-dynamodb
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: globalid
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1.0'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1.0'
|
|
110
|
+
description: |
|
|
111
|
+
A metadata backend that lets Active Storage store its Blob, Attachment, and
|
|
112
|
+
VariantRecord rows in Amazon DynamoDB (through the aws-record gem) instead of
|
|
113
|
+
Active Record. Blob bytes still flow through any Active Storage Service
|
|
114
|
+
(Disk, S3, ...); only the metadata lives in DynamoDB. Implements the generic
|
|
115
|
+
custom Active Storage backend contract.
|
|
116
|
+
executables: []
|
|
117
|
+
extensions: []
|
|
118
|
+
extra_rdoc_files: []
|
|
119
|
+
files:
|
|
120
|
+
- CHANGELOG.md
|
|
121
|
+
- LICENSE
|
|
122
|
+
- PLAN.md
|
|
123
|
+
- README.md
|
|
124
|
+
- lib/active_storage/aws_record.rb
|
|
125
|
+
- lib/active_storage/aws_record/attachable.rb
|
|
126
|
+
- lib/active_storage/aws_record/attachment.rb
|
|
127
|
+
- lib/active_storage/aws_record/blob.rb
|
|
128
|
+
- lib/active_storage/aws_record/configuration.rb
|
|
129
|
+
- lib/active_storage/aws_record/item.rb
|
|
130
|
+
- lib/active_storage/aws_record/owner.rb
|
|
131
|
+
- lib/active_storage/aws_record/persistence.rb
|
|
132
|
+
- lib/active_storage/aws_record/railtie.rb
|
|
133
|
+
- lib/active_storage/aws_record/relation.rb
|
|
134
|
+
- lib/active_storage/aws_record/schema.rb
|
|
135
|
+
- lib/active_storage/aws_record/tables.rb
|
|
136
|
+
- lib/active_storage/aws_record/tasks.rake
|
|
137
|
+
- lib/active_storage/aws_record/transaction.rb
|
|
138
|
+
- lib/active_storage/aws_record/variant_record.rb
|
|
139
|
+
- lib/active_storage/aws_record/version.rb
|
|
140
|
+
- lib/activestorage-aws-record.rb
|
|
141
|
+
homepage: https://github.com/thomaswitt/activestorage-aws-record
|
|
142
|
+
licenses:
|
|
143
|
+
- MIT
|
|
144
|
+
metadata:
|
|
145
|
+
homepage_uri: https://github.com/thomaswitt/activestorage-aws-record
|
|
146
|
+
source_code_uri: https://github.com/thomaswitt/activestorage-aws-record
|
|
147
|
+
changelog_uri: https://github.com/thomaswitt/activestorage-aws-record/blob/main/CHANGELOG.md
|
|
148
|
+
rubygems_mfa_required: 'true'
|
|
149
|
+
rdoc_options: []
|
|
150
|
+
require_paths:
|
|
151
|
+
- lib
|
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
153
|
+
requirements:
|
|
154
|
+
- - ">="
|
|
155
|
+
- !ruby/object:Gem::Version
|
|
156
|
+
version: 3.4.0
|
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
|
+
requirements:
|
|
159
|
+
- - ">="
|
|
160
|
+
- !ruby/object:Gem::Version
|
|
161
|
+
version: '0'
|
|
162
|
+
requirements: []
|
|
163
|
+
rubygems_version: 4.0.12
|
|
164
|
+
specification_version: 4
|
|
165
|
+
summary: Run Active Storage on Amazon DynamoDB via aws-record.
|
|
166
|
+
test_files: []
|