module Shrine::Attacher::InstanceMethods

  1. lib/shrine/attacher.rb

Included modules

  1. InstanceMethods

Attributes

context [R]

Returns options that are automatically forwarded to the uploader. Can be modified with additional data.

file [R]

Returns the attached uploaded file.

Public Class methods

new(file: nil, cache: :cache, store: :store)

Initializes the attached file, temporary and permanent storage.

[show source]
   # File lib/shrine/attacher.rb
41 def initialize(file: nil, cache: :cache, store: :store)
42   @file     = file
43   @cache    = cache
44   @store    = store
45   @context  = {}
46   @previous = nil
47 end

Public Instance methods

assign(value, **)

Calls attach_cached, but skips if value is an empty string (this is useful when the uploaded file comes from form fields). Forwards any additional options to attach_cached.

attacher.assign(File.open(...))
attacher.assign(File.open(...), metadata: { "foo" => "bar" })
attacher.assign('{"id":"...","storage":"cache","metadata":{...}}')
attacher.assign({ "id" => "...", "storage" => "cache", "metadata" => {} })

# ignores the assignment when a blank string is given
attacher.assign("")
[show source]
   # File lib/shrine/attacher.rb
70 def assign(value, **)
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
75   end
76 
77   attach_cached(value, **)
78 end
attach(io, storage: store_key, **)

Uploads given IO object and changes the uploaded file.

# uploads the file to permanent storage
attacher.attach(io)

# uploads the file to specified storage
attacher.attach(io, storage: :other_store)

# forwards additional options to the uploader
attacher.attach(io, upload_options: { acl: "public-read" }, metadata: { "foo" => "bar" })

# removes the attachment
attacher.attach(nil)
[show source]
    # File lib/shrine/attacher.rb
116 def attach(io, storage: store_key, **)
117   file = upload(io, storage, **) if io
118 
119   change(file)
120 end
attach_cached(value, **)

Sets an existing cached file, or uploads an IO object to temporary storage and sets it via attach. Forwards any additional options to

attach.

# upload file to temporary storage and set the uploaded file.
attacher.attach_cached(File.open(...))

# foward additional options to the uploader
attacher.attach_cached(File.open(...), metadata: { "foo" => "bar" })

# sets an existing cached file from JSON data
attacher.attach_cached('{"id":"...","storage":"cache","metadata":{...}}')

# sets an existing cached file from Hash data
attacher.attach_cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
[show source]
    # File lib/shrine/attacher.rb
 95 def attach_cached(value, **)
 96   if value.is_a?(String) || value.is_a?(Hash)
 97     change(cached(value, **))
 98   else
 99     attach(value, storage: cache_key, action: :cache, **)
100   end
101 end
attached?()

Returns whether a file is attached.

attacher.attach(io)
attacher.attached? #=> true

attacher.attach(nil)
attacher.attached? #=> false
[show source]
    # File lib/shrine/attacher.rb
272 def attached?
273   !!file
274 end
cache(= shrine_class.new(cache_key))

Returns the uploader that is used for the temporary storage.

[show source]
    # File lib/shrine/attacher.rb
 55     def cache = shrine_class.new(cache_key)
 56     # Returns the uploader that is used for the permanent storage.
 57     def store = shrine_class.new(store_key)
 58 
 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("")
 70     def assign(value, **)
 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
 75       end
 76 
 77       attach_cached(value, **)
 78     end
 79 
 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, **)
 96       if value.is_a?(String) || value.is_a?(Hash)
 97         change(cached(value, **))
 98       else
 99         attach(value, storage: cache_key, action: :cache, **)
100       end
101     end
102 
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, **)
117       file = upload(io, storage, **) if io
118 
119       change(file)
120     end
121 
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
147     end
148 
149     # Plugins can override this if they want something to be done in a
150     # "before save" callback.
151     def save
152     end
153 
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(**)
162       promote(**) if promote?
163     end
164 
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, **)
171       set upload(file, storage, action: :store, **)
172     end
173 
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, **)
182       shrine_class.upload(io, storage, **context, **)
183     end
184 
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?
193     end
194 
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?
203     end
204 
205     # Destroys the attachment.
206     #
207     #     attacher.file.exists? #=> true
208     #     attacher.destroy
209     #     attacher.file.exists? #=> false
210     def destroy
211       file&.delete
212     end
213 
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
231     end
232 
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
252     def url(**)
253       file&.url(**)
254     end
255 
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
263     end
264 
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
274     end
275 
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)
282     end
283 
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)
290     end
291 
292     # Generates serializable data for the attachment.
293     #
294     #     attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
295     def data
296       file&.data
297     end
298 
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)
306     end
307 
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
318     end
319 
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"
323     end
324 
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)
334     end
335 
336     # Returns the Shrine class that this attacher's class is namespaced
337     # under.
338     def shrine_class
339       self.class.shrine_class
340     end
341 
342     private
343 
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
349     end
350 
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
369 
370       uploaded_file
371     end
372 
373     # Whether attached file should be uploaded to permanent storage.
374     def promote?
375       changed? && cached?
376     end
377 
378     # Whether attached file should be deleted.
379     def destroy?
380       attached? && !cached?
381     end
382 
383     # Whether assigning the given file is considered a change.
384     def change?(file)
385       @file != file
386     end
387 
388     # Returns whether the file is uploaded to specified storage.
389     def uploaded?(file, storage_key)
390       file&.storage_key == storage_key
391     end
392   end
393 
394   extend ClassMethods
395   include InstanceMethods
396 end
cache_key(= @cache.to_sym)

Returns the temporary storage identifier.

[show source]
    # File lib/shrine/attacher.rb
 50     def cache_key = @cache.to_sym
 51     # Returns the permanent storage identifier.
 52     def store_key = @store.to_sym
 53 
 54     # Returns the uploader that is used for the temporary storage.
 55     def cache = shrine_class.new(cache_key)
 56     # Returns the uploader that is used for the permanent storage.
 57     def store = shrine_class.new(store_key)
 58 
 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("")
 70     def assign(value, **)
 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
 75       end
 76 
 77       attach_cached(value, **)
 78     end
 79 
 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, **)
 96       if value.is_a?(String) || value.is_a?(Hash)
 97         change(cached(value, **))
 98       else
 99         attach(value, storage: cache_key, action: :cache, **)
100       end
101     end
102 
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, **)
117       file = upload(io, storage, **) if io
118 
119       change(file)
120     end
121 
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
147     end
148 
149     # Plugins can override this if they want something to be done in a
150     # "before save" callback.
151     def save
152     end
153 
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(**)
162       promote(**) if promote?
163     end
164 
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, **)
171       set upload(file, storage, action: :store, **)
172     end
173 
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, **)
182       shrine_class.upload(io, storage, **context, **)
183     end
184 
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?
193     end
194 
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?
203     end
204 
205     # Destroys the attachment.
206     #
207     #     attacher.file.exists? #=> true
208     #     attacher.destroy
209     #     attacher.file.exists? #=> false
210     def destroy
211       file&.delete
212     end
213 
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
231     end
232 
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
252     def url(**)
253       file&.url(**)
254     end
255 
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
263     end
264 
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
274     end
275 
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)
282     end
283 
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)
290     end
291 
292     # Generates serializable data for the attachment.
293     #
294     #     attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
295     def data
296       file&.data
297     end
298 
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)
306     end
307 
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
318     end
319 
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"
323     end
324 
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)
334     end
335 
336     # Returns the Shrine class that this attacher's class is namespaced
337     # under.
338     def shrine_class
339       self.class.shrine_class
340     end
341 
342     private
343 
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
349     end
350 
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
369 
370       uploaded_file
371     end
372 
373     # Whether attached file should be uploaded to permanent storage.
374     def promote?
375       changed? && cached?
376     end
377 
378     # Whether attached file should be deleted.
379     def destroy?
380       attached? && !cached?
381     end
382 
383     # Whether assigning the given file is considered a change.
384     def change?(file)
385       @file != file
386     end
387 
388     # Returns whether the file is uploaded to specified storage.
389     def uploaded?(file, storage_key)
390       file&.storage_key == storage_key
391     end
392   end
393 
394   extend ClassMethods
395   include InstanceMethods
396 end
cached(value, **)

Converts a String or Hash value into an UploadedFile object and ensures it’s uploaded to temporary storage.

# from JSON data
attacher.cached('{"id":"...","storage":"cache","metadata":{...}}')
#=> #<Shrine::UploadedFile>

# from Hash data
attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => { ... } })
#=> #<Shrine::UploadedFile>
[show source]
    # File lib/shrine/attacher.rb
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
369 
370   uploaded_file
371 end
cached?(file = self.file)

Returns whether the file is uploaded to temporary storage.

attacher.cached?       # checks current file
attacher.cached?(file) # checks given file
[show source]
    # File lib/shrine/attacher.rb
280 def cached?(file = self.file)
281   uploaded?(file, cache_key)
282 end
change(file)

Sets the uploaded file with dirty tracking, and runs validations.

attacher.change(uploaded_file)
attacher.file #=> #<Shrine::UploadedFile>
attacher.changed? #=> true
[show source]
    # File lib/shrine/attacher.rb
219 def change(file)
220   @previous = dup if change?(file)
221   set(file)
222 end
change?(file)

Whether assigning the given file is considered a change.

[show source]
    # File lib/shrine/attacher.rb
384 def change?(file)
385   @file != file
386 end
changed?()

Returns whether the attachment has changed.

attacher.changed? #=> false
attacher.attach(file)
attacher.changed? #=> true
[show source]
    # File lib/shrine/attacher.rb
261 def changed?
262   !!@previous
263 end
data()

Generates serializable data for the attachment.

attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
[show source]
    # File lib/shrine/attacher.rb
295 def data
296   file&.data
297 end
destroy()

Destroys the attachment.

attacher.file.exists? #=> true
attacher.destroy
attacher.file.exists? #=> false
[show source]
    # File lib/shrine/attacher.rb
210 def destroy
211   file&.delete
212 end
destroy?()

Whether attached file should be deleted.

[show source]
    # File lib/shrine/attacher.rb
379 def destroy?
380   attached? && !cached?
381 end
destroy_attached()

Destroys the attached file if it exists and is uploaded to permanent storage.

attacher.file.exists? #=> true
attacher.destroy_attached
attacher.file.exists? #=> false
[show source]
    # File lib/shrine/attacher.rb
201 def destroy_attached
202   destroy if destroy?
203 end
destroy_previous()

If a new file was attached, deletes previously attached file if any.

previous_file = attacher.file
attacher.attach(file)
attacher.destroy_previous
previous_file.exists? #=> false
[show source]
    # File lib/shrine/attacher.rb
191 def destroy_previous
192   @previous.destroy_attached if changed?
193 end
file!()

Returns attached file or raises an exception if no file is attached.

[show source]
    # File lib/shrine/attacher.rb
321 def file!
322   file or fail Error, "no file is attached"
323 end
file=(file)

Saves the given uploaded file to an instance variable.

attacher.file = uploaded_file
attacher.file #=> #<Shrine::UploadedFile>
[show source]
    # File lib/shrine/attacher.rb
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
318 end
finalize()

Deletes any previous file and promotes newly attached cached file. It also clears any dirty tracking.

# promoting cached file
attacher.assign(io)
attacher.cached? #=> true
attacher.finalize
attacher.stored?

# deleting previous file
previous_file = attacher.file
previous_file.exists? #=> true
attacher.assign(io)
attacher.finalize
previous_file.exists? #=> false

# clearing dirty tracking
attacher.assign(io)
attacher.changed? #=> true
attacher.finalize
attacher.changed? #=> false
[show source]
    # File lib/shrine/attacher.rb
143 def finalize
144   destroy_previous
145   promote_cached
146   @previous = nil
147 end
get()

Returns the attached file.

# when a file is attached
attacher.get #=> #<Shrine::UploadedFile>

# when no file is attached
attacher.get #=> nil
[show source]
    # File lib/shrine/attacher.rb
240 def get
241   file
242 end
initialize_copy(other)

The copy constructor that’s called on dup and clone We need to duplicate the context to prevent it from being shared

[show source]
    # File lib/shrine/attacher.rb
346 def initialize_copy(other)
347   super
348   @context = @context.dup
349 end
load_data(data)

Loads the uploaded file from data generated by Attacher#data.

attacher.file #=> nil
attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => { ... } })
attacher.file #=> #<Shrine::UploadedFile>
[show source]
    # File lib/shrine/attacher.rb
304 def load_data(data)
305   @file = data && uploaded_file(data)
306 end
promote(storage: store_key, **)

Uploads current file to permanent storage and sets the stored file.

attacher.cached? #=> true
attacher.promote
attacher.stored? #=> true
[show source]
    # File lib/shrine/attacher.rb
170 def promote(storage: store_key, **)
171   set upload(file, storage, action: :store, **)
172 end
promote?()

Whether attached file should be uploaded to permanent storage.

[show source]
    # File lib/shrine/attacher.rb
374 def promote?
375   changed? && cached?
376 end
promote_cached(**)

If a new cached file has been attached, uploads it to permanent storage. Any additional options are forwarded to promote.

attacher.assign(io)
attacher.cached? #=> true
attacher.promote_cached
attacher.stored? #=> true
[show source]
    # File lib/shrine/attacher.rb
161 def promote_cached(**)
162   promote(**) if promote?
163 end
save()

Plugins can override this if they want something to be done in a “before save” callback.

[show source]
    # File lib/shrine/attacher.rb
151 def save
152 end
set(file)

Sets the uploaded file.

attacher.set(uploaded_file)
attacher.file #=> #<Shrine::UploadedFile>
attacher.changed? #=> false
[show source]
    # File lib/shrine/attacher.rb
229 def set(file)
230   self.file = file
231 end
shrine_class()

Returns the Shrine class that this attacher’s class is namespaced under.

[show source]
    # File lib/shrine/attacher.rb
338 def shrine_class
339   self.class.shrine_class
340 end
store(= shrine_class.new(store_key))
[show source]
    # File lib/shrine/attacher.rb
 57   def store = shrine_class.new(store_key)
 58 
 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("")
 70   def assign(value, **)
 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
 75     end
 76 
 77     attach_cached(value, **)
 78   end
 79 
 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, **)
 96     if value.is_a?(String) || value.is_a?(Hash)
 97       change(cached(value, **))
 98     else
 99       attach(value, storage: cache_key, action: :cache, **)
100     end
101   end
102 
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, **)
117     file = upload(io, storage, **) if io
118 
119     change(file)
120   end
121 
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
147   end
148 
149   # Plugins can override this if they want something to be done in a
150   # "before save" callback.
151   def save
152   end
153 
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(**)
162     promote(**) if promote?
163   end
164 
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, **)
171     set upload(file, storage, action: :store, **)
172   end
173 
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, **)
182     shrine_class.upload(io, storage, **context, **)
183   end
184 
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?
193   end
194 
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?
203   end
204 
205   # Destroys the attachment.
206   #
207   #     attacher.file.exists? #=> true
208   #     attacher.destroy
209   #     attacher.file.exists? #=> false
210   def destroy
211     file&.delete
212   end
213 
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
231   end
232 
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
252   def url(**)
253     file&.url(**)
254   end
255 
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
263   end
264 
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
274   end
275 
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)
282   end
283 
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)
290   end
291 
292   # Generates serializable data for the attachment.
293   #
294   #     attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
295   def data
296     file&.data
297   end
298 
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)
306   end
307 
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
318   end
319 
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"
323   end
324 
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)
334   end
335 
336   # Returns the Shrine class that this attacher's class is namespaced
337   # under.
338   def shrine_class
339     self.class.shrine_class
340   end
341 
342   private
343 
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
349   end
350 
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
369 
370     uploaded_file
371   end
372 
373   # Whether attached file should be uploaded to permanent storage.
374   def promote?
375     changed? && cached?
376   end
377 
378   # Whether attached file should be deleted.
379   def destroy?
380     attached? && !cached?
381   end
382 
383   # Whether assigning the given file is considered a change.
384   def change?(file)
385     @file != file
386   end
387 
388   # Returns whether the file is uploaded to specified storage.
389   def uploaded?(file, storage_key)
390     file&.storage_key == storage_key
391   end
392 end
store_key(= @store.to_sym)
[show source]
    # File lib/shrine/attacher.rb
 52       def store_key = @store.to_sym
 53 
 54       # Returns the uploader that is used for the temporary storage.
 55       def cache = shrine_class.new(cache_key)
 56       # Returns the uploader that is used for the permanent storage.
 57       def store = shrine_class.new(store_key)
 58 
 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("")
 70       def assign(value, **)
 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
 75         end
 76 
 77         attach_cached(value, **)
 78       end
 79 
 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, **)
 96         if value.is_a?(String) || value.is_a?(Hash)
 97           change(cached(value, **))
 98         else
 99           attach(value, storage: cache_key, action: :cache, **)
100         end
101       end
102 
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, **)
117         file = upload(io, storage, **) if io
118 
119         change(file)
120       end
121 
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
147       end
148 
149       # Plugins can override this if they want something to be done in a
150       # "before save" callback.
151       def save
152       end
153 
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(**)
162         promote(**) if promote?
163       end
164 
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, **)
171         set upload(file, storage, action: :store, **)
172       end
173 
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, **)
182         shrine_class.upload(io, storage, **context, **)
183       end
184 
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?
193       end
194 
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?
203       end
204 
205       # Destroys the attachment.
206       #
207       #     attacher.file.exists? #=> true
208       #     attacher.destroy
209       #     attacher.file.exists? #=> false
210       def destroy
211         file&.delete
212       end
213 
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
231       end
232 
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
252       def url(**)
253         file&.url(**)
254       end
255 
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
263       end
264 
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
274       end
275 
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)
282       end
283 
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)
290       end
291 
292       # Generates serializable data for the attachment.
293       #
294       #     attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
295       def data
296         file&.data
297       end
298 
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)
306       end
307 
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
318       end
319 
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"
323       end
324 
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)
334       end
335 
336       # Returns the Shrine class that this attacher's class is namespaced
337       # under.
338       def shrine_class
339         self.class.shrine_class
340       end
341 
342       private
343 
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
349       end
350 
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
369 
370         uploaded_file
371       end
372 
373       # Whether attached file should be uploaded to permanent storage.
374       def promote?
375         changed? && cached?
376       end
377 
378       # Whether attached file should be deleted.
379       def destroy?
380         attached? && !cached?
381       end
382 
383       # Whether assigning the given file is considered a change.
384       def change?(file)
385         @file != file
386       end
387 
388       # Returns whether the file is uploaded to specified storage.
389       def uploaded?(file, storage_key)
390         file&.storage_key == storage_key
391       end
392     end
393 
394     extend ClassMethods
395     include InstanceMethods
396   end
397 end
stored?(file = self.file)

Returns whether the file is uploaded to permanent storage.

attacher.stored?       # checks current file
attacher.stored?(file) # checks given file
[show source]
    # File lib/shrine/attacher.rb
288 def stored?(file = self.file)
289   uploaded?(file, store_key)
290 end
upload(io, storage = store_key, **)

Delegates to Shrine.upload, passing the context.

# upload file to specified storage
attacher.upload(io, :store) #=> #<Shrine::UploadedFile>

# pass additional options for the uploader
attacher.upload(io, :store, metadata: { "foo" => "bar" })
[show source]
    # File lib/shrine/attacher.rb
181 def upload(io, storage = store_key, **)
182   shrine_class.upload(io, storage, **context, **)
183 end
uploaded?(file, storage_key)

Returns whether the file is uploaded to specified storage.

[show source]
    # File lib/shrine/attacher.rb
389 def uploaded?(file, storage_key)
390   file&.storage_key == storage_key
391 end
uploaded_file(value)

Converts JSON or Hash data into a Shrine::UploadedFile object.

attacher.uploaded_file('{"id":"...","storage":"...","metadata":{...}}')
#=> #<Shrine::UploadedFile ...>

attacher.uploaded_file({ "id" => "...", "storage" => "...", "metadata" => {} })
#=> #<Shrine::UploadedFile ...>
[show source]
    # File lib/shrine/attacher.rb
332 def uploaded_file(value)
333   shrine_class.uploaded_file(value)
334 end
url(**)

If a file is attached, returns the uploaded file URL, otherwise returns nil. Any options are forwarded to the storage.

attacher.file = file
attacher.url #=> "https://..."

attacher.file = nil
attacher.url #=> nil
[show source]
    # File lib/shrine/attacher.rb
252 def url(**)
253   file&.url(**)
254 end