activestorage_legacy 0.1
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/.babelrc +5 -0
- data/.codeclimate.yml +7 -0
- data/.eslintrc +19 -0
- data/.github/workflows/gem-push.yml +29 -0
- data/.github/workflows/ruby-tests.yml +37 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +125 -0
- data/.travis.yml +25 -0
- data/Gemfile +33 -0
- data/Gemfile.lock +271 -0
- data/MIT-LICENSE +20 -0
- data/README.md +160 -0
- data/Rakefile +12 -0
- data/activestorage.gemspec +27 -0
- data/app/assets/javascripts/activestorage.js +1 -0
- data/app/controllers/active_storage/blobs_controller.rb +22 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +21 -0
- data/app/controllers/active_storage/disk_controller.rb +52 -0
- data/app/controllers/active_storage/variants_controller.rb +28 -0
- data/app/helpers/active_storage/file_field_with_direct_upload_helper.rb +18 -0
- data/app/javascript/activestorage/blob_record.js +54 -0
- data/app/javascript/activestorage/blob_upload.js +34 -0
- data/app/javascript/activestorage/direct_upload.js +42 -0
- data/app/javascript/activestorage/direct_upload_controller.js +67 -0
- data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
- data/app/javascript/activestorage/file_checksum.js +53 -0
- data/app/javascript/activestorage/helpers.js +42 -0
- data/app/javascript/activestorage/index.js +11 -0
- data/app/javascript/activestorage/ujs.js +74 -0
- data/app/jobs/active_storage/purge_attachment_worker.rb +9 -0
- data/app/jobs/active_storage/purge_blob_worker.rb +9 -0
- data/app/models/active_storage/attachment.rb +33 -0
- data/app/models/active_storage/blob.rb +198 -0
- data/app/models/active_storage/filename.rb +49 -0
- data/app/models/active_storage/variant.rb +82 -0
- data/app/models/active_storage/variation.rb +53 -0
- data/config/routes.rb +9 -0
- data/config/storage_services.yml +34 -0
- data/lib/active_storage/attached/macros.rb +86 -0
- data/lib/active_storage/attached/many.rb +51 -0
- data/lib/active_storage/attached/one.rb +56 -0
- data/lib/active_storage/attached.rb +38 -0
- data/lib/active_storage/engine.rb +81 -0
- data/lib/active_storage/gem_version.rb +15 -0
- data/lib/active_storage/log_subscriber.rb +48 -0
- data/lib/active_storage/messages_metadata.rb +64 -0
- data/lib/active_storage/migration.rb +27 -0
- data/lib/active_storage/patches/active_record.rb +19 -0
- data/lib/active_storage/patches/delegation.rb +98 -0
- data/lib/active_storage/patches/secure_random.rb +26 -0
- data/lib/active_storage/patches.rb +4 -0
- data/lib/active_storage/service/azure_service.rb +115 -0
- data/lib/active_storage/service/configurator.rb +28 -0
- data/lib/active_storage/service/disk_service.rb +124 -0
- data/lib/active_storage/service/gcs_service.rb +79 -0
- data/lib/active_storage/service/mirror_service.rb +46 -0
- data/lib/active_storage/service/s3_service.rb +96 -0
- data/lib/active_storage/service.rb +113 -0
- data/lib/active_storage/verifier.rb +113 -0
- data/lib/active_storage/version.rb +8 -0
- data/lib/active_storage.rb +34 -0
- data/lib/tasks/activestorage.rake +20 -0
- data/package.json +33 -0
- data/test/controllers/direct_uploads_controller_test.rb +123 -0
- data/test/controllers/disk_controller_test.rb +57 -0
- data/test/controllers/variants_controller_test.rb +21 -0
- data/test/database/create_users_migration.rb +7 -0
- data/test/database/setup.rb +6 -0
- data/test/dummy/Rakefile +3 -0
- data/test/dummy/app/assets/config/manifest.js +5 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/jobs/application_job.rb +2 -0
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config/application.rb +22 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +49 -0
- data/test/dummy/config/environments/production.rb +82 -0
- data/test/dummy/config/environments/test.rb +33 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/test/dummy/config/initializers/assets.rb +14 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/secret_key.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/routes.rb +2 -0
- data/test/dummy/config/secrets.yml +32 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/config/storage_services.yml +3 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/db/.keep +0 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/package.json +5 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/filename_test.rb +36 -0
- data/test/fixtures/files/racecar.jpg +0 -0
- data/test/models/attachments_test.rb +122 -0
- data/test/models/blob_test.rb +47 -0
- data/test/models/variant_test.rb +27 -0
- data/test/service/.gitignore +1 -0
- data/test/service/azure_service_test.rb +14 -0
- data/test/service/configurations-example.yml +31 -0
- data/test/service/configurator_test.rb +14 -0
- data/test/service/disk_service_test.rb +12 -0
- data/test/service/gcs_service_test.rb +42 -0
- data/test/service/mirror_service_test.rb +62 -0
- data/test/service/s3_service_test.rb +52 -0
- data/test/service/shared_service_tests.rb +66 -0
- data/test/sidekiq/minitest_support.rb +6 -0
- data/test/support/assertions.rb +20 -0
- data/test/test_helper.rb +69 -0
- data/webpack.config.js +27 -0
- data/yarn.lock +3164 -0
- metadata +330 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Provides the class-level DSL for declaring that an Active Record model has attached blobs.
|
|
2
|
+
module ActiveStorage::Attached::Macros
|
|
3
|
+
# Specifies the relation between a single attachment and the model.
|
|
4
|
+
#
|
|
5
|
+
# class User < ActiveRecord::Base
|
|
6
|
+
# has_one_attached :avatar
|
|
7
|
+
# end
|
|
8
|
+
#
|
|
9
|
+
# There is no column defined on the model side, Active Storage takes
|
|
10
|
+
# care of the mapping between your records and the attachment.
|
|
11
|
+
#
|
|
12
|
+
# Under the covers, this relationship is implemented as a `has_one` association to a
|
|
13
|
+
# `ActiveStorage::Attachment` record and a `has_one-through` association to a
|
|
14
|
+
# `ActiveStorage::Blob` record. These associations are available as `avatar_attachment`
|
|
15
|
+
# and `avatar_blob`. But you shouldn't need to work with these associations directly in
|
|
16
|
+
# most circumstances.
|
|
17
|
+
#
|
|
18
|
+
# The system has been designed to having you go through the `ActiveStorage::Attached::One`
|
|
19
|
+
# proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`.
|
|
20
|
+
#
|
|
21
|
+
# If the +:dependent+ option isn't set, the attachment will be purged
|
|
22
|
+
# (i.e. destroyed) whenever the record is destroyed.
|
|
23
|
+
def has_one_attached(name, dependent: :purge_later)
|
|
24
|
+
define_method(name) do
|
|
25
|
+
instance_variable_get("@active_storage_attached_#{name}") ||
|
|
26
|
+
instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::One.new(name, self))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if Rails.version < '4.0'
|
|
30
|
+
has_one :"#{name}_attachment", conditions: proc { "name = '#{name}'" }, class_name: "ActiveStorage::Attachment", as: :record
|
|
31
|
+
else
|
|
32
|
+
has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
|
|
36
|
+
|
|
37
|
+
if dependent == :purge_later
|
|
38
|
+
before_destroy { public_send(name).purge_later }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Specifies the relation between multiple attachments and the model.
|
|
43
|
+
#
|
|
44
|
+
# class Gallery < ActiveRecord::Base
|
|
45
|
+
# has_many_attached :photos
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# There are no columns defined on the model side, Active Storage takes
|
|
49
|
+
# care of the mapping between your records and the attachments.
|
|
50
|
+
#
|
|
51
|
+
# To avoid N+1 queries, you can include the attached blobs in your query like so:
|
|
52
|
+
#
|
|
53
|
+
# Gallery.where(user: Current.user).with_attached_photos
|
|
54
|
+
#
|
|
55
|
+
# Under the covers, this relationship is implemented as a `has_many` association to a
|
|
56
|
+
# `ActiveStorage::Attachment` record and a `has_many-through` association to a
|
|
57
|
+
# `ActiveStorage::Blob` record. These associations are available as `photos_attachments`
|
|
58
|
+
# and `photos_blobs`. But you shouldn't need to work with these associations directly in
|
|
59
|
+
# most circumstances.
|
|
60
|
+
#
|
|
61
|
+
# The system has been designed to having you go through the `ActiveStorage::Attached::Many`
|
|
62
|
+
# proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`.
|
|
63
|
+
#
|
|
64
|
+
# If the +:dependent+ option isn't set, all the attachments will be purged
|
|
65
|
+
# (i.e. destroyed) whenever the record is destroyed.
|
|
66
|
+
def has_many_attached(name, dependent: :purge_later)
|
|
67
|
+
define_method(name) do
|
|
68
|
+
instance_variable_get("@active_storage_attached_#{name}") ||
|
|
69
|
+
instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::Many.new(name, self))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if Rails.version < '4.0'
|
|
73
|
+
has_many :"#{name}_attachments", conditions: proc { "name = '#{name}'" }, as: :record, class_name: "ActiveStorage::Attachment"
|
|
74
|
+
else
|
|
75
|
+
has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
|
|
79
|
+
|
|
80
|
+
scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
|
|
81
|
+
|
|
82
|
+
if dependent == :purge_later
|
|
83
|
+
before_destroy { public_send(name).purge_later }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Decorated proxy object representing of multiple attachments to a model.
|
|
2
|
+
class ActiveStorage::Attached::Many < ActiveStorage::Attached
|
|
3
|
+
delegate_missing_to :attachments
|
|
4
|
+
|
|
5
|
+
# Returns all the associated attachment records.
|
|
6
|
+
#
|
|
7
|
+
# All methods called on this proxy object that aren't listed here will automatically be delegated to `attachments`.
|
|
8
|
+
def attachments
|
|
9
|
+
record.public_send("#{name}_attachments")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Associates one or several attachments with the current record, saving them to the database.
|
|
13
|
+
# Examples:
|
|
14
|
+
#
|
|
15
|
+
# document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
|
|
16
|
+
# document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
|
|
17
|
+
# document.images.attach(io: File.open("~/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg")
|
|
18
|
+
# document.images.attach([ first_blob, second_blob ])
|
|
19
|
+
def attach(*attachables)
|
|
20
|
+
attachables.flatten.collect do |attachable|
|
|
21
|
+
attachments.create!(name: name, blob: create_blob_from(attachable))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns true if any attachments has been made.
|
|
26
|
+
#
|
|
27
|
+
# class Gallery < ActiveRecord::Base
|
|
28
|
+
# has_many_attached :photos
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# Gallery.new.photos.attached? # => false
|
|
32
|
+
def attached?
|
|
33
|
+
attachments.any?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Directly purges each associated attachment (i.e. destroys the blobs and
|
|
37
|
+
# attachments and deletes the files on the service).
|
|
38
|
+
def purge
|
|
39
|
+
if attached?
|
|
40
|
+
attachments.each(&:purge)
|
|
41
|
+
attachments.reload
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Purges each associated attachment through the queuing system.
|
|
46
|
+
def purge_later
|
|
47
|
+
if attached?
|
|
48
|
+
attachments.each(&:purge_later)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Representation of a single attachment to a model.
|
|
2
|
+
class ActiveStorage::Attached::One < ActiveStorage::Attached
|
|
3
|
+
delegate_missing_to :attachment
|
|
4
|
+
|
|
5
|
+
# Returns the associated attachment record.
|
|
6
|
+
#
|
|
7
|
+
# You don't have to call this method to access the attachment's methods as
|
|
8
|
+
# they are all available at the model level.
|
|
9
|
+
def attachment
|
|
10
|
+
record.public_send("#{name}_attachment")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Associates a given attachment with the current record, saving it to the database.
|
|
14
|
+
# Examples:
|
|
15
|
+
#
|
|
16
|
+
# person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
|
|
17
|
+
# person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
|
|
18
|
+
# person.avatar.attach(io: File.open("~/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
|
|
19
|
+
# person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
|
|
20
|
+
def attach(attachable)
|
|
21
|
+
write_attachment \
|
|
22
|
+
ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns true if an attachment has been made.
|
|
26
|
+
#
|
|
27
|
+
# class User < ActiveRecord::Base
|
|
28
|
+
# has_one_attached :avatar
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# User.new.avatar.attached? # => false
|
|
32
|
+
def attached?
|
|
33
|
+
attachment.present?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Directly purges the attachment (i.e. destroys the blob and
|
|
37
|
+
# attachment and deletes the file on the service).
|
|
38
|
+
def purge
|
|
39
|
+
if attached?
|
|
40
|
+
attachment.purge
|
|
41
|
+
write_attachment nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Purges the attachment through the queuing system.
|
|
46
|
+
def purge_later
|
|
47
|
+
if attached?
|
|
48
|
+
attachment.purge_later
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
def write_attachment(attachment)
|
|
54
|
+
record.public_send("#{name}_attachment=", attachment)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "active_storage/blob"
|
|
2
|
+
require "active_storage/attachment"
|
|
3
|
+
|
|
4
|
+
require "action_dispatch/http/upload"
|
|
5
|
+
require "active_storage/patches/delegation"
|
|
6
|
+
|
|
7
|
+
# Abstract baseclass for the concrete `ActiveStorage::Attached::One` and `ActiveStorage::Attached::Many`
|
|
8
|
+
# classes that both provide proxy access to the blob association for a record.
|
|
9
|
+
class ActiveStorage::Attached
|
|
10
|
+
attr_reader :name, :record
|
|
11
|
+
|
|
12
|
+
def initialize(name, record)
|
|
13
|
+
@name, @record = name, record
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
def create_blob_from(attachable)
|
|
18
|
+
case attachable
|
|
19
|
+
when ActiveStorage::Blob
|
|
20
|
+
attachable
|
|
21
|
+
when ActionDispatch::Http::UploadedFile
|
|
22
|
+
ActiveStorage::Blob.create_after_upload! \
|
|
23
|
+
io: attachable.open,
|
|
24
|
+
filename: attachable.original_filename,
|
|
25
|
+
content_type: attachable.content_type
|
|
26
|
+
when Hash
|
|
27
|
+
ActiveStorage::Blob.create_after_upload!(attachable)
|
|
28
|
+
when String
|
|
29
|
+
ActiveStorage::Blob.find_signed(attachable)
|
|
30
|
+
else
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
require "active_storage/attached/one"
|
|
37
|
+
require "active_storage/attached/many"
|
|
38
|
+
require "active_storage/attached/macros"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require "rails/engine"
|
|
2
|
+
|
|
3
|
+
module ActiveStorage
|
|
4
|
+
class Engine < Rails::Engine # :nodoc:
|
|
5
|
+
config.active_storage = ActiveSupport::OrderedOptions.new
|
|
6
|
+
|
|
7
|
+
# config.eager_load_namespaces << ActiveStorage
|
|
8
|
+
|
|
9
|
+
initializer "active_storage.logger" do
|
|
10
|
+
require "active_storage/service"
|
|
11
|
+
|
|
12
|
+
config.after_initialize do |app|
|
|
13
|
+
ActiveStorage::Service.logger = app.config.active_storage.logger || Rails.logger
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer 'active_storage.extend_active_record' do
|
|
18
|
+
require "active_storage/patches"
|
|
19
|
+
require "active_storage/patches/active_record"
|
|
20
|
+
|
|
21
|
+
ActiveSupport.on_load :active_record do
|
|
22
|
+
extend ActiveStorage::Patches::ActiveRecord
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
initializer "active_storage.attached" do
|
|
27
|
+
require "active_storage/attached"
|
|
28
|
+
|
|
29
|
+
ActiveSupport.on_load(:active_record) do
|
|
30
|
+
extend ActiveStorage::Attached::Macros
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Port of Rails.application.key_generator.generate_key('ActiveStorage')
|
|
35
|
+
# Used to sign ActiveStorage::Blob
|
|
36
|
+
# See: https://github.com/rails/activestorage/blob/archive/lib/active_storage/engine.rb#L27
|
|
37
|
+
# https://github.com/rails/rails/blob/7-0-stable/activesupport/lib/active_support/key_generator.rb#L40
|
|
38
|
+
initializer "active_storage.verifier" do
|
|
39
|
+
require 'active_storage/verifier'
|
|
40
|
+
|
|
41
|
+
config.after_initialize do |app|
|
|
42
|
+
key = OpenSSL::PKCS5.pbkdf2_hmac(
|
|
43
|
+
app.config.secret_token,
|
|
44
|
+
'ActiveStorage',
|
|
45
|
+
1000,
|
|
46
|
+
64,
|
|
47
|
+
OpenSSL::Digest::SHA256.new
|
|
48
|
+
)
|
|
49
|
+
ActiveStorage.verifier = ActiveStorage::Verifier.new(key)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
initializer "active_storage.services" do
|
|
54
|
+
config.after_initialize do |app|
|
|
55
|
+
if config_choice = app.config.active_storage.service
|
|
56
|
+
config_file = Pathname.new(Rails.root.join("config/storage_services.yml"))
|
|
57
|
+
raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
|
|
58
|
+
|
|
59
|
+
require "yaml"
|
|
60
|
+
require "erb"
|
|
61
|
+
|
|
62
|
+
configs =
|
|
63
|
+
begin
|
|
64
|
+
YAML.load(ERB.new(config_file.read).result) || {}
|
|
65
|
+
rescue Psych::SyntaxError => e
|
|
66
|
+
raise "YAML syntax error occurred while parsing #{config_file}. " \
|
|
67
|
+
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
|
|
68
|
+
"Error: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
ActiveStorage::Blob.service =
|
|
72
|
+
begin
|
|
73
|
+
ActiveStorage::Service.configure config_choice, configs
|
|
74
|
+
rescue => e
|
|
75
|
+
raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module ActiveStorage
|
|
2
|
+
# Returns the version of the currently loaded Active Storage as a <tt>Gem::Version</tt>
|
|
3
|
+
def self.gem_version
|
|
4
|
+
Gem::Version.new VERSION::STRING
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module VERSION
|
|
8
|
+
MAJOR = 0
|
|
9
|
+
MINOR = 1
|
|
10
|
+
TINY = 0
|
|
11
|
+
PRE = "alpha"
|
|
12
|
+
|
|
13
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "active_support/log_subscriber"
|
|
2
|
+
|
|
3
|
+
class ActiveStorage::LogSubscriber < ActiveSupport::LogSubscriber
|
|
4
|
+
def service_upload(event)
|
|
5
|
+
message = "Uploaded file to key: #{key_in(event)}"
|
|
6
|
+
message << " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
|
|
7
|
+
info event, color(message, GREEN)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def service_download(event)
|
|
11
|
+
info event, color("Downloaded file from key: #{key_in(event)}", BLUE)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def service_delete(event)
|
|
15
|
+
info event, color("Deleted file from key: #{key_in(event)}", RED)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def service_exist(event)
|
|
19
|
+
debug event, color("Checked if file exist at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def service_url(event)
|
|
23
|
+
debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def logger
|
|
27
|
+
ActiveStorage::Service.logger
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
def info(event, colored_message)
|
|
32
|
+
super log_prefix_for_service(event) + colored_message
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def debug(event, colored_message)
|
|
36
|
+
super log_prefix_for_service(event) + colored_message
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def log_prefix_for_service(event)
|
|
40
|
+
color " #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def key_in(event)
|
|
44
|
+
event.payload[:key]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
ActiveStorage::LogSubscriber.attach_to :active_storage
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
class ActiveSupport::MessagesMetadata #:nodoc:
|
|
2
|
+
def initialize(message, expires_at = nil, purpose = nil)
|
|
3
|
+
@message, @expires_at, @purpose = message, expires_at, purpose
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def as_json(options = {})
|
|
7
|
+
{ _rails: { message: @message, exp: @expires_at, pur: @purpose } }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def wrap(message, expires_at: nil, expires_in: nil, purpose: nil)
|
|
12
|
+
if expires_at || expires_in || purpose
|
|
13
|
+
ActiveSupport::JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose)
|
|
14
|
+
else
|
|
15
|
+
message
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def verify(message, purpose)
|
|
20
|
+
extract_metadata(message).verify(purpose)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
def pick_expiry(expires_at, expires_in)
|
|
25
|
+
if expires_at
|
|
26
|
+
expires_at.utc.iso8601(3)
|
|
27
|
+
elsif expires_in
|
|
28
|
+
Time.now.utc.advance(seconds: expires_in).iso8601(3)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def extract_metadata(message)
|
|
33
|
+
data = ActiveSupport::JSON.decode(message) rescue nil
|
|
34
|
+
|
|
35
|
+
if data.is_a?(Hash) && data.key?("_rails")
|
|
36
|
+
new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"])
|
|
37
|
+
else
|
|
38
|
+
new(message)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def encode(message)
|
|
43
|
+
::Base64.strict_encode64(message)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def decode(message)
|
|
47
|
+
::Base64.strict_decode64(message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def verify(purpose)
|
|
52
|
+
@message if match?(purpose) && fresh?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
def match?(purpose)
|
|
57
|
+
@purpose.to_s == purpose.to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def fresh?
|
|
61
|
+
@expires_at.nil? || Time.now.utc < Time.iso8601(@expires_at)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class ActiveStorageCreateTables < ActiveRecord::Migration
|
|
2
|
+
def change
|
|
3
|
+
create_table :active_storage_blobs do |t|
|
|
4
|
+
t.string :key
|
|
5
|
+
t.string :filename
|
|
6
|
+
t.string :content_type
|
|
7
|
+
t.text :metadata
|
|
8
|
+
t.integer :byte_size
|
|
9
|
+
t.string :checksum
|
|
10
|
+
t.datetime :created_at
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :active_storage_blobs, :key, unique: true
|
|
14
|
+
|
|
15
|
+
create_table :active_storage_attachments do |t|
|
|
16
|
+
t.string :name
|
|
17
|
+
t.string :record_type
|
|
18
|
+
t.integer :record_id
|
|
19
|
+
t.integer :blob_id
|
|
20
|
+
|
|
21
|
+
t.datetime :created_at
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
add_index :active_storage_attachments, :blob_id
|
|
25
|
+
add_index :active_storage_attachments, [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Provides the class-level DSL for declaring that an Active Record model has attached blobs.
|
|
2
|
+
|
|
3
|
+
module ActiveStorage::Patches::ActiveRecord
|
|
4
|
+
class MinimumLengthError < StandardError; end
|
|
5
|
+
MINIMUM_TOKEN_LENGTH = 24
|
|
6
|
+
|
|
7
|
+
def has_secure_token(attribute = :token, length: MINIMUM_TOKEN_LENGTH)
|
|
8
|
+
if length < MINIMUM_TOKEN_LENGTH
|
|
9
|
+
raise MinimumLengthError, "Token requires a minimum length of #{MINIMUM_TOKEN_LENGTH} characters."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
define_method("regenerate_#{attribute}") { update_attributes! attribute => self.class.generate_unique_secure_token(length: length) }
|
|
13
|
+
before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
|
|
17
|
+
SecureRandom.base58(length)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
class Module
|
|
6
|
+
# Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
|
|
7
|
+
# option is not used.
|
|
8
|
+
class DelegationError < NoMethodError; end
|
|
9
|
+
|
|
10
|
+
RUBY_RESERVED_KEYWORDS = %w(__ENCODING__ __LINE__ __FILE__ alias and BEGIN begin break
|
|
11
|
+
case class def defined? do else elsif END end ensure false for if in module next nil
|
|
12
|
+
not or redo rescue retry return self super then true undef unless until when while yield)
|
|
13
|
+
DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
|
|
14
|
+
DELEGATION_RESERVED_METHOD_NAMES = Set.new(
|
|
15
|
+
RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
|
|
16
|
+
).freeze
|
|
17
|
+
|
|
18
|
+
# When building decorators, a common pattern may emerge:
|
|
19
|
+
#
|
|
20
|
+
# class Partition
|
|
21
|
+
# def initialize(event)
|
|
22
|
+
# @event = event
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# def person
|
|
26
|
+
# detail.person || creator
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# private
|
|
30
|
+
# def respond_to_missing?(name, include_private = false)
|
|
31
|
+
# @event.respond_to?(name, include_private)
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# def method_missing(method, *args, &block)
|
|
35
|
+
# @event.send(method, *args, &block)
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
|
|
40
|
+
#
|
|
41
|
+
# class Partition
|
|
42
|
+
# delegate_missing_to :@event
|
|
43
|
+
#
|
|
44
|
+
# def initialize(event)
|
|
45
|
+
# @event = event
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# def person
|
|
49
|
+
# detail.person || creator
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# The target can be anything callable within the object, e.g. instance
|
|
54
|
+
# variables, methods, constants, etc.
|
|
55
|
+
#
|
|
56
|
+
# The delegated method must be public on the target, otherwise it will
|
|
57
|
+
# raise +DelegationError+. If you wish to instead return +nil+,
|
|
58
|
+
# use the <tt>:allow_nil</tt> option.
|
|
59
|
+
#
|
|
60
|
+
# The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
|
|
61
|
+
# delegation due to possible interference when calling
|
|
62
|
+
# <tt>Marshal.dump(object)</tt>, should the delegation target method
|
|
63
|
+
# of <tt>object</tt> add or remove instance variables.
|
|
64
|
+
def delegate_missing_to(target, allow_nil: nil)
|
|
65
|
+
target = target.to_s
|
|
66
|
+
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
|
|
67
|
+
|
|
68
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
69
|
+
def respond_to_missing?(name, include_private = false)
|
|
70
|
+
# It may look like an oversight, but we deliberately do not pass
|
|
71
|
+
# +include_private+, because they do not get delegated.
|
|
72
|
+
|
|
73
|
+
return false if name == :marshal_dump || name == :_dump
|
|
74
|
+
#{target}.respond_to?(name) || super
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def method_missing(method, *args, &block)
|
|
78
|
+
if #{target}.respond_to?(method)
|
|
79
|
+
#{target}.public_send(method, *args, &block)
|
|
80
|
+
else
|
|
81
|
+
begin
|
|
82
|
+
super
|
|
83
|
+
rescue NoMethodError
|
|
84
|
+
if #{target}.nil?
|
|
85
|
+
if #{allow_nil == true}
|
|
86
|
+
nil
|
|
87
|
+
else
|
|
88
|
+
raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
raise
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
RUBY
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module SecureRandom
|
|
4
|
+
BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"] unless defined?(BASE58_ALPHABET)
|
|
5
|
+
BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a unless defined?(BASE36_ALPHABET)
|
|
6
|
+
|
|
7
|
+
unless SecureRandom.methods.include?(:base58)
|
|
8
|
+
def self.base58(n = 16)
|
|
9
|
+
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
|
|
10
|
+
idx = byte % 64
|
|
11
|
+
idx = SecureRandom.random_number(58) if idx >= 58
|
|
12
|
+
BASE58_ALPHABET[idx]
|
|
13
|
+
end.join
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless SecureRandom.methods.include?(:base36)
|
|
18
|
+
def self.base36(n = 16)
|
|
19
|
+
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
|
|
20
|
+
idx = byte % 64
|
|
21
|
+
idx = SecureRandom.random_number(36) if idx >= 36
|
|
22
|
+
BASE36_ALPHABET[idx]
|
|
23
|
+
end.join
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|