activestorage 6.1.7.2 → 7.0.0.alpha1

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -291
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +25 -11
  5. data/app/assets/javascripts/activestorage.esm.js +844 -0
  6. data/app/assets/javascripts/activestorage.js +257 -376
  7. data/app/controllers/active_storage/base_controller.rb +1 -10
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -4
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +6 -4
  12. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  13. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  14. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  15. data/app/controllers/concerns/active_storage/streaming.rb +65 -0
  16. data/app/javascript/activestorage/ujs.js +1 -1
  17. data/app/models/active_storage/attachment.rb +35 -2
  18. data/app/models/active_storage/blob/representable.rb +7 -5
  19. data/app/models/active_storage/blob.rb +26 -27
  20. data/app/models/active_storage/current.rb +12 -2
  21. data/app/models/active_storage/preview.rb +6 -4
  22. data/app/models/active_storage/record.rb +1 -1
  23. data/app/models/active_storage/variant.rb +6 -9
  24. data/app/models/active_storage/variant_record.rb +2 -0
  25. data/app/models/active_storage/variant_with_record.rb +9 -5
  26. data/app/models/active_storage/variation.rb +3 -3
  27. data/config/routes.rb +10 -10
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +11 -2
  29. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +0 -4
  30. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +1 -3
  31. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  32. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  33. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  34. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  35. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  36. data/lib/active_storage/analyzer.rb +8 -4
  37. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  38. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  39. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  40. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  41. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  42. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  43. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  44. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  45. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  46. data/lib/active_storage/attached/changes.rb +7 -1
  47. data/lib/active_storage/attached/many.rb +27 -15
  48. data/lib/active_storage/attached/model.rb +31 -5
  49. data/lib/active_storage/attached/one.rb +32 -27
  50. data/lib/active_storage/downloader.rb +2 -2
  51. data/lib/active_storage/engine.rb +28 -16
  52. data/lib/active_storage/fixture_set.rb +76 -0
  53. data/lib/active_storage/gem_version.rb +4 -4
  54. data/lib/active_storage/previewer/video_previewer.rb +0 -2
  55. data/lib/active_storage/previewer.rb +4 -4
  56. data/lib/active_storage/reflection.rb +12 -2
  57. data/lib/active_storage/service/azure_storage_service.rb +1 -1
  58. data/lib/active_storage/service/configurator.rb +1 -1
  59. data/lib/active_storage/service/disk_service.rb +13 -18
  60. data/lib/active_storage/service/gcs_service.rb +91 -7
  61. data/lib/active_storage/service/mirror_service.rb +1 -1
  62. data/lib/active_storage/service/registry.rb +1 -1
  63. data/lib/active_storage/service/s3_service.rb +4 -4
  64. data/lib/active_storage/service.rb +3 -3
  65. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -66
  66. data/lib/active_storage/transformers/transformer.rb +1 -1
  67. data/lib/active_storage.rb +3 -292
  68. metadata +32 -24
  69. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -8,9 +8,9 @@ require "mini_mime"
8
8
  # In case you do need to use this directly, it's instantiated using a hash of transformations where
9
9
  # the key is the command and the value is the arguments. Example:
10
10
  #
11
- # ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
11
+ # ActiveStorage::Variation.new(resize_to_limit: [100, 100], colourspace: "b-w", rotate: "-90", saver: { trim: true })
12
12
  #
13
- # The options map directly to {ImageProcessing}[https://github.com/janko/image_processing] commands.
13
+ # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
14
14
  class ActiveStorage::Variation
15
15
  attr_reader :transformations
16
16
 
@@ -75,7 +75,7 @@ class ActiveStorage::Variation
75
75
  end
76
76
 
77
77
  def digest
78
- Digest::SHA1.base64digest Marshal.dump(transformations)
78
+ OpenSSL::Digest::SHA1.base64digest Marshal.dump(transformations)
79
79
  end
80
80
 
81
81
  private
data/config/routes.rb CHANGED
@@ -16,11 +16,7 @@ Rails.application.routes.draw do
16
16
  end
17
17
 
18
18
  direct :rails_representation do |representation, options|
19
- signed_blob_id = representation.blob.signed_id
20
- variation_key = representation.variation.key
21
- filename = representation.blob.filename
22
-
23
- route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
19
+ route_for(ActiveStorage.resolve_model_to_route, representation, options)
24
20
  end
25
21
 
26
22
  resolve("ActiveStorage::Variant") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
@@ -28,22 +24,24 @@ Rails.application.routes.draw do
28
24
  resolve("ActiveStorage::Preview") { |preview, options| route_for(ActiveStorage.resolve_model_to_route, preview, options) }
29
25
 
30
26
  direct :rails_blob do |blob, options|
31
- route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
27
+ route_for(ActiveStorage.resolve_model_to_route, blob, options)
32
28
  end
33
29
 
34
30
  resolve("ActiveStorage::Blob") { |blob, options| route_for(ActiveStorage.resolve_model_to_route, blob, options) }
35
31
  resolve("ActiveStorage::Attachment") { |attachment, options| route_for(ActiveStorage.resolve_model_to_route, attachment.blob, options) }
36
32
 
37
33
  direct :rails_storage_proxy do |model, options|
34
+ expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
35
+
38
36
  if model.respond_to?(:signed_id)
39
37
  route_for(
40
38
  :rails_service_blob_proxy,
41
- model.signed_id,
39
+ model.signed_id(expires_in: expires_in),
42
40
  model.filename,
43
41
  options
44
42
  )
45
43
  else
46
- signed_blob_id = model.blob.signed_id
44
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in)
47
45
  variation_key = model.variation.key
48
46
  filename = model.blob.filename
49
47
 
@@ -58,15 +56,17 @@ Rails.application.routes.draw do
58
56
  end
59
57
 
60
58
  direct :rails_storage_redirect do |model, options|
59
+ expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
60
+
61
61
  if model.respond_to?(:signed_id)
62
62
  route_for(
63
63
  :rails_service_blob,
64
- model.signed_id,
64
+ model.signed_id(expires_in: expires_in),
65
65
  model.filename,
66
66
  options
67
67
  )
68
68
  else
69
- signed_blob_id = model.blob.signed_id
69
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in)
70
70
  variation_key = model.variation.key
71
71
  filename = model.blob.filename
72
72
 
@@ -11,7 +11,12 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
11
11
  t.string :service_name, null: false
12
12
  t.bigint :byte_size, null: false
13
13
  t.string :checksum, null: false
14
- t.datetime :created_at, null: false
14
+
15
+ if connection.supports_datetime_with_precision?
16
+ t.datetime :created_at, precision: 6, null: false
17
+ else
18
+ t.datetime :created_at, null: false
19
+ end
15
20
 
16
21
  t.index [ :key ], unique: true
17
22
  end
@@ -21,7 +26,11 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
21
26
  t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
22
27
  t.references :blob, null: false, type: foreign_key_type
23
28
 
24
- t.datetime :created_at, null: false
29
+ if connection.supports_datetime_with_precision?
30
+ t.datetime :created_at, precision: 6, null: false
31
+ else
32
+ t.datetime :created_at, null: false
33
+ end
25
34
 
26
35
  t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
27
36
  t.foreign_key :active_storage_blobs, column: :blob_id
@@ -1,7 +1,5 @@
1
1
  class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
2
2
  def up
3
- return unless table_exists?(:active_storage_blobs)
4
-
5
3
  unless column_exists?(:active_storage_blobs, :service_name)
6
4
  add_column :active_storage_blobs, :service_name, :string
7
5
 
@@ -14,8 +12,6 @@ class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
14
12
  end
15
13
 
16
14
  def down
17
- return unless table_exists?(:active_storage_blobs)
18
-
19
15
  remove_column :active_storage_blobs, :service_name
20
16
  end
21
17
  end
@@ -1,9 +1,7 @@
1
1
  class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
2
2
  def change
3
- return unless table_exists?(:active_storage_blobs)
4
-
5
3
  # Use Active Record's configured type for primary key
6
- create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
4
+ create_table :active_storage_variant_records, id: primary_key_type do |t|
7
5
  t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
8
6
  t.string :variation_digest, null: false
9
7
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # Extracts duration (seconds) and bit_rate (bits/s) from an audio blob.
5
+ #
6
+ # Example:
7
+ #
8
+ # ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata
9
+ # # => { duration: 5.0, bit_rate: 320340 }
10
+ #
11
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
12
+ class Analyzer::AudioAnalyzer < Analyzer
13
+ def self.accept?(blob)
14
+ blob.audio?
15
+ end
16
+
17
+ def metadata
18
+ { duration: duration, bit_rate: bit_rate }.compact
19
+ end
20
+
21
+ private
22
+ def duration
23
+ duration = audio_stream["duration"]
24
+ Float(duration) if duration
25
+ end
26
+
27
+ def bit_rate
28
+ bit_rate = audio_stream["bit_rate"]
29
+ Integer(bit_rate) if bit_rate
30
+ end
31
+
32
+ def audio_stream
33
+ @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
34
+ end
35
+
36
+ def streams
37
+ probe["streams"] || []
38
+ end
39
+
40
+ def probe
41
+ @probe ||= download_blob_to_tempfile { |file| probe_from(file) }
42
+ end
43
+
44
+ def probe_from(file)
45
+ instrument(File.basename(ffprobe_path)) do
46
+ IO.popen([ ffprobe_path,
47
+ "-print_format", "json",
48
+ "-show_streams",
49
+ "-show_format",
50
+ "-v", "error",
51
+ file.path
52
+ ]) do |output|
53
+ JSON.parse(output.read)
54
+ end
55
+ end
56
+ rescue Errno::ENOENT
57
+ logger.info "Skipping audio analysis because FFmpeg isn't installed"
58
+ {}
59
+ end
60
+
61
+ def ffprobe_path
62
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
5
+ # the {ImageMagick}[http://www.imagemagick.org] system library.
6
+ class Analyzer::ImageAnalyzer::ImageMagick < Analyzer::ImageAnalyzer
7
+ def self.accept?(blob)
8
+ super && ActiveStorage.variant_processor == :mini_magick
9
+ end
10
+
11
+ private
12
+ def read_image
13
+ download_blob_to_tempfile do |file|
14
+ require "mini_magick"
15
+
16
+ image = instrument("mini_magick") do
17
+ MiniMagick::Image.new(file.path)
18
+ end
19
+
20
+ if image.valid?
21
+ yield image
22
+ else
23
+ logger.info "Skipping image analysis because ImageMagick doesn't support the file"
24
+ {}
25
+ end
26
+ end
27
+ rescue LoadError
28
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
29
+ {}
30
+ rescue MiniMagick::Error => error
31
+ logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
32
+ {}
33
+ end
34
+
35
+ def rotated_image?(image)
36
+ %w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"])
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires
5
+ # the {libvips}[https://libvips.github.io/libvips/] system library.
6
+ class Analyzer::ImageAnalyzer::Vips < Analyzer::ImageAnalyzer
7
+ def self.accept?(blob)
8
+ super && ActiveStorage.variant_processor == :vips
9
+ end
10
+
11
+ private
12
+ def read_image
13
+ download_blob_to_tempfile do |file|
14
+ require "ruby-vips"
15
+
16
+ image = instrument("vips") do
17
+ ::Vips::Image.new_from_file(file.path, access: :sequential)
18
+ end
19
+
20
+ if valid_image?(image)
21
+ yield image
22
+ else
23
+ logger.info "Skipping image analysis because Vips doesn't support the file"
24
+ {}
25
+ end
26
+ end
27
+ rescue LoadError
28
+ logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
29
+ {}
30
+ rescue ::Vips::Error => error
31
+ logger.error "Skipping image analysis due to an Vips error: #{error.message}"
32
+ {}
33
+ end
34
+
35
+ ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
36
+ def rotated_image?(image)
37
+ ROTATIONS === image.get("exif-ifd0-Orientation")
38
+ rescue ::Vips::Error
39
+ false
40
+ end
41
+
42
+ def valid_image?(image)
43
+ image.avg
44
+ true
45
+ rescue ::Vips::Error
46
+ false
47
+ end
48
+ end
49
+ end
@@ -1,17 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- # Extracts width and height in pixels from an image blob.
4
+ # This is an abstract base class for image analyzers, which extract width and height from an image blob.
5
5
  #
6
6
  # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
7
7
  #
8
8
  # Example:
9
9
  #
10
- # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
10
+ # ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(blob).metadata
11
11
  # # => { width: 4104, height: 2736 }
12
- #
13
- # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
14
- # the {ImageMagick}[http://www.imagemagick.org] system library.
15
12
  class Analyzer::ImageAnalyzer < Analyzer
16
13
  def self.accept?(blob)
17
14
  blob.image?
@@ -26,30 +23,5 @@ module ActiveStorage
26
23
  end
27
24
  end
28
25
  end
29
-
30
- private
31
- def read_image
32
- download_blob_to_tempfile do |file|
33
- require "mini_magick"
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
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
- {}
49
- end
50
-
51
- def rotated_image?(image)
52
- %w[ RightTop LeftBottom ].include?(image["%[orientation]"])
53
- end
54
26
  end
55
27
  end
@@ -8,11 +8,13 @@ module ActiveStorage
8
8
  # * Duration (seconds)
9
9
  # * Angle (degrees)
10
10
  # * Display aspect ratio
11
+ # * Audio (true if file has an audio channel, false if not)
12
+ # * Video (true if file has an video channel, false if not)
11
13
  #
12
14
  # Example:
13
15
  #
14
16
  # ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
15
- # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
17
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3], audio: true, video: true }
16
18
  #
17
19
  # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
18
20
  #
@@ -23,7 +25,7 @@ module ActiveStorage
23
25
  end
24
26
 
25
27
  def metadata
26
- { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
28
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio, audio: audio?, video: video? }.compact
27
29
  end
28
30
 
29
31
  private
@@ -63,11 +65,18 @@ module ActiveStorage
63
65
  end
64
66
  end
65
67
 
66
-
67
68
  def rotated?
68
69
  angle == 90 || angle == 270
69
70
  end
70
71
 
72
+ def audio?
73
+ audio_stream.present?
74
+ end
75
+
76
+ def video?
77
+ video_stream.present?
78
+ end
79
+
71
80
  def computed_height
72
81
  if encoded_width && display_height_scale
73
82
  encoded_width * display_height_scale
@@ -95,6 +104,10 @@ module ActiveStorage
95
104
  @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
96
105
  end
97
106
 
107
+ def audio_stream
108
+ @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
109
+ end
110
+
98
111
  def streams
99
112
  probe["streams"] || []
100
113
  end
@@ -108,14 +121,16 @@ module ActiveStorage
108
121
  end
109
122
 
110
123
  def probe_from(file)
111
- IO.popen([ ffprobe_path,
112
- "-print_format", "json",
113
- "-show_streams",
114
- "-show_format",
115
- "-v", "error",
116
- file.path
117
- ]) do |output|
118
- JSON.parse(output.read)
124
+ instrument(File.basename(ffprobe_path)) do
125
+ IO.popen([ ffprobe_path,
126
+ "-print_format", "json",
127
+ "-show_streams",
128
+ "-show_format",
129
+ "-v", "error",
130
+ file.path
131
+ ]) do |output|
132
+ JSON.parse(output.read)
133
+ end
119
134
  end
120
135
  rescue Errno::ENOENT
121
136
  logger.info "Skipping video analysis because FFmpeg isn't installed"
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ActiveStorage
4
4
  # This is an abstract base class for analyzers, which extract metadata from blobs. See
5
- # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
5
+ # ActiveStorage::Analyzer::VideoAnalyzer for an example of a concrete subclass.
6
6
  class Analyzer
7
7
  attr_reader :blob
8
8
 
@@ -29,16 +29,20 @@ module ActiveStorage
29
29
 
30
30
  private
31
31
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
32
- def download_blob_to_tempfile(&block) #:doc:
32
+ def download_blob_to_tempfile(&block) # :doc:
33
33
  blob.open tmpdir: tmpdir, &block
34
34
  end
35
35
 
36
- def logger #:doc:
36
+ def logger # :doc:
37
37
  ActiveStorage.logger
38
38
  end
39
39
 
40
- def tmpdir #:doc:
40
+ def tmpdir # :doc:
41
41
  Dir.tmpdir
42
42
  end
43
+
44
+ def instrument(analyzer, &block) # :doc:
45
+ ActiveSupport::Notifications.instrument("analyze.active_storage", analyzer: analyzer, &block)
46
+ end
43
47
  end
44
48
  end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Attached::Changes::CreateMany #:nodoc:
4
+ class Attached::Changes::CreateMany # :nodoc:
5
5
  attr_reader :name, :record, :attachables
6
6
 
7
7
  def initialize(name, record, attachables)
8
8
  @name, @record, @attachables = name, record, Array(attachables)
9
9
  blobs.each(&:identify_without_saving)
10
+ attachments
10
11
  end
11
12
 
12
13
  def attachments
@@ -35,13 +36,16 @@ module ActiveStorage
35
36
  ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable)
36
37
  end
37
38
 
38
-
39
39
  def assign_associated_attachments
40
- record.public_send("#{name}_attachments=", attachments)
40
+ record.public_send("#{name}_attachments=", persisted_or_new_attachments)
41
41
  end
42
42
 
43
43
  def reset_associated_blobs
44
44
  record.public_send("#{name}_blobs").reset
45
45
  end
46
+
47
+ def persisted_or_new_attachments
48
+ attachments.select { |attachment| attachment.persisted? || attachment.new_record? }
49
+ end
46
50
  end
47
51
  end
@@ -4,7 +4,7 @@ require "action_dispatch"
4
4
  require "action_dispatch/http/upload"
5
5
 
6
6
  module ActiveStorage
7
- class Attached::Changes::CreateOne #:nodoc:
7
+ class Attached::Changes::CreateOne # :nodoc:
8
8
  attr_reader :name, :record, :attachable
9
9
 
10
10
  def initialize(name, record, attachable)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Attached::Changes::CreateOneOfMany < Attached::Changes::CreateOne #:nodoc:
4
+ class Attached::Changes::CreateOneOfMany < Attached::Changes::CreateOne # :nodoc:
5
5
  private
6
6
  def find_attachment
7
7
  record.public_send("#{name}_attachments").detect { |attachment| attachment.blob_id == blob.id }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Attached::Changes::DeleteMany #:nodoc:
4
+ class Attached::Changes::DeleteMany # :nodoc:
5
5
  attr_reader :name, :record
6
6
 
7
7
  def initialize(name, record)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Attached::Changes::DeleteOne #:nodoc:
4
+ class Attached::Changes::DeleteOne # :nodoc:
5
5
  attr_reader :name, :record
6
6
 
7
7
  def initialize(name, record)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Attached::Changes::DetachMany # :nodoc:
5
+ attr_reader :name, :record, :attachments
6
+
7
+ def initialize(name, record, attachments)
8
+ @name, @record, @attachments = name, record, attachments
9
+ end
10
+
11
+ def detach
12
+ if attachments.any?
13
+ attachments.delete_all if attachments.respond_to?(:delete_all)
14
+ record.attachment_changes.delete(name)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Attached::Changes::DetachOne # :nodoc:
5
+ attr_reader :name, :record, :attachment
6
+
7
+ def initialize(name, record, attachment)
8
+ @name, @record, @attachment = name, record, attachment
9
+ end
10
+
11
+ def detach
12
+ if attachment.present?
13
+ attachment.delete
14
+ reset
15
+ end
16
+ end
17
+
18
+ private
19
+ def reset
20
+ record.attachment_changes.delete(name)
21
+ record.public_send("#{name}_attachment=", nil)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Attached::Changes::PurgeMany # :nodoc:
5
+ attr_reader :name, :record, :attachments
6
+
7
+ def initialize(name, record, attachments)
8
+ @name, @record, @attachments = name, record, attachments
9
+ end
10
+
11
+ def purge
12
+ attachments.each(&:purge)
13
+ reset
14
+ end
15
+
16
+ def purge_later
17
+ attachments.each(&:purge_later)
18
+ reset
19
+ end
20
+
21
+ private
22
+ def reset
23
+ record.attachment_changes.delete(name)
24
+ record.public_send("#{name}_attachments").reset
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Attached::Changes::PurgeOne # :nodoc:
5
+ attr_reader :name, :record, :attachment
6
+
7
+ def initialize(name, record, attachment)
8
+ @name, @record, @attachment = name, record, attachment
9
+ end
10
+
11
+ def purge
12
+ attachment&.purge
13
+ reset
14
+ end
15
+
16
+ def purge_later
17
+ attachment&.purge_later
18
+ reset
19
+ end
20
+
21
+ private
22
+ def reset
23
+ record.attachment_changes.delete(name)
24
+ record.public_send("#{name}_attachment=", nil)
25
+ end
26
+ end
27
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- module Attached::Changes #:nodoc:
4
+ module Attached::Changes # :nodoc:
5
5
  extend ActiveSupport::Autoload
6
6
 
7
7
  eager_autoload do
@@ -11,6 +11,12 @@ module ActiveStorage
11
11
 
12
12
  autoload :DeleteOne
13
13
  autoload :DeleteMany
14
+
15
+ autoload :DetachOne
16
+ autoload :DetachMany
17
+
18
+ autoload :PurgeOne
19
+ autoload :PurgeMany
14
20
  end
15
21
  end
16
22
  end