activestorage 7.2.2.1 → 8.1.3
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 +4 -4
- data/CHANGELOG.md +126 -55
- data/README.md +8 -5
- data/app/assets/javascripts/activestorage.esm.js +38 -2
- data/app/assets/javascripts/activestorage.js +38 -1
- data/app/controllers/active_storage/direct_uploads_controller.rb +1 -1
- data/app/controllers/active_storage/disk_controller.rb +6 -2
- data/app/controllers/concerns/active_storage/streaming.rb +17 -1
- data/app/javascript/activestorage/direct_upload_controller.js +48 -1
- data/app/javascript/activestorage/index.js +2 -1
- data/app/models/active_storage/blob/representable.rb +71 -5
- data/app/models/active_storage/blob.rb +25 -21
- data/app/models/active_storage/filename.rb +2 -1
- data/app/models/active_storage/variant.rb +12 -12
- data/app/models/active_storage/variation.rb +1 -1
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +11 -4
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +23 -5
- data/lib/active_storage/analyzer/image_analyzer.rb +5 -0
- data/lib/active_storage/attached/changes/create_one.rb +1 -1
- data/lib/active_storage/attached/model.rb +41 -18
- data/lib/active_storage/attached.rb +0 -1
- data/lib/active_storage/engine.rb +38 -7
- data/lib/active_storage/errors.rb +4 -0
- data/lib/active_storage/fixture_set.rb +1 -1
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/log_subscriber.rb +1 -1
- data/lib/active_storage/service/configurator.rb +6 -0
- data/lib/active_storage/service/disk_service.rb +46 -2
- data/lib/active_storage/service/gcs_service.rb +15 -5
- data/lib/active_storage/service/mirror_service.rb +13 -4
- data/lib/active_storage/service/registry.rb +6 -0
- data/lib/active_storage/service/s3_service.rb +19 -4
- data/lib/active_storage/service.rb +5 -1
- data/lib/active_storage/structured_event_subscriber.rb +79 -0
- data/lib/active_storage/transformers/image_magick.rb +72 -0
- data/lib/active_storage/transformers/image_processing_transformer.rb +5 -67
- data/lib/active_storage/transformers/null_transformer.rb +12 -0
- data/lib/active_storage/transformers/vips.rb +11 -0
- data/lib/active_storage.rb +19 -3
- metadata +19 -19
- data/lib/active_storage/service/azure_storage_service.rb +0 -194
|
@@ -16,10 +16,18 @@
|
|
|
16
16
|
# Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
|
|
17
17
|
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
|
|
18
18
|
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
|
|
19
|
+
#
|
|
20
|
+
# When using a custom +key+, the value is treated as trusted. Using untrusted user input
|
|
21
|
+
# as the key may result in unexpected behavior.
|
|
19
22
|
class ActiveStorage::Blob < ActiveStorage::Record
|
|
20
23
|
MINIMUM_TOKEN_LENGTH = 28
|
|
21
24
|
|
|
22
25
|
has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
|
|
26
|
+
|
|
27
|
+
# FIXME: these property should never have been stored in the metadata.
|
|
28
|
+
# The blob table should be migrated to have dedicated columns for theses.
|
|
29
|
+
PROTECTED_METADATA = %w(analyzed identified composed)
|
|
30
|
+
private_constant :PROTECTED_METADATA
|
|
23
31
|
store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
|
|
24
32
|
|
|
25
33
|
class_attribute :services, default: {}
|
|
@@ -29,7 +37,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
29
37
|
# :method:
|
|
30
38
|
#
|
|
31
39
|
# Returns the associated ActiveStorage::Attachment instances.
|
|
32
|
-
has_many :attachments
|
|
40
|
+
has_many :attachments, autosave: false
|
|
33
41
|
|
|
34
42
|
##
|
|
35
43
|
# :singleton-method:
|
|
@@ -71,9 +79,8 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
71
79
|
end
|
|
72
80
|
|
|
73
81
|
# Works like +find_signed+, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
|
|
74
|
-
# exception if the +signed_id+ has either expired, has a purpose mismatch,
|
|
75
|
-
#
|
|
76
|
-
# the valid signed id can't find a record.
|
|
82
|
+
# exception if the +signed_id+ has either expired, has a purpose mismatch, or has been tampered with.
|
|
83
|
+
# It will also raise an +ActiveRecord::RecordNotFound+ exception if the valid signed id can't find a record.
|
|
77
84
|
def find_signed!(id, record: nil, purpose: :blob_id)
|
|
78
85
|
super(id, purpose: purpose)
|
|
79
86
|
end
|
|
@@ -93,6 +100,9 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
93
100
|
# be saved before the upload begins to prevent the upload clobbering another due to key collisions.
|
|
94
101
|
# When providing a content type, pass <tt>identify: false</tt> to bypass
|
|
95
102
|
# automatic content type inference.
|
|
103
|
+
#
|
|
104
|
+
# The optional +key+ parameter is treated as trusted. Using untrusted user input
|
|
105
|
+
# as the key may result in unexpected behavior.
|
|
96
106
|
def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
|
|
97
107
|
create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
|
|
98
108
|
blob.upload_without_unfurling(io)
|
|
@@ -105,6 +115,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
105
115
|
# Once the form using the direct upload is submitted, the blob can be associated with the right record using
|
|
106
116
|
# the signed ID.
|
|
107
117
|
def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
|
|
118
|
+
metadata = filter_metadata(metadata)
|
|
108
119
|
create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
|
|
109
120
|
end
|
|
110
121
|
|
|
@@ -153,21 +164,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
153
164
|
end
|
|
154
165
|
end
|
|
155
166
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
private
|
|
168
|
+
def filter_metadata(metadata)
|
|
169
|
+
if metadata.is_a?(Hash)
|
|
170
|
+
metadata.without(*PROTECTED_METADATA)
|
|
171
|
+
else
|
|
172
|
+
metadata
|
|
160
173
|
end
|
|
161
|
-
else
|
|
162
|
-
validate_global_service_configuration
|
|
163
174
|
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def validate_global_service_configuration # :nodoc:
|
|
167
|
-
if connected? && table_exists? && Rails.configuration.active_storage.service.nil?
|
|
168
|
-
raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
175
|
end
|
|
172
176
|
|
|
173
177
|
include Analyzable
|
|
@@ -206,22 +210,22 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
|
206
210
|
|
|
207
211
|
# Returns true if the content_type of this blob is in the image range, like image/png.
|
|
208
212
|
def image?
|
|
209
|
-
content_type
|
|
213
|
+
content_type&.start_with?("image")
|
|
210
214
|
end
|
|
211
215
|
|
|
212
216
|
# Returns true if the content_type of this blob is in the audio range, like audio/mpeg.
|
|
213
217
|
def audio?
|
|
214
|
-
content_type
|
|
218
|
+
content_type&.start_with?("audio")
|
|
215
219
|
end
|
|
216
220
|
|
|
217
221
|
# Returns true if the content_type of this blob is in the video range, like video/mp4.
|
|
218
222
|
def video?
|
|
219
|
-
content_type
|
|
223
|
+
content_type&.start_with?("video")
|
|
220
224
|
end
|
|
221
225
|
|
|
222
226
|
# Returns true if the content_type of this blob is in the text range, like text/plain.
|
|
223
227
|
def text?
|
|
224
|
-
content_type
|
|
228
|
+
content_type&.start_with?("text")
|
|
225
229
|
end
|
|
226
230
|
|
|
227
231
|
# Returns the URL of the blob on the service. This returns a permanent URL for public files, and returns a
|
|
@@ -57,13 +57,14 @@ class ActiveStorage::Filename
|
|
|
57
57
|
#
|
|
58
58
|
# Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash.
|
|
59
59
|
def sanitized
|
|
60
|
-
@filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}
|
|
60
|
+
@filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/<>?*\"\t\r\n\\", "-")
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Returns the sanitized version of the filename.
|
|
64
64
|
def to_s
|
|
65
65
|
sanitized.to_s
|
|
66
66
|
end
|
|
67
|
+
alias_method :to_str, :to_s
|
|
67
68
|
|
|
68
69
|
def as_json(*)
|
|
69
70
|
to_s
|
|
@@ -8,29 +8,29 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Variants rely on {ImageProcessing}[https://github.com/janko/image_processing] gem for the actual transformations
|
|
10
10
|
# of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
|
|
11
|
-
# default, images will be processed with {
|
|
12
|
-
# {
|
|
13
|
-
# {
|
|
11
|
+
# default, images will be processed with {libvips}[http://libvips.github.io/libvips/] using the
|
|
12
|
+
# {ruby-vips}[https://github.com/libvips/ruby-vips] gem, but you can also switch to the
|
|
13
|
+
# {ImageMagick}[http://imagemagick.org] processor operated by the {MiniMagick}[https://github.com/minimagick/minimagick]
|
|
14
14
|
# gem).
|
|
15
15
|
#
|
|
16
16
|
# Rails.application.config.active_storage.variant_processor
|
|
17
|
-
# # => :mini_magick
|
|
18
|
-
#
|
|
19
|
-
# Rails.application.config.active_storage.variant_processor = :vips
|
|
20
17
|
# # => :vips
|
|
21
18
|
#
|
|
19
|
+
# Rails.application.config.active_storage.variant_processor = :mini_magick
|
|
20
|
+
# # => :mini_magick
|
|
21
|
+
#
|
|
22
22
|
# Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
|
|
23
23
|
# you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
|
|
24
24
|
# in a template, for example. Delay the processing to an on-demand controller, like the one provided in
|
|
25
|
-
# ActiveStorage::
|
|
25
|
+
# ActiveStorage::Representations::ProxyController and ActiveStorage::Representations::RedirectController.
|
|
26
26
|
#
|
|
27
27
|
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
|
|
28
28
|
# by Active Storage like so:
|
|
29
29
|
#
|
|
30
30
|
# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
|
|
31
31
|
#
|
|
32
|
-
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::
|
|
33
|
-
# can then produce on-demand.
|
|
32
|
+
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::Representations::ProxyController
|
|
33
|
+
# or ActiveStorage::Representations::RedirectController can then produce on-demand.
|
|
34
34
|
#
|
|
35
35
|
# When you do want to actually produce the variant needed, call +processed+. This will check that the variant
|
|
36
36
|
# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
|
|
@@ -74,11 +74,11 @@ class ActiveStorage::Variant
|
|
|
74
74
|
"variants/#{blob.key}/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
# Returns the URL of the blob variant on the service. See
|
|
77
|
+
# Returns the URL of the blob variant on the service. See ActiveStorage::Blob#url for details.
|
|
78
78
|
#
|
|
79
79
|
# Use <tt>url_for(variant)</tt> (or the implied form, like <tt>link_to variant</tt> or <tt>redirect_to variant</tt>) to get the stable URL
|
|
80
|
-
# for a variant that points to the ActiveStorage::
|
|
81
|
-
# for its redirection.
|
|
80
|
+
# for a variant that points to the ActiveStorage::Representations::ProxyController or ActiveStorage::Representations::RedirectController,
|
|
81
|
+
# which in turn will use this +service_call+ method for its redirection.
|
|
82
82
|
def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
|
|
83
83
|
service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
|
|
84
84
|
end
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
begin
|
|
4
|
+
gem "mini_magick"
|
|
5
|
+
require "mini_magick"
|
|
6
|
+
ActiveStorage::MINIMAGICK_AVAILABLE = true # :nodoc:
|
|
7
|
+
rescue LoadError => error
|
|
8
|
+
ActiveStorage::MINIMAGICK_AVAILABLE = false # :nodoc:
|
|
9
|
+
raise error unless error.message.include?("mini_magick")
|
|
10
|
+
end
|
|
11
|
+
|
|
3
12
|
module ActiveStorage
|
|
4
13
|
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
|
|
5
14
|
# the {ImageMagick}[http://www.imagemagick.org] system library.
|
|
@@ -10,10 +19,8 @@ module ActiveStorage
|
|
|
10
19
|
|
|
11
20
|
private
|
|
12
21
|
def read_image
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
rescue LoadError
|
|
16
|
-
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
|
|
22
|
+
unless MINIMAGICK_AVAILABLE
|
|
23
|
+
logger.error "Skipping image analysis because the mini_magick gem isn't installed"
|
|
17
24
|
return {}
|
|
18
25
|
end
|
|
19
26
|
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
begin
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Ensure nokogiri is loaded before vips, which also depends on libxml2.
|
|
7
|
+
# See Nokogiri RFC: Stop exporting symbols:
|
|
8
|
+
# https://github.com/sparklemotion/nokogiri/discussions/2746
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
gem "ruby-vips"
|
|
13
|
+
require "ruby-vips"
|
|
14
|
+
ActiveStorage::VIPS_AVAILABLE = true # :nodoc:
|
|
15
|
+
rescue LoadError => error
|
|
16
|
+
ActiveStorage::VIPS_AVAILABLE = false # :nodoc:
|
|
17
|
+
raise error unless error.message.match?(/libvips|ruby-vips/)
|
|
18
|
+
end
|
|
19
|
+
|
|
3
20
|
module ActiveStorage
|
|
4
21
|
# This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires
|
|
5
22
|
# the {libvips}[https://libvips.github.io/libvips/] system library.
|
|
@@ -10,10 +27,8 @@ module ActiveStorage
|
|
|
10
27
|
|
|
11
28
|
private
|
|
12
29
|
def read_image
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
rescue LoadError
|
|
16
|
-
logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
|
|
30
|
+
unless VIPS_AVAILABLE
|
|
31
|
+
logger.error "Skipping image analysis because the ruby-vips gem isn't installed"
|
|
17
32
|
return {}
|
|
18
33
|
end
|
|
19
34
|
|
|
@@ -32,9 +47,12 @@ module ActiveStorage
|
|
|
32
47
|
{}
|
|
33
48
|
end
|
|
34
49
|
rescue ::Vips::Error => error
|
|
35
|
-
logger.error "Skipping image analysis due to
|
|
50
|
+
logger.error "Skipping image analysis due to a Vips error: #{error.message}"
|
|
36
51
|
{}
|
|
37
52
|
end
|
|
53
|
+
rescue ::Vips::Error => error
|
|
54
|
+
logger.error "Skipping image analysis due to an Vips error: #{error.message}"
|
|
55
|
+
{}
|
|
38
56
|
end
|
|
39
57
|
|
|
40
58
|
ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
|
|
@@ -12,6 +12,11 @@ module ActiveStorage
|
|
|
12
12
|
# ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(blob).metadata
|
|
13
13
|
# # => { width: 4104, height: 2736 }
|
|
14
14
|
class Analyzer::ImageAnalyzer < Analyzer
|
|
15
|
+
extend ActiveSupport::Autoload
|
|
16
|
+
|
|
17
|
+
autoload :Vips
|
|
18
|
+
autoload :ImageMagick
|
|
19
|
+
|
|
15
20
|
def self.accept?(blob)
|
|
16
21
|
blob.image?
|
|
17
22
|
end
|
|
@@ -121,7 +121,7 @@ module ActiveStorage
|
|
|
121
121
|
service_name = record.attachment_reflections[name].options[:service_name]
|
|
122
122
|
if service_name.is_a?(Proc)
|
|
123
123
|
service_name = service_name.call(record)
|
|
124
|
-
|
|
124
|
+
Attached::Model.validate_service_configuration(service_name, record.class, name)
|
|
125
125
|
end
|
|
126
126
|
service_name
|
|
127
127
|
end
|
|
@@ -61,18 +61,16 @@ module ActiveStorage
|
|
|
61
61
|
# There is no column defined on the model side, Active Storage takes
|
|
62
62
|
# care of the mapping between your records and the attachment.
|
|
63
63
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# User.with_attached_avatar
|
|
67
|
-
#
|
|
68
|
-
# Under the covers, this relationship is implemented as a +has_one+ association to a
|
|
69
|
-
# ActiveStorage::Attachment record and a +has_one-through+ association to a
|
|
64
|
+
# Under the covers, this relationship is implemented as a +has_one+ association to an
|
|
65
|
+
# ActiveStorage::Attachment record and a +has_one-through+ association to an
|
|
70
66
|
# ActiveStorage::Blob record. These associations are available as +avatar_attachment+
|
|
71
67
|
# and +avatar_blob+. But you shouldn't need to work with these associations directly in
|
|
72
68
|
# most circumstances.
|
|
73
69
|
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
70
|
+
# Instead, +has_one_attached+ generates an ActiveStorage::Attached::One proxy to
|
|
71
|
+
# provide access to the associations and factory methods, like +attach+:
|
|
72
|
+
#
|
|
73
|
+
# user.avatar.attach(uploaded_file)
|
|
76
74
|
#
|
|
77
75
|
# The +:dependent+ option defaults to +:purge_later+. This means the attachment will be
|
|
78
76
|
# purged (i.e. destroyed) in the background whenever the record is destroyed.
|
|
@@ -92,6 +90,10 @@ module ActiveStorage
|
|
|
92
90
|
# has_one_attached :avatar, service: ->(user) { user.in_europe_region? ? :s3_europe : :s3_usa }
|
|
93
91
|
# end
|
|
94
92
|
#
|
|
93
|
+
# To avoid N+1 queries, you can include the attached blobs in your query like so:
|
|
94
|
+
#
|
|
95
|
+
# User.with_attached_avatar
|
|
96
|
+
#
|
|
95
97
|
# If you need to enable +strict_loading+ to prevent lazy loading of attachment,
|
|
96
98
|
# pass the +:strict_loading+ option. You can do:
|
|
97
99
|
#
|
|
@@ -104,7 +106,7 @@ module ActiveStorage
|
|
|
104
106
|
# <tt>active_storage_attachments.record_type</tt> polymorphic type column of
|
|
105
107
|
# the corresponding rows.
|
|
106
108
|
def has_one_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
|
|
107
|
-
|
|
109
|
+
Attached::Model.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
|
|
108
110
|
|
|
109
111
|
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
110
112
|
# frozen_string_literal: true
|
|
@@ -161,18 +163,16 @@ module ActiveStorage
|
|
|
161
163
|
# There are no columns defined on the model side, Active Storage takes
|
|
162
164
|
# care of the mapping between your records and the attachments.
|
|
163
165
|
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
# Gallery.where(user: Current.user).with_attached_photos
|
|
167
|
-
#
|
|
168
|
-
# Under the covers, this relationship is implemented as a +has_many+ association to a
|
|
169
|
-
# ActiveStorage::Attachment record and a +has_many-through+ association to a
|
|
166
|
+
# Under the covers, this relationship is implemented as a +has_many+ association to an
|
|
167
|
+
# ActiveStorage::Attachment record and a +has_many-through+ association to an
|
|
170
168
|
# ActiveStorage::Blob record. These associations are available as +photos_attachments+
|
|
171
169
|
# and +photos_blobs+. But you shouldn't need to work with these associations directly in
|
|
172
170
|
# most circumstances.
|
|
173
171
|
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
172
|
+
# Instead, +has_many_attached+ generates an ActiveStorage::Attached::Many proxy to
|
|
173
|
+
# provide access to the associations and factory methods, like +attach+:
|
|
174
|
+
#
|
|
175
|
+
# user.photos.attach(uploaded_file)
|
|
176
176
|
#
|
|
177
177
|
# The +:dependent+ option defaults to +:purge_later+. This means the attachments will be
|
|
178
178
|
# purged (i.e. destroyed) in the background whenever the record is destroyed.
|
|
@@ -192,6 +192,10 @@ module ActiveStorage
|
|
|
192
192
|
# has_many_attached :photos, service: ->(gallery) { gallery.personal? ? :personal_s3 : :s3 }
|
|
193
193
|
# end
|
|
194
194
|
#
|
|
195
|
+
# To avoid N+1 queries, you can include the attached blobs in your query like so:
|
|
196
|
+
#
|
|
197
|
+
# Gallery.where(user: Current.user).with_attached_photos
|
|
198
|
+
#
|
|
195
199
|
# If you need to enable +strict_loading+ to prevent lazy loading of attachments,
|
|
196
200
|
# pass the +:strict_loading+ option. You can do:
|
|
197
201
|
#
|
|
@@ -204,7 +208,7 @@ module ActiveStorage
|
|
|
204
208
|
# <tt>active_storage_attachments.record_type</tt> polymorphic type column of
|
|
205
209
|
# the corresponding rows.
|
|
206
210
|
def has_many_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
|
|
207
|
-
|
|
211
|
+
Attached::Model.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
|
|
208
212
|
|
|
209
213
|
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
210
214
|
# frozen_string_literal: true
|
|
@@ -255,6 +259,25 @@ module ActiveStorage
|
|
|
255
259
|
end
|
|
256
260
|
end
|
|
257
261
|
|
|
262
|
+
class << self
|
|
263
|
+
def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
|
|
264
|
+
if service_name
|
|
265
|
+
ActiveStorage::Blob.services.fetch(service_name) do
|
|
266
|
+
raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
|
|
267
|
+
end
|
|
268
|
+
else
|
|
269
|
+
validate_global_service_configuration(model_class)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private
|
|
274
|
+
def validate_global_service_configuration(model_class)
|
|
275
|
+
if model_class.connected? && ActiveStorage::Blob.table_exists? && Rails.configuration.active_storage.service.nil?
|
|
276
|
+
raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
258
281
|
def attachment_changes # :nodoc:
|
|
259
282
|
@attachment_changes ||= {}
|
|
260
283
|
end
|
|
@@ -12,8 +12,6 @@ require "active_storage/previewer/mupdf_previewer"
|
|
|
12
12
|
require "active_storage/previewer/video_previewer"
|
|
13
13
|
|
|
14
14
|
require "active_storage/analyzer/image_analyzer"
|
|
15
|
-
require "active_storage/analyzer/image_analyzer/image_magick"
|
|
16
|
-
require "active_storage/analyzer/image_analyzer/vips"
|
|
17
15
|
require "active_storage/analyzer/video_analyzer"
|
|
18
16
|
require "active_storage/analyzer/audio_analyzer"
|
|
19
17
|
|
|
@@ -84,11 +82,44 @@ module ActiveStorage
|
|
|
84
82
|
end
|
|
85
83
|
|
|
86
84
|
initializer "active_storage.configs" do
|
|
85
|
+
config.before_initialize do |app|
|
|
86
|
+
ActiveStorage.touch_attachment_records = app.config.active_storage.touch_attachment_records != false
|
|
87
|
+
end
|
|
88
|
+
|
|
87
89
|
config.after_initialize do |app|
|
|
88
90
|
ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
|
|
89
91
|
ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
|
|
90
92
|
ActiveStorage.previewers = app.config.active_storage.previewers || []
|
|
91
93
|
ActiveStorage.analyzers = app.config.active_storage.analyzers || []
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
ActiveStorage.variant_transformer =
|
|
97
|
+
case ActiveStorage.variant_processor
|
|
98
|
+
when :disabled
|
|
99
|
+
ActiveStorage::Transformers::NullTransformer
|
|
100
|
+
when :vips
|
|
101
|
+
ActiveStorage::Transformers::Vips
|
|
102
|
+
when :mini_magick
|
|
103
|
+
ActiveStorage::Transformers::ImageMagick
|
|
104
|
+
end
|
|
105
|
+
rescue LoadError => error
|
|
106
|
+
case error.message
|
|
107
|
+
when /libvips/
|
|
108
|
+
ActiveStorage.logger.warn <<~WARNING.squish
|
|
109
|
+
Using vips to process variants requires the libvips library.
|
|
110
|
+
Please install libvips using the instructions on the libvips website.
|
|
111
|
+
WARNING
|
|
112
|
+
when /image_processing/
|
|
113
|
+
ActiveStorage.logger.warn <<~WARNING.squish
|
|
114
|
+
Generating image variants require the image_processing gem.
|
|
115
|
+
Please add `gem "image_processing", "~> 1.2"` to your Gemfile
|
|
116
|
+
or set `config.active_storage.variant_processor = :disabled`.
|
|
117
|
+
WARNING
|
|
118
|
+
else
|
|
119
|
+
raise
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
92
123
|
ActiveStorage.paths = app.config.active_storage.paths || {}
|
|
93
124
|
ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
|
|
94
125
|
ActiveStorage.draw_routes = app.config.active_storage.draw_routes != false
|
|
@@ -112,13 +143,13 @@ module ActiveStorage
|
|
|
112
143
|
ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
|
|
113
144
|
ActiveStorage.web_image_content_types = app.config.active_storage.web_image_content_types || []
|
|
114
145
|
ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
|
|
115
|
-
ActiveStorage.touch_attachment_records = app.config.active_storage.touch_attachment_records != false
|
|
116
146
|
ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
|
|
117
147
|
ActiveStorage.urls_expire_in = app.config.active_storage.urls_expire_in
|
|
118
148
|
ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
|
|
119
149
|
ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
|
|
120
150
|
ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
|
|
121
151
|
ActiveStorage.track_variants = app.config.active_storage.track_variants || false
|
|
152
|
+
ActiveStorage.streaming_chunk_max_size = app.config.active_storage.streaming_chunk_max_size || 100.megabytes
|
|
122
153
|
end
|
|
123
154
|
end
|
|
124
155
|
|
|
@@ -136,9 +167,9 @@ module ActiveStorage
|
|
|
136
167
|
end
|
|
137
168
|
end
|
|
138
169
|
|
|
139
|
-
initializer "active_storage.services" do
|
|
170
|
+
initializer "active_storage.services" do |app|
|
|
140
171
|
ActiveSupport.on_load(:active_storage_blob) do
|
|
141
|
-
configs =
|
|
172
|
+
configs = app.config.active_storage.service_configurations ||=
|
|
142
173
|
begin
|
|
143
174
|
config_file = Rails.root.join("config/storage/#{Rails.env}.yml")
|
|
144
175
|
config_file = Rails.root.join("config/storage.yml") unless config_file.exist?
|
|
@@ -149,7 +180,7 @@ module ActiveStorage
|
|
|
149
180
|
|
|
150
181
|
ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
|
|
151
182
|
|
|
152
|
-
if config_choice =
|
|
183
|
+
if config_choice = app.config.active_storage.service
|
|
153
184
|
ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(config_choice)
|
|
154
185
|
end
|
|
155
186
|
end
|
|
@@ -171,7 +202,7 @@ module ActiveStorage
|
|
|
171
202
|
initializer "action_view.configuration" do
|
|
172
203
|
config.after_initialize do |app|
|
|
173
204
|
ActiveSupport.on_load(:action_view) do
|
|
174
|
-
multiple_file_field_include_hidden = app.config.active_storage.
|
|
205
|
+
multiple_file_field_include_hidden = app.config.active_storage.multiple_file_field_include_hidden
|
|
175
206
|
|
|
176
207
|
unless multiple_file_field_include_hidden.nil?
|
|
177
208
|
ActionView::Helpers::FormHelper.multiple_file_field_include_hidden = multiple_file_field_include_hidden
|
|
@@ -26,4 +26,8 @@ module ActiveStorage
|
|
|
26
26
|
|
|
27
27
|
# Raised when a Previewer is unable to generate a preview image.
|
|
28
28
|
class PreviewError < Error; end
|
|
29
|
+
|
|
30
|
+
# Raised when a storage key resolves to a path outside the service's root
|
|
31
|
+
# directory, indicating a potential path traversal attack.
|
|
32
|
+
class InvalidKeyError < Error; end
|
|
29
33
|
end
|
|
@@ -50,7 +50,7 @@ module ActiveStorage
|
|
|
50
50
|
# by ActiveSupport::Testing::FileFixtures.file_fixture, and upload
|
|
51
51
|
# the file to the Service
|
|
52
52
|
#
|
|
53
|
-
#
|
|
53
|
+
# ==== Examples
|
|
54
54
|
#
|
|
55
55
|
# # tests/fixtures/active_storage/blobs.yml
|
|
56
56
|
# second_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "active_support/log_subscriber"
|
|
4
4
|
|
|
5
5
|
module ActiveStorage
|
|
6
|
-
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
|
|
7
7
|
def service_upload(event)
|
|
8
8
|
message = "Uploaded file to key: #{key_in(event)}"
|
|
9
9
|
message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
|
|
@@ -19,6 +19,12 @@ module ActiveStorage
|
|
|
19
19
|
)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def inspect # :nodoc:
|
|
23
|
+
attrs = configurations.any? ?
|
|
24
|
+
" configurations=[#{configurations.keys.map(&:inspect).join(", ")}]" : ""
|
|
25
|
+
"#<#{self.class}#{attrs}>"
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
private
|
|
23
29
|
def config_for(name)
|
|
24
30
|
configurations.fetch name do
|
|
@@ -60,7 +60,16 @@ module ActiveStorage
|
|
|
60
60
|
|
|
61
61
|
def delete_prefixed(prefix)
|
|
62
62
|
instrument :delete_prefixed, prefix: prefix do
|
|
63
|
-
|
|
63
|
+
prefix_path = path_for(prefix)
|
|
64
|
+
|
|
65
|
+
# File.expand_path (called within path_for) strips trailing slashes.
|
|
66
|
+
# Restore trailing separator if the original prefix had one, so that
|
|
67
|
+
# the glob "prefix/*" matches files inside the directory, not siblings
|
|
68
|
+
# whose names start with the prefix string.
|
|
69
|
+
prefix_path += "/" if prefix.end_with?("/")
|
|
70
|
+
|
|
71
|
+
escaped = escape_glob_metacharacters(prefix_path)
|
|
72
|
+
Dir.glob("#{escaped}*").each do |path|
|
|
64
73
|
FileUtils.rm_rf(path)
|
|
65
74
|
end
|
|
66
75
|
end
|
|
@@ -98,8 +107,39 @@ module ActiveStorage
|
|
|
98
107
|
{ "Content-Type" => content_type }
|
|
99
108
|
end
|
|
100
109
|
|
|
110
|
+
# Every filesystem operation in DiskService resolves paths through this method (or through
|
|
111
|
+
# make_path_for, which delegates here). This is the primary filesystem security check: all
|
|
112
|
+
# path-traversal protection is enforced here. New methods that touch the filesystem MUST use
|
|
113
|
+
# path_for or make_path_for -- never construct paths from +root+ directly.
|
|
101
114
|
def path_for(key) # :nodoc:
|
|
102
|
-
|
|
115
|
+
if key.blank?
|
|
116
|
+
raise ActiveStorage::InvalidKeyError, "key is blank"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Reject keys with dot segments as defense in depth. This prevents path traversal both outside
|
|
120
|
+
# and within the storage root. The root containment check below is a more fundamental check on
|
|
121
|
+
# path traversal outside of the disk service root.
|
|
122
|
+
begin
|
|
123
|
+
if key.split("/").intersect?(%w[. ..])
|
|
124
|
+
raise ActiveStorage::InvalidKeyError, "key has path traversal segments"
|
|
125
|
+
end
|
|
126
|
+
rescue Encoding::CompatibilityError
|
|
127
|
+
raise ActiveStorage::InvalidKeyError, "key has incompatible encoding"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
begin
|
|
131
|
+
path = File.expand_path(File.join(root, folder_for(key), key))
|
|
132
|
+
rescue ArgumentError
|
|
133
|
+
# ArgumentError catches null bytes
|
|
134
|
+
raise ActiveStorage::InvalidKeyError, "key is an invalid string"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The resolved path must be inside the root directory.
|
|
138
|
+
unless path.start_with?(File.expand_path(root) + "/")
|
|
139
|
+
raise ActiveStorage::InvalidKeyError, "key is outside of disk service root"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
path
|
|
103
143
|
end
|
|
104
144
|
|
|
105
145
|
def compose(source_keys, destination_key, **)
|
|
@@ -156,6 +196,10 @@ module ActiveStorage
|
|
|
156
196
|
[ key[0..1], key[2..3] ].join("/")
|
|
157
197
|
end
|
|
158
198
|
|
|
199
|
+
def escape_glob_metacharacters(path)
|
|
200
|
+
path.gsub(/[\[\]*?{}\\]/) { |c| "\\#{c}" }
|
|
201
|
+
end
|
|
202
|
+
|
|
159
203
|
def make_path_for(key)
|
|
160
204
|
path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
|
|
161
205
|
end
|