Shrine 3.0.0
This guide covers all the changes in the 3.0.0 version of Shrine. If you're currently using Shrine 2.x, see Upgrading to Shrine 3.x for instructions on how to upgrade.
Major features
Derivatives
The new derivatives
plugin has been added for storing
additional processed files alongside the main file.
Shrine.plugin :derivatives
class ImageUploader < Shrine
Attacher.derivatives_processor do |original|
magick = ImageProcessing::MiniMagick.source(original)
{
large: magick.resize_to_limit!(800, 800),
medium: magick.resize_to_limit!(500, 500),
small: magick.resize_to_limit!(300, 300),
}
end
end
photo = Photo.new(photo_params)
photo.image_derivatives! # creates derivatives
photo.save
This is a rewrite of the versions
plugin, bringing numerous
improvements:
processed files are separated from the main file
photo.image_data #=> # { # "id": "original.jpg", # "storage": "store", # "metadata": { ... }, # "derivatives": { # "large": { "id": "large.jpg", "storage": "store", "metadata": { ... } }, # "medium": { "id": "medium.jpg", "storage": "store", "metadata": { ... } }, # "small": { "id": "small.jpg", "storage": "store", "metadata": { ... } } # } # } photo.image #=> #<Shrine::UploadedFile id="original.jpg" ...> photo.image_derivatives #=> # { # large: #<Shrine::UploadedFile id="large.jpg" ...>, # medium: #<Shrine::UploadedFile id="medium.jpg" ...>, # small: #<Shrine::UploadedFile id="small.jpg" ...>, # }
processing is decoupled from promotion
photo = Photo.create(image: file) # promote original file to permanent storage photo.image_derivatives! # generate derivatives after promotion photo.save # save derivatives data
ability to add or remove processed files at any point
class ImageUploader < Shrine Attacher.derivatives_processor :thumbnails do |original| # ... end Attacher.derivatives_processor :crop do |original, left:, top:, width:, height:| vips = ImageProcessing::Vips.source(original) { cropped: vips.crop!(left, top, width, height) } end end
photo.image_derivatives!(:thumbnails) photo.image_derivatives #=> { large: ..., medium: ..., small: ... } photo.save # ... sometime later ... photo.image_derivatives!(:crop, left: 0, top: 0, width: 300, height: 300) photo.image_derivatives #=> { large: ..., medium: ..., small: ..., cropped: ... } photo.save
possibility of uploading processed files to different storage
class ImageUploader < Shrine # specify storage for all derivatives Attacher.derivatives_storage :other_store # or specify storage per derivative Attacher.derivatives_storage { |derivative| :other_store } end
photo = Photo.create(image: file) photo.image.storage_key #=> :store photo.image_derivatives! photo.image_derivatives[:large].storage_key #=> :other_store
Attacher redesign
The Shrine::Attacher
class has been rewritten and can now be
used without models:
attacher = Shrine::Attacher.new
attacher.attach(file)
attacher.file #=> #<Shrine::UploadedFile>
The Attacher#data
, Attacher#load_data
, and Attacher.from_data
methods
have been added for dumping and loading the attached file:
# dump attached file into a serializable Hash
data = attacher.data #=> { "id" => "abc123.jpg", "storage" => "store", "metadata" => { ... } }
# initialize attacher from attached file data...
attacher = Shrine::Attacher.from_data(data)
attacher.file #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store metadata={...}>
# ...or load attached file into an existing attacher
attacher = Shrine::Attacher.new
attacher.load_data(data)
attacher.file #=> #<Shrine::UploadedFile>
Several more methods have been added:
Attacher#attach
– attaches the file directly to permanent storageAttacher#attach_cached
– extracted fromAttacher#assign
Attacher#upload
– callsShrine#upload
, passing:record
and:name
contextAttacher#file
– alias forAttacher#get
Attacher#cache_key
– returns temporary storage key (:cache
by default)Attacher#store_key
– returns permanent storage key (:store
by default)
Column
The new column
plugin adds the ability to serialize attached file
data, in format suitable for writing into a database column.
Shrine.plugin :column
# dump attached file data into a JSON string
data = attacher.column_data #=> '{"id":"abc123.jpg","storage":"store","metadata":{...}}'
# initialize attacher from attached file data...
attacher = Shrine::Attacher.from_column(data)
attacher.file #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store metadata={...}>
# ...or load attached file into an existing attacher
attacher = Shrine::Attacher.new
attacher.load_column(data)
attacher.file #=> #<Shrine::UploadedFile>
Entity
The new entity
plugin adds support for immutable structs, which
are commonly used with ROM, Hanami and dry-rb.
Shrine.plugin :entity
class Photo < Hanami::Entity
include Shrine::Attachment(:image)
end
photo = Photo.new(image_data: '{"id":"abc123.jpg","storage":"store","metadata":{...}}')
photo.image #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:store ...>
Model
The new model
plugin adds support for mutable structs, which is
used by activerecord
and sequel
plugins.
Shrine.plugin :model
class Photo < Struct.new(:image_data)
include Shrine::Attachment(:image)
end
photo = Photo.new
photo.image = file
photo.image #=> #<Shrine::UploadedFile id="abc123.jpg" storage=:cache ...>
photo.image_data #=> #=> '{"id":"abc123.jpg", "storage":"cache", "metadata":{...}}'
Backgrounding rewrite
- The
backgrounding
plugin has been rewritten for more flexibility and simplicity. The new usage is much more explicit:
Shrine.plugin :backgrounding
Shrine::Attacher.promote_block do
PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
end
Shrine::Attacher.destroy_block do
DestroyJob.perform_async(self.class.name, data)
end
class PromoteJob
include Sidekiq::Worker
def perform(attacher_class, record_class, record.id, name, file_data)
attacher_class = Object.const_get(attacher_class)
record = Object.const_get(record_class).find(record_id) # if using Active Record
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
attacher.atomic_promote
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
# attachment has changed or the record has been deleted, nothing to do
end
end
class DestroyJob
include Sidekiq::Worker
def perform(attacher_class, data)
attacher_class = Object.const_get(attacher_class)
attacher = attacher_class.from_data(data)
attacher.destroy
end
end
There are several main differences compared to the old implementation:
- we are in charge of passing the record to the background job
- we can access the attacher before promotion
- we can react to errors that caused promotion to abort
We can now also register backgrounding hooks on an attacher instance, allowing us to pass additional parameters to the background job:
photo = Photo.new(photo_params)
photo.image_attacher.promote_block do |attacher|
PromoteJob.perform_async(
attacher.class.name,
attacher.record.class.name,
attacher.record.id,
attacher.name,
attacher.file_data,
current_user.id, # <== parameters from the controller
)
end
photo.save # will call our instance-level backgrounding hook
Persistence interface
The persistence plugins (activerecord
, sequel
) now implement a unified
persistence interface:
Method | Description |
---|---|
Attacher#persist | persists attachment data |
Attacher#atomic_persist | persists attachment data if attachment hasn’t changed |
Attacher#atomic_promote | promotes cached file and atomically persists changes |
The "atomic" methods use the new atomic_helpers
plugin,
and are useful for background jobs. For example, this is how we'd use them to
implement metadata extraction in the background in a concurrency-safe way:
MetadataJob.perform_async(
attacher.class.name,
attacher.record.class.name,
attacher.record.id,
attacher.name,
attacher.file_data,
)
class MetadataJob
include Sidekiq::Worker
def perform(attacher_class, record_class, record_id, name, file_data)
attacher_class = Object.const_get(attacher_class)
record = Object.const_get(record_class).find(record_id) # if using Active Record
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
attacher.refresh_metadata! # extract metadata
attacher.atomic_persist # persist if attachment hasn't changed
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
# attachment has changed or record has been deleted, nothing to do
end
end
Other new plugins
The new
mirroring
plugin has been added for replicating uploads and deletes to other storages.Shrine.storages = { cache: ..., store: ..., backup: ... } Shrine.plugin :mirroring, mirror: { store: :backup }
file = Shrine.upload(io, :store) # uploads to :store and :backup file.delete # deletes from :store and :backup
The new
multi_cache
plugin has been added for allowing an attacher to accept files from additional temporary storages.Shrine.storages = { cache: ..., cache_one: ..., cache_two: ..., store: ... } Shrine.plugin :multi_cache, additional_cache: [:cache_one, :cache_two]
photo.image = { "id" => "...", "storage" => "cache", "metadata" => { ... } } photo.image.storage_key #=> :cache # or photo.image = { "id" => "...", "storage" => "cache_one", "metadata" => { ... } } photo.image.storage_key #=> :cache_one # or photo.image = { "id" => "...", "storage" => "cache_two", "metadata" => { ... } } photo.image.storage_key #=> :cache_two
The new
form_assign
plugin has been added for assigning files directly from form params.Shrine.plugin :form_assign
attacher = photo.image_attacher attacher.form_assign({ "image" => file, "title" => "...", "description" => "..." }) attacher.file #=> #<Shrine::UploadedFile id="..." storage=:cache ...>
Other features
Model file assignment can now be configured to upload directly to permanent storage.
Shrine.plugin :model, cache: false
photo.image = file photo.image.storage_key #=> :store (permanent storage)
New
Shrine.download_response
method has been added to thedownload_endpoint
plugin for generating file response from the controller.Rails.application.routes.draw do get "/attachments" => "files#download" end
class FilesController < ApplicationController def download # ... we can now perform things like authentication here ... set_rack_response Shrine.download_response(env) end private def set_rack_response((status, headers, body)) self.status = status self.headers.merge!(headers) self.response_body = body end end
The
Attacher#refresh_metadata!
method has been added torefresh_metadata
plugin. It refreshes metadata and writes new attached file data back into the data attribute.attacher.file.refresh_metadata! attacher.write # can now be shortened to attacher.refresh_metadata!
Including a
Shrine::Attachment
module now defines a.<name>_attacher
class method on the target class.class Photo include ImageUploader::Attachment(:image) end
Photo.image_attacher #=> #<ImageUploader::Attacher ...>
The attachment data serializer is now configurable (by default
JSON
standard library is used):require "oj" # https://github.com/ohler55/oj Shrine.plugin :column, serializer: Oj
It's now possible to pass options to the validate block via the
:validate
option:attacher.assign(file, validate: { foo: "bar" })
class MyUploader < Shrine Attacher.validate do |**options| options #=> { foo: "bar" } end end
Validation can now be skipped on assignment by passing
validate: false
.attacher.attach(file, validate: false) # skip validation
Closing the uploaded file can now be prevented by passing
close: false
toShrine#upload
.Uploaded file can now be automatically deleted by passing
delete: true
toShrine#upload
.New
Attacher#file!
method has been added for retrieving the attached file and raising and exception if it doesn't exist.New
Derivation#opened
method has been added for retrieving an opened derivative inderivation_endpoint
plugin.New
Storage#delete_prefixed
method has been added for deleting all files in specified directory.storage.delete_prefixed("some_directory/")
Performance improvements
The attached file is now parsed and loaded from record column only once, which can greatly improve performance if the same attached file is being accessed multiple times.
photo = Photo.find(photo_id) photo.image # parses and loads attached file photo.image # returns memoized attached file
The
S3#open
method doesn't perform a#head_obect
request anymore.The
derivation_endpoint
plugin doesn't perform aStorage#exists?
call anymore when:upload
is enabled.The
download_endpoint
plugin doesn't perform aStorage#exists?
call anymore.
Other Improvements
Core improvements
Shrine now works again with MRI 2.3.
The memory storage from the shrine-memory gem has been merged into core.
# Gemfile gem "shrine-memory" # this can be removed
The
Attacher#assign
method now accepts cached file data as a Hash.photo.image = { "id" => "...", "storage" => "cache", "metadata" => { ... } }
An
UploadedFile
object can now be initialized with symbol keys.Shrine.uploaded_file(id: "...", storage: :store, metadata: { ... })
The temporary storage doesn't need to be defined anymore if it's not used.
Any changes to
Shrine.storages
will now be applied to existingShrine
andAttacher
instances.When copying the S3 object to another location, any specified upload options will now be applied.
Deprecation of passing unknown options to
FileSystem#open
has been reverted. This allows users to continue usingFileSystem
storage in tests as a mock storage.The
Shrine#upload
method now infers file extension fromfilename
metadata, making possible to usefilename
to specify file extension.file = uploader.upload(StringIO.new("some text"), metadata: { "filename" => "file.txt" }) file.id #=> "2a2467ee6acbc5cb.txt"
The
Shrine.opts
hash is now deep-copied on subclassing. This allows plugins to freely mutate hashes and arrays inShrine.opts
, knowing they won't be shared across subclasses.The
down
dependency has been updated to~> 5.0
.The
Shrine::Attachment[]
method has been added as an alternative syntax for creating attachment modules.class Photo include ImageUploader::Attachment[:image] end
Plugin improvements
The
activerecord
plugin now works with Active Record 3.Callback code from
activerecord
andsequel
plugin has been moved into attacher methods, allowing the user to override them.Attacher#(activerecord|sequel)_before_save
Attacher#(activerecord|sequel)_after_save
Attacher#(activerecord|sequel)_after_destroy
The
url_options
plugin now allows you to override URL options by deleting them.uploaded_file.url(response_content_disposition: "attachment")
plugin :url_options, store: -> (io, options) { disposition = options.delete(:response_content_disposition, "inline") { response_content_disposition: ContentDisposition.format( disposition: disposition, filename: io.original_filename, ) } }
The
:upload_options
hash passed to the uploader is now merged with any options defined with theupload_options
plugin.The
default_storage
now evaluates the storage block in context of theAttacher
instance.New
Attacher.default_cache
andAttacher.default_store
methods have been added to thedefault_storage
plugin for declaratively setting default storage.Attacher.default_cache { ... } Attacher.default_store { ... }
The
derivation_endpoint
plugin now handles string derivation names.The
Derivation#upload
method fromderivation_endpoint
plugin now accepts additional uploader optionsThe
Derivation#upload
method fromderivation_endpoint
plugin now accepts any IO-like object.The
derivation_endpoint
plugin doesn't re-openFile
objects returned in derivation block anymore.The
instrumentation
plugin now instrumentsUploadedFile#open
calls as a newopen.shrine
event.UploadedFile#download
is still instrumented asdownload.shrine
.The
upload.shrine
event now has:metadata
on the top level ininstrumentation
plugin.The width & height validators in
store_dimensions
plugin don't requireUploadedFile#width
andUploadedFile#height
methods to be defined anymore, only that the corresponding metadata exists.Any options passed to
Attacher#attach_cached
are now forwarded to metadata extraction whenrestore_cached_data
plugin is loaded.The
infer_extension
plugin now works correctly withpretty_location
plugin whenpretty_location
was loaded afterinfer_extension
.The
pretty_location
plugin now accepts:class_underscore
option for underscoring class names.plugin :pretty_location # "blogpost/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg" plugin :pretty_location, class_underscore: :true # "blog_post/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
You can now load multiple persistence plugins simulatenously, and the correct one will be activated during persistence.
Backwards compatibility
Plugin deprecation and removal
The
backgrounding
plugin has been rewritten and has a new API. While the new API works in a similar way, no backwards compatibility has been kept with the previous API.The
versions
,processing
,recache
, anddelete_raw
plugins have been deprecated in favor of the newderivatives
plugin.The
module_include
plugin has been deprecated over overriding core classes directly.The
hooks
,parallelize
,parsed_json
, anddelete_promoted
plugins have been removed.The deprecated
copy
,backup
,multi_delete
,moving
,logging
,direct_upload
background_helpers
, andmigration_helpers
plugins have been removed.The
default_url_options
plugin has been renamed tourl_options
.
Attacher API
The
Attacher.new
method now only accepts a hash of options, useAttacher.from_model
for initializing from a model.If you're changing the attachment data column directly, you'll now need to call
Attacher#reload
to make the attacher reflect those changes.The
Attacher#promote
method now only saves the promoted file in memory, it doesn't persist the changes.The
Attacher#_set
andAttacher#set
methods have been renamed toAttacher#set
andAttacher#changed
.The
Attacher#cache!
,Attacher#store!
, andAttacher#delete!
methods have been removed.The
Attacher#_promote
andAttacher#_delete
methods have been removed.The
Attacher#swap
,Attacher#update
,Attacher#read
,Attacher#write
,Attacher#convert_to_data
,Attacher#convert_before_write
, andAttache#convert_after_read
methods have been removed.The
Attacher.validate
,Attacher#validate
andAttacher#errors
methods have been extracted into the newvalidation
plugin.The
Attacher#data_attribute
method has been renamed toAttacher#attribute
.The
Attacher#replace
method has been renamed toAttacher#destroy_previous
.The
Attacher#assign
method now raises an exception when non-cached uploaded file is assigned.The
Attacher#attached?
method now returns whether a file is attached, regardless of whether it was changed or not.
Attachment API
- The
Shrine::Attachment
module doesn't define any instance methods by itself anymore. This has been moved intoentity
andmodel
plugins.
Uploader API
The
:phase
option has been removed.The
Shrine#process
andShrine#processed
methods have been removed.The
Shrine#store
,Shrine#_store
,Shrine#put
, andShrine#copy
methods have been removed.The
Shrine#delete
,Shrine#_delete
, andShrine#remove
methods have been removed.The
Shrine#uploaded?
method has been removed.The
Shrine.uploaded_file
method doesn't yield files anymore by default.The options for
Shrine#upload
andShrine#extract_metadata
are now required to have symbol keys.The
Shrine.uploaded_file
method now raisesArgumentError
on invalid arguments.The
Shrine#upload
method doesn't rescue exceptions that happen inIO#close
anymore.The deprecated
Shrine::IO_METHODS
constant has been removed.
Uploaded File API
The
UploadedFile
method now extracts data from the given hash on initialization, it doesn't store the whole hash anymore. This means the following potential code won't work anymore:uploaded_file.id #=> "foo" uploaded_file.data["id"] = "bar" uploaded_file.id #=> "foo"
The
UploadedFile#storage_key
method now returns a Symbol instead of a String.# previous behaviour uploaded_file.storage_key #=> "store" # new behaviour uploaded_file.storage_key #=> :store
The
UploadedFile#==
method now requires both uploaded file objects to be of the same class.
Storage API
The
Storage#open
method is now required to accept additional options.# this won't work anymore def open(id) # ... end # this is now required def open(id, **options) # ... end
The
Storage#open
method is now required to raiseShrine::FileNotFound
exception when the file is missing.
S3 API
The support for
aws-sdk
2.x andaws-sdk-s3
< 1.14 has been removed.S3#open
now raisesShrine::FileNotFound
exception when S3 object doesn't exist on the bucket.The
S3#upload
method will now override any S3 object data when copying from another S3 object. If you were relying on S3 object data being inherited on copy, you will need to update your code.The
:host
option inS3#initialize
has been removed.The
:download
option inS3#url
has been removed.Specifying
:multipart_threshold
as an integer is not supported anymore.Non URI-escaped
:content_disposition
and:response_content_disposition
values are not supported anymore.The
S3#presign
method now returns a hash instead of an object for default POST method.The deprecated
S3#stream
,S3#download
, andS3#s3
methods have been removed.The
S3#open
method doesn't include an:object
inDown::ChunkedIO#data
anymore.
FileSystem API
FileSystem#open
now raisesShrine::FileNotFound
exception when file does not exist on the filesystem.The deprecated
:host
option inFileSystem#initialize
has been removed.The deprecated
:older_than
option inFileSystem#clear!
has been removed.The deprecated
FileSystem#download
method has been removed.The
FileSystem#movable?
andFileSystem#move
methods have been made private.The
FileSystem#open
method doesn't accept a block anymore.
Plugins API
remote_url
- A custom downloader is now required to raise
Shrine::Plugins::RemoteUrl::DownloadError
in order for the exception to be converted into a validation error.Down::NotFound
andDown::TooLarge
exceptions are converted by default. All other exceptions are propagated.
- A custom downloader is now required to raise
upload_endpoint
The deprecated
Shrine::Plugins::UploadEndpoint::App
constant has been removed.The
:request
option holding theRack::Request
object isn't passed to the uploader anymore.
presign_endpoint
Storage#presign
results that cannot coerce themselves into a Hash are not supported anymore.The deprecated
Shrine::Plugins::PresignEndpoint::App
constant has been removed.
derivation_endpoint
The derivation block is now evaluated in context of a
Shrine::Derivation
instance.The derivation block can now return only
File
andTempfile
objects.The
:download_errors
option has been removed, as it's now obsolete.The
:include_uploaded_file
option has been removed, as it's now obsolete.The source
UploadedFile
is not passed to the derivation block anymore ondownload: false
.The
Derivation#upload
method now closes the uploaded file.The
derivation.upload
instrumentation event payload now includes only:derivation
key.
download_endpoint
Support for legacy
/:storage/:id
URLs has been dropped.The
:storages
plugin option has been removed.The
Shrine::Plugins::DownloadEndpoint::App
constant has been removed.
data_uri
The deprecated
:filename
plugin option has been removed.The deprecated
Shrine::Plugins::DataUri::DataFile
constant has been removed.
rack_file
The deprecated
Shrine::Plugins::RackFile::UploadedFile
constant has been removed.Passing a Rack uploaded file hash to
Shrine#upload
is not supported anymore.
cached_attachment_data
The
#<name>_cached_data=
model method has been removed.The
Attacher#read_cached
method has been renamed toAttacher#cached_data
.
default_url
- Passing a block when loading the plugin is not supported anymore.
determine_mime_type
The deprecated
:default
analyzer alias has been removed.The private
Shrine#mime_type_analyzers
method has been removed.
store_dimensions
Failing to extract dimensions now prints out a warning by default.
The private
Shrine#extract_dimensions
andShrine#dimensions_analyzers
methods have been removed.
infer_extension
The
:mini_mime
inferrer is now the default inferrer, which requires themini_mime
gem.The private
Shrine#infer_extension
method has been removed.
validation_helpers
The width & height validators will now raise an exception if
width
orheight
metadata is missing.Support for regexes has been dropped for MIME type and extension validators.
versions
- The deprecated
:version_names
,Shrine.version_names
andShrine.version?
have been removed fromversions
plugin.
- The deprecated
keep_files
- The plugin will now always prevent deletion of both replaced and destroyed
attachments. The
:replaced
and:destroyed
options don't have effect anymore.
- The plugin will now always prevent deletion of both replaced and destroyed
attachments. The
dynamic_storage
- The
Shrine.dynamic_storages
method has been removed.
- The
instrumentation
The
:location
,:upload_options
, and:metadata
keys have been removed from:options
inupload.shrine
event payload.The
:metadata
key has been removed frommetadata.shrine
event payload.
default_storage
- Passing
record
&name
arguments to the storage block has been deprecated over evaluating the block in context of the attacher instance.
- Passing
sequel
- The
:callbacks
plugin option has been renamed to:hooks
.
- The