activestorage 7.0.8.7 → 7.2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -390
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +6 -6
  5. data/app/assets/javascripts/activestorage.esm.js +11 -7
  6. data/app/assets/javascripts/activestorage.js +12 -6
  7. data/app/controllers/active_storage/disk_controller.rb +4 -2
  8. data/app/controllers/active_storage/representations/proxy_controller.rb +1 -1
  9. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  10. data/app/javascript/activestorage/blob_record.js +4 -1
  11. data/app/javascript/activestorage/direct_upload.js +3 -2
  12. data/app/javascript/activestorage/index.js +3 -1
  13. data/app/javascript/activestorage/ujs.js +3 -3
  14. data/app/jobs/active_storage/analyze_job.rb +1 -1
  15. data/app/jobs/active_storage/mirror_job.rb +1 -1
  16. data/app/jobs/active_storage/preview_image_job.rb +16 -0
  17. data/app/jobs/active_storage/purge_job.rb +1 -1
  18. data/app/jobs/active_storage/transform_job.rb +12 -0
  19. data/app/models/active_storage/attachment.rb +101 -16
  20. data/app/models/active_storage/blob/analyzable.rb +4 -3
  21. data/app/models/active_storage/blob/identifiable.rb +1 -0
  22. data/app/models/active_storage/blob/representable.rb +15 -3
  23. data/app/models/active_storage/blob/servable.rb +22 -0
  24. data/app/models/active_storage/blob.rb +59 -72
  25. data/app/models/active_storage/current.rb +0 -10
  26. data/app/models/active_storage/filename.rb +2 -4
  27. data/app/models/active_storage/named_variant.rb +21 -0
  28. data/app/models/active_storage/preview.rb +23 -8
  29. data/app/models/active_storage/variant.rb +10 -7
  30. data/app/models/active_storage/variant_record.rb +0 -2
  31. data/app/models/active_storage/variant_with_record.rb +21 -7
  32. data/app/models/active_storage/variation.rb +5 -3
  33. data/config/routes.rb +6 -4
  34. data/db/migrate/20170806125915_create_active_storage_tables.rb +2 -2
  35. data/lib/active_storage/analyzer/audio_analyzer.rb +16 -4
  36. data/lib/active_storage/analyzer/image_analyzer/vips.rb +5 -9
  37. data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
  38. data/lib/active_storage/analyzer/video_analyzer.rb +9 -3
  39. data/lib/active_storage/analyzer.rb +2 -0
  40. data/lib/active_storage/attached/changes/create_many.rb +8 -3
  41. data/lib/active_storage/attached/changes/create_one.rb +51 -4
  42. data/lib/active_storage/attached/changes/create_one_of_many.rb +5 -1
  43. data/lib/active_storage/attached/many.rb +5 -4
  44. data/lib/active_storage/attached/model.rb +96 -60
  45. data/lib/active_storage/attached/one.rb +5 -4
  46. data/lib/active_storage/attached.rb +2 -0
  47. data/lib/active_storage/deprecator.rb +7 -0
  48. data/lib/active_storage/engine.rb +7 -9
  49. data/lib/active_storage/fixture_set.rb +7 -1
  50. data/lib/active_storage/gem_version.rb +4 -4
  51. data/lib/active_storage/log_subscriber.rb +12 -0
  52. data/lib/active_storage/previewer/mupdf_previewer.rb +6 -2
  53. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +6 -2
  54. data/lib/active_storage/previewer/video_previewer.rb +1 -1
  55. data/lib/active_storage/previewer.rb +8 -1
  56. data/lib/active_storage/reflection.rb +3 -3
  57. data/lib/active_storage/service/azure_storage_service.rb +2 -0
  58. data/lib/active_storage/service/disk_service.rb +2 -0
  59. data/lib/active_storage/service/gcs_service.rb +11 -20
  60. data/lib/active_storage/service/mirror_service.rb +10 -5
  61. data/lib/active_storage/service/s3_service.rb +2 -0
  62. data/lib/active_storage/service.rb +4 -2
  63. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  64. data/lib/active_storage/transformers/transformer.rb +2 -0
  65. data/lib/active_storage/version.rb +1 -1
  66. data/lib/active_storage.rb +5 -4
  67. metadata +18 -27
@@ -2,14 +2,4 @@
2
2
 
3
3
  class ActiveStorage::Current < ActiveSupport::CurrentAttributes # :nodoc:
4
4
  attribute :url_options
5
-
6
- def host=(host)
7
- ActiveSupport::Deprecation.warn("ActiveStorage::Current.host= is deprecated, instead use ActiveStorage::Current.url_options=")
8
- self.url_options = { host: host }
9
- end
10
-
11
- def host
12
- ActiveSupport::Deprecation.warn("ActiveStorage::Current.host is deprecated, instead use ActiveStorage::Current.url_options")
13
- self.url_options&.dig(:host)
14
- end
15
5
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Filename
4
+ #
3
5
  # Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
4
6
  # A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
5
7
  class ActiveStorage::Filename
@@ -67,10 +69,6 @@ class ActiveStorage::Filename
67
69
  to_s
68
70
  end
69
71
 
70
- def to_json
71
- to_s
72
- end
73
-
74
72
  def <=>(other)
75
73
  to_s.downcase <=> other.to_s.downcase
76
74
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::NamedVariant # :nodoc:
4
+ attr_reader :transformations, :preprocessed
5
+
6
+ def initialize(transformations)
7
+ @preprocessed = transformations[:preprocessed]
8
+ @transformations = transformations.except(:preprocessed)
9
+ end
10
+
11
+ def preprocessed?(record)
12
+ case preprocessed
13
+ when Symbol
14
+ record.send(preprocessed)
15
+ when Proc
16
+ preprocessed.call(record)
17
+ else
18
+ preprocessed
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Preview
4
+ #
3
5
  # Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
4
6
  # extracting its first frame, and a PDF blob can be previewed by extracting its first page.
5
7
  #
@@ -10,7 +12,7 @@
10
12
  # documentation for more details on what's required of previewers.
11
13
  #
12
14
  # To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the
13
- # first previewer for which +accept?+ returns true when given the blob. In a Rails application, add or remove previewers
15
+ # first previewer for which +accept?+ returns true when given the blob. In a \Rails application, add or remove previewers
14
16
  # by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
15
17
  #
16
18
  # Rails.application.config.active_storage.previewers
@@ -20,24 +22,28 @@
20
22
  # Rails.application.config.active_storage.previewers << DOCXPreviewer
21
23
  # # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
22
24
  #
23
- # Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
25
+ # Outside of a \Rails application, modify +ActiveStorage.previewers+ instead.
24
26
  #
25
27
  # The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
26
28
  # {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
27
29
  # and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
28
30
  #
29
- # These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
31
+ # These libraries are not provided by \Rails. You must install them yourself to use the built-in previewers. Before you
30
32
  # install and use third-party software, make sure you understand the licensing implications of doing so.
31
33
  class ActiveStorage::Preview
34
+ include ActiveStorage::Blob::Servable
35
+
32
36
  class UnprocessedError < StandardError; end
33
37
 
38
+ delegate :filename, :content_type, to: :presentation
39
+
34
40
  attr_reader :blob, :variation
35
41
 
36
42
  def initialize(blob, variation_or_variation_key)
37
43
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
38
44
  end
39
45
 
40
- # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
46
+ # Processes the preview if it has not been processed yet. Returns the receiving +ActiveStorage::Preview+ instance for convenience:
41
47
  #
42
48
  # blob.preview(resize_to_limit: [100, 100]).processed.url
43
49
  #
@@ -45,6 +51,7 @@ class ActiveStorage::Preview
45
51
  # image is stored with the blob, it is only generated once.
46
52
  def processed
47
53
  process unless processed?
54
+ variant.processed if variant?
48
55
  self
49
56
  end
50
57
 
@@ -60,7 +67,7 @@ class ActiveStorage::Preview
60
67
  # a stable URL that redirects to the URL returned by this method.
61
68
  def url(**options)
62
69
  if processed?
63
- variant.url(**options)
70
+ presentation.url(**options)
64
71
  else
65
72
  raise UnprocessedError
66
73
  end
@@ -69,7 +76,7 @@ class ActiveStorage::Preview
69
76
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
70
77
  def key
71
78
  if processed?
72
- variant.key
79
+ presentation.key
73
80
  else
74
81
  raise UnprocessedError
75
82
  end
@@ -82,7 +89,7 @@ class ActiveStorage::Preview
82
89
  # if the preview has not been processed yet.
83
90
  def download(&block)
84
91
  if processed?
85
- variant.download(&block)
92
+ presentation.download(&block)
86
93
  else
87
94
  raise UnprocessedError
88
95
  end
@@ -102,7 +109,15 @@ class ActiveStorage::Preview
102
109
  end
103
110
 
104
111
  def variant
105
- image.variant(variation).processed
112
+ image.variant(variation)
113
+ end
114
+
115
+ def variant?
116
+ variation.transformations.present?
117
+ end
118
+
119
+ def presentation
120
+ variant? ? variant.processed : image
106
121
  end
107
122
 
108
123
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Variant
4
+ #
3
5
  # Image blobs can have variants that are the result of a set of transformations applied to the original.
4
6
  # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
5
7
  # original.
@@ -51,6 +53,8 @@
51
53
  # * {ImageProcessing::Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md#methods]
52
54
  # * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
53
55
  class ActiveStorage::Variant
56
+ include ActiveStorage::Blob::Servable
57
+
54
58
  attr_reader :blob, :variation
55
59
  delegate :service, to: :blob
56
60
  delegate :content_type, to: :variation
@@ -72,7 +76,7 @@ class ActiveStorage::Variant
72
76
 
73
77
  # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
74
78
  #
75
- # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
79
+ # Use <tt>url_for(variant)</tt> (or the implied form, like <tt>link_to variant</tt> or <tt>redirect_to variant</tt>) to get the stable URL
76
80
  # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
77
81
  # for its redirection.
78
82
  def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
@@ -89,17 +93,16 @@ class ActiveStorage::Variant
89
93
  ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
90
94
  end
91
95
 
92
- alias_method :content_type_for_serving, :content_type
93
-
94
- def forced_disposition_for_serving # :nodoc:
95
- nil
96
- end
97
-
98
96
  # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
99
97
  def image
100
98
  self
101
99
  end
102
100
 
101
+ # Deletes variant file from service.
102
+ def destroy
103
+ service.delete(key)
104
+ end
105
+
103
106
  private
104
107
  def processed?
105
108
  service.exist?(key)
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ActiveStorage::VariantRecord < ActiveStorage::Record
4
- self.table_name = "active_storage_variant_records"
5
-
6
4
  belongs_to :blob
7
5
  has_one_attached :image
8
6
  end
@@ -1,35 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Variant With Record
4
+ #
3
5
  # Like an ActiveStorage::Variant, but keeps detail about the variant in the database as an
4
6
  # ActiveStorage::VariantRecord. This is only used if +ActiveStorage.track_variants+ is enabled.
5
7
  class ActiveStorage::VariantWithRecord
8
+ include ActiveStorage::Blob::Servable
9
+
6
10
  attr_reader :blob, :variation
7
11
  delegate :service, to: :blob
12
+ delegate :content_type, to: :variation
8
13
 
9
14
  def initialize(blob, variation)
10
15
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
11
16
  end
12
17
 
13
18
  def processed
14
- process
19
+ process unless processed?
15
20
  self
16
21
  end
17
22
 
18
- def process
19
- transform_blob { |image| create_or_find_record(image: image) } unless processed?
23
+ def image
24
+ record&.image
20
25
  end
21
26
 
22
- def processed?
23
- record.present?
27
+ def filename
28
+ ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
24
29
  end
25
30
 
26
- def image
27
- record&.image
31
+ # Destroys record and deletes file from service.
32
+ def destroy
33
+ record&.destroy
28
34
  end
29
35
 
30
36
  delegate :key, :url, :download, to: :image, allow_nil: true
31
37
 
32
38
  private
39
+ def processed?
40
+ record.present?
41
+ end
42
+
43
+ def process
44
+ transform_blob { |image| create_or_find_record(image: image) }
45
+ end
46
+
33
47
  def transform_blob
34
48
  blob.open do |input|
35
49
  variation.transform(input) do |output|
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mini_mime"
3
+ require "marcel"
4
4
 
5
+ # = Active Storage \Variation
6
+ #
5
7
  # A set of transformations that can be applied to a blob to create a variant. This class is exposed via
6
8
  # the ActiveStorage::Blob#variant method and should rarely be used directly.
7
9
  #
@@ -59,14 +61,14 @@ class ActiveStorage::Variation
59
61
 
60
62
  def format
61
63
  transformations.fetch(:format, :png).tap do |format|
62
- if MiniMime.lookup_by_extension(format.to_s).nil?
64
+ if Marcel::Magic.by_extension(format.to_s).nil?
63
65
  raise ArgumentError, "Invalid variant format (#{format.inspect})"
64
66
  end
65
67
  end
66
68
  end
67
69
 
68
70
  def content_type
69
- MiniMime.lookup_by_extension(format.to_s).content_type
71
+ Marcel::MimeType.for(extension: format.to_s)
70
72
  end
71
73
 
72
74
  # Returns a signed key for all the +transformations+ that this variation was instantiated with.
data/config/routes.rb CHANGED
@@ -32,16 +32,17 @@ Rails.application.routes.draw do
32
32
 
33
33
  direct :rails_storage_proxy do |model, options|
34
34
  expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
35
+ expires_at = options.delete(:expires_at)
35
36
 
36
37
  if model.respond_to?(:signed_id)
37
38
  route_for(
38
39
  :rails_service_blob_proxy,
39
- model.signed_id(expires_in: expires_in),
40
+ model.signed_id(expires_in: expires_in, expires_at: expires_at),
40
41
  model.filename,
41
42
  options
42
43
  )
43
44
  else
44
- signed_blob_id = model.blob.signed_id(expires_in: expires_in)
45
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in, expires_at: expires_at)
45
46
  variation_key = model.variation.key
46
47
  filename = model.blob.filename
47
48
 
@@ -57,16 +58,17 @@ Rails.application.routes.draw do
57
58
 
58
59
  direct :rails_storage_redirect do |model, options|
59
60
  expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
61
+ expires_at = options.delete(:expires_at)
60
62
 
61
63
  if model.respond_to?(:signed_id)
62
64
  route_for(
63
65
  :rails_service_blob,
64
- model.signed_id(expires_in: expires_in),
66
+ model.signed_id(expires_in: expires_in, expires_at: expires_at),
65
67
  model.filename,
66
68
  options
67
69
  )
68
70
  else
69
- signed_blob_id = model.blob.signed_id(expires_in: expires_in)
71
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in, expires_at: expires_at)
70
72
  variation_key = model.variation.key
71
73
  filename = model.blob.filename
72
74
 
@@ -1,4 +1,4 @@
1
- class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
1
+ class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  # Use Active Record's configured type for primary and foreign keys
4
4
  primary_key_type, foreign_key_type = primary_and_foreign_key_types
@@ -51,6 +51,6 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
51
51
  setting = config.options[config.orm][:primary_key_type]
52
52
  primary_key_type = setting || :primary_key
53
53
  foreign_key_type = setting || :bigint
54
- [primary_key_type, foreign_key_type]
54
+ [ primary_key_type, foreign_key_type ]
55
55
  end
56
56
  end
@@ -1,21 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- # Extracts duration (seconds) and bit_rate (bits/s) from an audio blob.
4
+ # = Active Storage Audio \Analyzer
5
+ #
6
+ # Extracts duration (seconds), bit_rate (bits/s), sample_rate (hertz) and tags (internal metadata) from an audio blob.
5
7
  #
6
8
  # Example:
7
9
  #
8
10
  # ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata
9
- # # => { duration: 5.0, bit_rate: 320340 }
11
+ # # => { duration: 5.0, bit_rate: 320340, sample_rate: 44100, tags: { encoder: "Lavc57.64", ... } }
10
12
  #
11
- # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
13
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails.
12
14
  class Analyzer::AudioAnalyzer < Analyzer
13
15
  def self.accept?(blob)
14
16
  blob.audio?
15
17
  end
16
18
 
17
19
  def metadata
18
- { duration: duration, bit_rate: bit_rate }.compact
20
+ { duration: duration, bit_rate: bit_rate, sample_rate: sample_rate, tags: tags }.compact
19
21
  end
20
22
 
21
23
  private
@@ -29,6 +31,16 @@ module ActiveStorage
29
31
  Integer(bit_rate) if bit_rate
30
32
  end
31
33
 
34
+ def sample_rate
35
+ sample_rate = audio_stream["sample_rate"]
36
+ Integer(sample_rate) if sample_rate
37
+ end
38
+
39
+ def tags
40
+ tags = audio_stream["tags"]
41
+ Hash(tags) if tags
42
+ end
43
+
32
44
  def audio_stream
33
45
  @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
34
46
  end
@@ -19,13 +19,16 @@ module ActiveStorage
19
19
 
20
20
  download_blob_to_tempfile do |file|
21
21
  image = instrument("vips") do
22
+ # ruby-vips will raise Vips::Error if it can't find an appropriate loader for the file
22
23
  ::Vips::Image.new_from_file(file.path, access: :sequential)
24
+ rescue ::Vips::Error
25
+ logger.info "Skipping image analysis because Vips doesn't support the file"
26
+ nil
23
27
  end
24
28
 
25
- if valid_image?(image)
29
+ if image
26
30
  yield image
27
31
  else
28
- logger.info "Skipping image analysis because Vips doesn't support the file"
29
32
  {}
30
33
  end
31
34
  rescue ::Vips::Error => error
@@ -40,12 +43,5 @@ module ActiveStorage
40
43
  rescue ::Vips::Error
41
44
  false
42
45
  end
43
-
44
- def valid_image?(image)
45
- image.avg
46
- true
47
- rescue ::Vips::Error
48
- false
49
- end
50
46
  end
51
47
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
+ # = Active Storage Image \Analyzer
5
+ #
4
6
  # This is an abstract base class for image analyzers, which extract width and height from an image blob.
5
7
  #
6
8
  # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
+ # = Active Storage Video \Analyzer
5
+ #
4
6
  # Extracts the following from a video blob:
5
7
  #
6
8
  # * Width (pixels)
@@ -18,7 +20,7 @@ module ActiveStorage
18
20
  #
19
21
  # When a video's angle is 90, -90, 270 or -270 degrees, its width and height are automatically swapped for convenience.
20
22
  #
21
- # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
23
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails.
22
24
  class Analyzer::VideoAnalyzer < Analyzer
23
25
  def self.accept?(blob)
24
26
  blob.video?
@@ -53,11 +55,15 @@ module ActiveStorage
53
55
  def angle
54
56
  if tags["rotate"]
55
57
  Integer(tags["rotate"])
56
- elsif side_data && side_data[0] && side_data[0]["rotation"]
57
- Integer(side_data[0]["rotation"])
58
+ elsif display_matrix && display_matrix["rotation"]
59
+ Integer(display_matrix["rotation"])
58
60
  end
59
61
  end
60
62
 
63
+ def display_matrix
64
+ side_data.detect { |data| data["side_data_type"] == "Display Matrix" }
65
+ end
66
+
61
67
  def display_aspect_ratio
62
68
  if descriptor = video_stream["display_aspect_ratio"]
63
69
  if terms = descriptor.split(":", 2)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
+ # = Active Storage \Analyzer
5
+ #
4
6
  # This is an abstract base class for analyzers, which extract metadata from blobs. See
5
7
  # ActiveStorage::Analyzer::VideoAnalyzer for an example of a concrete subclass.
6
8
  class Analyzer
@@ -2,11 +2,12 @@
2
2
 
3
3
  module ActiveStorage
4
4
  class Attached::Changes::CreateMany # :nodoc:
5
- attr_reader :name, :record, :attachables
5
+ attr_reader :name, :record, :attachables, :pending_uploads
6
6
 
7
- def initialize(name, record, attachables)
7
+ def initialize(name, record, attachables, pending_uploads: [])
8
8
  @name, @record, @attachables = name, record, Array(attachables)
9
9
  blobs.each(&:identify_without_saving)
10
+ @pending_uploads = Array(pending_uploads) + subchanges_without_blobs
10
11
  attachments
11
12
  end
12
13
 
@@ -19,7 +20,7 @@ module ActiveStorage
19
20
  end
20
21
 
21
22
  def upload
22
- subchanges.each(&:upload)
23
+ pending_uploads.each(&:upload)
23
24
  end
24
25
 
25
26
  def save
@@ -36,6 +37,10 @@ module ActiveStorage
36
37
  ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable)
37
38
  end
38
39
 
40
+ def subchanges_without_blobs
41
+ subchanges.reject { |subchange| subchange.attachable.is_a?(ActiveStorage::Blob) }
42
+ end
43
+
39
44
  def assign_associated_attachments
40
45
  record.public_send("#{name}_attachments=", persisted_or_new_attachments)
41
46
  end
@@ -22,10 +22,26 @@ module ActiveStorage
22
22
 
23
23
  def upload
24
24
  case attachable
25
- when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
25
+ when ActionDispatch::Http::UploadedFile
26
26
  blob.upload_without_unfurling(attachable.open)
27
+ when Rack::Test::UploadedFile
28
+ blob.upload_without_unfurling(
29
+ attachable.respond_to?(:open) ? attachable.open : attachable
30
+ )
27
31
  when Hash
28
32
  blob.upload_without_unfurling(attachable.fetch(:io))
33
+ when File
34
+ blob.upload_without_unfurling(attachable)
35
+ when Pathname
36
+ blob.upload_without_unfurling(attachable.open)
37
+ when ActiveStorage::Blob
38
+ when String
39
+ else
40
+ raise(
41
+ ArgumentError,
42
+ "Could not upload: expected attachable, " \
43
+ "got #{attachable.inspect}"
44
+ )
29
45
  end
30
46
  end
31
47
 
@@ -53,7 +69,7 @@ module ActiveStorage
53
69
  case attachable
54
70
  when ActiveStorage::Blob
55
71
  attachable
56
- when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
72
+ when ActionDispatch::Http::UploadedFile
57
73
  ActiveStorage::Blob.build_after_unfurling(
58
74
  io: attachable.open,
59
75
  filename: attachable.original_filename,
@@ -61,6 +77,14 @@ module ActiveStorage
61
77
  record: record,
62
78
  service_name: attachment_service_name
63
79
  )
80
+ when Rack::Test::UploadedFile
81
+ ActiveStorage::Blob.build_after_unfurling(
82
+ io: attachable.respond_to?(:open) ? attachable.open : attachable,
83
+ filename: attachable.original_filename,
84
+ content_type: attachable.content_type,
85
+ record: record,
86
+ service_name: attachment_service_name
87
+ )
64
88
  when Hash
65
89
  ActiveStorage::Blob.build_after_unfurling(
66
90
  **attachable.reverse_merge(
@@ -70,13 +94,36 @@ module ActiveStorage
70
94
  )
71
95
  when String
72
96
  ActiveStorage::Blob.find_signed!(attachable, record: record)
97
+ when File
98
+ ActiveStorage::Blob.build_after_unfurling(
99
+ io: attachable,
100
+ filename: File.basename(attachable),
101
+ record: record,
102
+ service_name: attachment_service_name
103
+ )
104
+ when Pathname
105
+ ActiveStorage::Blob.build_after_unfurling(
106
+ io: attachable.open,
107
+ filename: File.basename(attachable),
108
+ record: record,
109
+ service_name: attachment_service_name
110
+ )
73
111
  else
74
- raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
112
+ raise(
113
+ ArgumentError,
114
+ "Could not find or build blob: expected attachable, " \
115
+ "got #{attachable.inspect}"
116
+ )
75
117
  end
76
118
  end
77
119
 
78
120
  def attachment_service_name
79
- record.attachment_reflections[name].options[:service_name]
121
+ service_name = record.attachment_reflections[name].options[:service_name]
122
+ if service_name.is_a?(Proc)
123
+ service_name = service_name.call(record)
124
+ ActiveStorage::Blob.validate_service_configuration(service_name, record.class, name)
125
+ end
126
+ service_name
80
127
  end
81
128
  end
82
129
  end
@@ -4,7 +4,11 @@ module ActiveStorage
4
4
  class Attached::Changes::CreateOneOfMany < Attached::Changes::CreateOne # :nodoc:
5
5
  private
6
6
  def find_attachment
7
- record.public_send("#{name}_attachments").detect { |attachment| attachment.blob_id == blob.id }
7
+ if blob.persisted?
8
+ record.public_send("#{name}_attachments").detect { |attachment| attachment.blob_id == blob.id }
9
+ else
10
+ blob.attachments.find { |attachment| attachment.record == record }
11
+ end
8
12
  end
9
13
  end
10
14
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
+ # = Active Storage \Attached \Many
5
+ #
4
6
  # Decorated proxy object representing of multiple attachments to a model.
5
7
  class Attached::Many < Attached
6
8
  ##
@@ -47,12 +49,11 @@ module ActiveStorage
47
49
  # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpeg")
48
50
  # document.images.attach([ first_blob, second_blob ])
49
51
  def attach(*attachables)
52
+ record.public_send("#{name}=", blobs + attachables.flatten)
50
53
  if record.persisted? && !record.changed?
51
- record.public_send("#{name}=", blobs + attachables.flatten)
52
- record.save
53
- else
54
- record.public_send("#{name}=", (change&.attachables || blobs) + attachables.flatten)
54
+ return if !record.save
55
55
  end
56
+ record.public_send("#{name}")
56
57
  end
57
58
 
58
59
  # Returns true if any attachments have been made.