activestorage 5.2.4.4 → 6.0.0.beta1

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -81
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +6 -5
  5. data/app/assets/javascripts/activestorage.js +4 -1
  6. data/app/controllers/active_storage/base_controller.rb +3 -5
  7. data/app/controllers/active_storage/blobs_controller.rb +1 -1
  8. data/app/controllers/active_storage/disk_controller.rb +4 -1
  9. data/app/controllers/active_storage/representations_controller.rb +1 -1
  10. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  11. data/app/javascript/activestorage/blob_record.js +6 -1
  12. data/app/jobs/active_storage/analyze_job.rb +4 -0
  13. data/app/jobs/active_storage/base_job.rb +0 -1
  14. data/app/jobs/active_storage/purge_job.rb +3 -0
  15. data/app/models/active_storage/attachment.rb +18 -9
  16. data/app/models/active_storage/blob.rb +63 -22
  17. data/app/models/active_storage/blob/representable.rb +5 -5
  18. data/app/models/active_storage/filename.rb +0 -6
  19. data/app/models/active_storage/preview.rb +3 -3
  20. data/app/models/active_storage/variant.rb +51 -52
  21. data/app/models/active_storage/variation.rb +23 -32
  22. data/config/routes.rb +13 -12
  23. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +7 -0
  24. data/lib/active_storage.rb +13 -2
  25. data/lib/active_storage/analyzer.rb +9 -4
  26. data/lib/active_storage/analyzer/video_analyzer.rb +2 -4
  27. data/lib/active_storage/attached.rb +7 -22
  28. data/lib/active_storage/attached/changes.rb +16 -0
  29. data/lib/active_storage/attached/changes/create_many.rb +46 -0
  30. data/lib/active_storage/attached/changes/create_one.rb +68 -0
  31. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  32. data/lib/active_storage/attached/changes/delete_many.rb +23 -0
  33. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  34. data/lib/active_storage/attached/many.rb +16 -10
  35. data/lib/active_storage/attached/model.rb +140 -0
  36. data/lib/active_storage/attached/one.rb +16 -19
  37. data/lib/active_storage/downloader.rb +44 -0
  38. data/lib/active_storage/downloading.rb +8 -0
  39. data/lib/active_storage/engine.rb +35 -6
  40. data/lib/active_storage/errors.rb +22 -3
  41. data/lib/active_storage/gem_version.rb +4 -4
  42. data/lib/active_storage/previewer.rb +21 -11
  43. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +1 -1
  44. data/lib/active_storage/previewer/video_previewer.rb +2 -3
  45. data/lib/active_storage/reflection.rb +64 -0
  46. data/lib/active_storage/service.rb +5 -6
  47. data/lib/active_storage/service/azure_storage_service.rb +28 -12
  48. data/lib/active_storage/service/configurator.rb +3 -1
  49. data/lib/active_storage/service/disk_service.rb +20 -16
  50. data/lib/active_storage/service/gcs_service.rb +48 -46
  51. data/lib/active_storage/service/mirror_service.rb +1 -1
  52. data/lib/active_storage/service/s3_service.rb +10 -7
  53. data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
  54. data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
  55. data/lib/active_storage/transformers/transformer.rb +42 -0
  56. data/lib/tasks/activestorage.rake +7 -0
  57. metadata +26 -14
  58. data/app/models/active_storage/filename/parameters.rb +0 -36
  59. data/lib/active_storage/attached/macros.rb +0 -110
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Rails.application.routes.draw do
4
- get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
5
-
6
- direct :rails_blob do |blob, options|
7
- route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
8
- end
9
-
10
- resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
11
- resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
4
+ scope ActiveStorage.routes_prefix do
5
+ get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
12
6
 
7
+ get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
13
8
 
14
- get "/rails/active_storage/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
9
+ get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
10
+ put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
11
+ post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
12
+ end
15
13
 
16
14
  direct :rails_representation do |representation, options|
17
15
  signed_blob_id = representation.blob.signed_id
@@ -25,7 +23,10 @@ Rails.application.routes.draw do
25
23
  resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
26
24
 
27
25
 
28
- get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
29
- put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
30
- post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
26
+ direct :rails_blob do |blob, options|
27
+ route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
28
+ end
29
+
30
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
31
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
31
32
  end
@@ -0,0 +1,7 @@
1
+ class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0]
2
+ def up
3
+ unless foreign_key_exists?(:active_storage_attachments, column: :blob_id)
4
+ add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id
5
+ end
6
+ end
7
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
4
+ # Copyright (c) 2017-2019 David Heinemeier Hansson, Basecamp
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining
7
7
  # a copy of this software and associated documentation files (the
@@ -42,12 +42,23 @@ module ActiveStorage
42
42
 
43
43
  mattr_accessor :logger
44
44
  mattr_accessor :verifier
45
- mattr_accessor :queue
45
+ mattr_accessor :queues, default: {}
46
46
  mattr_accessor :previewers, default: []
47
47
  mattr_accessor :analyzers, default: []
48
+ mattr_accessor :variant_processor, default: :mini_magick
48
49
  mattr_accessor :paths, default: {}
49
50
  mattr_accessor :variable_content_types, default: []
50
51
  mattr_accessor :content_types_to_serve_as_binary, default: []
51
52
  mattr_accessor :content_types_allowed_inline, default: []
52
53
  mattr_accessor :binary_content_type, default: "application/octet-stream"
54
+ mattr_accessor :service_urls_expire_in, default: 5.minutes
55
+ mattr_accessor :routes_prefix, default: "/rails/active_storage"
56
+
57
+ module Transformers
58
+ extend ActiveSupport::Autoload
59
+
60
+ autoload :Transformer
61
+ autoload :ImageProcessingTransformer
62
+ autoload :MiniMagickTransformer
63
+ end
53
64
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_storage/downloading"
4
-
5
3
  module ActiveStorage
6
4
  # This is an abstract base class for analyzers, which extract metadata from blobs. See
7
5
  # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
8
6
  class Analyzer
9
- include Downloading
10
-
11
7
  attr_reader :blob
12
8
 
13
9
  # Implement this method in a concrete subclass. Have it return true when given a blob from which
@@ -26,8 +22,17 @@ module ActiveStorage
26
22
  end
27
23
 
28
24
  private
25
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
26
+ def download_blob_to_tempfile(&block) #:doc:
27
+ blob.open tempdir: tempdir, &block
28
+ end
29
+
29
30
  def logger #:doc:
30
31
  ActiveStorage.logger
31
32
  end
33
+
34
+ def tempdir #:doc:
35
+ Dir.tmpdir
36
+ end
32
37
  end
33
38
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/hash/compact"
4
-
5
3
  module ActiveStorage
6
4
  # Extracts the following from a video blob:
7
5
  #
@@ -18,7 +16,7 @@ module ActiveStorage
18
16
  #
19
17
  # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
20
18
  #
21
- # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
19
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
22
20
  class Analyzer::VideoAnalyzer < Analyzer
23
21
  def self.accept?(blob)
24
22
  blob.video?
@@ -109,7 +107,7 @@ module ActiveStorage
109
107
  JSON.parse(output.read)
110
108
  end
111
109
  rescue Errno::ENOENT
112
- logger.info "Skipping video analysis because ffmpeg isn't installed"
110
+ logger.info "Skipping video analysis because FFmpeg isn't installed"
113
111
  {}
114
112
  end
115
113
 
@@ -1,40 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_dispatch"
4
- require "action_dispatch/http/upload"
5
3
  require "active_support/core_ext/module/delegation"
6
4
 
7
5
  module ActiveStorage
8
6
  # Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many
9
7
  # classes that both provide proxy access to the blob association for a record.
10
8
  class Attached
11
- attr_reader :name, :record, :dependent
9
+ attr_reader :name, :record
12
10
 
13
- def initialize(name, record, dependent:)
14
- @name, @record, @dependent = name, record, dependent
11
+ def initialize(name, record)
12
+ @name, @record = name, record
15
13
  end
16
14
 
17
15
  private
18
- def create_blob_from(attachable)
19
- case attachable
20
- when ActiveStorage::Blob
21
- attachable
22
- when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
23
- ActiveStorage::Blob.create_after_upload! \
24
- io: attachable.open,
25
- filename: attachable.original_filename,
26
- content_type: attachable.content_type
27
- when Hash
28
- ActiveStorage::Blob.create_after_upload!(attachable)
29
- when String
30
- ActiveStorage::Blob.find_signed(attachable)
31
- else
32
- nil
33
- end
16
+ def change
17
+ record.attachment_changes[name]
34
18
  end
35
19
  end
36
20
  end
37
21
 
22
+ require "active_storage/attached/model"
38
23
  require "active_storage/attached/one"
39
24
  require "active_storage/attached/many"
40
- require "active_storage/attached/macros"
25
+ require "active_storage/attached/changes"
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module Attached::Changes #:nodoc:
5
+ extend ActiveSupport::Autoload
6
+
7
+ eager_autoload do
8
+ autoload :CreateOne
9
+ autoload :CreateMany
10
+ autoload :CreateOneOfMany
11
+
12
+ autoload :DeleteOne
13
+ autoload :DeleteMany
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Attached::Changes::CreateMany #:nodoc:
5
+ attr_reader :name, :record, :attachables
6
+
7
+ def initialize(name, record, attachables)
8
+ @name, @record, @attachables = name, record, Array(attachables)
9
+ end
10
+
11
+ def attachments
12
+ @attachments ||= subchanges.collect(&:attachment)
13
+ end
14
+
15
+ def blobs
16
+ @blobs ||= subchanges.collect(&:blob)
17
+ end
18
+
19
+ def upload
20
+ subchanges.each(&:upload)
21
+ end
22
+
23
+ def save
24
+ assign_associated_attachments
25
+ reset_associated_blobs
26
+ end
27
+
28
+ private
29
+ def subchanges
30
+ @subchanges ||= attachables.collect { |attachable| build_subchange_from(attachable) }
31
+ end
32
+
33
+ def build_subchange_from(attachable)
34
+ ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable)
35
+ end
36
+
37
+
38
+ def assign_associated_attachments
39
+ record.public_send("#{name}_attachments=", attachments)
40
+ end
41
+
42
+ def reset_associated_blobs
43
+ record.public_send("#{name}_blobs").reset
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+ require "action_dispatch/http/upload"
5
+
6
+ module ActiveStorage
7
+ class Attached::Changes::CreateOne #:nodoc:
8
+ attr_reader :name, :record, :attachable
9
+
10
+ def initialize(name, record, attachable)
11
+ @name, @record, @attachable = name, record, attachable
12
+ end
13
+
14
+ def attachment
15
+ @attachment ||= find_or_build_attachment
16
+ end
17
+
18
+ def blob
19
+ @blob ||= find_or_build_blob
20
+ end
21
+
22
+ def upload
23
+ case attachable
24
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
25
+ blob.upload_without_unfurling(attachable.open)
26
+ when Hash
27
+ blob.upload_without_unfurling(attachable.fetch(:io))
28
+ end
29
+ end
30
+
31
+ def save
32
+ record.public_send("#{name}_attachment=", attachment)
33
+ end
34
+
35
+ private
36
+ def find_or_build_attachment
37
+ find_attachment || build_attachment
38
+ end
39
+
40
+ def find_attachment
41
+ if record.public_send("#{name}_blob") == blob
42
+ record.public_send("#{name}_attachment")
43
+ end
44
+ end
45
+
46
+ def build_attachment
47
+ ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
48
+ end
49
+
50
+ def find_or_build_blob
51
+ case attachable
52
+ when ActiveStorage::Blob
53
+ attachable
54
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
55
+ ActiveStorage::Blob.build_after_unfurling \
56
+ io: attachable.open,
57
+ filename: attachable.original_filename,
58
+ content_type: attachable.content_type
59
+ when Hash
60
+ ActiveStorage::Blob.build_after_unfurling(attachable)
61
+ when String
62
+ ActiveStorage::Blob.find_signed(attachable)
63
+ else
64
+ raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Attached::Changes::CreateOneOfMany < Attached::Changes::CreateOne #:nodoc:
5
+ private
6
+ def find_attachment
7
+ record.public_send("#{name}_attachments").detect { |attachment| attachment.blob_id == blob.id }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Attached::Changes::DeleteMany #:nodoc:
5
+ attr_reader :name, :record
6
+
7
+ def initialize(name, record)
8
+ @name, @record = name, record
9
+ end
10
+
11
+ def attachments
12
+ ActiveStorage::Attachment.none
13
+ end
14
+
15
+ def blobs
16
+ ActiveStorage::Blob.none
17
+ end
18
+
19
+ def save
20
+ record.public_send("#{name}_attachments=", [])
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Attached::Changes::DeleteOne #:nodoc:
5
+ attr_reader :name, :record
6
+
7
+ def initialize(name, record)
8
+ @name, @record = name, record
9
+ end
10
+
11
+ def attachment
12
+ nil
13
+ end
14
+
15
+ def save
16
+ record.public_send("#{name}_attachment=", nil)
17
+ end
18
+ end
19
+ end
@@ -9,22 +9,29 @@ module ActiveStorage
9
9
  #
10
10
  # All methods called on this proxy object that aren't listed here will automatically be delegated to +attachments+.
11
11
  def attachments
12
- record.public_send("#{name}_attachments")
12
+ change.present? ? change.attachments : record.public_send("#{name}_attachments")
13
13
  end
14
14
 
15
- # Associates one or several attachments with the current record, saving them to the database.
15
+ # Returns all attached blobs.
16
+ def blobs
17
+ change.present? ? change.blobs : record.public_send("#{name}_blobs")
18
+ end
19
+
20
+ # Attaches one or more +attachables+ to the record.
21
+ #
22
+ # If the record is persisted and unchanged, the attachments are saved to
23
+ # the database immediately. Otherwise, they'll be saved to the DB when the
24
+ # record is next saved.
16
25
  #
17
26
  # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects
18
27
  # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
19
28
  # document.images.attach(io: File.open("/path/to/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg")
20
29
  # document.images.attach([ first_blob, second_blob ])
21
30
  def attach(*attachables)
22
- attachables.flatten.collect do |attachable|
23
- if record.new_record?
24
- attachments.build(record: record, blob: create_blob_from(attachable))
25
- else
26
- attachments.create!(record: record, blob: create_blob_from(attachable))
27
- end
31
+ if record.persisted? && !record.changed?
32
+ record.update(name => blobs + attachables.flatten)
33
+ else
34
+ record.public_send("#{name}=", blobs + attachables.flatten)
28
35
  end
29
36
  end
30
37
 
@@ -41,7 +48,7 @@ module ActiveStorage
41
48
 
42
49
  # Deletes associated attachments without purging them, leaving their respective blobs in place.
43
50
  def detach
44
- attachments.destroy_all if attached?
51
+ attachments.delete_all if attached?
45
52
  end
46
53
 
47
54
  ##
@@ -50,7 +57,6 @@ module ActiveStorage
50
57
  # Directly purges each associated attachment (i.e. destroys the blobs and
51
58
  # attachments and deletes the files on the service).
52
59
 
53
-
54
60
  ##
55
61
  # :method: purge_later
56
62
  #
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # Provides the class-level DSL for declaring an Active Record model's attachments.
5
+ module Attached::Model
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # Specifies the relation between a single attachment and the model.
10
+ #
11
+ # class User < ActiveRecord::Base
12
+ # has_one_attached :avatar
13
+ # end
14
+ #
15
+ # There is no column defined on the model side, Active Storage takes
16
+ # care of the mapping between your records and the attachment.
17
+ #
18
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
19
+ #
20
+ # User.with_attached_avatar
21
+ #
22
+ # Under the covers, this relationship is implemented as a +has_one+ association to a
23
+ # ActiveStorage::Attachment record and a +has_one-through+ association to a
24
+ # ActiveStorage::Blob record. These associations are available as +avatar_attachment+
25
+ # and +avatar_blob+. But you shouldn't need to work with these associations directly in
26
+ # most circumstances.
27
+ #
28
+ # The system has been designed to having you go through the ActiveStorage::Attached::One
29
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
30
+ #
31
+ # If the +:dependent+ option isn't set, the attachment will be purged
32
+ # (i.e. destroyed) whenever the record is destroyed.
33
+ def has_one_attached(name, dependent: :purge_later)
34
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
35
+ def #{name}
36
+ @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self)
37
+ end
38
+
39
+ def #{name}=(attachable)
40
+ attachment_changes["#{name}"] =
41
+ if attachable.nil?
42
+ ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
43
+ else
44
+ ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
45
+ end
46
+ end
47
+ CODE
48
+
49
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy
50
+ has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
51
+
52
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
53
+
54
+ after_save { attachment_changes[name.to_s]&.save }
55
+
56
+ after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
57
+
58
+ ActiveRecord::Reflection.add_attachment_reflection(
59
+ self,
60
+ name,
61
+ ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self)
62
+ )
63
+ end
64
+
65
+ # Specifies the relation between multiple attachments and the model.
66
+ #
67
+ # class Gallery < ActiveRecord::Base
68
+ # has_many_attached :photos
69
+ # end
70
+ #
71
+ # There are no columns defined on the model side, Active Storage takes
72
+ # care of the mapping between your records and the attachments.
73
+ #
74
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
75
+ #
76
+ # Gallery.where(user: Current.user).with_attached_photos
77
+ #
78
+ # Under the covers, this relationship is implemented as a +has_many+ association to a
79
+ # ActiveStorage::Attachment record and a +has_many-through+ association to a
80
+ # ActiveStorage::Blob record. These associations are available as +photos_attachments+
81
+ # and +photos_blobs+. But you shouldn't need to work with these associations directly in
82
+ # most circumstances.
83
+ #
84
+ # The system has been designed to having you go through the ActiveStorage::Attached::Many
85
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
86
+ #
87
+ # If the +:dependent+ option isn't set, all the attachments will be purged
88
+ # (i.e. destroyed) whenever the record is destroyed.
89
+ def has_many_attached(name, dependent: :purge_later)
90
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
91
+ def #{name}
92
+ @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self)
93
+ end
94
+
95
+ def #{name}=(attachables)
96
+ attachment_changes["#{name}"] =
97
+ if attachables.nil? || Array(attachables).none?
98
+ ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
99
+ else
100
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
101
+ end
102
+ end
103
+ CODE
104
+
105
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: :destroy do
106
+ def purge
107
+ each(&:purge)
108
+ reset
109
+ end
110
+
111
+ def purge_later
112
+ each(&:purge_later)
113
+ reset
114
+ end
115
+ end
116
+ has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
117
+
118
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
119
+
120
+ after_save { attachment_changes[name.to_s]&.save }
121
+
122
+ after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
123
+
124
+ ActiveRecord::Reflection.add_attachment_reflection(
125
+ self,
126
+ name,
127
+ ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self)
128
+ )
129
+ end
130
+ end
131
+
132
+ def attachment_changes #:nodoc:
133
+ @attachment_changes ||= {}
134
+ end
135
+
136
+ def reload(*) #:nodoc:
137
+ super.tap { @attachment_changes = nil }
138
+ end
139
+ end
140
+ end