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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +225 -93
- data/MIT-LICENSE +1 -1
- data/README.md +43 -8
- data/app/assets/javascripts/activestorage.js +5 -2
- data/app/controllers/active_storage/base_controller.rb +13 -4
- data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
- data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +3 -3
- data/app/controllers/active_storage/direct_uploads_controller.rb +2 -2
- data/app/controllers/active_storage/disk_controller.rb +13 -22
- data/app/controllers/active_storage/representations/base_controller.rb +14 -0
- data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
- data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +3 -5
- data/app/controllers/concerns/active_storage/file_server.rb +18 -0
- data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
- data/app/javascript/activestorage/blob_record.js +7 -2
- data/app/jobs/active_storage/analyze_job.rb +5 -0
- data/app/jobs/active_storage/base_job.rb +0 -1
- data/app/jobs/active_storage/mirror_job.rb +15 -0
- data/app/jobs/active_storage/purge_job.rb +3 -0
- data/app/models/active_storage/attachment.rb +35 -16
- data/app/models/active_storage/blob/analyzable.rb +6 -2
- data/app/models/active_storage/blob/identifiable.rb +7 -6
- data/app/models/active_storage/blob/representable.rb +36 -6
- data/app/models/active_storage/blob.rb +186 -68
- data/app/models/active_storage/filename.rb +0 -6
- data/app/models/active_storage/preview.rb +37 -12
- data/app/models/active_storage/record.rb +7 -0
- data/app/models/active_storage/variant.rb +53 -67
- data/app/models/active_storage/variant_record.rb +8 -0
- data/app/models/active_storage/variant_with_record.rb +54 -0
- data/app/models/active_storage/variation.rb +30 -94
- data/config/routes.rb +66 -15
- data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
- data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +14 -4
- data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +17 -8
- data/lib/active_storage/analyzer.rb +15 -4
- data/lib/active_storage/attached/changes/create_many.rb +47 -0
- data/lib/active_storage/attached/changes/create_one.rb +82 -0
- data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
- data/lib/active_storage/attached/changes/delete_many.rb +27 -0
- data/lib/active_storage/attached/changes/delete_one.rb +19 -0
- data/lib/active_storage/attached/changes.rb +16 -0
- data/lib/active_storage/attached/many.rb +19 -12
- data/lib/active_storage/attached/model.rb +212 -0
- data/lib/active_storage/attached/one.rb +19 -21
- data/lib/active_storage/attached.rb +7 -22
- data/lib/active_storage/downloader.rb +43 -0
- data/lib/active_storage/engine.rb +60 -38
- data/lib/active_storage/errors.rb +25 -3
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/log_subscriber.rb +6 -0
- data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
- data/lib/active_storage/previewer/video_previewer.rb +17 -10
- data/lib/active_storage/previewer.rb +34 -14
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service/azure_storage_service.rb +65 -44
- data/lib/active_storage/service/configurator.rb +6 -2
- data/lib/active_storage/service/disk_service.rb +57 -44
- data/lib/active_storage/service/gcs_service.rb +68 -64
- data/lib/active_storage/service/mirror_service.rb +31 -7
- data/lib/active_storage/service/registry.rb +32 -0
- data/lib/active_storage/service/s3_service.rb +56 -24
- data/lib/active_storage/service.rb +44 -12
- data/lib/active_storage/transformers/image_processing_transformer.rb +45 -0
- data/lib/active_storage/transformers/transformer.rb +39 -0
- data/lib/active_storage.rb +31 -296
- data/lib/tasks/activestorage.rake +11 -0
- metadata +82 -16
- data/app/models/active_storage/filename/parameters.rb +0 -36
- data/lib/active_storage/attached/macros.rb +0 -110
- 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
|
-
#
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
39
|
+
# Returns true if any attachments have been made.
|
32
40
|
#
|
33
|
-
# class Gallery <
|
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.
|
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
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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 <
|
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.
|
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
|
9
|
+
attr_reader :name, :record
|
12
10
|
|
13
|
-
def initialize(name, record
|
14
|
-
@name, @record
|
11
|
+
def initialize(name, record)
|
12
|
+
@name, @record = name, record
|
15
13
|
end
|
16
14
|
|
17
15
|
private
|
18
|
-
def
|
19
|
-
|
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/
|
25
|
+
require "active_storage/attached/changes"
|