activestorage 5.2.7.1 → 6.1.4.6

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +225 -93
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +43 -8
  5. data/app/assets/javascripts/activestorage.js +5 -2
  6. data/app/controllers/active_storage/base_controller.rb +13 -4
  7. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  8. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +3 -3
  9. data/app/controllers/active_storage/direct_uploads_controller.rb +2 -2
  10. data/app/controllers/active_storage/disk_controller.rb +13 -22
  11. data/app/controllers/active_storage/representations/base_controller.rb +14 -0
  12. data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
  13. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +3 -5
  14. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  15. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  16. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  17. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  18. data/app/javascript/activestorage/blob_record.js +7 -2
  19. data/app/jobs/active_storage/analyze_job.rb +5 -0
  20. data/app/jobs/active_storage/base_job.rb +0 -1
  21. data/app/jobs/active_storage/mirror_job.rb +15 -0
  22. data/app/jobs/active_storage/purge_job.rb +3 -0
  23. data/app/models/active_storage/attachment.rb +35 -16
  24. data/app/models/active_storage/blob/analyzable.rb +6 -2
  25. data/app/models/active_storage/blob/identifiable.rb +7 -6
  26. data/app/models/active_storage/blob/representable.rb +36 -6
  27. data/app/models/active_storage/blob.rb +186 -68
  28. data/app/models/active_storage/filename.rb +0 -6
  29. data/app/models/active_storage/preview.rb +37 -12
  30. data/app/models/active_storage/record.rb +7 -0
  31. data/app/models/active_storage/variant.rb +53 -67
  32. data/app/models/active_storage/variant_record.rb +8 -0
  33. data/app/models/active_storage/variant_with_record.rb +54 -0
  34. data/app/models/active_storage/variation.rb +30 -94
  35. data/config/routes.rb +66 -15
  36. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  37. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  38. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  39. data/lib/active_storage/analyzer/image_analyzer.rb +14 -4
  40. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  41. data/lib/active_storage/analyzer/video_analyzer.rb +17 -8
  42. data/lib/active_storage/analyzer.rb +15 -4
  43. data/lib/active_storage/attached/changes/create_many.rb +47 -0
  44. data/lib/active_storage/attached/changes/create_one.rb +82 -0
  45. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  46. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  47. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  48. data/lib/active_storage/attached/changes.rb +16 -0
  49. data/lib/active_storage/attached/many.rb +19 -12
  50. data/lib/active_storage/attached/model.rb +212 -0
  51. data/lib/active_storage/attached/one.rb +19 -21
  52. data/lib/active_storage/attached.rb +7 -22
  53. data/lib/active_storage/downloader.rb +43 -0
  54. data/lib/active_storage/engine.rb +60 -38
  55. data/lib/active_storage/errors.rb +25 -3
  56. data/lib/active_storage/gem_version.rb +4 -4
  57. data/lib/active_storage/log_subscriber.rb +6 -0
  58. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  59. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
  60. data/lib/active_storage/previewer/video_previewer.rb +17 -10
  61. data/lib/active_storage/previewer.rb +34 -14
  62. data/lib/active_storage/reflection.rb +64 -0
  63. data/lib/active_storage/service/azure_storage_service.rb +65 -44
  64. data/lib/active_storage/service/configurator.rb +6 -2
  65. data/lib/active_storage/service/disk_service.rb +57 -44
  66. data/lib/active_storage/service/gcs_service.rb +68 -64
  67. data/lib/active_storage/service/mirror_service.rb +31 -7
  68. data/lib/active_storage/service/registry.rb +32 -0
  69. data/lib/active_storage/service/s3_service.rb +56 -24
  70. data/lib/active_storage/service.rb +44 -12
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +45 -0
  72. data/lib/active_storage/transformers/transformer.rb +39 -0
  73. data/lib/active_storage.rb +31 -296
  74. data/lib/tasks/activestorage.rake +11 -0
  75. metadata +82 -16
  76. data/app/models/active_storage/filename/parameters.rb +0 -36
  77. data/lib/active_storage/attached/macros.rb +0 -110
  78. data/lib/active_storage/downloading.rb +0 -39
@@ -1,24 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_storage/downloading"
4
-
5
3
  # Image blobs can have variants that are the result of a set of transformations applied to the original.
6
4
  # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
7
5
  # original.
8
6
  #
9
- # Variants rely on {MiniMagick}[https://github.com/minimagick/minimagick] for the actual transformations
10
- # of the file, so you must add <tt>gem "mini_magick"</tt> to your Gemfile if you wish to use variants.
7
+ # Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
8
+ # of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
9
+ # default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
10
+ # {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
11
+ # {libvips}[http://libvips.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/libvips/ruby-vips]
12
+ # gem).
13
+ #
14
+ # Rails.application.config.active_storage.variant_processor
15
+ # # => :mini_magick
16
+ #
17
+ # Rails.application.config.active_storage.variant_processor = :vips
18
+ # # => :vips
11
19
  #
12
- # Note that to create a variant it's necessary to download the entire blob file from the service and load it
13
- # into memory. The larger the image, the more memory is used. Because of this process, you also want to be
14
- # considerate about when the variant is actually processed. You shouldn't be processing variants inline in a
15
- # template, for example. Delay the processing to an on-demand controller, like the one provided in
20
+ # Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
21
+ # you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
22
+ # in a template, for example. Delay the processing to an on-demand controller, like the one provided in
16
23
  # ActiveStorage::RepresentationsController.
17
24
  #
18
25
  # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
19
26
  # by Active Storage like so:
20
27
  #
21
- # <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
28
+ # <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
22
29
  #
23
30
  # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
24
31
  # can then produce on-demand.
@@ -27,22 +34,26 @@ require "active_storage/downloading"
27
34
  # has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
28
35
  # the transformations, upload the variant to the service, and return itself again. Example:
29
36
  #
30
- # avatar.variant(resize: "100x100").processed.service_url
37
+ # avatar.variant(resize_to_limit: [100, 100]).processed.url
31
38
  #
32
39
  # This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
33
40
  # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
34
41
  #
35
- # A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php. You can
36
- # combine as many as you like freely:
42
+ # You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
43
+ # ImageProcessing gem (such as +resize_to_limit+):
44
+ #
45
+ # avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
46
+ #
47
+ # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
37
48
  #
38
- # avatar.variant(resize: "100x100", monochrome: true, rotate: "-90")
49
+ # * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
50
+ # * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
51
+ # * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
52
+ # * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
39
53
  class ActiveStorage::Variant
40
- include ActiveStorage::Downloading
41
-
42
- WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif )
43
-
44
54
  attr_reader :blob, :variation
45
55
  delegate :service, to: :blob
56
+ delegate :content_type, to: :variation
46
57
 
47
58
  def initialize(blob, variation_or_variation_key)
48
59
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
@@ -59,18 +70,34 @@ class ActiveStorage::Variant
59
70
  "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
60
71
  end
61
72
 
62
- # Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly
63
- # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
64
- # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
65
- # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
73
+ # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
66
74
  #
67
75
  # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
68
76
  # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
69
77
  # for its redirection.
70
- def service_url(expires_in: service.url_expires_in, disposition: :inline)
78
+ def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
71
79
  service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
72
80
  end
73
81
 
82
+ alias_method :service_url, :url
83
+ deprecate service_url: :url
84
+
85
+ # Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned.
86
+ # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
87
+ def download(&block)
88
+ service.download key, &block
89
+ end
90
+
91
+ def filename
92
+ ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
93
+ end
94
+
95
+ alias_method :content_type_for_serving, :content_type
96
+
97
+ def forced_disposition_for_serving #:nodoc:
98
+ nil
99
+ end
100
+
74
101
  # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
75
102
  def image
76
103
  self
@@ -82,51 +109,10 @@ class ActiveStorage::Variant
82
109
  end
83
110
 
84
111
  def process
85
- open_image do |image|
86
- transform image
87
- format image
88
- upload image
89
- end
90
- end
91
-
92
-
93
- def filename
94
- if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
95
- blob.filename
96
- else
97
- ActiveStorage::Filename.new("#{blob.filename.base}.png")
98
- end
99
- end
100
-
101
- def content_type
102
- blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png"
103
- end
104
-
105
-
106
- def open_image(&block)
107
- image = download_image
108
-
109
- begin
110
- yield image
111
- ensure
112
- image.destroy!
112
+ blob.open do |input|
113
+ variation.transform(input) do |output|
114
+ service.upload(key, output, content_type: content_type)
115
+ end
113
116
  end
114
117
  end
115
-
116
- def download_image
117
- require "mini_magick"
118
- MiniMagick::Image.create(blob.filename.extension_with_delimiter) { |file| download_blob_to(file) }
119
- end
120
-
121
- def transform(image)
122
- variation.transform(image)
123
- end
124
-
125
- def format(image)
126
- image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
127
- end
128
-
129
- def upload(image)
130
- File.open(image.path, "r") { |file| service.upload(key, file) }
131
- end
132
118
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::VariantRecord < ActiveStorage::Record
4
+ self.table_name = "active_storage_variant_records"
5
+
6
+ belongs_to :blob
7
+ has_one_attached :image
8
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::VariantWithRecord
4
+ attr_reader :blob, :variation
5
+
6
+ def initialize(blob, variation)
7
+ @blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
8
+ end
9
+
10
+ def processed
11
+ process
12
+ self
13
+ end
14
+
15
+ def process
16
+ transform_blob { |image| create_or_find_record(image: image) } unless processed?
17
+ end
18
+
19
+ def processed?
20
+ record.present?
21
+ end
22
+
23
+ def image
24
+ record&.image
25
+ end
26
+
27
+ delegate :key, :url, :download, to: :image, allow_nil: true
28
+
29
+ alias_method :service_url, :url
30
+ deprecate service_url: :url
31
+
32
+ private
33
+ def transform_blob
34
+ blob.open do |input|
35
+ variation.transform(input) do |output|
36
+ yield io: output, filename: "#{blob.filename.base}.#{variation.format.downcase}",
37
+ content_type: variation.content_type, service_name: blob.service.name
38
+ end
39
+ end
40
+ end
41
+
42
+ def create_or_find_record(image:)
43
+ @record =
44
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
45
+ blob.variant_records.create_or_find_by!(variation_digest: variation.digest) do |record|
46
+ record.image.attach(image)
47
+ end
48
+ end
49
+ end
50
+
51
+ def record
52
+ @record ||= blob.variant_records.find_by(variation_digest: variation.digest)
53
+ end
54
+ end
@@ -1,28 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "mini_mime"
4
+
3
5
  # A set of transformations that can be applied to a blob to create a variant. This class is exposed via
4
6
  # the ActiveStorage::Blob#variant method and should rarely be used directly.
5
7
  #
6
8
  # In case you do need to use this directly, it's instantiated using a hash of transformations where
7
9
  # the key is the command and the value is the arguments. Example:
8
10
  #
9
- # ActiveStorage::Variation.new(resize: "100x100", monochrome: true, trim: true, rotate: "-90")
10
- #
11
- # You can also combine multiple transformations in one step, e.g. for center-weighted cropping:
11
+ # ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
12
12
  #
13
- # ActiveStorage::Variation.new(combine_options: {
14
- # resize: "100x100^",
15
- # gravity: "center",
16
- # crop: "100x100+0+0",
17
- # })
18
- #
19
- # A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php.
13
+ # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
20
14
  class ActiveStorage::Variation
21
15
  attr_reader :transformations
22
16
 
23
- class UnsupportedImageProcessingMethod < StandardError; end
24
- class UnsupportedImageProcessingArgument < StandardError; end
25
-
26
17
  class << self
27
18
  # Returns a Variation instance based on the given variator. If the variator is a Variation, it is
28
19
  # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
@@ -51,99 +42,44 @@ class ActiveStorage::Variation
51
42
  end
52
43
 
53
44
  def initialize(transformations)
54
- @transformations = transformations
45
+ @transformations = transformations.deep_symbolize_keys
55
46
  end
56
47
 
57
- # Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>,
58
- # and performs the +transformations+ against it. The transformed image instance is then returned.
59
- def transform(image)
48
+ def default_to(defaults)
49
+ self.class.new transformations.reverse_merge(defaults)
50
+ end
51
+
52
+ # Accepts a File object, performs the +transformations+ against it, and
53
+ # saves the transformed image into a temporary file.
54
+ def transform(file, &block)
60
55
  ActiveSupport::Notifications.instrument("transform.active_storage") do
61
- transformations.each do |name, argument_or_subtransformations|
62
- validate_transformation(name, argument_or_subtransformations)
63
- image.mogrify do |command|
64
- if name.to_s == "combine_options"
65
- argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
66
- validate_transformation(subtransformation_name, subtransformation_argument)
67
- pass_transform_argument(command, subtransformation_name, subtransformation_argument)
68
- end
69
- else
70
- validate_transformation(name, argument_or_subtransformations)
71
- pass_transform_argument(command, name, argument_or_subtransformations)
72
- end
73
- end
56
+ transformer.transform(file, format: format, &block)
57
+ end
58
+ end
59
+
60
+ def format
61
+ transformations.fetch(:format, :png).tap do |format|
62
+ if MiniMime.lookup_by_extension(format.to_s).nil?
63
+ raise ArgumentError, "Invalid variant format (#{format.inspect})"
74
64
  end
75
65
  end
76
66
  end
77
67
 
68
+ def content_type
69
+ MiniMime.lookup_by_extension(format.to_s).content_type
70
+ end
71
+
78
72
  # Returns a signed key for all the +transformations+ that this variation was instantiated with.
79
73
  def key
80
74
  self.class.encode(transformations)
81
75
  end
82
76
 
83
- private
84
- def pass_transform_argument(command, method, argument)
85
- if eligible_argument?(argument)
86
- command.public_send(method, argument)
87
- else
88
- command.public_send(method)
89
- end
90
- end
91
-
92
- def eligible_argument?(argument)
93
- argument.present? && argument != true
94
- end
95
-
96
- def validate_transformation(name, argument)
97
- method_name = name.to_s.gsub("-","_")
98
-
99
- unless ActiveStorage.supported_image_processing_methods.any? { |method| method_name == method }
100
- raise UnsupportedImageProcessingMethod, <<~ERROR.squish
101
- One or more of the provided transformation methods is not supported.
102
- ERROR
103
- end
104
-
105
- if argument.present?
106
- if argument.is_a?(String) || argument.is_a?(Symbol)
107
- validate_arg_string(argument)
108
- elsif argument.is_a?(Array)
109
- validate_arg_array(argument)
110
- elsif argument.is_a?(Hash)
111
- validate_arg_hash(argument)
112
- end
113
- end
114
- end
115
-
116
- def validate_arg_string(argument)
117
- if ActiveStorage.unsupported_image_processing_arguments.any? { |bad_arg| argument.to_s.downcase.include?(bad_arg) }; raise UnsupportedImageProcessingArgument end
118
- end
119
-
120
- def validate_arg_array(argument)
121
- argument.each do |arg|
122
- if arg.is_a?(Integer) || arg.is_a?(Float)
123
- next
124
- elsif arg.is_a?(String) || arg.is_a?(Symbol)
125
- validate_arg_string(arg)
126
- elsif arg.is_a?(Array)
127
- validate_arg_array(arg)
128
- elsif arg.is_a?(Hash)
129
- validate_arg_hash(arg)
130
- end
131
- end
132
- end
133
-
134
- def validate_arg_hash(argument)
135
- argument.each do |key, value|
136
- validate_arg_string(key)
77
+ def digest
78
+ Digest::SHA1.base64digest Marshal.dump(transformations)
79
+ end
137
80
 
138
- if value.is_a?(Integer) || value.is_a?(Float)
139
- next
140
- elsif value.is_a?(String) || value.is_a?(Symbol)
141
- validate_arg_string(value)
142
- elsif value.is_a?(Array)
143
- validate_arg_array(value)
144
- elsif value.is_a?(Hash)
145
- validate_arg_hash(value)
146
- end
147
- end
81
+ private
82
+ def transformer
83
+ ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations.except(:format))
148
84
  end
149
85
  end
data/config/routes.rb CHANGED
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Rails.application.routes.draw do
4
- get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
4
+ scope ActiveStorage.routes_prefix do
5
+ get "/blobs/redirect/:signed_id/*filename" => "active_storage/blobs/redirect#show", as: :rails_service_blob
6
+ get "/blobs/proxy/:signed_id/*filename" => "active_storage/blobs/proxy#show", as: :rails_service_blob_proxy
7
+ get "/blobs/:signed_id/*filename" => "active_storage/blobs/redirect#show"
5
8
 
6
- direct :rails_blob do |blob, options|
7
- route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
8
- end
9
-
10
- resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
11
- resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
9
+ get "/representations/redirect/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/redirect#show", as: :rails_blob_representation
10
+ get "/representations/proxy/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/proxy#show", as: :rails_blob_representation_proxy
11
+ get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/redirect#show"
12
12
 
13
-
14
- get "/rails/active_storage/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
13
+ get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
14
+ put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
15
+ post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
16
+ end
15
17
 
16
18
  direct :rails_representation do |representation, options|
17
19
  signed_blob_id = representation.blob.signed_id
@@ -21,11 +23,60 @@ Rails.application.routes.draw do
21
23
  route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
22
24
  end
23
25
 
24
- resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
25
- resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
26
+ resolve("ActiveStorage::Variant") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
27
+ resolve("ActiveStorage::VariantWithRecord") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
28
+ resolve("ActiveStorage::Preview") { |preview, options| route_for(ActiveStorage.resolve_model_to_route, preview, options) }
26
29
 
30
+ direct :rails_blob do |blob, options|
31
+ route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
32
+ end
33
+
34
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(ActiveStorage.resolve_model_to_route, blob, options) }
35
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(ActiveStorage.resolve_model_to_route, attachment.blob, options) }
27
36
 
28
- get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
29
- put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
30
- post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
31
- end
37
+ direct :rails_storage_proxy do |model, options|
38
+ if model.respond_to?(:signed_id)
39
+ route_for(
40
+ :rails_service_blob_proxy,
41
+ model.signed_id,
42
+ model.filename,
43
+ options
44
+ )
45
+ else
46
+ signed_blob_id = model.blob.signed_id
47
+ variation_key = model.variation.key
48
+ filename = model.blob.filename
49
+
50
+ route_for(
51
+ :rails_blob_representation_proxy,
52
+ signed_blob_id,
53
+ variation_key,
54
+ filename,
55
+ options
56
+ )
57
+ end
58
+ end
59
+
60
+ direct :rails_storage_redirect do |model, options|
61
+ if model.respond_to?(:signed_id)
62
+ route_for(
63
+ :rails_service_blob,
64
+ model.signed_id,
65
+ model.filename,
66
+ options
67
+ )
68
+ else
69
+ signed_blob_id = model.blob.signed_id
70
+ variation_key = model.variation.key
71
+ filename = model.blob.filename
72
+
73
+ route_for(
74
+ :rails_blob_representation,
75
+ signed_blob_id,
76
+ variation_key,
77
+ filename,
78
+ options
79
+ )
80
+ end
81
+ end
82
+ end if ActiveStorage.draw_routes
@@ -1,13 +1,14 @@
1
1
  class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
2
2
  def change
3
3
  create_table :active_storage_blobs do |t|
4
- t.string :key, null: false
5
- t.string :filename, null: false
4
+ t.string :key, null: false
5
+ t.string :filename, null: false
6
6
  t.string :content_type
7
7
  t.text :metadata
8
- t.bigint :byte_size, null: false
9
- t.string :checksum, null: false
10
- t.datetime :created_at, null: false
8
+ t.string :service_name, null: false
9
+ t.bigint :byte_size, null: false
10
+ t.string :checksum, null: false
11
+ t.datetime :created_at, null: false
11
12
 
12
13
  t.index [ :key ], unique: true
13
14
  end
@@ -22,5 +23,13 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
22
23
  t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
23
24
  t.foreign_key :active_storage_blobs, column: :blob_id
24
25
  end
26
+
27
+ create_table :active_storage_variant_records do |t|
28
+ t.belongs_to :blob, null: false, index: false
29
+ t.string :variation_digest, null: false
30
+
31
+ t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
32
+ t.foreign_key :active_storage_blobs, column: :blob_id
33
+ end
25
34
  end
26
35
  end
@@ -0,0 +1,17 @@
1
+ class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
2
+ def up
3
+ unless column_exists?(:active_storage_blobs, :service_name)
4
+ add_column :active_storage_blobs, :service_name, :string
5
+
6
+ if configured_service = ActiveStorage::Blob.service.name
7
+ ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
8
+ end
9
+
10
+ change_column :active_storage_blobs, :service_name, :string, null: false
11
+ end
12
+ end
13
+
14
+ def down
15
+ remove_column :active_storage_blobs, :service_name
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :active_storage_variant_records do |t|
4
+ t.belongs_to :blob, null: false, index: false
5
+ t.string :variation_digest, null: false
6
+
7
+ t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
8
+ t.foreign_key :active_storage_blobs, column: :blob_id
9
+ end
10
+ end
11
+ end
@@ -25,17 +25,27 @@ module ActiveStorage
25
25
  { width: image.width, height: image.height }
26
26
  end
27
27
  end
28
- rescue LoadError
29
- logger.info "Skipping image analysis because the mini_magick gem isn't installed"
30
- {}
31
28
  end
32
29
 
33
30
  private
34
31
  def read_image
35
32
  download_blob_to_tempfile do |file|
36
33
  require "mini_magick"
37
- yield MiniMagick::Image.new(file.path)
34
+ image = MiniMagick::Image.new(file.path)
35
+
36
+ if image.valid?
37
+ yield image
38
+ else
39
+ logger.info "Skipping image analysis because ImageMagick doesn't support the file"
40
+ {}
41
+ end
38
42
  end
43
+ rescue LoadError
44
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
45
+ {}
46
+ rescue MiniMagick::Error => error
47
+ logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
48
+ {}
39
49
  end
40
50
 
41
51
  def rotated_image?(image)
@@ -6,6 +6,10 @@ module ActiveStorage
6
6
  true
7
7
  end
8
8
 
9
+ def self.analyze_later?
10
+ false
11
+ end
12
+
9
13
  def metadata
10
14
  {}
11
15
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/hash/compact"
4
-
5
3
  module ActiveStorage
6
4
  # Extracts the following from a video blob:
7
5
  #
@@ -13,12 +11,12 @@ module ActiveStorage
13
11
  #
14
12
  # Example:
15
13
  #
16
- # ActiveStorage::VideoAnalyzer.new(blob).metadata
14
+ # ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
17
15
  # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
18
16
  #
19
17
  # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
20
18
  #
21
- # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
19
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
22
20
  class Analyzer::VideoAnalyzer < Analyzer
23
21
  def self.accept?(blob)
24
22
  blob.video?
@@ -46,7 +44,8 @@ module ActiveStorage
46
44
  end
47
45
 
48
46
  def duration
49
- Float(video_stream["duration"]) if video_stream["duration"]
47
+ duration = video_stream["duration"] || container["duration"]
48
+ Float(duration) if duration
50
49
  end
51
50
 
52
51
  def angle
@@ -100,16 +99,26 @@ module ActiveStorage
100
99
  probe["streams"] || []
101
100
  end
102
101
 
102
+ def container
103
+ probe["format"] || {}
104
+ end
105
+
103
106
  def probe
104
- download_blob_to_tempfile { |file| probe_from(file) }
107
+ @probe ||= download_blob_to_tempfile { |file| probe_from(file) }
105
108
  end
106
109
 
107
110
  def probe_from(file)
108
- IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
111
+ IO.popen([ ffprobe_path,
112
+ "-print_format", "json",
113
+ "-show_streams",
114
+ "-show_format",
115
+ "-v", "error",
116
+ file.path
117
+ ]) do |output|
109
118
  JSON.parse(output.read)
110
119
  end
111
120
  rescue Errno::ENOENT
112
- logger.info "Skipping video analysis because ffmpeg isn't installed"
121
+ logger.info "Skipping video analysis because FFmpeg isn't installed"
113
122
  {}
114
123
  end
115
124