activestorage_legacy 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|