activestorage 6.0.5 → 6.1.0.rc1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +137 -233
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +36 -4
  5. data/app/controllers/active_storage/base_controller.rb +11 -0
  6. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  7. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +2 -2
  8. data/app/controllers/active_storage/disk_controller.rb +8 -20
  9. data/app/controllers/active_storage/representations/proxy_controller.rb +19 -0
  10. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +2 -2
  11. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  12. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  13. data/app/controllers/concerns/active_storage/set_current.rb +2 -2
  14. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  15. data/app/jobs/active_storage/mirror_job.rb +15 -0
  16. data/app/models/active_storage/attachment.rb +18 -10
  17. data/app/models/active_storage/blob/analyzable.rb +6 -2
  18. data/app/models/active_storage/blob/identifiable.rb +7 -6
  19. data/app/models/active_storage/blob/representable.rb +34 -4
  20. data/app/models/active_storage/blob.rb +114 -57
  21. data/app/models/active_storage/preview.rb +31 -10
  22. data/app/models/active_storage/record.rb +7 -0
  23. data/app/models/active_storage/variant.rb +28 -41
  24. data/app/models/active_storage/variant_record.rb +8 -0
  25. data/app/models/active_storage/variant_with_record.rb +54 -0
  26. data/app/models/active_storage/variation.rb +25 -20
  27. data/config/routes.rb +58 -8
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  29. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  30. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  31. data/lib/active_storage/analyzer/image_analyzer.rb +3 -0
  32. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  33. data/lib/active_storage/analyzer/video_analyzer.rb +14 -3
  34. data/lib/active_storage/analyzer.rb +6 -0
  35. data/lib/active_storage/attached/changes/create_many.rb +1 -0
  36. data/lib/active_storage/attached/changes/create_one.rb +17 -4
  37. data/lib/active_storage/attached/many.rb +4 -3
  38. data/lib/active_storage/attached/model.rb +49 -10
  39. data/lib/active_storage/attached/one.rb +4 -3
  40. data/lib/active_storage/engine.rb +25 -43
  41. data/lib/active_storage/gem_version.rb +3 -3
  42. data/lib/active_storage/log_subscriber.rb +6 -0
  43. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  44. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
  45. data/lib/active_storage/previewer/video_previewer.rb +2 -2
  46. data/lib/active_storage/previewer.rb +3 -2
  47. data/lib/active_storage/service/azure_storage_service.rb +40 -35
  48. data/lib/active_storage/service/configurator.rb +3 -1
  49. data/lib/active_storage/service/disk_service.rb +36 -31
  50. data/lib/active_storage/service/gcs_service.rb +18 -16
  51. data/lib/active_storage/service/mirror_service.rb +31 -7
  52. data/lib/active_storage/service/registry.rb +32 -0
  53. data/lib/active_storage/service/s3_service.rb +51 -23
  54. data/lib/active_storage/service.rb +35 -7
  55. data/lib/active_storage/transformers/image_processing_transformer.rb +13 -365
  56. data/lib/active_storage/transformers/transformer.rb +0 -3
  57. data/lib/active_storage.rb +9 -8
  58. metadata +60 -25
  59. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +0 -9
  60. data/lib/active_storage/downloading.rb +0 -47
  61. data/lib/active_storage/transformers/mini_magick_transformer.rb +0 -38
@@ -3,8 +3,9 @@
3
3
  # Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
4
4
  # extracting its first frame, and a PDF blob can be previewed by extracting its first page.
5
5
  #
6
- # A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs:
7
- # ActiveStorage::Previewer::VideoPreviewer and ActiveStorage::Previewer::PDFPreviewer. Build custom previewers by
6
+ # A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs.
7
+ # ActiveStorage::Previewer::VideoPreviewer is used for videos whereas ActiveStorage::Previewer::PopplerPDFPreviewer
8
+ # and ActiveStorage::Previewer::MuPDFPreviewer are used for PDFs. Build custom previewers by
8
9
  # subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer
9
10
  # documentation for more details on what's required of previewers.
10
11
  #
@@ -13,11 +14,11 @@
13
14
  # by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
14
15
  #
15
16
  # Rails.application.config.active_storage.previewers
16
- # # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
17
+ # # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
17
18
  #
18
19
  # # Add a custom previewer for Microsoft Office documents:
19
20
  # Rails.application.config.active_storage.previewers << DOCXPreviewer
20
- # # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
21
+ # # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
21
22
  #
22
23
  # Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
23
24
  #
@@ -38,7 +39,7 @@ class ActiveStorage::Preview
38
39
 
39
40
  # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
40
41
  #
41
- # blob.preview(resize_to_limit: [100, 100]).processed.service_url
42
+ # blob.preview(resize_to_limit: [100, 100]).processed.url
42
43
  #
43
44
  # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
44
45
  # image is stored with the blob, it is only generated once.
@@ -56,10 +57,30 @@ class ActiveStorage::Preview
56
57
  # preview has not been processed yet.
57
58
  #
58
59
  # This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate
59
- # a stable URL that redirects to the short-lived URL returned by this method.
60
- def service_url(**options)
60
+ # a stable URL that redirects to the URL returned by this method.
61
+ def url(**options)
61
62
  if processed?
62
- variant.service_url(**options)
63
+ variant.url(**options)
64
+ else
65
+ raise UnprocessedError
66
+ end
67
+ end
68
+
69
+ alias_method :service_url, :url
70
+ deprecate service_url: :url
71
+
72
+ # Returns a combination key of the blob and the variation that together identifies a specific variant.
73
+ def key
74
+ if processed?
75
+ variant.key
76
+ else
77
+ raise UnprocessedError
78
+ end
79
+ end
80
+
81
+ def download(&block)
82
+ if processed?
83
+ variant.download(&block)
63
84
  else
64
85
  raise UnprocessedError
65
86
  end
@@ -71,7 +92,7 @@ class ActiveStorage::Preview
71
92
  end
72
93
 
73
94
  def process
74
- previewer.preview do |attachable|
95
+ previewer.preview(service_name: blob.service_name) do |attachable|
75
96
  ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
76
97
  image.attach(attachable)
77
98
  end
@@ -79,7 +100,7 @@ class ActiveStorage::Preview
79
100
  end
80
101
 
81
102
  def variant
82
- ActiveStorage::Variant.new(image, variation).processed
103
+ image.variant(variation).processed
83
104
  end
84
105
 
85
106
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::Record < ActiveRecord::Base #:nodoc:
4
+ self.abstract_class = true
5
+ end
6
+
7
+ ActiveSupport.run_load_hooks :active_storage_record, ActiveStorage::Record
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ostruct"
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.
@@ -10,7 +8,7 @@ require "ostruct"
10
8
  # of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
11
9
  # default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
12
10
  # {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
13
- # {libvips}[http://jcupitt.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/jcupitt/ruby-vips]
11
+ # {libvips}[http://libvips.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/libvips/ruby-vips]
14
12
  # gem).
15
13
  #
16
14
  # Rails.application.config.active_storage.variant_processor
@@ -36,7 +34,7 @@ require "ostruct"
36
34
  # has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
37
35
  # the transformations, upload the variant to the service, and return itself again. Example:
38
36
  #
39
- # avatar.variant(resize_to_limit: [100, 100]).processed.service_url
37
+ # avatar.variant(resize_to_limit: [100, 100]).processed.url
40
38
  #
41
39
  # This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
42
40
  # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
@@ -53,10 +51,9 @@ require "ostruct"
53
51
  # * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
54
52
  # * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
55
53
  class ActiveStorage::Variant
56
- WEB_IMAGE_CONTENT_TYPES = %w[ image/png image/jpeg image/jpg image/gif ]
57
-
58
54
  attr_reader :blob, :variation
59
55
  delegate :service, to: :blob
56
+ delegate :content_type, to: :variation
60
57
 
61
58
  def initialize(blob, variation_or_variation_key)
62
59
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
@@ -73,18 +70,34 @@ class ActiveStorage::Variant
73
70
  "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
74
71
  end
75
72
 
76
- # Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly
77
- # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
78
- # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
79
- # 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.
80
74
  #
81
75
  # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
82
76
  # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
83
77
  # for its redirection.
84
- def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
78
+ def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
85
79
  service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
86
80
  end
87
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}"
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
+
88
101
  # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
89
102
  def image
90
103
  self
@@ -96,36 +109,10 @@ class ActiveStorage::Variant
96
109
  end
97
110
 
98
111
  def process
99
- blob.open do |image|
100
- transform(image) { |output| upload(output) }
101
- end
102
- end
103
-
104
- def transform(image, &block)
105
- variation.transform(image, format: format, &block)
106
- end
107
-
108
- def upload(file)
109
- service.upload(key, file)
110
- end
111
-
112
-
113
- def specification
114
- @specification ||=
115
- if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
116
- Specification.new \
117
- filename: blob.filename,
118
- content_type: blob.content_type,
119
- format: nil
120
- else
121
- Specification.new \
122
- filename: ActiveStorage::Filename.new("#{blob.filename.base}.png"),
123
- content_type: "image/png",
124
- format: "png"
112
+ blob.open do |input|
113
+ variation.transform(input) do |output|
114
+ service.upload(key, output, content_type: content_type)
125
115
  end
116
+ end
126
117
  end
127
-
128
- delegate :filename, :content_type, :format, to: :specification
129
-
130
- class Specification < OpenStruct; end
131
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}",
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,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "mimemagic"
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
  #
@@ -43,38 +45,41 @@ class ActiveStorage::Variation
43
45
  @transformations = transformations.deep_symbolize_keys
44
46
  end
45
47
 
48
+ def default_to(defaults)
49
+ self.class.new transformations.reverse_merge(defaults)
50
+ end
51
+
46
52
  # Accepts a File object, performs the +transformations+ against it, and
47
- # saves the transformed image into a temporary file. If +format+ is specified
48
- # it will be the format of the result image, otherwise the result image
49
- # retains the source format.
50
- def transform(file, format: nil, &block)
53
+ # saves the transformed image into a temporary file.
54
+ def transform(file, &block)
51
55
  ActiveSupport::Notifications.instrument("transform.active_storage") do
52
56
  transformer.transform(file, format: format, &block)
53
57
  end
54
58
  end
55
59
 
60
+ def format
61
+ transformations.fetch(:format, :png).tap do |format|
62
+ if MimeMagic.by_extension(format).nil?
63
+ raise ArgumentError, "Invalid variant format (#{format.inspect})"
64
+ end
65
+ end
66
+ end
67
+
68
+ def content_type
69
+ MimeMagic.by_extension(format).to_s
70
+ end
71
+
56
72
  # Returns a signed key for all the +transformations+ that this variation was instantiated with.
57
73
  def key
58
74
  self.class.encode(transformations)
59
75
  end
60
76
 
77
+ def digest
78
+ Digest::SHA1.base64digest Marshal.dump(transformations)
79
+ end
80
+
61
81
  private
62
82
  def transformer
63
- if ActiveStorage.variant_processor
64
- begin
65
- require "image_processing"
66
- rescue LoadError
67
- ActiveSupport::Deprecation.warn <<~WARNING.squish
68
- Generating image variants will require the image_processing gem in Rails 6.1.
69
- Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.
70
- WARNING
71
-
72
- ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
73
- else
74
- ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations)
75
- end
76
- else
77
- ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
78
- end
83
+ ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations.except(:format))
79
84
  end
80
85
  end
data/config/routes.rb CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  Rails.application.routes.draw do
4
4
  scope ActiveStorage.routes_prefix do
5
- get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
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"
6
8
 
7
- get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
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"
8
12
 
9
13
  get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
10
14
  put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
@@ -19,14 +23,60 @@ Rails.application.routes.draw do
19
23
  route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
20
24
  end
21
25
 
22
- resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
23
- resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
24
-
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) }
25
29
 
26
30
  direct :rails_blob do |blob, options|
27
31
  route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
28
32
  end
29
33
 
30
- resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
31
- resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
32
- end
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) }
36
+
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
@@ -43,6 +43,9 @@ module ActiveStorage
43
43
  rescue LoadError
44
44
  logger.info "Skipping image analysis because the mini_magick gem isn't installed"
45
45
  {}
46
+ rescue MiniMagick::Error => error
47
+ logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
48
+ {}
46
49
  end
47
50
 
48
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
@@ -44,7 +44,8 @@ module ActiveStorage
44
44
  end
45
45
 
46
46
  def duration
47
- Float(video_stream["duration"]) if video_stream["duration"]
47
+ duration = video_stream["duration"] || container["duration"]
48
+ Float(duration) if duration
48
49
  end
49
50
 
50
51
  def angle
@@ -98,12 +99,22 @@ module ActiveStorage
98
99
  probe["streams"] || []
99
100
  end
100
101
 
102
+ def container
103
+ probe["format"] || {}
104
+ end
105
+
101
106
  def probe
102
- download_blob_to_tempfile { |file| probe_from(file) }
107
+ @probe ||= download_blob_to_tempfile { |file| probe_from(file) }
103
108
  end
104
109
 
105
110
  def probe_from(file)
106
- 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|
107
118
  JSON.parse(output.read)
108
119
  end
109
120
  rescue Errno::ENOENT
@@ -12,6 +12,12 @@ module ActiveStorage
12
12
  false
13
13
  end
14
14
 
15
+ # Implement this method in concrete subclasses. It will determine if blob analysis
16
+ # should be done in a job or performed inline. By default, analysis is enqueued in a job.
17
+ def self.analyze_later?
18
+ true
19
+ end
20
+
15
21
  def initialize(blob)
16
22
  @blob = blob
17
23
  end
@@ -6,6 +6,7 @@ module ActiveStorage
6
6
 
7
7
  def initialize(name, record, attachables)
8
8
  @name, @record, @attachables = name, record, Array(attachables)
9
+ blobs.each(&:identify_without_saving)
9
10
  end
10
11
 
11
12
  def attachments
@@ -9,6 +9,7 @@ module ActiveStorage
9
9
 
10
10
  def initialize(name, record, attachable)
11
11
  @name, @record, @attachable = name, record, attachable
12
+ blob.identify_without_saving
12
13
  end
13
14
 
14
15
  def attachment
@@ -53,17 +54,29 @@ module ActiveStorage
53
54
  when ActiveStorage::Blob
54
55
  attachable
55
56
  when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
56
- ActiveStorage::Blob.build_after_unfurling \
57
+ ActiveStorage::Blob.build_after_unfurling(
57
58
  io: attachable.open,
58
59
  filename: attachable.original_filename,
59
- content_type: attachable.content_type
60
+ content_type: attachable.content_type,
61
+ record: record,
62
+ service_name: attachment_service_name
63
+ )
60
64
  when Hash
61
- ActiveStorage::Blob.build_after_unfurling(**attachable.symbolize_keys)
65
+ ActiveStorage::Blob.build_after_unfurling(
66
+ **attachable.reverse_merge(
67
+ record: record,
68
+ service_name: attachment_service_name
69
+ ).symbolize_keys
70
+ )
62
71
  when String
63
- ActiveStorage::Blob.find_signed(attachable)
72
+ ActiveStorage::Blob.find_signed!(attachable, record: record)
64
73
  else
65
74
  raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
66
75
  end
67
76
  end
77
+
78
+ def attachment_service_name
79
+ record.attachment_reflections[name].options[:service_name]
80
+ end
68
81
  end
69
82
  end
@@ -29,15 +29,16 @@ module ActiveStorage
29
29
  # document.images.attach([ first_blob, second_blob ])
30
30
  def attach(*attachables)
31
31
  if record.persisted? && !record.changed?
32
- record.update(name => blobs + attachables.flatten)
32
+ record.public_send("#{name}=", blobs + attachables.flatten)
33
+ record.save
33
34
  else
34
35
  record.public_send("#{name}=", (change&.attachables || blobs) + attachables.flatten)
35
36
  end
36
37
  end
37
38
 
38
- # Returns true if any attachments has been made.
39
+ # Returns true if any attachments have been made.
39
40
  #
40
- # class Gallery < ActiveRecord::Base
41
+ # class Gallery < ApplicationRecord
41
42
  # has_many_attached :photos
42
43
  # end
43
44
  #