attacher.md

doc/attacher.md
Last Update: 2023-07-30 18:26:06 +0200

title: Using Attacher

This guide explains what is Shrine::Attacher and how to use it.

Introduction

The attachment logic is handled by a Shrine::Attacher object. The Shrine::Attachment module simply provides a convenience layer around a Shrine::Attacher object, which can be accessed via the #<name>_attacher attribute.

class Photo
  include ImageUploader::Attachment(:image)
end
photo = Photo.new
photo.image_attacher #=> #<ImageUploader::Attacher>

We can also instantiate the same Shrine::Attacher object directly:

attacher = ImageUploader::Attacher.from_model(photo, :image)
attacher.file # called by `photo.image`
attacher.url  # called by `photo.image_url`

The {model}, {entity}, and {column} plugins provide additional Shrine::Attacher methods (such as Shrine::Attacher.from_model we see above), but in this guide we’ll focus only on the core Shrine::Attacher methods.

So, we’ll assume a Shrine::Attacher object not backed by any model/entity:

attacher = Shrine::Attacher.new

Storage

By default, an Attacher will use the :cache storage as the temporary storage, and the :store storage as the permanent storage.

Shrine.storages = {
  cache: Shrine::Storage::Memory.new,
  store: Shrine::Storage::Memory.new,
}
attacher = Shrine::Attacher.new
attacher.cache_key #=> :cache
attacher.store_key #=> :store

We can also change the default storage:

attacher = Shrine::Attacher.new(cache: :other_cache, store: :other_store)
attacher.cache_key #=> :other_cache
attacher.store_key #=> :other_store

You can also change default attacher options on the Shrine::Attachment module:

class Photo
  include ImageUploader::Attachment(:image, cache: :other_cache, store: :other_store)
end

The Attacher#cache and Attacher#store methods will retrieve corresponding uploaders:

attacher.cache #=> #<MyUploader @storage_key=:cache>
attacher.store #=> #<MyUploader @storage_key=:store>

Attaching

Attaching cached

For attaching files submitted via a web form, Attacher#assign can be used:

attacher.assign(file)

If given a raw file, it will upload it to temporary storage:

attacher.assign(file)
attacher.file #=> #<Shrine::UploadedFile id="asdf.jpg" storage=:cache ...>

If given cached file data (JSON or Hash), it will set the cached file:

attacher.assign('{"id":"asdf.jpg","storage":"cache","metadata":{...}}')
attacher.file #=> #<Shrine::UploadedFile id="asdf.jpg" storage=:cache ...>

If given an empty string, it will no-op:

attacher.assign("") # no-op

If given nil, it will clear the attached file:

attacher.file #=> <Shrine::UploadedFile>
attacher.assign(nil)
attacher.file #=> nil

This plays nicely with the recommended HTML form fields for the attachment. If you’re not using the hidden form field (and therefore don’t need empty strings to be handled), you can also use Attacher#attach_cached:

# uploads file to cache
attacher.attach_cached(file)

# sets cached file
attacher.attach_cached('{"id":"asdf.jpg","storage":"cache","metadata":{...}}')
attacher.attach_cached({ "id" => "asdf.jpg", "storage" => "cache", "metadata" => { ... } })
attacher.attach_cached({ id: "asdf.jpg", storage: "cache", metadata: { ... } })

# unsets attached file
attacher.attach_cached(nil)

Attaching stored

The Attacher#attach method uploads a given file to permanent storage:

attacher.attach(file)
attacher.file #=> #<Shrine::UploadedFile id="asdf.jpg" storage=:store ...>

This method is useful when attaching files from scripts, where validation doesn’t need to be performed, and where temporary storage can be skipped.

You can specify a different destination storage with the :storage option:

attacher.attach(file, storage: :other_store)
attacher.file #=> #<Shrine::UploadedFile id="asdf.jpg" storage=:other_store ...>

Any additional options passed to Attacher#attach, Attacher#attach_cached and Attacher#assign are forwarded to the uploader:

attacher.attach(file, metadata: { "foo" => "bar" })       # adding metadata
attacher.attach(file, upload_options: { acl: "private" }) # setting upload options
attacher.attach(file, location: "path/to/file")           # setting upload location

Uploading

If you want to upload a file to without attaching it, you can use Attacher#upload:

attacher.upload(file)               #=> #<Shrine::UploadedFile storage=:store ...>
attacher.upload(file, :cache)       #=> #<Shrine::UploadedFile storage=:cache ...>
attacher.upload(file, :other_store) #=> #<Shrine::UploadedFile storage=:other_store ...>

This is useful if you want to attacher context such as :record and :name to be automatically passed to the uploader.

You can also pass additional options for Shrine#upload:

attacher.upload(file, metadata: { "foo" => "bar" })       # adding metadata
attacher.upload(file, upload_options: { acl: "private" }) # setting upload options
attacher.upload(file, location: "path/to/file")           # setting upload location

Changes

When a new file is attached, calling {Attacher#finalize} will perform additional actions such as promotion and deleting any previous file. It will also trigger validation.

You can check whether a new file has been attached with Attacher#changed?:

attacher.changed? #=> true

You can use Attacher#change to attach an UploadedFile object as is:

uploaded_file #=> #<Shrine::UploadedFile id="foo" ...>
attacher.change(uploaded_file)
attacher.file #=> #<Shrine::UploadedFile id="foo" ...>
attacher.changed? #=> true

If you want to attach a file without triggering dirty tracking or validation, you can use Attacher#set:

uploaded_file #=> #<Shrine::UploadedFile id="foo" ...>
attacher.set(uploaded_file)
attacher.file #=> #<Shrine::UploadedFile id="foo" ...>
attacher.changed? #=> false

Finalizing

After the file is attached (with Attacher#assign, Attacher#attach_cached, or Attacher#attach), and data has been validated, the attachment can be “finalized”:

attacher.finalize

The Attacher#finalize method performs promoting and replacing. It also clears dirty tracking:

attacher.changed? #=> true
attacher.finalize
attacher.changed? #=> false

Promoting

Attacher#finalize checks if the attached file has been uploaded to temporary storage, and in this case uploads it to permanent storage.

attacher.attach_cached(io)
attacher.finalize # uploads attached file to permanent storage
attacher.file #=> #<Shrine::UploadedFile storage=:store ...>

Internally it calls Attacher#promote_cached, which you can call directly if you want to pass any promote options:

attacher.file #=> #<Shrine::UploadedFile storage=:cache ...>
attacher.promote_cached # uploads attached file to permanent storage if new and cached
attacher.file #=> #<Shrine::UploadedFile storage=:store ...>

You can also call Attacher#promote if you want to upload attached file to permanent storage, regardless of whether it’s cached or newly attached:

attacher.promote

Any options passed to Attacher#promote_cached or Attacher#promote will be forwarded to Shrine#upload.

Replacing

Attacher#finalize also deletes the previous attached file if any:

previous_file = attacher.file

attacher.attach(io)
attacher.finalize

previous_file.exists? #=> false

Internally it calls Attacher#destroy_previous to do this:

attacher.destroy_previous

Retrieving

File

The Attacher#file is used to retrieve the attached file:

attacher.file #=> #<Shrine::UploadedFile>

If no file is attached, Attacher#file returns nil:

attacher.file #=> nil

If you want to assert a file is attached, you can use Attacher#file!:

attacher.file! #~> Shrine::Error: no file is attached

Attached

You can also check whether a file is attached with Attacher#attached?:

attacher.attached? # returns whether file is attached

If you want to check to which storage a file is uploaded to, you can use Attacher#cached? and Attacher#stored?:

attacher.attach(io)
attacher.stored?                #=> true (checks current file)
attacher.stored?(attacher.file) #=> true (checks given file)
attacher.attach_cached(io)
attacher.cached?                #=> true (checks current file)
attacher.cached?(attacher.file) #=> true (checks given file)

URL

The attached file URL can be retrieved with Attacher#url:

attacher.url #=> "https://example.com/file.jpg"

If no file is attached, Attacher#url returns nil:

attacher.url #=> nil

Data

You can retrieve plain attached file data with Attacher#data:

attacher.data #=>
# {
#   "id" => "abc123.jpg",
#   "storage" => "store",
#   "metadata" => {
#     "size" => 223984,
#     "filename" => "nature.jpg",
#     "mime_type" => "image/jpeg",
#   }
# }

This data can be stored somewhere, and later the attached file can be loaded from it:

# new attacher
attacher = Shrine::Attacher.from_data(data)
attacher.file #=> #<Shrine::UploadedFile>

# existing attacher
attacher.file #=> nil
attacher.load_data(data)
attacher.file #=> #<Shrine::UploadedFile>

Internally Attacher#uploaded_file is used to convert uploaded file data into a Shrine::UploadedFile object:

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

You will likely want to use a higher level abstraction for saving and loading this data, see {column}, {entity} and {model} plugins for more details.

Deleting

The attached file can be deleted via Attacher#destroy_attached:

attacher.destroy_attached

This will not delete cached files, to not interrupt any potential backgrounding that might be in process.

If you want to delete the attached file regardless of storage it’s uploaded to, you can use Attacher#destroy:

attacher.destroy

Context

The Attacher#context hash is automatically forwarded to the uploader on Attacher#upload. When {model} or {entity} plugin is loaded, this will include :record and :name values:

attacher = Shrine::Attacher.from_model(photo, :image)
attacher.context #=> { record: #<Photo>, name: :image }

You can add here any other parameters you want to forward to the uploader:

attacher.context[:foo] = "bar"

However, it’s generally better practice to pass uploader options directly to Attacher#assign, Attacher#attach, Attacher#promote or any other method that’s calling Attacher#upload.