activestorage 5.2.7.1 → 6.1.4.6

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +225 -93
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +43 -8
  5. data/app/assets/javascripts/activestorage.js +5 -2
  6. data/app/controllers/active_storage/base_controller.rb +13 -4
  7. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  8. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +3 -3
  9. data/app/controllers/active_storage/direct_uploads_controller.rb +2 -2
  10. data/app/controllers/active_storage/disk_controller.rb +13 -22
  11. data/app/controllers/active_storage/representations/base_controller.rb +14 -0
  12. data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
  13. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +3 -5
  14. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  15. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  16. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  17. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  18. data/app/javascript/activestorage/blob_record.js +7 -2
  19. data/app/jobs/active_storage/analyze_job.rb +5 -0
  20. data/app/jobs/active_storage/base_job.rb +0 -1
  21. data/app/jobs/active_storage/mirror_job.rb +15 -0
  22. data/app/jobs/active_storage/purge_job.rb +3 -0
  23. data/app/models/active_storage/attachment.rb +35 -16
  24. data/app/models/active_storage/blob/analyzable.rb +6 -2
  25. data/app/models/active_storage/blob/identifiable.rb +7 -6
  26. data/app/models/active_storage/blob/representable.rb +36 -6
  27. data/app/models/active_storage/blob.rb +186 -68
  28. data/app/models/active_storage/filename.rb +0 -6
  29. data/app/models/active_storage/preview.rb +37 -12
  30. data/app/models/active_storage/record.rb +7 -0
  31. data/app/models/active_storage/variant.rb +53 -67
  32. data/app/models/active_storage/variant_record.rb +8 -0
  33. data/app/models/active_storage/variant_with_record.rb +54 -0
  34. data/app/models/active_storage/variation.rb +30 -94
  35. data/config/routes.rb +66 -15
  36. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  37. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  38. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  39. data/lib/active_storage/analyzer/image_analyzer.rb +14 -4
  40. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  41. data/lib/active_storage/analyzer/video_analyzer.rb +17 -8
  42. data/lib/active_storage/analyzer.rb +15 -4
  43. data/lib/active_storage/attached/changes/create_many.rb +47 -0
  44. data/lib/active_storage/attached/changes/create_one.rb +82 -0
  45. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  46. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  47. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  48. data/lib/active_storage/attached/changes.rb +16 -0
  49. data/lib/active_storage/attached/many.rb +19 -12
  50. data/lib/active_storage/attached/model.rb +212 -0
  51. data/lib/active_storage/attached/one.rb +19 -21
  52. data/lib/active_storage/attached.rb +7 -22
  53. data/lib/active_storage/downloader.rb +43 -0
  54. data/lib/active_storage/engine.rb +60 -38
  55. data/lib/active_storage/errors.rb +25 -3
  56. data/lib/active_storage/gem_version.rb +4 -4
  57. data/lib/active_storage/log_subscriber.rb +6 -0
  58. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  59. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
  60. data/lib/active_storage/previewer/video_previewer.rb +17 -10
  61. data/lib/active_storage/previewer.rb +34 -14
  62. data/lib/active_storage/reflection.rb +64 -0
  63. data/lib/active_storage/service/azure_storage_service.rb +65 -44
  64. data/lib/active_storage/service/configurator.rb +6 -2
  65. data/lib/active_storage/service/disk_service.rb +57 -44
  66. data/lib/active_storage/service/gcs_service.rb +68 -64
  67. data/lib/active_storage/service/mirror_service.rb +31 -7
  68. data/lib/active_storage/service/registry.rb +32 -0
  69. data/lib/active_storage/service/s3_service.rb +56 -24
  70. data/lib/active_storage/service.rb +44 -12
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +45 -0
  72. data/lib/active_storage/transformers/transformer.rb +39 -0
  73. data/lib/active_storage.rb +31 -296
  74. data/lib/tasks/activestorage.rake +11 -0
  75. metadata +82 -16
  76. data/app/models/active_storage/filename/parameters.rb +0 -36
  77. data/lib/active_storage/attached/macros.rb +0 -110
  78. data/lib/active_storage/downloading.rb +0 -39
@@ -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
@@ -16,6 +12,12 @@ module ActiveStorage
16
12
  false
17
13
  end
18
14
 
15
+ # Implement this method in concrete subclasses. It will determine if blob analysis
16
+ # should be done in a job or performed inline. By default, analysis is enqueued in a job.
17
+ def self.analyze_later?
18
+ true
19
+ end
20
+
19
21
  def initialize(blob)
20
22
  @blob = blob
21
23
  end
@@ -26,8 +28,17 @@ module ActiveStorage
26
28
  end
27
29
 
28
30
  private
31
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
32
+ def download_blob_to_tempfile(&block) #:doc:
33
+ blob.open tmpdir: tmpdir, &block
34
+ end
35
+
29
36
  def logger #:doc:
30
37
  ActiveStorage.logger
31
38
  end
39
+
40
+ def tmpdir #:doc:
41
+ Dir.tmpdir
42
+ end
32
43
  end
33
44
  end
@@ -0,0 +1,47 @@
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
+ blobs.each(&:identify_without_saving)
10
+ end
11
+
12
+ def attachments
13
+ @attachments ||= subchanges.collect(&:attachment)
14
+ end
15
+
16
+ def blobs
17
+ @blobs ||= subchanges.collect(&:blob)
18
+ end
19
+
20
+ def upload
21
+ subchanges.each(&:upload)
22
+ end
23
+
24
+ def save
25
+ assign_associated_attachments
26
+ reset_associated_blobs
27
+ end
28
+
29
+ private
30
+ def subchanges
31
+ @subchanges ||= attachables.collect { |attachable| build_subchange_from(attachable) }
32
+ end
33
+
34
+ def build_subchange_from(attachable)
35
+ ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable)
36
+ end
37
+
38
+
39
+ def assign_associated_attachments
40
+ record.public_send("#{name}_attachments=", attachments)
41
+ end
42
+
43
+ def reset_associated_blobs
44
+ record.public_send("#{name}_blobs").reset
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,82 @@
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
+ blob.identify_without_saving
13
+ end
14
+
15
+ def attachment
16
+ @attachment ||= find_or_build_attachment
17
+ end
18
+
19
+ def blob
20
+ @blob ||= find_or_build_blob
21
+ end
22
+
23
+ def upload
24
+ case attachable
25
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
26
+ blob.upload_without_unfurling(attachable.open)
27
+ when Hash
28
+ blob.upload_without_unfurling(attachable.fetch(:io))
29
+ end
30
+ end
31
+
32
+ def save
33
+ record.public_send("#{name}_attachment=", attachment)
34
+ record.public_send("#{name}_blob=", blob)
35
+ end
36
+
37
+ private
38
+ def find_or_build_attachment
39
+ find_attachment || build_attachment
40
+ end
41
+
42
+ def find_attachment
43
+ if record.public_send("#{name}_blob") == blob
44
+ record.public_send("#{name}_attachment")
45
+ end
46
+ end
47
+
48
+ def build_attachment
49
+ ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
50
+ end
51
+
52
+ def find_or_build_blob
53
+ case attachable
54
+ when ActiveStorage::Blob
55
+ attachable
56
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
57
+ ActiveStorage::Blob.build_after_unfurling(
58
+ io: attachable.open,
59
+ filename: attachable.original_filename,
60
+ content_type: attachable.content_type,
61
+ record: record,
62
+ service_name: attachment_service_name
63
+ )
64
+ when Hash
65
+ ActiveStorage::Blob.build_after_unfurling(
66
+ **attachable.reverse_merge(
67
+ record: record,
68
+ service_name: attachment_service_name
69
+ ).symbolize_keys
70
+ )
71
+ when String
72
+ ActiveStorage::Blob.find_signed!(attachable, record: record)
73
+ else
74
+ raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
75
+ end
76
+ end
77
+
78
+ def attachment_service_name
79
+ record.attachment_reflections[name].options[:service_name]
80
+ end
81
+ end
82
+ 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,27 @@
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 attachables
12
+ []
13
+ end
14
+
15
+ def attachments
16
+ ActiveStorage::Attachment.none
17
+ end
18
+
19
+ def blobs
20
+ ActiveStorage::Blob.none
21
+ end
22
+
23
+ def save
24
+ record.public_send("#{name}_attachments=", [])
25
+ end
26
+ end
27
+ 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
@@ -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
@@ -9,28 +9,36 @@ 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.public_send("#{name}=", blobs + attachables.flatten)
33
+ record.save
34
+ else
35
+ record.public_send("#{name}=", (change&.attachables || blobs) + attachables.flatten)
28
36
  end
29
37
  end
30
38
 
31
- # Returns true if any attachments has been made.
39
+ # Returns true if any attachments have been made.
32
40
  #
33
- # class Gallery < ActiveRecord::Base
41
+ # class Gallery < ApplicationRecord
34
42
  # has_many_attached :photos
35
43
  # end
36
44
  #
@@ -41,7 +49,7 @@ module ActiveStorage
41
49
 
42
50
  # Deletes associated attachments without purging them, leaving their respective blobs in place.
43
51
  def detach
44
- attachments.destroy_all if attached?
52
+ attachments.delete_all if attached?
45
53
  end
46
54
 
47
55
  ##
@@ -50,7 +58,6 @@ module ActiveStorage
50
58
  # Directly purges each associated attachment (i.e. destroys the blobs and
51
59
  # attachments and deletes the files on the service).
52
60
 
53
-
54
61
  ##
55
62
  # :method: purge_later
56
63
  #
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/try"
4
+
5
+ module ActiveStorage
6
+ # Provides the class-level DSL for declaring an Active Record model's attachments.
7
+ module Attached::Model
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ # Specifies the relation between a single attachment and the model.
12
+ #
13
+ # class User < ApplicationRecord
14
+ # has_one_attached :avatar
15
+ # end
16
+ #
17
+ # There is no column defined on the model side, Active Storage takes
18
+ # care of the mapping between your records and the attachment.
19
+ #
20
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
21
+ #
22
+ # User.with_attached_avatar
23
+ #
24
+ # Under the covers, this relationship is implemented as a +has_one+ association to a
25
+ # ActiveStorage::Attachment record and a +has_one-through+ association to a
26
+ # ActiveStorage::Blob record. These associations are available as +avatar_attachment+
27
+ # and +avatar_blob+. But you shouldn't need to work with these associations directly in
28
+ # most circumstances.
29
+ #
30
+ # The system has been designed to having you go through the ActiveStorage::Attached::One
31
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
32
+ #
33
+ # If the +:dependent+ option isn't set, the attachment will be purged
34
+ # (i.e. destroyed) whenever the record is destroyed.
35
+ #
36
+ # If you need the attachment to use a service which differs from the globally configured one,
37
+ # pass the +:service+ option. For instance:
38
+ #
39
+ # class User < ActiveRecord::Base
40
+ # has_one_attached :avatar, service: :s3
41
+ # end
42
+ #
43
+ # If you need to enable +strict_loading+ to prevent lazy loading of attachment,
44
+ # pass the +:strict_loading+ option. You can do:
45
+ #
46
+ # class User < ApplicationRecord
47
+ # has_one_attached :avatar, strict_loading: true
48
+ # end
49
+ #
50
+ def has_one_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
51
+ validate_service_configuration(name, service)
52
+
53
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
54
+ # frozen_string_literal: true
55
+ def #{name}
56
+ @active_storage_attached ||= {}
57
+ @active_storage_attached[:#{name}] ||= ActiveStorage::Attached::One.new("#{name}", self)
58
+ end
59
+
60
+ def #{name}=(attachable)
61
+ attachment_changes["#{name}"] =
62
+ if attachable.nil?
63
+ ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
64
+ else
65
+ ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
66
+ end
67
+ end
68
+ CODE
69
+
70
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy, strict_loading: strict_loading
71
+ has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob, strict_loading: strict_loading
72
+
73
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
74
+
75
+ after_save { attachment_changes[name.to_s]&.save }
76
+
77
+ after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
78
+
79
+ reflection = ActiveRecord::Reflection.create(
80
+ :has_one_attached,
81
+ name,
82
+ nil,
83
+ { dependent: dependent, service_name: service },
84
+ self
85
+ )
86
+ ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)
87
+ end
88
+
89
+ # Specifies the relation between multiple attachments and the model.
90
+ #
91
+ # class Gallery < ApplicationRecord
92
+ # has_many_attached :photos
93
+ # end
94
+ #
95
+ # There are no columns defined on the model side, Active Storage takes
96
+ # care of the mapping between your records and the attachments.
97
+ #
98
+ # To avoid N+1 queries, you can include the attached blobs in your query like so:
99
+ #
100
+ # Gallery.where(user: Current.user).with_attached_photos
101
+ #
102
+ # Under the covers, this relationship is implemented as a +has_many+ association to a
103
+ # ActiveStorage::Attachment record and a +has_many-through+ association to a
104
+ # ActiveStorage::Blob record. These associations are available as +photos_attachments+
105
+ # and +photos_blobs+. But you shouldn't need to work with these associations directly in
106
+ # most circumstances.
107
+ #
108
+ # The system has been designed to having you go through the ActiveStorage::Attached::Many
109
+ # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
110
+ #
111
+ # If the +:dependent+ option isn't set, all the attachments will be purged
112
+ # (i.e. destroyed) whenever the record is destroyed.
113
+ #
114
+ # If you need the attachment to use a service which differs from the globally configured one,
115
+ # pass the +:service+ option. For instance:
116
+ #
117
+ # class Gallery < ActiveRecord::Base
118
+ # has_many_attached :photos, service: :s3
119
+ # end
120
+ #
121
+ # If you need to enable +strict_loading+ to prevent lazy loading of attachments,
122
+ # pass the +:strict_loading+ option. You can do:
123
+ #
124
+ # class Gallery < ApplicationRecord
125
+ # has_many_attached :photos, strict_loading: true
126
+ # end
127
+ #
128
+ def has_many_attached(name, dependent: :purge_later, service: nil, strict_loading: false)
129
+ validate_service_configuration(name, service)
130
+
131
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
132
+ # frozen_string_literal: true
133
+ def #{name}
134
+ @active_storage_attached ||= {}
135
+ @active_storage_attached[:#{name}] ||= ActiveStorage::Attached::Many.new("#{name}", self)
136
+ end
137
+
138
+ def #{name}=(attachables)
139
+ if ActiveStorage.replace_on_assign_to_many
140
+ attachment_changes["#{name}"] =
141
+ if Array(attachables).none?
142
+ ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
143
+ else
144
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
145
+ end
146
+ else
147
+ if Array(attachables).any?
148
+ attachment_changes["#{name}"] =
149
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
150
+ end
151
+ end
152
+ end
153
+ CODE
154
+
155
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: :destroy, strict_loading: strict_loading do
156
+ def purge
157
+ each(&:purge)
158
+ reset
159
+ end
160
+
161
+ def purge_later
162
+ each(&:purge_later)
163
+ reset
164
+ end
165
+ end
166
+ has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob, strict_loading: strict_loading
167
+
168
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
169
+
170
+ after_save { attachment_changes[name.to_s]&.save }
171
+
172
+ after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
173
+
174
+ reflection = ActiveRecord::Reflection.create(
175
+ :has_many_attached,
176
+ name,
177
+ nil,
178
+ { dependent: dependent, service_name: service },
179
+ self
180
+ )
181
+ ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)
182
+ end
183
+
184
+ private
185
+ def validate_service_configuration(association_name, service)
186
+ if service.present?
187
+ ActiveStorage::Blob.services.fetch(service) do
188
+ raise ArgumentError, "Cannot configure service :#{service} for #{name}##{association_name}"
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ def attachment_changes #:nodoc:
195
+ @attachment_changes ||= {}
196
+ end
197
+
198
+ def changed_for_autosave? #:nodoc:
199
+ super || attachment_changes.any?
200
+ end
201
+
202
+ def initialize_dup(*) #:nodoc:
203
+ super
204
+ @active_storage_attached = nil
205
+ @attachment_changes = nil
206
+ end
207
+
208
+ def reload(*) #:nodoc:
209
+ super.tap { @attachment_changes = nil }
210
+ end
211
+ end
212
+ end
@@ -3,39 +3,42 @@
3
3
  module ActiveStorage
4
4
  # Representation of a single attachment to a model.
5
5
  class Attached::One < Attached
6
- delegate_missing_to :attachment
6
+ delegate_missing_to :attachment, allow_nil: true
7
7
 
8
8
  # Returns the associated attachment record.
9
9
  #
10
10
  # You don't have to call this method to access the attachment's methods as
11
11
  # they are all available at the model level.
12
12
  def attachment
13
- record.public_send("#{name}_attachment")
13
+ change.present? ? change.attachment : record.public_send("#{name}_attachment")
14
14
  end
15
15
 
16
- # Associates a given attachment with the current record, saving it to the database.
16
+ def blank?
17
+ !attached?
18
+ end
19
+
20
+ # Attaches an +attachable+ to the record.
21
+ #
22
+ # If the record is persisted and unchanged, the attachment is saved to
23
+ # the database immediately. Otherwise, it'll be saved to the DB when the
24
+ # record is next saved.
17
25
  #
18
26
  # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
19
27
  # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
20
28
  # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
21
29
  # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
22
30
  def attach(attachable)
23
- blob_was = blob if attached?
24
- blob = create_blob_from(attachable)
25
-
26
- unless blob == blob_was
27
- transaction do
28
- detach
29
- write_attachment build_attachment(blob: blob)
30
- end
31
-
32
- blob_was.purge_later if blob_was && dependent == :purge_later
31
+ if record.persisted? && !record.changed?
32
+ record.public_send("#{name}=", attachable)
33
+ record.save
34
+ else
35
+ record.public_send("#{name}=", attachable)
33
36
  end
34
37
  end
35
38
 
36
39
  # Returns +true+ if an attachment has been made.
37
40
  #
38
- # class User < ActiveRecord::Base
41
+ # class User < ApplicationRecord
39
42
  # has_one_attached :avatar
40
43
  # end
41
44
  #
@@ -47,7 +50,7 @@ module ActiveStorage
47
50
  # Deletes the attachment without purging it, leaving its blob in place.
48
51
  def detach
49
52
  if attached?
50
- attachment.destroy
53
+ attachment.delete
51
54
  write_attachment nil
52
55
  end
53
56
  end
@@ -65,16 +68,11 @@ module ActiveStorage
65
68
  def purge_later
66
69
  if attached?
67
70
  attachment.purge_later
71
+ write_attachment nil
68
72
  end
69
73
  end
70
74
 
71
75
  private
72
- delegate :transaction, to: :record
73
-
74
- def build_attachment(blob:)
75
- ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
76
- end
77
-
78
76
  def write_attachment(attachment)
79
77
  record.public_send("#{name}_attachment=", attachment)
80
78
  end
@@ -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"