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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -55
  3. data/README.md +8 -5
  4. data/app/assets/javascripts/activestorage.esm.js +38 -2
  5. data/app/assets/javascripts/activestorage.js +38 -1
  6. data/app/controllers/active_storage/direct_uploads_controller.rb +1 -1
  7. data/app/controllers/active_storage/disk_controller.rb +6 -2
  8. data/app/controllers/concerns/active_storage/streaming.rb +17 -1
  9. data/app/javascript/activestorage/direct_upload_controller.js +48 -1
  10. data/app/javascript/activestorage/index.js +2 -1
  11. data/app/models/active_storage/blob/representable.rb +71 -5
  12. data/app/models/active_storage/blob.rb +25 -21
  13. data/app/models/active_storage/filename.rb +2 -1
  14. data/app/models/active_storage/variant.rb +12 -12
  15. data/app/models/active_storage/variation.rb +1 -1
  16. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +11 -4
  17. data/lib/active_storage/analyzer/image_analyzer/vips.rb +23 -5
  18. data/lib/active_storage/analyzer/image_analyzer.rb +5 -0
  19. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  20. data/lib/active_storage/attached/model.rb +41 -18
  21. data/lib/active_storage/attached.rb +0 -1
  22. data/lib/active_storage/engine.rb +38 -7
  23. data/lib/active_storage/errors.rb +4 -0
  24. data/lib/active_storage/fixture_set.rb +1 -1
  25. data/lib/active_storage/gem_version.rb +4 -4
  26. data/lib/active_storage/log_subscriber.rb +1 -1
  27. data/lib/active_storage/service/configurator.rb +6 -0
  28. data/lib/active_storage/service/disk_service.rb +46 -2
  29. data/lib/active_storage/service/gcs_service.rb +15 -5
  30. data/lib/active_storage/service/mirror_service.rb +13 -4
  31. data/lib/active_storage/service/registry.rb +6 -0
  32. data/lib/active_storage/service/s3_service.rb +19 -4
  33. data/lib/active_storage/service.rb +5 -1
  34. data/lib/active_storage/structured_event_subscriber.rb +79 -0
  35. data/lib/active_storage/transformers/image_magick.rb +72 -0
  36. data/lib/active_storage/transformers/image_processing_transformer.rb +5 -67
  37. data/lib/active_storage/transformers/null_transformer.rb +12 -0
  38. data/lib/active_storage/transformers/vips.rb +11 -0
  39. data/lib/active_storage.rb +19 -3
  40. metadata +19 -19
  41. 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, is for another record,
75
- # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
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
- def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
157
- if service_name
158
- services.fetch(service_name) do
159
- raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
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.start_with?("image")
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.start_with?("audio")
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.start_with?("video")
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.start_with?("text")
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}%$|:;/\t\r\n\\", "-")
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 {ImageMagick}[http://imagemagick.org] using the
12
- # {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
13
- # {libvips}[http://libvips.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/libvips/ruby-vips]
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::RepresentationsController.
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::RepresentationsController
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 {ActiveStorage::Blob#url} for details.
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::RepresentationsController, which in turn will use this +service_call+ method
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
@@ -82,6 +82,6 @@ class ActiveStorage::Variation
82
82
 
83
83
  private
84
84
  def transformer
85
- ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations.except(:format))
85
+ ActiveStorage.variant_transformer.new(transformations.except(:format))
86
86
  end
87
87
  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
- begin
14
- require "mini_magick"
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
- begin
14
- require "ruby-vips"
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 an Vips error: #{error.message}"
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
- ActiveStorage::Blob.validate_service_configuration(service_name, record.class, name)
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
- # To avoid N+1 queries, you can include the attached blobs in your query like so:
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
- # The system has been designed to having you go through the ActiveStorage::Attached::One
75
- # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
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
- ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
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
- # To avoid N+1 queries, you can include the attached blobs in your query like so:
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
- # The system has been designed to having you go through the ActiveStorage::Attached::Many
175
- # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
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
- ActiveStorage::Blob.validate_service_configuration(service, self, name) unless service.is_a?(Proc)
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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/module/delegation"
4
3
 
5
4
  module ActiveStorage
6
5
  # = Active Storage \Attached
@@ -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 = Rails.configuration.active_storage.service_configurations ||=
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 = Rails.configuration.active_storage.service
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.delete(:multiple_file_field_include_hidden)
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
- # === Examples
53
+ # ==== Examples
54
54
  #
55
55
  # # tests/fixtures/active_storage/blobs.yml
56
56
  # second_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob(
@@ -7,10 +7,10 @@ module ActiveStorage
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
11
- MINOR = 2
12
- TINY = 2
13
- PRE = "1"
10
+ MAJOR = 8
11
+ MINOR = 1
12
+ TINY = 3
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -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
- Dir.glob(path_for("#{prefix}*")).each do |path|
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
- File.join root, folder_for(key), key
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