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 :derivativesclass 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
endphoto = Photo.new(photo_params)
photo.image_derivatives! # creates derivatives
photo.saveThis 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 endphoto.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 } endphoto = 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#assignAttacher#upload– callsShrine#upload, passing:recordand:namecontextAttacher#file– alias forAttacher#getAttacher#cache_key– returns temporary storage key (:cacheby default)Attacher#store_key– returns permanent storage key (:storeby 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 :entityclass Photo < Hanami::Entity
include Shrine::Attachment(:image)
endphoto = 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 :modelclass Photo < Struct.new(:image_data)
include Shrine::Attachment(:image)
endphoto = 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
backgroundingplugin 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)
endclass 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
endclass 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
endThere 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 hookPersistence 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
endOther new plugins
The new
mirroringplugin 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 :backupThe new
multi_cacheplugin 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_twoThe new
form_assignplugin has been added for assigning files directly from form params.Shrine.plugin :form_assignattacher = 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: falsephoto.image = file photo.image.storage_key #=> :store (permanent storage)New
Shrine.download_responsemethod has been added to thedownload_endpointplugin for generating file response from the controller.Rails.application.routes.draw do get "/attachments" => "files#download" endclass 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 endThe
Attacher#refresh_metadata!method has been added torefresh_metadataplugin. 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::Attachmentmodule now defines a.<name>_attacherclass method on the target class.class Photo include ImageUploader::Attachment(:image) endPhoto.image_attacher #=> #<ImageUploader::Attacher ...>The attachment data serializer is now configurable (by default
JSONstandard library is used):require "oj" # https://github.com/ohler55/oj Shrine.plugin :column, serializer: OjIt's now possible to pass options to the validate block via the
:validateoption:attacher.assign(file, validate: { foo: "bar" })class MyUploader < Shrine Attacher.validate do |**options| options #=> { foo: "bar" } end endValidation can now be skipped on assignment by passing
validate: false.attacher.attach(file, validate: false) # skip validationClosing the uploaded file can now be prevented by passing
close: falsetoShrine#upload.Uploaded file can now be automatically deleted by passing
delete: truetoShrine#upload.New
Attacher#file!method has been added for retrieving the attached file and raising and exception if it doesn't exist.New
Derivation#openedmethod has been added for retrieving an opened derivative inderivation_endpointplugin.New
Storage#delete_prefixedmethod 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 fileThe
S3#openmethod doesn't perform a#head_obectrequest anymore.The
derivation_endpointplugin doesn't perform aStorage#exists?call anymore when:uploadis enabled.The
download_endpointplugin 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 removedThe
Attacher#assignmethod now accepts cached file data as a Hash.photo.image = { "id" => "...", "storage" => "cache", "metadata" => { ... } }An
UploadedFileobject 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.storageswill now be applied to existingShrineandAttacherinstances.When copying the S3 object to another location, any specified upload options will now be applied.
Deprecation of passing unknown options to
FileSystem#openhas been reverted. This allows users to continue usingFileSystemstorage in tests as a mock storage.The
Shrine#uploadmethod now infers file extension fromfilenamemetadata, making possible to usefilenameto specify file extension.file = uploader.upload(StringIO.new("some text"), metadata: { "filename" => "file.txt" }) file.id #=> "2a2467ee6acbc5cb.txt"The
Shrine.optshash 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
downdependency 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
activerecordplugin now works with Active Record 3.Callback code from
activerecordandsequelplugin has been moved into attacher methods, allowing the user to override them.Attacher#(activerecord|sequel)_before_saveAttacher#(activerecord|sequel)_after_saveAttacher#(activerecord|sequel)_after_destroy
The
url_optionsplugin 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_optionshash passed to the uploader is now merged with any options defined with theupload_optionsplugin.The
default_storagenow evaluates the storage block in context of theAttacherinstance.New
Attacher.default_cacheandAttacher.default_storemethods have been added to thedefault_storageplugin for declaratively setting default storage.Attacher.default_cache { ... } Attacher.default_store { ... }The
derivation_endpointplugin now handles string derivation names.The
Derivation#uploadmethod fromderivation_endpointplugin now accepts additional uploader optionsThe
Derivation#uploadmethod fromderivation_endpointplugin now accepts any IO-like object.The
derivation_endpointplugin doesn't re-openFileobjects returned in derivation block anymore.The
instrumentationplugin now instrumentsUploadedFile#opencalls as a newopen.shrineevent.UploadedFile#downloadis still instrumented asdownload.shrine.The
upload.shrineevent now has:metadataon the top level ininstrumentationplugin.The width & height validators in
store_dimensionsplugin don't requireUploadedFile#widthandUploadedFile#heightmethods to be defined anymore, only that the corresponding metadata exists.Any options passed to
Attacher#attach_cachedare now forwarded to metadata extraction whenrestore_cached_dataplugin is loaded.The
infer_extensionplugin now works correctly withpretty_locationplugin whenpretty_locationwas loaded afterinfer_extension.The
pretty_locationplugin now accepts:class_underscoreoption 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
backgroundingplugin 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_rawplugins have been deprecated in favor of the newderivativesplugin.The
module_includeplugin has been deprecated over overriding core classes directly.The
hooks,parallelize,parsed_json, anddelete_promotedplugins have been removed.The deprecated
copy,backup,multi_delete,moving,logging,direct_uploadbackground_helpers, andmigration_helpersplugins have been removed.The
default_url_optionsplugin has been renamed tourl_options.
Attacher API
The
Attacher.newmethod now only accepts a hash of options, useAttacher.from_modelfor initializing from a model.If you're changing the attachment data column directly, you'll now need to call
Attacher#reloadto make the attacher reflect those changes.The
Attacher#promotemethod now only saves the promoted file in memory, it doesn't persist the changes.The
Attacher#_setandAttacher#setmethods have been renamed toAttacher#setandAttacher#changed.The
Attacher#cache!,Attacher#store!, andAttacher#delete!methods have been removed.The
Attacher#_promoteandAttacher#_deletemethods have been removed.The
Attacher#swap,Attacher#update,Attacher#read,Attacher#write,Attacher#convert_to_data,Attacher#convert_before_write, andAttache#convert_after_readmethods have been removed.The
Attacher.validate,Attacher#validateandAttacher#errorsmethods have been extracted into the newvalidationplugin.The
Attacher#data_attributemethod has been renamed toAttacher#attribute.The
Attacher#replacemethod has been renamed toAttacher#destroy_previous.The
Attacher#assignmethod 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::Attachmentmodule doesn't define any instance methods by itself anymore. This has been moved intoentityandmodelplugins.
Uploader API
The
:phaseoption has been removed.The
Shrine#processandShrine#processedmethods have been removed.The
Shrine#store,Shrine#_store,Shrine#put, andShrine#copymethods have been removed.The
Shrine#delete,Shrine#_delete, andShrine#removemethods have been removed.The
Shrine#uploaded?method has been removed.The
Shrine.uploaded_filemethod doesn't yield files anymore by default.The options for
Shrine#uploadandShrine#extract_metadataare now required to have symbol keys.The
Shrine.uploaded_filemethod now raisesArgumentErroron invalid arguments.The
Shrine#uploadmethod doesn't rescue exceptions that happen inIO#closeanymore.The deprecated
Shrine::IO_METHODSconstant has been removed.
Uploaded File API
The
UploadedFilemethod 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_keymethod now returns a Symbol instead of a String.# previous behaviour uploaded_file.storage_key #=> "store" # new behaviour uploaded_file.storage_key #=> :storeThe
UploadedFile#==method now requires both uploaded file objects to be of the same class.
Storage API
The
Storage#openmethod is now required to accept additional options.# this won't work anymore def open(id) # ... end # this is now required def open(id, **options) # ... endThe
Storage#openmethod is now required to raiseShrine::FileNotFoundexception when the file is missing.
S3 API
The support for
aws-sdk2.x andaws-sdk-s3< 1.14 has been removed.S3#opennow raisesShrine::FileNotFoundexception when S3 object doesn't exist on the bucket.The
S3#uploadmethod 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
:hostoption inS3#initializehas been removed.The
:downloadoption inS3#urlhas been removed.Specifying
:multipart_thresholdas an integer is not supported anymore.Non URI-escaped
:content_dispositionand:response_content_dispositionvalues are not supported anymore.The
S3#presignmethod now returns a hash instead of an object for default POST method.The deprecated
S3#stream,S3#download, andS3#s3methods have been removed.The
S3#openmethod doesn't include an:objectinDown::ChunkedIO#dataanymore.
FileSystem API
FileSystem#opennow raisesShrine::FileNotFoundexception when file does not exist on the filesystem.The deprecated
:hostoption inFileSystem#initializehas been removed.The deprecated
:older_thanoption inFileSystem#clear!has been removed.The deprecated
FileSystem#downloadmethod has been removed.The
FileSystem#movable?andFileSystem#movemethods have been made private.The
FileSystem#openmethod doesn't accept a block anymore.
Plugins API
remote_url- A custom downloader is now required to raise
Shrine::Plugins::RemoteUrl::DownloadErrorin order for the exception to be converted into a validation error.Down::NotFoundandDown::TooLargeexceptions are converted by default. All other exceptions are propagated.
- A custom downloader is now required to raise
upload_endpointThe deprecated
Shrine::Plugins::UploadEndpoint::Appconstant has been removed.The
:requestoption holding theRack::Requestobject isn't passed to the uploader anymore.
presign_endpointStorage#presignresults that cannot coerce themselves into a Hash are not supported anymore.The deprecated
Shrine::Plugins::PresignEndpoint::Appconstant has been removed.
derivation_endpointThe derivation block is now evaluated in context of a
Shrine::Derivationinstance.The derivation block can now return only
FileandTempfileobjects.The
:download_errorsoption has been removed, as it's now obsolete.The
:include_uploaded_fileoption has been removed, as it's now obsolete.The source
UploadedFileis not passed to the derivation block anymore ondownload: false.The
Derivation#uploadmethod now closes the uploaded file.The
derivation.uploadinstrumentation event payload now includes only:derivationkey.
download_endpointSupport for legacy
/:storage/:idURLs has been dropped.The
:storagesplugin option has been removed.The
Shrine::Plugins::DownloadEndpoint::Appconstant has been removed.
data_uriThe deprecated
:filenameplugin option has been removed.The deprecated
Shrine::Plugins::DataUri::DataFileconstant has been removed.
rack_fileThe deprecated
Shrine::Plugins::RackFile::UploadedFileconstant has been removed.Passing a Rack uploaded file hash to
Shrine#uploadis not supported anymore.
cached_attachment_dataThe
#<name>_cached_data=model method has been removed.The
Attacher#read_cachedmethod has been renamed toAttacher#cached_data.
default_url- Passing a block when loading the plugin is not supported anymore.
determine_mime_typeThe deprecated
:defaultanalyzer alias has been removed.The private
Shrine#mime_type_analyzersmethod has been removed.
store_dimensionsFailing to extract dimensions now prints out a warning by default.
The private
Shrine#extract_dimensionsandShrine#dimensions_analyzersmethods have been removed.
infer_extensionThe
:mini_mimeinferrer is now the default inferrer, which requires themini_mimegem.The private
Shrine#infer_extensionmethod has been removed.
validation_helpersThe width & height validators will now raise an exception if
widthorheightmetadata is missing.Support for regexes has been dropped for MIME type and extension validators.
versions- The deprecated
:version_names,Shrine.version_namesandShrine.version?have been removed fromversionsplugin.
- The deprecated
keep_files- The plugin will now always prevent deletion of both replaced and destroyed
attachments. The
:replacedand:destroyedoptions don't have effect anymore.
- The plugin will now always prevent deletion of both replaced and destroyed
attachments. The
dynamic_storage- The
Shrine.dynamic_storagesmethod has been removed.
- The
instrumentationThe
:location,:upload_options, and:metadatakeys have been removed from:optionsinupload.shrineevent payload.The
:metadatakey has been removed frommetadata.shrineevent payload.
default_storage- Passing
record&namearguments to the storage block has been deprecated over evaluating the block in context of the attacher instance.
- Passing
sequel- The
:callbacksplugin option has been renamed to:hooks.
- The