activestorage 5.2.3 → 6.0.0

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 +140 -59
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +9 -6
  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 +5 -2
  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 +20 -9
  16. data/app/models/active_storage/blob.rb +66 -24
  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 +24 -33
  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 +9 -0
  24. data/lib/active_storage.rb +26 -6
  25. data/lib/active_storage/analyzer.rb +9 -4
  26. data/lib/active_storage/analyzer/image_analyzer.rb +11 -4
  27. data/lib/active_storage/analyzer/video_analyzer.rb +3 -5
  28. data/lib/active_storage/attached.rb +7 -22
  29. data/lib/active_storage/attached/changes.rb +16 -0
  30. data/lib/active_storage/attached/changes/create_many.rb +46 -0
  31. data/lib/active_storage/attached/changes/create_one.rb +69 -0
  32. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  33. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  34. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  35. data/lib/active_storage/attached/many.rb +16 -10
  36. data/lib/active_storage/attached/model.rb +147 -0
  37. data/lib/active_storage/attached/one.rb +16 -19
  38. data/lib/active_storage/downloader.rb +43 -0
  39. data/lib/active_storage/downloading.rb +8 -0
  40. data/lib/active_storage/engine.rb +43 -6
  41. data/lib/active_storage/errors.rb +22 -3
  42. data/lib/active_storage/gem_version.rb +3 -3
  43. data/lib/active_storage/previewer.rb +21 -11
  44. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
  45. data/lib/active_storage/previewer/video_previewer.rb +2 -3
  46. data/lib/active_storage/reflection.rb +64 -0
  47. data/lib/active_storage/service.rb +9 -6
  48. data/lib/active_storage/service/azure_storage_service.rb +30 -14
  49. data/lib/active_storage/service/configurator.rb +3 -1
  50. data/lib/active_storage/service/disk_service.rb +24 -12
  51. data/lib/active_storage/service/gcs_service.rb +49 -47
  52. data/lib/active_storage/service/s3_service.rb +10 -6
  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 +38 -12
  58. data/app/models/active_storage/filename/parameters.rb +0 -36
  59. data/lib/active_storage/attached/macros.rb +0 -110
@@ -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
@@ -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}=", (change&.attachables || 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,147 @@
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
+ if ActiveStorage.replace_on_assign_to_many
97
+ attachment_changes["#{name}"] =
98
+ if attachables.nil? || Array(attachables).none?
99
+ ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
100
+ else
101
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
102
+ end
103
+ else
104
+ if !attachables.nil? || Array(attachables).any?
105
+ attachment_changes["#{name}"] =
106
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
107
+ end
108
+ end
109
+ end
110
+ CODE
111
+
112
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: :destroy do
113
+ def purge
114
+ each(&:purge)
115
+ reset
116
+ end
117
+
118
+ def purge_later
119
+ each(&:purge_later)
120
+ reset
121
+ end
122
+ end
123
+ has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
124
+
125
+ scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
126
+
127
+ after_save { attachment_changes[name.to_s]&.save }
128
+
129
+ after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
130
+
131
+ ActiveRecord::Reflection.add_attachment_reflection(
132
+ self,
133
+ name,
134
+ ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self)
135
+ )
136
+ end
137
+ end
138
+
139
+ def attachment_changes #:nodoc:
140
+ @attachment_changes ||= {}
141
+ end
142
+
143
+ def reload(*) #:nodoc:
144
+ super.tap { @attachment_changes = nil }
145
+ end
146
+ end
147
+ end
@@ -10,26 +10,28 @@ module ActiveStorage
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.update(name => attachable)
33
+ else
34
+ record.public_send("#{name}=", attachable)
33
35
  end
34
36
  end
35
37
 
@@ -47,7 +49,7 @@ module ActiveStorage
47
49
  # Deletes the attachment without purging it, leaving its blob in place.
48
50
  def detach
49
51
  if attached?
50
- attachment.destroy
52
+ attachment.delete
51
53
  write_attachment nil
52
54
  end
53
55
  end
@@ -65,16 +67,11 @@ module ActiveStorage
65
67
  def purge_later
66
68
  if attached?
67
69
  attachment.purge_later
70
+ write_attachment nil
68
71
  end
69
72
  end
70
73
 
71
74
  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
75
  def write_attachment(attachment)
79
76
  record.public_send("#{name}_attachment=", attachment)
80
77
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Downloader #:nodoc:
5
+ attr_reader :service
6
+
7
+ def initialize(service)
8
+ @service = service
9
+ end
10
+
11
+ def open(key, checksum:, name: "ActiveStorage-", tmpdir: nil)
12
+ open_tempfile(name, tmpdir) do |file|
13
+ download key, file
14
+ verify_integrity_of file, checksum: checksum
15
+ yield file
16
+ end
17
+ end
18
+
19
+ private
20
+ def open_tempfile(name, tmpdir = nil)
21
+ file = Tempfile.open(name, tmpdir)
22
+
23
+ begin
24
+ yield file
25
+ ensure
26
+ file.close!
27
+ end
28
+ end
29
+
30
+ def download(key, file)
31
+ file.binmode
32
+ service.download(key) { |chunk| file.write(chunk) }
33
+ file.flush
34
+ file.rewind
35
+ end
36
+
37
+ def verify_integrity_of(file, checksum:)
38
+ unless Digest::MD5.file(file).base64digest == checksum
39
+ raise ActiveStorage::IntegrityError
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "tmpdir"
4
+ require "active_support/core_ext/string/filters"
4
5
 
5
6
  module ActiveStorage
6
7
  module Downloading
8
+ def self.included(klass)
9
+ ActiveSupport::Deprecation.warn <<~MESSAGE.squish, caller_locations(2)
10
+ ActiveStorage::Downloading is deprecated and will be removed in Active Storage 6.1.
11
+ Use ActiveStorage::Blob#open instead.
12
+ MESSAGE
13
+ end
14
+
7
15
  private
8
16
  # Opens a new tempfile in #tempdir and copies blob data into it. Yields the tempfile.
9
17
  def download_blob_to_tempfile #:doc:
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
+ require "action_controller/railtie"
5
+ require "active_job/railtie"
6
+ require "active_record/railtie"
7
+
4
8
  require "active_storage"
5
9
 
6
10
  require "active_storage/previewer/poppler_pdf_previewer"
@@ -10,6 +14,8 @@ require "active_storage/previewer/video_previewer"
10
14
  require "active_storage/analyzer/image_analyzer"
11
15
  require "active_storage/analyzer/video_analyzer"
12
16
 
17
+ require "active_storage/reflection"
18
+
13
19
  module ActiveStorage
14
20
  class Engine < Rails::Engine # :nodoc:
15
21
  isolate_namespace ActiveStorage
@@ -18,12 +24,16 @@ module ActiveStorage
18
24
  config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
19
25
  config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
20
26
  config.active_storage.paths = ActiveSupport::OrderedOptions.new
27
+ config.active_storage.queues = ActiveSupport::OrderedOptions.new
21
28
 
22
29
  config.active_storage.variable_content_types = %w(
23
30
  image/png
24
31
  image/gif
25
32
  image/jpg
26
33
  image/jpeg
34
+ image/pjpeg
35
+ image/tiff
36
+ image/bmp
27
37
  image/vnd.adobe.photoshop
28
38
  image/vnd.microsoft.icon
29
39
  )
@@ -46,6 +56,8 @@ module ActiveStorage
46
56
  image/gif
47
57
  image/jpg
48
58
  image/jpeg
59
+ image/tiff
60
+ image/bmp
49
61
  image/vnd.adobe.photoshop
50
62
  image/vnd.microsoft.icon
51
63
  application/pdf
@@ -55,16 +67,20 @@ module ActiveStorage
55
67
 
56
68
  initializer "active_storage.configs" do
57
69
  config.after_initialize do |app|
58
- ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
59
- ActiveStorage.queue = app.config.active_storage.queue
60
- ActiveStorage.previewers = app.config.active_storage.previewers || []
61
- ActiveStorage.analyzers = app.config.active_storage.analyzers || []
62
- ActiveStorage.paths = app.config.active_storage.paths || {}
70
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
71
+ ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
72
+ ActiveStorage.previewers = app.config.active_storage.previewers || []
73
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
74
+ ActiveStorage.paths = app.config.active_storage.paths || {}
75
+ ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
63
76
 
64
77
  ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
65
78
  ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
79
+ ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
66
80
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
67
81
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
82
+
83
+ ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
68
84
  end
69
85
  end
70
86
 
@@ -72,7 +88,7 @@ module ActiveStorage
72
88
  require "active_storage/attached"
73
89
 
74
90
  ActiveSupport.on_load(:active_record) do
75
- extend ActiveStorage::Attached::Macros
91
+ include ActiveStorage::Attached::Model
76
92
  end
77
93
  end
78
94
 
@@ -108,5 +124,26 @@ module ActiveStorage
108
124
  end
109
125
  end
110
126
  end
127
+
128
+ initializer "active_storage.queues" do
129
+ config.after_initialize do |app|
130
+ if queue = app.config.active_storage.queue
131
+ ActiveSupport::Deprecation.warn \
132
+ "config.active_storage.queue is deprecated and will be removed in Rails 6.1. " \
133
+ "Set config.active_storage.queues.purge and config.active_storage.queues.analysis instead."
134
+
135
+ ActiveStorage.queues = { purge: queue, analysis: queue }
136
+ else
137
+ ActiveStorage.queues = app.config.active_storage.queues || {}
138
+ end
139
+ end
140
+ end
141
+
142
+ initializer "active_storage.reflection" do
143
+ ActiveSupport.on_load(:active_record) do
144
+ include Reflection::ActiveRecordExtensions
145
+ ActiveRecord::Reflection.singleton_class.prepend(Reflection::ReflectionExtension)
146
+ end
147
+ end
111
148
  end
112
149
  end