activestorage 6.1.6 → 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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -268
  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 +29 -8
  29. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +15 -2
  30. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  31. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  32. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  33. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  34. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  35. data/lib/active_storage/analyzer.rb +8 -4
  36. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  37. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  38. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  39. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  40. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  41. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  42. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  43. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  44. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  45. data/lib/active_storage/attached/changes.rb +7 -1
  46. data/lib/active_storage/attached/many.rb +27 -15
  47. data/lib/active_storage/attached/model.rb +31 -5
  48. data/lib/active_storage/attached/one.rb +32 -27
  49. data/lib/active_storage/downloader.rb +2 -2
  50. data/lib/active_storage/engine.rb +28 -16
  51. data/lib/active_storage/fixture_set.rb +76 -0
  52. data/lib/active_storage/gem_version.rb +4 -4
  53. data/lib/active_storage/previewer/video_previewer.rb +0 -2
  54. data/lib/active_storage/previewer.rb +4 -4
  55. data/lib/active_storage/reflection.rb +12 -2
  56. data/lib/active_storage/service/azure_storage_service.rb +1 -1
  57. data/lib/active_storage/service/configurator.rb +1 -1
  58. data/lib/active_storage/service/disk_service.rb +13 -18
  59. data/lib/active_storage/service/gcs_service.rb +91 -7
  60. data/lib/active_storage/service/mirror_service.rb +1 -1
  61. data/lib/active_storage/service/registry.rb +1 -1
  62. data/lib/active_storage/service/s3_service.rb +4 -4
  63. data/lib/active_storage/service.rb +3 -3
  64. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -66
  65. data/lib/active_storage/transformers/transformer.rb +1 -1
  66. data/lib/active_storage.rb +3 -292
  67. metadata +32 -24
  68. 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
 
@@ -1,6 +1,9 @@
1
1
  class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
2
2
  def change
3
- create_table :active_storage_blobs do |t|
3
+ # Use Active Record's configured type for primary and foreign keys
4
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
5
+
6
+ create_table :active_storage_blobs, id: primary_key_type do |t|
4
7
  t.string :key, null: false
5
8
  t.string :filename, null: false
6
9
  t.string :content_type
@@ -8,28 +11,46 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
8
11
  t.string :service_name, null: false
9
12
  t.bigint :byte_size, null: false
10
13
  t.string :checksum, null: false
11
- 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
12
20
 
13
21
  t.index [ :key ], unique: true
14
22
  end
15
23
 
16
- create_table :active_storage_attachments do |t|
24
+ create_table :active_storage_attachments, id: primary_key_type do |t|
17
25
  t.string :name, null: false
18
- t.references :record, null: false, polymorphic: true, index: false
19
- t.references :blob, null: false
26
+ t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
27
+ t.references :blob, null: false, type: foreign_key_type
20
28
 
21
- 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
22
34
 
23
35
  t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
24
36
  t.foreign_key :active_storage_blobs, column: :blob_id
25
37
  end
26
38
 
27
- create_table :active_storage_variant_records do |t|
28
- t.belongs_to :blob, null: false, index: false
39
+ create_table :active_storage_variant_records, id: primary_key_type do |t|
40
+ t.belongs_to :blob, null: false, index: false, type: foreign_key_type
29
41
  t.string :variation_digest, null: false
30
42
 
31
43
  t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
32
44
  t.foreign_key :active_storage_blobs, column: :blob_id
33
45
  end
34
46
  end
47
+
48
+ private
49
+ def primary_and_foreign_key_types
50
+ config = Rails.configuration.generators
51
+ setting = config.options[config.orm][:primary_key_type]
52
+ primary_key_type = setting || :primary_key
53
+ foreign_key_type = setting || :bigint
54
+ [primary_key_type, foreign_key_type]
55
+ end
35
56
  end
@@ -1,11 +1,24 @@
1
1
  class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
2
2
  def change
3
- create_table :active_storage_variant_records do |t|
4
- t.belongs_to :blob, null: false, index: false
3
+ # Use Active Record's configured type for primary key
4
+ create_table :active_storage_variant_records, id: primary_key_type do |t|
5
+ t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
5
6
  t.string :variation_digest, null: false
6
7
 
7
8
  t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
8
9
  t.foreign_key :active_storage_blobs, column: :blob_id
9
10
  end
10
11
  end
12
+
13
+ private
14
+ def primary_key_type
15
+ config = Rails.configuration.generators
16
+ config.options[config.orm][:primary_key_type] || :primary_key
17
+ end
18
+
19
+ def blobs_primary_key_type
20
+ pkey_name = connection.primary_key(:active_storage_blobs)
21
+ pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
22
+ pkey_column.bigint? ? :bigint : pkey_column.type
23
+ end
11
24
  end
@@ -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