activestorage 5.2.4.4 → 6.1.1

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +180 -69
  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/proxy_controller.rb +19 -0
  12. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +3 -3
  13. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  14. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  15. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  16. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  17. data/app/javascript/activestorage/blob_record.js +7 -2
  18. data/app/jobs/active_storage/analyze_job.rb +5 -0
  19. data/app/jobs/active_storage/base_job.rb +0 -1
  20. data/app/jobs/active_storage/mirror_job.rb +15 -0
  21. data/app/jobs/active_storage/purge_job.rb +3 -0
  22. data/app/models/active_storage/attachment.rb +35 -16
  23. data/app/models/active_storage/blob.rb +178 -68
  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/filename.rb +0 -6
  28. data/app/models/active_storage/preview.rb +37 -12
  29. data/app/models/active_storage/record.rb +7 -0
  30. data/app/models/active_storage/variant.rb +53 -67
  31. data/app/models/active_storage/variant_record.rb +8 -0
  32. data/app/models/active_storage/variant_with_record.rb +54 -0
  33. data/app/models/active_storage/variation.rb +30 -34
  34. data/config/routes.rb +66 -15
  35. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  36. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  37. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  38. data/lib/active_storage.rb +29 -6
  39. data/lib/active_storage/analyzer.rb +15 -4
  40. data/lib/active_storage/analyzer/image_analyzer.rb +14 -4
  41. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  42. data/lib/active_storage/analyzer/video_analyzer.rb +17 -8
  43. data/lib/active_storage/attached.rb +7 -22
  44. data/lib/active_storage/attached/changes.rb +16 -0
  45. data/lib/active_storage/attached/changes/create_many.rb +47 -0
  46. data/lib/active_storage/attached/changes/create_one.rb +82 -0
  47. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  48. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  49. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  50. data/lib/active_storage/attached/many.rb +19 -12
  51. data/lib/active_storage/attached/model.rb +212 -0
  52. data/lib/active_storage/attached/one.rb +19 -21
  53. data/lib/active_storage/downloader.rb +43 -0
  54. data/lib/active_storage/engine.rb +58 -23
  55. data/lib/active_storage/errors.rb +22 -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.rb +24 -13
  59. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  60. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +5 -5
  61. data/lib/active_storage/previewer/video_previewer.rb +17 -10
  62. data/lib/active_storage/reflection.rb +64 -0
  63. data/lib/active_storage/service.rb +44 -12
  64. data/lib/active_storage/service/azure_storage_service.rb +65 -44
  65. data/lib/active_storage/service/configurator.rb +6 -2
  66. data/lib/active_storage/service/disk_service.rb +57 -44
  67. data/lib/active_storage/service/gcs_service.rb +68 -64
  68. data/lib/active_storage/service/mirror_service.rb +31 -7
  69. data/lib/active_storage/service/registry.rb +32 -0
  70. data/lib/active_storage/service/s3_service.rb +58 -24
  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/tasks/activestorage.rake +7 -0
  74. metadata +84 -19
  75. data/app/models/active_storage/filename/parameters.rb +0 -36
  76. data/lib/active_storage/attached/macros.rb +0 -110
  77. data/lib/active_storage/downloading.rb +0 -39
@@ -29,12 +29,16 @@ module ActiveStorage::Blob::Analyzable
29
29
  update! metadata: metadata.merge(extract_metadata_via_analyzer)
30
30
  end
31
31
 
32
- # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
32
+ # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze, or calls #analyze inline based on analyzer class configuration.
33
33
  #
34
34
  # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
35
35
  # again (e.g. if you add a new analyzer or modify an existing one).
36
36
  def analyze_later
37
- ActiveStorage::AnalyzeJob.perform_later(self)
37
+ if analyzer_class.analyze_later?
38
+ ActiveStorage::AnalyzeJob.perform_later(self)
39
+ else
40
+ analyze
41
+ end
38
42
  end
39
43
 
40
44
  # Returns true if the blob has been analyzed.
@@ -2,9 +2,14 @@
2
2
 
3
3
  module ActiveStorage::Blob::Identifiable
4
4
  def identify
5
+ identify_without_saving
6
+ save!
7
+ end
8
+
9
+ def identify_without_saving
5
10
  unless identified?
6
- update! content_type: identify_content_type, identified: true
7
- update_service_metadata
11
+ self.content_type = identify_content_type
12
+ self.identified = true
8
13
  end
9
14
  end
10
15
 
@@ -24,8 +29,4 @@ module ActiveStorage::Blob::Identifiable
24
29
  ""
25
30
  end
26
31
  end
27
-
28
- def update_service_metadata
29
- service.update_metadata key, service_metadata if service_metadata.any?
30
- end
31
32
  end
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "mimemagic"
4
+
3
5
  module ActiveStorage::Blob::Representable
4
6
  extend ActiveSupport::Concern
5
7
 
6
8
  included do
9
+ has_many :variant_records, class_name: "ActiveStorage::VariantRecord", dependent: false
10
+ before_destroy { variant_records.destroy_all if ActiveStorage.track_variants }
11
+
7
12
  has_one_attached :preview_image
8
13
  end
9
14
 
10
15
  # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
11
16
  # files, and it allows any image to be transformed for size, colors, and the like. Example:
12
17
  #
13
- # avatar.variant(resize: "100x100").processed.service_url
18
+ # avatar.variant(resize_to_limit: [100, 100]).processed.url
14
19
  #
15
20
  # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
16
21
  # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
@@ -18,7 +23,7 @@ module ActiveStorage::Blob::Representable
18
23
  # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
19
24
  # specific variant that can be created by a controller on-demand. Like so:
20
25
  #
21
- # <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
26
+ # <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
22
27
  #
23
28
  # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
24
29
  # can then produce on-demand.
@@ -27,7 +32,7 @@ module ActiveStorage::Blob::Representable
27
32
  # variable, call ActiveStorage::Blob#variable?.
28
33
  def variant(transformations)
29
34
  if variable?
30
- ActiveStorage::Variant.new(self, transformations)
35
+ variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
31
36
  else
32
37
  raise ActiveStorage::InvariableError
33
38
  end
@@ -43,13 +48,13 @@ module ActiveStorage::Blob::Representable
43
48
  # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
44
49
  # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
45
50
  #
46
- # blob.preview(resize: "100x100").processed.service_url
51
+ # blob.preview(resize_to_limit: [100, 100]).processed.url
47
52
  #
48
53
  # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
49
54
  # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
50
55
  # how to use the built-in version:
51
56
  #
52
- # <%= image_tag video.preview(resize: "100x100") %>
57
+ # <%= image_tag video.preview(resize_to_limit: [100, 100]) %>
53
58
  #
54
59
  # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
55
60
  # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
@@ -69,7 +74,7 @@ module ActiveStorage::Blob::Representable
69
74
 
70
75
  # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
71
76
  #
72
- # blob.representation(resize: "100x100").processed.service_url
77
+ # blob.representation(resize_to_limit: [100, 100]).processed.url
73
78
  #
74
79
  # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
75
80
  # ActiveStorage::Blob#representable? to determine whether a blob is representable.
@@ -90,4 +95,29 @@ module ActiveStorage::Blob::Representable
90
95
  def representable?
91
96
  variable? || previewable?
92
97
  end
98
+
99
+ private
100
+ def default_variant_transformations
101
+ { format: default_variant_format }
102
+ end
103
+
104
+ def default_variant_format
105
+ if web_image?
106
+ format || :png
107
+ else
108
+ :png
109
+ end
110
+ end
111
+
112
+ def format
113
+ if filename.extension.present? && MimeMagic.by_extension(filename.extension)&.to_s == content_type
114
+ filename.extension
115
+ else
116
+ MimeMagic.new(content_type).extensions.first
117
+ end
118
+ end
119
+
120
+ def variant_class
121
+ ActiveStorage.track_variants ? ActiveStorage::VariantWithRecord : ActiveStorage::Variant
122
+ end
93
123
  end
@@ -3,8 +3,6 @@
3
3
  # Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
4
4
  # A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
5
5
  class ActiveStorage::Filename
6
- require_dependency "active_storage/filename/parameters"
7
-
8
6
  include Comparable
9
7
 
10
8
  class << self
@@ -60,10 +58,6 @@ class ActiveStorage::Filename
60
58
  @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
61
59
  end
62
60
 
63
- def parameters #:nodoc:
64
- Parameters.new self
65
- end
66
-
67
61
  # Returns the sanitized version of the filename.
68
62
  def to_s
69
63
  sanitized.to_s
@@ -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,17 +14,17 @@
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
  #
24
25
  # The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
25
- # {ffmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
26
- # and the other requires {mupdf}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or mupdf.
26
+ # {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
27
+ # and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
27
28
  #
28
29
  # These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
29
30
  # install and use third-party software, make sure you understand the licensing implications of doing so.
@@ -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: "100x100").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,11 +92,15 @@ class ActiveStorage::Preview
71
92
  end
72
93
 
73
94
  def process
74
- previewer.preview { |attachable| image.attach(attachable) }
95
+ previewer.preview(service_name: blob.service_name) do |attachable|
96
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
97
+ image.attach(attachable)
98
+ end
99
+ end
75
100
  end
76
101
 
77
102
  def variant
78
- ActiveStorage::Variant.new(image, variation).processed
103
+ image.variant(variation).processed
79
104
  end
80
105
 
81
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,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}"
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}",
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