shrine 2.19.3 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +523 -41
- data/LICENSE.txt +1 -1
- data/README.md +83 -979
- data/doc/advantages.md +231 -204
- data/doc/attacher.md +304 -153
- data/doc/carrierwave.md +297 -226
- data/doc/changing_derivatives.md +308 -0
- data/doc/changing_location.md +103 -21
- data/doc/changing_storage.md +110 -0
- data/doc/creating_persistence_plugins.md +132 -0
- data/doc/creating_plugins.md +43 -23
- data/doc/creating_storages.md +19 -5
- data/doc/design.md +147 -97
- data/doc/direct_s3.md +38 -28
- data/doc/external/articles.md +63 -0
- data/doc/external/extensions.md +53 -0
- data/doc/external/misc.md +32 -0
- data/doc/getting_started.md +1156 -0
- data/doc/metadata.md +190 -109
- data/doc/multiple_files.md +93 -30
- data/doc/paperclip.md +384 -262
- data/doc/plugins/activerecord.md +177 -46
- data/doc/plugins/add_metadata.md +139 -38
- data/doc/plugins/atomic_helpers.md +217 -0
- data/doc/plugins/backgrounding.md +156 -98
- data/doc/plugins/cached_attachment_data.md +7 -5
- data/doc/plugins/column.md +121 -0
- data/doc/plugins/data_uri.md +23 -22
- data/doc/plugins/default_storage.md +36 -10
- data/doc/plugins/default_url.md +30 -13
- data/doc/plugins/delete_raw.md +4 -2
- data/doc/plugins/derivation_endpoint.md +186 -101
- data/doc/plugins/derivatives.md +839 -0
- data/doc/plugins/determine_mime_type.md +4 -2
- data/doc/plugins/download_endpoint.md +64 -8
- data/doc/plugins/dynamic_storage.md +5 -3
- data/doc/plugins/entity.md +263 -0
- data/doc/plugins/form_assign.md +55 -0
- data/doc/plugins/included.md +31 -8
- data/doc/plugins/infer_extension.md +21 -10
- data/doc/plugins/instrumentation.md +38 -16
- data/doc/plugins/keep_files.md +16 -17
- data/doc/plugins/metadata_attributes.md +42 -13
- data/doc/plugins/mirroring.md +118 -0
- data/doc/plugins/model.md +210 -0
- data/doc/plugins/module_include.md +4 -2
- data/doc/plugins/multi_cache.md +24 -0
- data/doc/plugins/persistence.md +101 -0
- data/doc/plugins/presign_endpoint.md +9 -4
- data/doc/plugins/pretty_location.md +16 -3
- data/doc/plugins/processing.md +4 -2
- data/doc/plugins/rack_file.md +8 -2
- data/doc/plugins/rack_response.md +6 -2
- data/doc/plugins/recache.md +4 -2
- data/doc/plugins/refresh_metadata.md +49 -9
- data/doc/plugins/remote_url.md +84 -47
- data/doc/plugins/remove_attachment.md +27 -6
- data/doc/plugins/remove_invalid.md +21 -6
- data/doc/plugins/restore_cached_data.md +11 -3
- data/doc/plugins/sequel.md +159 -35
- data/doc/plugins/signature.md +16 -5
- data/doc/plugins/store_dimensions.md +14 -2
- data/doc/plugins/tempfile.md +4 -2
- data/doc/plugins/type_predicates.md +96 -0
- data/doc/plugins/upload_endpoint.md +13 -13
- data/doc/plugins/upload_options.md +6 -4
- data/doc/plugins/{default_url_options.md → url_options.md} +9 -7
- data/doc/plugins/validation.md +97 -0
- data/doc/plugins/validation_helpers.md +16 -13
- data/doc/plugins/versions.md +15 -19
- data/doc/processing.md +438 -221
- data/doc/refile.md +188 -170
- data/doc/release_notes/1.0.0.md +4 -0
- data/doc/release_notes/1.1.0.md +6 -2
- data/doc/release_notes/1.2.0.md +4 -0
- data/doc/release_notes/1.3.0.md +4 -0
- data/doc/release_notes/1.4.0.md +4 -0
- data/doc/release_notes/1.4.1.md +4 -0
- data/doc/release_notes/1.4.2.md +4 -0
- data/doc/release_notes/2.0.0.md +4 -0
- data/doc/release_notes/2.0.1.md +4 -0
- data/doc/release_notes/2.1.0.md +5 -1
- data/doc/release_notes/2.1.1.md +4 -0
- data/doc/release_notes/2.10.0.md +4 -0
- data/doc/release_notes/2.10.1.md +4 -0
- data/doc/release_notes/2.11.0.md +4 -0
- data/doc/release_notes/2.12.0.md +4 -0
- data/doc/release_notes/2.13.0.md +4 -0
- data/doc/release_notes/2.14.0.md +5 -1
- data/doc/release_notes/2.15.0.md +11 -7
- data/doc/release_notes/2.16.0.md +4 -0
- data/doc/release_notes/2.17.0.md +4 -0
- data/doc/release_notes/2.18.0.md +4 -0
- data/doc/release_notes/2.19.0.md +6 -3
- data/doc/release_notes/2.2.0.md +4 -0
- data/doc/release_notes/2.3.0.md +4 -0
- data/doc/release_notes/2.3.1.md +4 -0
- data/doc/release_notes/2.4.0.md +4 -0
- data/doc/release_notes/2.4.1.md +4 -0
- data/doc/release_notes/2.5.0.md +4 -0
- data/doc/release_notes/2.6.0.md +4 -0
- data/doc/release_notes/2.6.1.md +4 -0
- data/doc/release_notes/2.7.0.md +4 -0
- data/doc/release_notes/2.8.0.md +4 -0
- data/doc/release_notes/2.9.0.md +4 -0
- data/doc/release_notes/3.0.0.md +981 -0
- data/doc/release_notes/3.0.1.md +22 -0
- data/doc/release_notes/3.1.0.md +73 -0
- data/doc/release_notes/3.2.0.md +96 -0
- data/doc/release_notes/3.2.1.md +31 -0
- data/doc/release_notes/3.2.2.md +14 -0
- data/doc/release_notes/3.3.0.md +105 -0
- data/doc/release_notes/3.4.0.md +35 -0
- data/doc/release_notes/3.5.0.md +63 -0
- data/doc/release_notes/3.6.0.md +23 -0
- data/doc/retrieving_uploads.md +5 -2
- data/doc/securing_uploads.md +60 -37
- data/doc/storage/file_system.md +20 -3
- data/doc/storage/memory.md +19 -0
- data/doc/storage/s3.md +122 -78
- data/doc/testing.md +141 -133
- data/doc/upgrading_to_3.md +708 -0
- data/doc/validation.md +54 -90
- data/lib/shrine/attacher.rb +292 -169
- data/lib/shrine/attachment.rb +13 -46
- data/lib/shrine/plugins/_persistence.rb +93 -0
- data/lib/shrine/plugins/activerecord.rb +77 -34
- data/lib/shrine/plugins/add_metadata.rb +25 -17
- data/lib/shrine/plugins/atomic_helpers.rb +119 -0
- data/lib/shrine/plugins/backgrounding.rb +77 -113
- data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
- data/lib/shrine/plugins/column.rb +102 -0
- data/lib/shrine/plugins/data_uri.rb +38 -36
- data/lib/shrine/plugins/default_storage.rb +45 -15
- data/lib/shrine/plugins/default_url.rb +12 -24
- data/lib/shrine/plugins/default_url_options.rb +3 -30
- data/lib/shrine/plugins/delete_raw.rb +10 -16
- data/lib/shrine/plugins/derivation_endpoint.rb +130 -171
- data/lib/shrine/plugins/derivatives.rb +645 -0
- data/lib/shrine/plugins/determine_mime_type.rb +9 -21
- data/lib/shrine/plugins/download_endpoint.rb +118 -133
- data/lib/shrine/plugins/dynamic_storage.rb +5 -11
- data/lib/shrine/plugins/entity.rb +158 -0
- data/lib/shrine/plugins/form_assign.rb +108 -0
- data/lib/shrine/plugins/included.rb +6 -6
- data/lib/shrine/plugins/infer_extension.rb +17 -20
- data/lib/shrine/plugins/instrumentation.rb +59 -43
- data/lib/shrine/plugins/keep_files.rb +3 -15
- data/lib/shrine/plugins/metadata_attributes.rb +28 -19
- data/lib/shrine/plugins/mirroring.rb +142 -0
- data/lib/shrine/plugins/model.rb +160 -0
- data/lib/shrine/plugins/module_include.rb +3 -3
- data/lib/shrine/plugins/multi_cache.rb +27 -0
- data/lib/shrine/plugins/presign_endpoint.rb +27 -28
- data/lib/shrine/plugins/pretty_location.rb +15 -9
- data/lib/shrine/plugins/processing.rb +22 -9
- data/lib/shrine/plugins/rack_file.rb +2 -42
- data/lib/shrine/plugins/rack_response.rb +21 -10
- data/lib/shrine/plugins/recache.rb +6 -5
- data/lib/shrine/plugins/refresh_metadata.rb +13 -11
- data/lib/shrine/plugins/remote_url.rb +49 -49
- data/lib/shrine/plugins/remove_attachment.rb +12 -6
- data/lib/shrine/plugins/remove_invalid.rb +19 -8
- data/lib/shrine/plugins/restore_cached_data.rb +13 -7
- data/lib/shrine/plugins/sequel.rb +86 -36
- data/lib/shrine/plugins/signature.rb +10 -16
- data/lib/shrine/plugins/store_dimensions.rb +35 -40
- data/lib/shrine/plugins/tempfile.rb +1 -3
- data/lib/shrine/plugins/type_predicates.rb +113 -0
- data/lib/shrine/plugins/upload_endpoint.rb +28 -24
- data/lib/shrine/plugins/upload_options.rb +14 -15
- data/lib/shrine/plugins/url_options.rb +31 -0
- data/lib/shrine/plugins/validation.rb +80 -0
- data/lib/shrine/plugins/validation_helpers.rb +35 -58
- data/lib/shrine/plugins/versions.rb +107 -87
- data/lib/shrine/plugins.rb +22 -0
- data/lib/shrine/storage/file_system.rb +46 -64
- data/lib/shrine/storage/linter.rb +42 -7
- data/lib/shrine/storage/memory.rb +49 -0
- data/lib/shrine/storage/s3.rb +173 -160
- data/lib/shrine/uploaded_file.rb +32 -32
- data/lib/shrine/version.rb +3 -3
- data/lib/shrine.rb +87 -150
- data/shrine.gemspec +11 -12
- metadata +92 -82
- data/doc/migrating_storage.md +0 -76
- data/doc/plugins/backup.md +0 -31
- data/doc/plugins/copy.md +0 -24
- data/doc/plugins/delete_promoted.md +0 -12
- data/doc/plugins/direct_upload.md +0 -172
- data/doc/plugins/hooks.md +0 -58
- data/doc/plugins/logging.md +0 -42
- data/doc/plugins/migration_helpers.md +0 -60
- data/doc/plugins/moving.md +0 -19
- data/doc/plugins/multi_delete.md +0 -20
- data/doc/plugins/parallelize.md +0 -16
- data/doc/plugins/parsed_json.md +0 -23
- data/doc/regenerating_versions.md +0 -143
- data/lib/shrine/plugins/background_helpers.rb +0 -5
- data/lib/shrine/plugins/backup.rb +0 -90
- data/lib/shrine/plugins/copy.rb +0 -50
- data/lib/shrine/plugins/delete_promoted.rb +0 -20
- data/lib/shrine/plugins/direct_upload.rb +0 -217
- data/lib/shrine/plugins/hooks.rb +0 -90
- data/lib/shrine/plugins/logging.rb +0 -142
- data/lib/shrine/plugins/migration_helpers.rb +0 -70
- data/lib/shrine/plugins/moving.rb +0 -57
- data/lib/shrine/plugins/multi_delete.rb +0 -32
- data/lib/shrine/plugins/parallelize.rb +0 -78
- data/lib/shrine/plugins/parsed_json.rb +0 -29
data/lib/shrine/attacher.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Shrine
|
|
4
|
-
# Core class
|
|
5
|
-
#
|
|
4
|
+
# Core class that handles attaching files. It uses Shrine and
|
|
5
|
+
# Shrine::UploadedFile objects internally.
|
|
6
6
|
class Attacher
|
|
7
7
|
@shrine_class = ::Shrine
|
|
8
8
|
|
|
@@ -18,195 +18,319 @@ class Shrine
|
|
|
18
18
|
"#{shrine_class.inspect}::Attacher"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
# validation. Example:
|
|
21
|
+
# Initializes the attacher from a data hash generated from `Attacher#data`.
|
|
23
22
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
define_method(:validate_block, &block)
|
|
31
|
-
private :validate_block
|
|
23
|
+
# attacher = Attacher.from_data({ "id" => "...", "storage" => "...", "metadata" => { ... } })
|
|
24
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
|
25
|
+
def from_data(data, **options)
|
|
26
|
+
attacher = new(**options)
|
|
27
|
+
attacher.load_data(data)
|
|
28
|
+
attacher
|
|
32
29
|
end
|
|
33
30
|
end
|
|
34
31
|
|
|
35
32
|
module InstanceMethods
|
|
36
|
-
# Returns the
|
|
37
|
-
attr_reader :
|
|
38
|
-
|
|
39
|
-
# Returns the uploader that is used for the permanent storage.
|
|
40
|
-
attr_reader :store
|
|
33
|
+
# Returns the attached uploaded file.
|
|
34
|
+
attr_reader :file
|
|
41
35
|
|
|
42
|
-
# Returns
|
|
43
|
-
#
|
|
44
|
-
# uploader.
|
|
36
|
+
# Returns options that are automatically forwarded to the uploader.
|
|
37
|
+
# Can be modified with additional data.
|
|
45
38
|
attr_reader :context
|
|
46
39
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@
|
|
54
|
-
@store = shrine_class.new(store)
|
|
55
|
-
@context = { record: record, name: name }
|
|
56
|
-
@errors = []
|
|
40
|
+
# Initializes the attached file, temporary and permanent storage.
|
|
41
|
+
def initialize(file: nil, cache: :cache, store: :store)
|
|
42
|
+
@file = file
|
|
43
|
+
@cache = cache
|
|
44
|
+
@store = store
|
|
45
|
+
@context = {}
|
|
46
|
+
@previous = nil
|
|
57
47
|
end
|
|
58
48
|
|
|
59
|
-
# Returns the
|
|
60
|
-
def
|
|
49
|
+
# Returns the temporary storage identifier.
|
|
50
|
+
def cache_key; @cache.to_sym; end
|
|
51
|
+
# Returns the permanent storage identifier.
|
|
52
|
+
def store_key; @store.to_sym; end
|
|
61
53
|
|
|
62
|
-
# Returns the
|
|
63
|
-
def
|
|
54
|
+
# Returns the uploader that is used for the temporary storage.
|
|
55
|
+
def cache; shrine_class.new(cache_key); end
|
|
56
|
+
# Returns the uploader that is used for the permanent storage.
|
|
57
|
+
def store; shrine_class.new(store_key); end
|
|
64
58
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
59
|
+
# Calls #attach_cached, but skips if value is an empty string (this is
|
|
60
|
+
# useful when the uploaded file comes from form fields). Forwards any
|
|
61
|
+
# additional options to #attach_cached.
|
|
62
|
+
#
|
|
63
|
+
# attacher.assign(File.open(...))
|
|
64
|
+
# attacher.assign(File.open(...), metadata: { "foo" => "bar" })
|
|
65
|
+
# attacher.assign('{"id":"...","storage":"cache","metadata":{...}}')
|
|
66
|
+
# attacher.assign({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
|
67
|
+
#
|
|
68
|
+
# # ignores the assignment when a blank string is given
|
|
69
|
+
# attacher.assign("")
|
|
69
70
|
def assign(value, **options)
|
|
70
|
-
if value
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
uploaded_file = cache!(value, action: :cache, **options) if value
|
|
75
|
-
set(uploaded_file)
|
|
71
|
+
return if value == "" # skip empty hidden field
|
|
72
|
+
|
|
73
|
+
if value.is_a?(Hash) || value.is_a?(String)
|
|
74
|
+
return if uploaded_file(value) == file # skip assignment for current file
|
|
76
75
|
end
|
|
76
|
+
|
|
77
|
+
attach_cached(value, **options)
|
|
77
78
|
end
|
|
78
79
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
# Sets an existing cached file, or uploads an IO object to temporary
|
|
81
|
+
# storage and sets it via #attach. Forwards any additional options to
|
|
82
|
+
# #attach.
|
|
83
|
+
#
|
|
84
|
+
# # upload file to temporary storage and set the uploaded file.
|
|
85
|
+
# attacher.attach_cached(File.open(...))
|
|
86
|
+
#
|
|
87
|
+
# # foward additional options to the uploader
|
|
88
|
+
# attacher.attach_cached(File.open(...), metadata: { "foo" => "bar" })
|
|
89
|
+
#
|
|
90
|
+
# # sets an existing cached file from JSON data
|
|
91
|
+
# attacher.attach_cached('{"id":"...","storage":"cache","metadata":{...}}')
|
|
92
|
+
#
|
|
93
|
+
# # sets an existing cached file from Hash data
|
|
94
|
+
# attacher.attach_cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
|
95
|
+
def attach_cached(value, **options)
|
|
96
|
+
if value.is_a?(String) || value.is_a?(Hash)
|
|
97
|
+
change(cached(value, **options))
|
|
98
|
+
else
|
|
99
|
+
attach(value, storage: cache_key, action: :cache, **options)
|
|
100
|
+
end
|
|
87
101
|
end
|
|
88
102
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
# Uploads given IO object and changes the uploaded file.
|
|
104
|
+
#
|
|
105
|
+
# # uploads the file to permanent storage
|
|
106
|
+
# attacher.attach(io)
|
|
107
|
+
#
|
|
108
|
+
# # uploads the file to specified storage
|
|
109
|
+
# attacher.attach(io, storage: :other_store)
|
|
110
|
+
#
|
|
111
|
+
# # forwards additional options to the uploader
|
|
112
|
+
# attacher.attach(io, upload_options: { acl: "public-read" }, metadata: { "foo" => "bar" })
|
|
113
|
+
#
|
|
114
|
+
# # removes the attachment
|
|
115
|
+
# attacher.attach(nil)
|
|
116
|
+
def attach(io, storage: store_key, **options)
|
|
117
|
+
file = upload(io, storage, **options) if io
|
|
118
|
+
|
|
119
|
+
change(file)
|
|
93
120
|
end
|
|
94
121
|
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
122
|
+
# Deletes any previous file and promotes newly attached cached file.
|
|
123
|
+
# It also clears any dirty tracking.
|
|
124
|
+
#
|
|
125
|
+
# # promoting cached file
|
|
126
|
+
# attacher.assign(io)
|
|
127
|
+
# attacher.cached? #=> true
|
|
128
|
+
# attacher.finalize
|
|
129
|
+
# attacher.stored?
|
|
130
|
+
#
|
|
131
|
+
# # deleting previous file
|
|
132
|
+
# previous_file = attacher.file
|
|
133
|
+
# previous_file.exists? #=> true
|
|
134
|
+
# attacher.assign(io)
|
|
135
|
+
# attacher.finalize
|
|
136
|
+
# previous_file.exists? #=> false
|
|
137
|
+
#
|
|
138
|
+
# # clearing dirty tracking
|
|
139
|
+
# attacher.assign(io)
|
|
140
|
+
# attacher.changed? #=> true
|
|
141
|
+
# attacher.finalize
|
|
142
|
+
# attacher.changed? #=> false
|
|
143
|
+
def finalize
|
|
144
|
+
destroy_previous
|
|
145
|
+
promote_cached
|
|
146
|
+
@previous = nil
|
|
98
147
|
end
|
|
99
|
-
alias attached? changed?
|
|
100
148
|
|
|
101
|
-
# Plugins can override this if they want something to be done
|
|
102
|
-
# save.
|
|
149
|
+
# Plugins can override this if they want something to be done in a
|
|
150
|
+
# "before save" callback.
|
|
103
151
|
def save
|
|
104
152
|
end
|
|
105
153
|
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
154
|
+
# If a new cached file has been attached, uploads it to permanent storage.
|
|
155
|
+
# Any additional options are forwarded to #promote.
|
|
156
|
+
#
|
|
157
|
+
# attacher.assign(io)
|
|
158
|
+
# attacher.cached? #=> true
|
|
159
|
+
# attacher.promote_cached
|
|
160
|
+
# attacher.stored? #=> true
|
|
161
|
+
def promote_cached(**options)
|
|
162
|
+
promote(**options) if promote?
|
|
113
163
|
end
|
|
114
164
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
165
|
+
# Uploads current file to permanent storage and sets the stored file.
|
|
166
|
+
#
|
|
167
|
+
# attacher.cached? #=> true
|
|
168
|
+
# attacher.promote
|
|
169
|
+
# attacher.stored? #=> true
|
|
170
|
+
def promote(storage: store_key, **options)
|
|
171
|
+
set upload(file, storage, action: :store, **options)
|
|
118
172
|
end
|
|
119
173
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
174
|
+
# Delegates to `Shrine.upload`, passing the #context.
|
|
175
|
+
#
|
|
176
|
+
# # upload file to specified storage
|
|
177
|
+
# attacher.upload(io, :store) #=> #<Shrine::UploadedFile>
|
|
178
|
+
#
|
|
179
|
+
# # pass additional options for the uploader
|
|
180
|
+
# attacher.upload(io, :store, metadata: { "foo" => "bar" })
|
|
181
|
+
def upload(io, storage = store_key, **options)
|
|
182
|
+
shrine_class.upload(io, storage, **context, **options)
|
|
126
183
|
end
|
|
127
184
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
185
|
+
# If a new file was attached, deletes previously attached file if any.
|
|
186
|
+
#
|
|
187
|
+
# previous_file = attacher.file
|
|
188
|
+
# attacher.attach(file)
|
|
189
|
+
# attacher.destroy_previous
|
|
190
|
+
# previous_file.exists? #=> false
|
|
191
|
+
def destroy_previous
|
|
192
|
+
@previous.destroy_attached if changed?
|
|
133
193
|
end
|
|
134
194
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
195
|
+
# Destroys the attached file if it exists and is uploaded to permanent
|
|
196
|
+
# storage.
|
|
197
|
+
#
|
|
198
|
+
# attacher.file.exists? #=> true
|
|
199
|
+
# attacher.destroy_attached
|
|
200
|
+
# attacher.file.exists? #=> false
|
|
201
|
+
def destroy_attached
|
|
202
|
+
destroy if destroy?
|
|
139
203
|
end
|
|
140
204
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
205
|
+
# Destroys the attachment.
|
|
206
|
+
#
|
|
207
|
+
# attacher.file.exists? #=> true
|
|
208
|
+
# attacher.destroy
|
|
209
|
+
# attacher.file.exists? #=> false
|
|
143
210
|
def destroy
|
|
144
|
-
file
|
|
145
|
-
_delete(file, action: :destroy) if file && !cached?(file)
|
|
211
|
+
file&.delete
|
|
146
212
|
end
|
|
147
213
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
214
|
+
# Sets the uploaded file with dirty tracking, and runs validations.
|
|
215
|
+
#
|
|
216
|
+
# attacher.change(uploaded_file)
|
|
217
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
|
218
|
+
# attacher.changed? #=> true
|
|
219
|
+
def change(file)
|
|
220
|
+
@previous = dup if change?(file)
|
|
221
|
+
set(file)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Sets the uploaded file.
|
|
225
|
+
#
|
|
226
|
+
# attacher.set(uploaded_file)
|
|
227
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
|
228
|
+
# attacher.changed? #=> false
|
|
229
|
+
def set(file)
|
|
230
|
+
self.file = file
|
|
151
231
|
end
|
|
152
232
|
|
|
153
|
-
# Returns the
|
|
154
|
-
#
|
|
233
|
+
# Returns the attached file.
|
|
234
|
+
#
|
|
235
|
+
# # when a file is attached
|
|
236
|
+
# attacher.get #=> #<Shrine::UploadedFile>
|
|
237
|
+
#
|
|
238
|
+
# # when no file is attached
|
|
239
|
+
# attacher.get #=> nil
|
|
240
|
+
def get
|
|
241
|
+
file
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# If a file is attached, returns the uploaded file URL, otherwise returns
|
|
245
|
+
# nil. Any options are forwarded to the storage.
|
|
246
|
+
#
|
|
247
|
+
# attacher.file = file
|
|
248
|
+
# attacher.url #=> "https://..."
|
|
249
|
+
#
|
|
250
|
+
# attacher.file = nil
|
|
251
|
+
# attacher.url #=> nil
|
|
155
252
|
def url(**options)
|
|
156
|
-
|
|
253
|
+
file&.url(**options)
|
|
157
254
|
end
|
|
158
255
|
|
|
159
|
-
# Returns
|
|
160
|
-
|
|
161
|
-
|
|
256
|
+
# Returns whether the attachment has changed.
|
|
257
|
+
#
|
|
258
|
+
# attacher.changed? #=> false
|
|
259
|
+
# attacher.attach(file)
|
|
260
|
+
# attacher.changed? #=> true
|
|
261
|
+
def changed?
|
|
262
|
+
!!@previous
|
|
162
263
|
end
|
|
163
264
|
|
|
164
|
-
# Returns
|
|
165
|
-
|
|
166
|
-
|
|
265
|
+
# Returns whether a file is attached.
|
|
266
|
+
#
|
|
267
|
+
# attacher.attach(io)
|
|
268
|
+
# attacher.attached? #=> true
|
|
269
|
+
#
|
|
270
|
+
# attacher.attach(nil)
|
|
271
|
+
# attacher.attached? #=> false
|
|
272
|
+
def attached?
|
|
273
|
+
!!file
|
|
167
274
|
end
|
|
168
275
|
|
|
169
|
-
# Returns
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
276
|
+
# Returns whether the file is uploaded to temporary storage.
|
|
277
|
+
#
|
|
278
|
+
# attacher.cached? # checks current file
|
|
279
|
+
# attacher.cached?(file) # checks given file
|
|
280
|
+
def cached?(file = self.file)
|
|
281
|
+
uploaded?(file, cache_key)
|
|
173
282
|
end
|
|
174
283
|
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
284
|
+
# Returns whether the file is uploaded to permanent storage.
|
|
285
|
+
#
|
|
286
|
+
# attacher.stored? # checks current file
|
|
287
|
+
# attacher.stored?(file) # checks given file
|
|
288
|
+
def stored?(file = self.file)
|
|
289
|
+
uploaded?(file, store_key)
|
|
180
290
|
end
|
|
181
291
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
292
|
+
# Generates serializable data for the attachment.
|
|
293
|
+
#
|
|
294
|
+
# attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
|
|
295
|
+
def data
|
|
296
|
+
file&.data
|
|
186
297
|
end
|
|
187
298
|
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
299
|
+
# Loads the uploaded file from data generated by `Attacher#data`.
|
|
300
|
+
#
|
|
301
|
+
# attacher.file #=> nil
|
|
302
|
+
# attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => { ... } })
|
|
303
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
|
304
|
+
def load_data(data)
|
|
305
|
+
@file = data && uploaded_file(data)
|
|
192
306
|
end
|
|
193
307
|
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
308
|
+
# Saves the given uploaded file to an instance variable.
|
|
309
|
+
#
|
|
310
|
+
# attacher.file = uploaded_file
|
|
311
|
+
# attacher.file #=> #<Shrine::UploadedFile>
|
|
312
|
+
def file=(file)
|
|
313
|
+
unless file.is_a?(Shrine::UploadedFile) || file.nil?
|
|
314
|
+
fail ArgumentError, "expected file to be a Shrine::UploadedFile or nil, got #{file.inspect}"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
@file = file
|
|
198
318
|
end
|
|
199
319
|
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
shrine_class.uploaded_file(object, &block)
|
|
320
|
+
# Returns attached file or raises an exception if no file is attached.
|
|
321
|
+
def file!
|
|
322
|
+
file or fail Error, "no file is attached"
|
|
204
323
|
end
|
|
205
324
|
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
325
|
+
# Converts JSON or Hash data into a Shrine::UploadedFile object.
|
|
326
|
+
#
|
|
327
|
+
# attacher.uploaded_file('{"id":"...","storage":"...","metadata":{...}}')
|
|
328
|
+
# #=> #<Shrine::UploadedFile ...>
|
|
329
|
+
#
|
|
330
|
+
# attacher.uploaded_file({ "id" => "...", "storage" => "...", "metadata" => {} })
|
|
331
|
+
# #=> #<Shrine::UploadedFile ...>
|
|
332
|
+
def uploaded_file(value)
|
|
333
|
+
shrine_class.uploaded_file(value)
|
|
210
334
|
end
|
|
211
335
|
|
|
212
336
|
# Returns the Shrine class that this attacher's class is namespaced
|
|
@@ -217,54 +341,53 @@ class Shrine
|
|
|
217
341
|
|
|
218
342
|
private
|
|
219
343
|
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
344
|
+
# The copy constructor that's called on #dup and #clone
|
|
345
|
+
# We need to duplicate the context to prevent it from being shared
|
|
346
|
+
def initialize_copy(other)
|
|
347
|
+
super
|
|
348
|
+
@context = @context.dup
|
|
223
349
|
end
|
|
224
350
|
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
351
|
+
# Converts a String or Hash value into an UploadedFile object and ensures
|
|
352
|
+
# it's uploaded to temporary storage.
|
|
353
|
+
#
|
|
354
|
+
# # from JSON data
|
|
355
|
+
# attacher.cached('{"id":"...","storage":"cache","metadata":{...}}')
|
|
356
|
+
# #=> #<Shrine::UploadedFile>
|
|
357
|
+
#
|
|
358
|
+
# # from Hash data
|
|
359
|
+
# attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => { ... } })
|
|
360
|
+
# #=> #<Shrine::UploadedFile>
|
|
361
|
+
def cached(value, **)
|
|
362
|
+
uploaded_file = uploaded_file(value)
|
|
363
|
+
|
|
364
|
+
# reject files not uploaded to temporary storage, because otherwise
|
|
365
|
+
# attackers could hijack other users' attachments
|
|
366
|
+
unless cached?(uploaded_file)
|
|
367
|
+
fail Shrine::Error, "expected cached file, got #{uploaded_file.inspect}"
|
|
368
|
+
end
|
|
242
369
|
|
|
243
|
-
|
|
244
|
-
def write(value)
|
|
245
|
-
record.send(:"#{data_attribute}=", value)
|
|
370
|
+
uploaded_file
|
|
246
371
|
end
|
|
247
372
|
|
|
248
|
-
#
|
|
249
|
-
def
|
|
250
|
-
|
|
373
|
+
# Whether attached file should be uploaded to permanent storage.
|
|
374
|
+
def promote?
|
|
375
|
+
changed? && cached?
|
|
251
376
|
end
|
|
252
377
|
|
|
253
|
-
#
|
|
254
|
-
def
|
|
255
|
-
|
|
378
|
+
# Whether attached file should be deleted.
|
|
379
|
+
def destroy?
|
|
380
|
+
attached? && !cached?
|
|
256
381
|
end
|
|
257
382
|
|
|
258
|
-
#
|
|
259
|
-
def
|
|
260
|
-
|
|
383
|
+
# Whether assigning the given file is considered a change.
|
|
384
|
+
def change?(file)
|
|
385
|
+
@file != file
|
|
261
386
|
end
|
|
262
387
|
|
|
263
|
-
#
|
|
264
|
-
def
|
|
265
|
-
|
|
266
|
-
options[:action] = options[:phase] if options.key?(:phase)
|
|
267
|
-
options
|
|
388
|
+
# Returns whether the file is uploaded to specified storage.
|
|
389
|
+
def uploaded?(file, storage_key)
|
|
390
|
+
file&.storage_key == storage_key
|
|
268
391
|
end
|
|
269
392
|
end
|
|
270
393
|
|
data/lib/shrine/attachment.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Shrine
|
|
4
|
-
# Core class
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Core class that provides an attachment interface for a specified attribute
|
|
5
|
+
# name, which can be added to model/entity classes. The model/entity plugins
|
|
6
|
+
# define the main interface, which delegates to a Shrine::Attacher object.
|
|
7
7
|
class Attachment < Module
|
|
8
8
|
@shrine_class = ::Shrine
|
|
9
9
|
|
|
@@ -18,6 +18,13 @@ class Shrine
|
|
|
18
18
|
def inspect
|
|
19
19
|
"#{shrine_class.inspect}::Attachment"
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
# Shorthand for `Attachment.new`.
|
|
23
|
+
#
|
|
24
|
+
# Shrine::Attachment[:image]
|
|
25
|
+
def [](*args, **options)
|
|
26
|
+
new(*args, **options)
|
|
27
|
+
end
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
module InstanceMethods
|
|
@@ -27,40 +34,6 @@ class Shrine
|
|
|
27
34
|
def initialize(name, **options)
|
|
28
35
|
@name = name.to_sym
|
|
29
36
|
@options = options
|
|
30
|
-
|
|
31
|
-
define_attachment_methods!
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Defines attachment methods for the specified attachment name. These
|
|
35
|
-
# methods will be added to any model that includes this module.
|
|
36
|
-
def define_attachment_methods!
|
|
37
|
-
attachment = self
|
|
38
|
-
name = attachment_name
|
|
39
|
-
|
|
40
|
-
define_method :"#{name}_attacher" do |**options|
|
|
41
|
-
if !instance_variable_get(:"@#{name}_attacher") || options.any?
|
|
42
|
-
instance_variable_set(:"@#{name}_attacher", attachment.build_attacher(self, options))
|
|
43
|
-
else
|
|
44
|
-
instance_variable_get(:"@#{name}_attacher")
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
define_method :"#{name}=" do |value|
|
|
49
|
-
send(:"#{name}_attacher").assign(value)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
define_method :"#{name}" do
|
|
53
|
-
send(:"#{name}_attacher").get
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
define_method :"#{name}_url" do |*args|
|
|
57
|
-
send(:"#{name}_attacher").url(*args)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Creates an instance of the corresponding Attacher subclass.
|
|
62
|
-
def build_attacher(object, options)
|
|
63
|
-
shrine_class::Attacher.new(object, @name, @options.merge(options))
|
|
64
37
|
end
|
|
65
38
|
|
|
66
39
|
# Returns name of the attachment this module provides.
|
|
@@ -75,17 +48,11 @@ class Shrine
|
|
|
75
48
|
|
|
76
49
|
# Returns class name with attachment name included.
|
|
77
50
|
#
|
|
78
|
-
# Shrine
|
|
79
|
-
def to_s
|
|
80
|
-
"#<#{self.class.inspect}(#{attachment_name})>"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Returns class name with attachment name included.
|
|
84
|
-
#
|
|
85
|
-
# Shrine[:image].inspect #=> "#<Shrine::Attachment(image)>"
|
|
51
|
+
# Shrine::Attachment.new(:image).to_s #=> "#<Shrine::Attachment(image)>"
|
|
86
52
|
def inspect
|
|
87
|
-
"#<#{self.class.inspect}(#{
|
|
53
|
+
"#<#{self.class.inspect}(#{@name})>"
|
|
88
54
|
end
|
|
55
|
+
alias to_s inspect
|
|
89
56
|
|
|
90
57
|
# Returns the Shrine class that this attachment's class is namespaced
|
|
91
58
|
# under.
|