activestorage 5.2.4.4 → 6.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activestorage might be problematic. Click here for more details.

Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -81
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +6 -5
  5. data/app/assets/javascripts/activestorage.js +4 -1
  6. data/app/controllers/active_storage/base_controller.rb +3 -5
  7. data/app/controllers/active_storage/blobs_controller.rb +1 -1
  8. data/app/controllers/active_storage/disk_controller.rb +4 -1
  9. data/app/controllers/active_storage/representations_controller.rb +1 -1
  10. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  11. data/app/javascript/activestorage/blob_record.js +6 -1
  12. data/app/jobs/active_storage/analyze_job.rb +4 -0
  13. data/app/jobs/active_storage/base_job.rb +0 -1
  14. data/app/jobs/active_storage/purge_job.rb +3 -0
  15. data/app/models/active_storage/attachment.rb +18 -9
  16. data/app/models/active_storage/blob.rb +63 -22
  17. data/app/models/active_storage/blob/representable.rb +5 -5
  18. data/app/models/active_storage/filename.rb +0 -6
  19. data/app/models/active_storage/preview.rb +3 -3
  20. data/app/models/active_storage/variant.rb +51 -52
  21. data/app/models/active_storage/variation.rb +23 -32
  22. data/config/routes.rb +13 -12
  23. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +7 -0
  24. data/lib/active_storage.rb +13 -2
  25. data/lib/active_storage/analyzer.rb +9 -4
  26. data/lib/active_storage/analyzer/video_analyzer.rb +2 -4
  27. data/lib/active_storage/attached.rb +7 -22
  28. data/lib/active_storage/attached/changes.rb +16 -0
  29. data/lib/active_storage/attached/changes/create_many.rb +46 -0
  30. data/lib/active_storage/attached/changes/create_one.rb +68 -0
  31. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  32. data/lib/active_storage/attached/changes/delete_many.rb +23 -0
  33. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  34. data/lib/active_storage/attached/many.rb +16 -10
  35. data/lib/active_storage/attached/model.rb +140 -0
  36. data/lib/active_storage/attached/one.rb +16 -19
  37. data/lib/active_storage/downloader.rb +44 -0
  38. data/lib/active_storage/downloading.rb +8 -0
  39. data/lib/active_storage/engine.rb +35 -6
  40. data/lib/active_storage/errors.rb +22 -3
  41. data/lib/active_storage/gem_version.rb +4 -4
  42. data/lib/active_storage/previewer.rb +21 -11
  43. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +1 -1
  44. data/lib/active_storage/previewer/video_previewer.rb +2 -3
  45. data/lib/active_storage/reflection.rb +64 -0
  46. data/lib/active_storage/service.rb +5 -6
  47. data/lib/active_storage/service/azure_storage_service.rb +28 -12
  48. data/lib/active_storage/service/configurator.rb +3 -1
  49. data/lib/active_storage/service/disk_service.rb +20 -16
  50. data/lib/active_storage/service/gcs_service.rb +48 -46
  51. data/lib/active_storage/service/mirror_service.rb +1 -1
  52. data/lib/active_storage/service/s3_service.rb +10 -7
  53. data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
  54. data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
  55. data/lib/active_storage/transformers/transformer.rb +42 -0
  56. data/lib/tasks/activestorage.rake +7 -0
  57. metadata +26 -14
  58. data/app/models/active_storage/filename/parameters.rb +0 -36
  59. data/lib/active_storage/attached/macros.rb +0 -110
@@ -19,10 +19,8 @@ module ActiveStorage
19
19
 
20
20
  def upload(key, io, checksum: nil, **)
21
21
  instrument :upload, key: key, checksum: checksum do
22
- begin
22
+ handle_errors do
23
23
  blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum)
24
- rescue Azure::Core::Http::HTTPError
25
- raise ActiveStorage::IntegrityError
26
24
  end
27
25
  end
28
26
  end
@@ -34,26 +32,29 @@ module ActiveStorage
34
32
  end
35
33
  else
36
34
  instrument :download, key: key do
37
- _, io = blobs.get_blob(container, key)
38
- io.force_encoding(Encoding::BINARY)
35
+ handle_errors do
36
+ _, io = blobs.get_blob(container, key)
37
+ io.force_encoding(Encoding::BINARY)
38
+ end
39
39
  end
40
40
  end
41
41
  end
42
42
 
43
43
  def download_chunk(key, range)
44
44
  instrument :download_chunk, key: key, range: range do
45
- _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
46
- io.force_encoding(Encoding::BINARY)
45
+ handle_errors do
46
+ _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
47
+ io.force_encoding(Encoding::BINARY)
48
+ end
47
49
  end
48
50
  end
49
51
 
50
52
  def delete(key)
51
53
  instrument :delete, key: key do
52
- begin
53
- blobs.delete_blob(container, key)
54
- rescue Azure::Core::Http::HTTPError
55
- # Ignore files already deleted
56
- end
54
+ blobs.delete_blob(container, key)
55
+ rescue Azure::Core::Http::HTTPError => e
56
+ raise unless e.type == "BlobNotFound"
57
+ # Ignore files already deleted
57
58
  end
58
59
  end
59
60
 
@@ -139,11 +140,26 @@ module ActiveStorage
139
140
  chunk_size = 5.megabytes
140
141
  offset = 0
141
142
 
143
+ raise ActiveStorage::FileNotFoundError unless blob.present?
144
+
142
145
  while offset < blob.properties[:content_length]
143
146
  _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
144
147
  yield chunk.force_encoding(Encoding::BINARY)
145
148
  offset += chunk_size
146
149
  end
147
150
  end
151
+
152
+ def handle_errors
153
+ yield
154
+ rescue Azure::Core::Http::HTTPError => e
155
+ case e.type
156
+ when "BlobNotFound"
157
+ raise ActiveStorage::FileNotFoundError
158
+ when "Md5Mismatch"
159
+ raise ActiveStorage::IntegrityError
160
+ else
161
+ raise
162
+ end
163
+ end
148
164
  end
149
165
  end
@@ -26,7 +26,9 @@ module ActiveStorage
26
26
 
27
27
  def resolve(class_name)
28
28
  require "active_storage/service/#{class_name.to_s.underscore}_service"
29
- ActiveStorage::Service.const_get(:"#{class_name}Service")
29
+ ActiveStorage::Service.const_get(:"#{class_name.camelize}Service")
30
+ rescue LoadError
31
+ raise "Missing service adapter for #{class_name.inspect}"
30
32
  end
31
33
  end
32
34
  end
@@ -22,18 +22,16 @@ module ActiveStorage
22
22
  end
23
23
  end
24
24
 
25
- def download(key)
25
+ def download(key, &block)
26
26
  if block_given?
27
27
  instrument :streaming_download, key: key do
28
- File.open(path_for(key), "rb") do |file|
29
- while data = file.read(5.megabytes)
30
- yield data
31
- end
32
- end
28
+ stream key, &block
33
29
  end
34
30
  else
35
31
  instrument :download, key: key do
36
32
  File.binread path_for(key)
33
+ rescue Errno::ENOENT
34
+ raise ActiveStorage::FileNotFoundError
37
35
  end
38
36
  end
39
37
  end
@@ -44,16 +42,16 @@ module ActiveStorage
44
42
  file.seek range.begin
45
43
  file.read range.size
46
44
  end
45
+ rescue Errno::ENOENT
46
+ raise ActiveStorage::FileNotFoundError
47
47
  end
48
48
  end
49
49
 
50
50
  def delete(key)
51
51
  instrument :delete, key: key do
52
- begin
53
- File.delete path_for(key)
54
- rescue Errno::ENOENT
55
- # Ignore files already deleted
56
- end
52
+ File.delete path_for(key)
53
+ rescue Errno::ENOENT
54
+ # Ignore files already deleted
57
55
  end
58
56
  end
59
57
 
@@ -86,12 +84,8 @@ module ActiveStorage
86
84
  purpose: :blob_key }
87
85
  )
88
86
 
89
- current_uri = URI.parse(current_host)
90
-
91
87
  generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration,
92
- protocol: current_uri.scheme,
93
- host: current_uri.host,
94
- port: current_uri.port,
88
+ host: current_host,
95
89
  disposition: content_disposition,
96
90
  content_type: content_type,
97
91
  filename: filename
@@ -132,6 +126,16 @@ module ActiveStorage
132
126
  end
133
127
 
134
128
  private
129
+ def stream(key)
130
+ File.open(path_for(key), "rb") do |file|
131
+ while data = file.read(5.megabytes)
132
+ yield data
133
+ end
134
+ end
135
+ rescue Errno::ENOENT
136
+ raise ActiveStorage::FileNotFoundError
137
+ end
138
+
135
139
  def folder_for(key)
136
140
  [ key[0..1], key[2..3] ].join("/")
137
141
  end
@@ -1,11 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem "google-cloud-storage", "~> 1.8"
4
-
3
+ gem "google-cloud-storage", "~> 1.11"
5
4
  require "google/cloud/storage"
6
- require "net/http"
7
-
8
- require "active_support/core_ext/object/to_query"
9
5
 
10
6
  module ActiveStorage
11
7
  # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
@@ -17,15 +13,27 @@ module ActiveStorage
17
13
 
18
14
  def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil)
19
15
  instrument :upload, key: key, checksum: checksum do
20
- begin
21
- # GCS's signed URLs don't include params such as response-content-type response-content_disposition
22
- # in the signature, which means an attacker can modify them and bypass our effort to force these to
23
- # binary and attachment when the file's content type requires it. The only way to force them is to
24
- # store them as object's metadata.
25
- content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
26
- bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition)
27
- rescue Google::Cloud::InvalidArgumentError
28
- raise ActiveStorage::IntegrityError
16
+ # GCS's signed URLs don't include params such as response-content-type response-content_disposition
17
+ # in the signature, which means an attacker can modify them and bypass our effort to force these to
18
+ # binary and attachment when the file's content type requires it. The only way to force them is to
19
+ # store them as object's metadata.
20
+ content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
21
+ bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition)
22
+ rescue Google::Cloud::InvalidArgumentError
23
+ raise ActiveStorage::IntegrityError
24
+ end
25
+ end
26
+
27
+ def download(key, &block)
28
+ if block_given?
29
+ instrument :streaming_download, key: key do
30
+ stream(key, &block)
31
+ end
32
+ else
33
+ instrument :download, key: key do
34
+ file_for(key).download.string
35
+ rescue Google::Cloud::NotFoundError
36
+ raise ActiveStorage::FileNotFoundError
29
37
  end
30
38
  end
31
39
  end
@@ -39,49 +47,28 @@ module ActiveStorage
39
47
  end
40
48
  end
41
49
 
42
- # FIXME: Download in chunks when given a block.
43
- def download(key)
44
- instrument :download, key: key do
45
- io = file_for(key).download
46
- io.rewind
47
-
48
- if block_given?
49
- yield io.string
50
- else
51
- io.string
52
- end
53
- end
54
- end
55
-
56
50
  def download_chunk(key, range)
57
51
  instrument :download_chunk, key: key, range: range do
58
- file = file_for(key)
59
- uri = URI(file.signed_url(expires: 30.seconds))
60
-
61
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
62
- client.get(uri, "Range" => "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body
63
- end
52
+ file_for(key).download(range: range).string
53
+ rescue Google::Cloud::NotFoundError
54
+ raise ActiveStorage::FileNotFoundError
64
55
  end
65
56
  end
66
57
 
67
58
  def delete(key)
68
59
  instrument :delete, key: key do
69
- begin
70
- file_for(key).delete
71
- rescue Google::Cloud::NotFoundError
72
- # Ignore files already deleted
73
- end
60
+ file_for(key).delete
61
+ rescue Google::Cloud::NotFoundError
62
+ # Ignore files already deleted
74
63
  end
75
64
  end
76
65
 
77
66
  def delete_prefixed(prefix)
78
67
  instrument :delete_prefixed, prefix: prefix do
79
68
  bucket.files(prefix: prefix).all do |file|
80
- begin
81
- file.delete
82
- rescue Google::Cloud::NotFoundError
83
- # Ignore concurrently-deleted files
84
- end
69
+ file.delete
70
+ rescue Google::Cloud::NotFoundError
71
+ # Ignore concurrently-deleted files
85
72
  end
86
73
  end
87
74
  end
@@ -124,8 +111,23 @@ module ActiveStorage
124
111
  private
125
112
  attr_reader :config
126
113
 
127
- def file_for(key)
128
- bucket.file(key, skip_lookup: true)
114
+ def file_for(key, skip_lookup: true)
115
+ bucket.file(key, skip_lookup: skip_lookup)
116
+ end
117
+
118
+ # Reads the file for the given key in chunks, yielding each to the block.
119
+ def stream(key)
120
+ file = file_for(key, skip_lookup: false)
121
+
122
+ chunk_size = 5.megabytes
123
+ offset = 0
124
+
125
+ raise ActiveStorage::FileNotFoundError unless file.present?
126
+
127
+ while offset < file.size
128
+ yield file.download(range: offset..(offset + chunk_size - 1)).string
129
+ offset += chunk_size
130
+ end
129
131
  end
130
132
 
131
133
  def bucket
@@ -9,7 +9,7 @@ module ActiveStorage
9
9
  class Service::MirrorService < Service
10
10
  attr_reader :primary, :mirrors
11
11
 
12
- delegate :download, :download_chunk, :exist?, :url, :path_for, to: :primary
12
+ delegate :download, :download_chunk, :exist?, :url, to: :primary
13
13
 
14
14
  # Stitch together from named services.
15
15
  def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
@@ -18,11 +18,9 @@ module ActiveStorage
18
18
 
19
19
  def upload(key, io, checksum: nil, **)
20
20
  instrument :upload, key: key, checksum: checksum do
21
- begin
22
- object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
23
- rescue Aws::S3::Errors::BadDigest
24
- raise ActiveStorage::IntegrityError
25
- end
21
+ object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
22
+ rescue Aws::S3::Errors::BadDigest
23
+ raise ActiveStorage::IntegrityError
26
24
  end
27
25
  end
28
26
 
@@ -34,6 +32,8 @@ module ActiveStorage
34
32
  else
35
33
  instrument :download, key: key do
36
34
  object_for(key).get.body.string.force_encoding(Encoding::BINARY)
35
+ rescue Aws::S3::Errors::NoSuchKey
36
+ raise ActiveStorage::FileNotFoundError
37
37
  end
38
38
  end
39
39
  end
@@ -41,6 +41,8 @@ module ActiveStorage
41
41
  def download_chunk(key, range)
42
42
  instrument :download_chunk, key: key, range: range do
43
43
  object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY)
44
+ rescue Aws::S3::Errors::NoSuchKey
45
+ raise ActiveStorage::FileNotFoundError
44
46
  end
45
47
  end
46
48
 
@@ -79,8 +81,7 @@ module ActiveStorage
79
81
  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
80
82
  instrument :url, key: key do |payload|
81
83
  generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
82
- content_type: content_type, content_length: content_length, content_md5: checksum,
83
- whitelist_headers: ['content-length']
84
+ content_type: content_type, content_length: content_length, content_md5: checksum
84
85
 
85
86
  payload[:url] = generated_url
86
87
 
@@ -104,6 +105,8 @@ module ActiveStorage
104
105
  chunk_size = 5.megabytes
105
106
  offset = 0
106
107
 
108
+ raise ActiveStorage::FileNotFoundError unless object.exists?
109
+
107
110
  while offset < object.content_length
108
111
  yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
109
112
  offset += chunk_size
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "image_processing"
4
+
5
+ module ActiveStorage
6
+ module Transformers
7
+ class ImageProcessingTransformer < Transformer
8
+ private
9
+ def process(file, format:)
10
+ processor.
11
+ source(file).
12
+ loader(page: 0).
13
+ convert(format).
14
+ apply(operations).
15
+ call
16
+ end
17
+
18
+ def processor
19
+ ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize)
20
+ end
21
+
22
+ def operations
23
+ transformations.each_with_object([]) do |(name, argument), list|
24
+ if name.to_s == "combine_options"
25
+ ActiveSupport::Deprecation.warn <<~WARNING
26
+ Active Storage's ImageProcessing transformer doesn't support :combine_options,
27
+ as it always generates a single ImageMagick command. Passing :combine_options will
28
+ not be supported in Rails 6.1.
29
+ WARNING
30
+
31
+ list.concat argument.keep_if { |key, value| value.present? }.to_a
32
+ elsif argument.present?
33
+ list << [ name, argument ]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_magick"
4
+
5
+ module ActiveStorage
6
+ module Transformers
7
+ class MiniMagickTransformer < Transformer
8
+ private
9
+ def process(file, format:)
10
+ image = MiniMagick::Image.new(file.path, file)
11
+
12
+ transformations.each do |name, argument_or_subtransformations|
13
+ image.mogrify do |command|
14
+ if name.to_s == "combine_options"
15
+ argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
16
+ pass_transform_argument(command, subtransformation_name, subtransformation_argument)
17
+ end
18
+ else
19
+ pass_transform_argument(command, name, argument_or_subtransformations)
20
+ end
21
+ end
22
+ end
23
+
24
+ image.format(format) if format
25
+
26
+ image.tempfile.tap(&:open)
27
+ end
28
+
29
+ def pass_transform_argument(command, method, argument)
30
+ if argument == true
31
+ command.public_send(method)
32
+ elsif argument.present?
33
+ command.public_send(method, argument)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module Transformers
5
+ # A Transformer applies a set of transformations to an image.
6
+ #
7
+ # The following concrete subclasses are included in Active Storage:
8
+ #
9
+ # * ActiveStorage::Transformers::ImageProcessingTransformer:
10
+ # backed by ImageProcessing, a common interface for MiniMagick and ruby-vips
11
+ #
12
+ # * ActiveStorage::Transformers::MiniMagickTransformer:
13
+ # backed by MiniMagick, a wrapper around the ImageMagick CLI
14
+ class Transformer
15
+ attr_reader :transformations
16
+
17
+ def initialize(transformations)
18
+ @transformations = transformations
19
+ end
20
+
21
+ # Applies the transformations to the source image in +file+, producing a target image in the
22
+ # specified +format+. Yields an open Tempfile containing the target image. Closes and unlinks
23
+ # the output tempfile after yielding to the given block. Returns the result of the block.
24
+ def transform(file, format:)
25
+ output = process(file, format: format)
26
+
27
+ begin
28
+ yield output
29
+ ensure
30
+ output.close!
31
+ end
32
+ end
33
+
34
+ private
35
+ # Returns an open Tempfile containing a transformed image in the given +format+.
36
+ # All subclasses implement this method.
37
+ def process(file, format:) #:doc:
38
+ raise NotImplementedError
39
+ end
40
+ end
41
+ end
42
+ end