Active Storage Overview
This guide covers how to attach files to your Active Record models.
After reading this guide, you will know:
- How to attach one or many files to a record.
- How to delete an attached file.
- How to link to an attached file.
- How to use variants to transform images.
- How to generate an image representation of a non-image file, such as a PDF or a video.
- How to send file uploads directly from browsers to a storage service,bypassing your application servers.
- How to clean up files stored during testing.
- How to implement support for additional storage services.
1. What is Active Storage?
Active Storage facilitates uploading files to a cloud storage service likeAmazon S3, or Google Cloud Storage and attaching thosefiles to Active Record objects. It comes with a local disk-based service fordevelopment and testing and supports mirroring files to subordinate services forbackups and migrations.
Using Active Storage, an application can transform image uploads or generate imagerepresentations of non-image uploads like PDFs and videos, and extract metadata fromarbitrary files.
1.1. Requirements
Various features of Active Storage depend on third-party software which Railswill not install, and must be installed separately:
- libvips v8.6+ orImageMagick for image analysis and transformations
- ffmpeg v3.4+ for video previews and ffprobe for video/audio analysis
- poppler ormuPDF for PDF previews
Compared to libvips, ImageMagick is better known and more widely available. However, libvips can beup to 10x faster and consume 1/10 the memory. For JPEG files, this can be further improved by replacinglibjpeg-dev withlibjpeg-turbo-dev, which is2-7x faster.
Before you install and use third-party software, make sure you understand the licensing implications of doing so. MuPDF, in particular, is licensed under AGPL and requires a commercial license for some use.
2. Setup
$bin/railsactive_storage:install$bin/railsdb:migrateThis sets up configuration, and creates the three tables Active Storage uses:active_storage_blobs,active_storage_attachments, andactive_storage_variant_records.
| Table | Purpose |
|---|---|
active_storage_blobs | Stores data about uploaded files, such as filename and content type. |
active_storage_attachments | A polymorphic join table thatconnects your models to blobs. If your model's class name changes, you will need to run a migration on this table to update the underlyingrecord_type to your model's new class name. |
active_storage_variant_records | Ifvariant tracking is enabled, stores records for each variant that has been generated. |
If you are using UUIDs instead of integers as the primary key on your models, you should setRails.application.config.generators { |g| g.orm :active_record, primary_key_type: :uuid } in a config file.
Declare Active Storage services inconfig/storage.yml. For each service yourapplication uses, provide a name and the requisite configuration. The examplebelow declares three services namedlocal,test, andamazon:
local:service:Diskroot:<%= Rails.root.join("storage") %>test:service:Diskroot:<%= Rails.root.join("tmp/storage") %># Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)amazon:service:S3access_key_id:<%= Rails.application.credentials.dig(:aws, :access_key_id) %>secret_access_key:<%= Rails.application.credentials.dig(:aws, :secret_access_key) %>bucket:your_own_bucket-<%= Rails.env %>region:""# e.g. 'us-east-1'Tell Active Storage which service to use by settingRails.application.config.active_storage.service. Because each environment willlikely use a different service, it is recommended to do this on aper-environment basis. To use the disk service from the previous example in thedevelopment environment, you would add the following toconfig/environments/development.rb:
# Store files locally.config.active_storage.service=:localTo use the S3 service in production, you would add the following toconfig/environments/production.rb:
# Store files on Amazon S3.config.active_storage.service=:amazonTo use the test service when testing, you would add the following toconfig/environments/test.rb:
# Store uploaded files on the local file system in a temporary directory.config.active_storage.service=:testConfiguration files that are environment-specific will take precedence:in production, for example, theconfig/storage/production.yml file (if existent)will take precedence over theconfig/storage.yml file.
It is recommended to useRails.env in the bucket names to further reduce the risk of accidentally destroying production data.
amazon:service:S3# ...bucket:your_own_bucket-<%= Rails.env %>google:service:GCS# ...bucket:your_own_bucket-<%= Rails.env %>Continue reading for more information on the built-in service adapters (e.g.Disk andS3) and the configuration they require.
2.1. Disk Service
Declare a Disk service inconfig/storage.yml:
local:service:Diskroot:<%= Rails.root.join("storage") %>2.2. S3 Service (Amazon S3 and S3-compatible APIs)
To connect to Amazon S3, declare an S3 service inconfig/storage.yml:
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)amazon:service:S3access_key_id:<%= Rails.application.credentials.dig(:aws, :access_key_id) %>secret_access_key:<%= Rails.application.credentials.dig(:aws, :secret_access_key) %>region:""# e.g. 'us-east-1'bucket:your_own_bucket-<%= Rails.env %>Optionally provide client and upload options:
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)amazon:service:S3access_key_id:<%= Rails.application.credentials.dig(:aws, :access_key_id) %>secret_access_key:<%= Rails.application.credentials.dig(:aws, :secret_access_key) %>region:""# e.g. 'us-east-1'bucket:your_own_bucket-<%= Rails.env %>http_open_timeout:0http_read_timeout:0retry_limit:0upload:server_side_encryption:""# 'aws:kms' or 'AES256'cache_control:"private,max-age=<%=1.day.to_i%>"Set sensible client HTTP timeouts and retry limits for your application. In certain failure scenarios, the default AWS client configuration may cause connections to be held for up to several minutes and lead to request queuing.
Add theaws-sdk-s3 gem to yourGemfile:
gem"aws-sdk-s3",require:falseThe core features of Active Storage require the following permissions:s3:ListBucket,s3:PutObject,s3:GetObject, ands3:DeleteObject.Public access additionally requiress3:PutObjectAcl. If you have additional upload options configured such as setting ACLs then additional permissions may be required.
If you want to use environment variables, standard SDK configuration files, profiles,IAM instance profiles or task roles, you can omit theaccess_key_id,secret_access_key,andregion keys in the example above. The S3 Service supports all of theauthentication options described in theAWS SDK documentation.
To connect to an S3-compatible object storage API such as DigitalOcean Spaces, provide theendpoint:
digitalocean:service:S3endpoint:https://nyc3.digitaloceanspaces.comaccess_key_id:<%= Rails.application.credentials.dig(:digitalocean, :access_key_id) %>secret_access_key:<%= Rails.application.credentials.dig(:digitalocean, :secret_access_key) %># ...and other optionsThere are many other options available. You can check them inAWS S3 Client documentation.
2.3. Google Cloud Storage Service
Declare a Google Cloud Storage service inconfig/storage.yml:
google:service:GCScredentials:<%= Rails.root.join("path/to/keyfile.json") %>project:""bucket:your_own_bucket-<%= Rails.env %>Optionally provide a Hash of credentials instead of a keyfile path:
# Use bin/rails credentials:edit to set the GCS secrets (as gcs:private_key_id|private_key)google:service:GCScredentials:type:"service_account"project_id:""private_key_id:<%= Rails.application.credentials.dig(:gcs, :private_key_id) %>private_key:<%= Rails.application.credentials.dig(:gcs, :private_key).dump %>client_email:""client_id:""auth_uri:"https://accounts.google.com/o/oauth2/auth"token_uri:"https://accounts.google.com/o/oauth2/token"auth_provider_x509_cert_url:"https://www.googleapis.com/oauth2/v1/certs"client_x509_cert_url:""project:""bucket:your_own_bucket-<%= Rails.env %>Optionally provide a Cache-Control metadata to set on uploaded assets:
google:service:GCS...cache_control:"public,max-age=3600"Optionally useIAM instead of thecredentials when signing URLs. This is useful if you are authenticating your GKE applications with Workload Identity, seethis Google Cloud blog post for more information.
google:service:GCS...iam:trueOptionally use a specific GSA when signing URLs. When using IAM, themetadata server will be contacted to get the GSA email, but this metadata server is not always present (e.g. local tests) and you may wish to use a non-default GSA.
google:service:GCS...iam:truegsa_email:"foobar@baz.iam.gserviceaccount.com"Add thegoogle-cloud-storage gem to yourGemfile:
gem"google-cloud-storage","~> 1.11",require:false2.4. Mirror Service
You can keep multiple services in sync by defining a mirror service. A mirrorservice replicates uploads and deletes across two or more subordinate services.
A mirror service is intended to be used temporarily during a migration betweenservices in production. You can start mirroring to a new service, copypre-existing files from the old service to the new, then go all-in on the newservice.
Mirroring is not atomic. It is possible for an upload to succeed on theprimary service and fail on any of the subordinate services. Before goingall-in on a new service, verify that all files have been copied.
Define each of the services you'd like to mirror as described above. Referencethem by name when defining a mirror service:
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)s3_west_coast:service:S3access_key_id:<%= Rails.application.credentials.dig(:aws, :access_key_id) %>secret_access_key:<%= Rails.application.credentials.dig(:aws, :secret_access_key) %>region:""# e.g. 'us-west-1'bucket:your_own_bucket-<%= Rails.env %>s3_east_coast:service:S3access_key_id:<%= Rails.application.credentials.dig(:aws, :access_key_id) %>secret_access_key:<%= Rails.application.credentials.dig(:aws, :secret_access_key) %>region:""# e.g. 'us-east-1'bucket:your_own_bucket-<%= Rails.env %>production:service:Mirrorprimary:s3_east_coastmirrors:-s3_west_coastAlthough all secondary services receive uploads, downloads are always handledby the primary service.
Mirror services are compatible with direct uploads. New files are directlyuploaded to the primary service. When a directly-uploaded file is attached to arecord, a background job is enqueued to copy it to the secondary services.
2.5. Public access
By default, Active Storage assumes private access to services. This means generating signed, single-use URLs for blobs. If you'd rather make blobs publicly accessible, specifypublic: true in your app'sconfig/storage.yml:
gcs:&gcsservice:GCSproject:""private_gcs:<<:*gcscredentials:<%= Rails.root.join("path/to/private_key.json") %>bucket:your_own_bucket-<%= Rails.env %>public_gcs:<<:*gcscredentials:<%= Rails.root.join("path/to/public_key.json") %>bucket:your_own_bucket-<%= Rails.env %>public:trueMake sure your buckets are properly configured for public access. See docs on how to enable public read permissions forAmazon S3 andGoogle Cloud Storage storage services. Amazon S3 additionally requires that you have thes3:PutObjectAcl permission.
When converting an existing application to usepublic: true, make sure to update every individual file in the bucket to be publicly-readable before switching over.
3. Attaching Files to Records
3.1.has_one_attached
Thehas_one_attached macro sets up a one-to-one mapping between records andfiles. Each record can have one file attached to it.
For example, suppose your application has aUser model. If you want each user tohave an avatar, define theUser model as follows:
classUser<ApplicationRecordhas_one_attached:avatarendor if you are using Rails 6.0+, you can run a model generator command like this:
$bin/railsgenerate model User avatar:attachmentYou can create a user with an avatar:
<%=form.file_field:avatar%>classSignupController<ApplicationControllerdefcreateuser=User.create!(user_params)session[:user_id]=user.idredirect_toroot_pathendprivatedefuser_paramsparams.expect(user:[:email_address,:password,:avatar])endendCallavatar.attach to attach an avatar to an existing user:
user.avatar.attach(params[:avatar])Callavatar.attached? to determine whether a particular user has an avatar:
user.avatar.attached?In some cases you might want to override a default service for a specific attachment.You can configure specific services per attachment using theservice option with the name of your service:
classUser<ApplicationRecordhas_one_attached:avatar,service: :googleendYou can configure specific variants per attachment by calling thevariant method on yielded attachable object:
classUser<ApplicationRecordhas_one_attached:avatardo|attachable|attachable.variant:thumb,resize_to_limit:[100,100]endendCallavatar.variant(:thumb) to get a thumb variant of an avatar:
<%=image_taguser.avatar.variant(:thumb)%>You can use specific variants for previews as well:
classUser<ApplicationRecordhas_one_attached:videodo|attachable|attachable.variant:thumb,resize_to_limit:[100,100]endend<%=image_taguser.video.preview(:thumb)%>If you know in advance that your variants will be accessed, you can specify thatRails should generate them ahead of time:
classUser<ApplicationRecordhas_one_attached:videodo|attachable|attachable.variant:thumb,resize_to_limit:[100,100],preprocessed:trueendendRails will enqueue a job to generate the variant after the attachment is attached to the record.
Since Active Storage relies on polymorphic associations, andpolymorphic associations rely on storing class names in the database, that data must remain synchronized with the class name used by the Ruby code. When renaming classes that usehas_one_attached, make sure to also update the class names in theactive_storage_attachments.record_type polymorphic type column of the corresponding rows.
3.2.has_many_attached
Thehas_many_attached macro sets up a one-to-many relationship between recordsand files. Each record can have many files attached to it.
For example, suppose your application has aMessage model. If you want eachmessage to have many images, define theMessage model as follows:
classMessage<ApplicationRecordhas_many_attached:imagesendor if you are using Rails 6.0+, you can run a model generator command like this:
$bin/railsgenerate model Message images:attachmentsYou can create a message with images:
classMessagesController<ApplicationControllerdefcreatemessage=Message.create!(message_params)redirect_tomessageendprivatedefmessage_paramsparams.expect(message:[:title,:content,images:[]])endendCallimages.attach to add new images to an existing message:
@message.images.attach(params[:images])Callimages.attached? to determine whether a particular message has any images:
@message.images.attached?Overriding the default service is done the same way ashas_one_attached, by using theservice option:
classMessage<ApplicationRecordhas_many_attached:images,service: :s3endConfiguring specific variants is done the same way ashas_one_attached, by calling thevariant method on the yielded attachable object:
classMessage<ApplicationRecordhas_many_attached:imagesdo|attachable|attachable.variant:thumb,resize_to_limit:[100,100]endendSince Active Storage relies on polymorphic associations, andpolymorphic associations rely on storing class names in the database, that data must remain synchronized with the class name used by the Ruby code. When renaming classes that usehas_many_attached, make sure to also update the class names in theactive_storage_attachments.record_type polymorphic type column of the corresponding rows.
3.3. Attaching File/IO Objects
Sometimes you need to attach a file that doesn’t arrive via an HTTP request.For example, you may want to attach a file you generated on disk or downloadedfrom a user-submitted URL. You may also want to attach a fixture file in amodel test. To do that, provide a Hash containing at least an open IO objectand a filename:
@message.images.attach(io:File.open("/path/to/file"),filename:"file.pdf")When possible, provide a content type as well. Active Storage attempts todetermine a file’s content type from its data. It falls back to the contenttype you provide if it can’t do that.
@message.images.attach(io:File.open("/path/to/file"),filename:"file.pdf",content_type:"application/pdf")You can bypass the content type inference from the data by passing inidentify: false along with thecontent_type.
@message.images.attach(io:File.open("/path/to/file"),filename:"file.pdf",content_type:"application/pdf",identify:false)If you don’t provide a content type and Active Storage can’t determine thefile’s content type automatically, it defaults to application/octet-stream.
There is an additional parameterkey that can be used to specify folders/sub-foldersin your S3 Bucket. AWS S3 otherwise uses a random key to name your files. Thisapproach is helpful if you want to organize your S3 Bucket files better.
@message.images.attach(io:File.open("/path/to/file"),filename:"file.pdf",content_type:"application/pdf",key:"#{Rails.env}/blog_content/intuitive_filename.pdf",identify:false)This way the file will get saved in the folder[S3_BUCKET]/development/blog_content/when you test this from your development environment. Note that if you use the keyparameter, you have to ensure the key to be unique for the upload to go through. It isrecommended to append the filename with a unique random key, something like:
defs3_file_key"#{Rails.env}/blog_content/intuitive_filename-#{SecureRandom.uuid}.pdf"end@message.images.attach(io:File.open("/path/to/file"),filename:"file.pdf",content_type:"application/pdf",key:s3_file_key,identify:false)3.4. Replacing vs Adding Attachments
By default in Rails, attaching files to ahas_many_attached association will replaceany existing attachments.
To keep existing attachments, you can use hidden form fields with thesigned_idof each attached file:
<%@message.images.eachdo|image|%><%=form.hidden_field:images,multiple:true,value:image.signed_id%><%end%><%=form.file_field:images,multiple:true%>This has the advantage of making it possible to remove existing attachmentsselectively, e.g. by using JavaScript to remove individual hidden fields.
3.5. Form Validation
Attachments aren't sent to the storage service until a successfulsave on theassociated record. This means that if a form submission fails validation, any newattachment(s) will be lost and must be uploaded again. Sincedirect uploadsare stored before the form is submitted, they can be used to retain uploads when validation fails:
<%=form.hidden_field:avatar,value:@user.avatar.signed_idif@user.avatar.attached?%><%=form.file_field:avatar,direct_upload:true%>4. Querying
Active Storage attachments are Active Record associations behind the scenes, so you can use the usualquery methods to look up records for attachments that meet specific criteria.
4.1.has_one_attached
has_one_attached creates ahas_one association named"<name>_attachment" and ahas_one :through association named"<name>_blob".To select every user where the avatar is a PNG, run the following:
User.joins(:avatar_blob).where(active_storage_blobs:{content_type:"image/png"})4.2.has_many_attached
has_many_attached creates ahas_many association called"<name>_attachments" and ahas_many :through association called"<name>_blobs" (note the plural).To select all messages where images are videos rather than photos you can do the following:
Message.joins(:images_blobs).where(active_storage_blobs:{content_type:"video/mp4"})The query will filter on theActiveStorage::Blob, not theattachment record because these are plain SQL joins. You can combine the blob predicates above with any other scope conditions, just as you would with any other Active Record query.
5. Removing Files
To remove an attachment from a model, callpurge on theattachment. If your application is set up to use Active Job, removal can be donein the background instead by callingpurge_later.Purging deletes the blob and the file from the storage service.
# Synchronously destroy the avatar and actual resource files.user.avatar.purge# Destroy the associated models and actual resource files async, via Active Job.user.avatar.purge_later6. Serving Files
Active Storage supports two ways to serve files: redirecting and proxying.
All Active Storage controllers are publicly accessible by default. Thegenerated URLs are hard to guess, but permanent by design. If your filesrequire a higher level of protection consider implementingAuthenticated Controllers.
6.1. Redirect Mode
To generate a permanent URL for a blob, you can pass the attachment or the blob totheurl_for view helper. This generates aURL with the blob'ssigned_idthat is routed to the blob'sRedirectController
url_for(user.avatar)# => https://www.example.com/rails/active_storage/blobs/redirect/:signed_id/my-avatar.pngTheRedirectController redirects to the actual service endpoint. Thisindirection decouples the service URL from the actual one, and allows, forexample, mirroring attachments in different services for high-availability. Theredirection has an HTTP expiration of 5 minutes.
To create a download link, use therails_blob_{path|url} helper. Using thishelper allows you to set the disposition.
rails_blob_path(user.avatar,disposition:"attachment")To prevent XSS attacks, Active Storage forces the Content-Disposition headerto "attachment" for some kind of files. To change this behavior see theavailable configuration options inConfiguring Rails Applications.
If you need to create a link from outside of controller/view context (Backgroundjobs, Cronjobs, etc.), you can access therails_blob_path like this:
Rails.application.routes.url_helpers.rails_blob_path(user.avatar,only_path:true)6.2. Proxy Mode
Optionally, files can be proxied instead. This means that your application servers will download file data from the storage service in response to requests. This can be useful for serving files from a CDN.
You can configure Active Storage to use proxying by default:
# config/initializers/active_storage.rbRails.application.config.active_storage.resolve_model_to_route=:rails_storage_proxyOr if you want to explicitly proxy specific attachments there are URL helpers you can use in the form ofrails_storage_proxy_path andrails_storage_proxy_url.
<%=image_tagrails_storage_proxy_path(@user.avatar)%>6.2.1. Putting a CDN in Front of Active Storage
Additionally, in order to use a CDN for Active Storage attachments, you will need to generate URLs with proxy mode so that they are served by your app and the CDN will cache the attachment without any extra configuration. This works out of the box because the default Active Storage proxy controller sets an HTTP header indicating to the CDN to cache the response.
You should also make sure that the generated URLs use the CDN host instead of your app host. There are multiple ways to achieve this, but in general it involves tweaking yourconfig/routes.rb file so that you can generate the proper URLs for the attachments and their variations. As an example, you could add this:
# config/routes.rbdirect:cdn_imagedo|model,options|expires_in=options.delete(:expires_in){ActiveStorage.urls_expire_in}ifmodel.respond_to?(:signed_id)route_for(:rails_service_blob_proxy,model.signed_id(expires_in:expires_in),model.filename,options.merge(host:ENV["CDN_HOST"]))elsesigned_blob_id=model.blob.signed_id(expires_in:expires_in)variation_key=model.variation.keyfilename=model.blob.filenameroute_for(:rails_blob_representation_proxy,signed_blob_id,variation_key,filename,options.merge(host:ENV["CDN_HOST"]))endendand then generate routes like this:
<%=cdn_image_url(user.avatar.variant(resize_to_limit:[128,128]))%>6.3. Authenticated Controllers
All Active Storage controllers are publicly accessible by default. The generatedURLs use a plainsigned_id, making them hard toguess but permanent. Anyone that knows the blob URL will be able to access it,even if abefore_action in yourApplicationController would otherwiserequire a login. If your files require a higher level of protection, you canimplement your own authenticated controllers, based on theActiveStorage::Blobs::RedirectController,ActiveStorage::Blobs::ProxyController,ActiveStorage::Representations::RedirectController andActiveStorage::Representations::ProxyController
To only allow an account to access their own logo you could do the following:
# config/routes.rbresource:accountdoresource:logoend# app/controllers/logos_controller.rbclassLogosController<ApplicationController# Through ApplicationController:# include Authenticate, SetCurrentAccountdefshowredirect_toCurrent.account.logo.urlendend<%=image_tagaccount_logo_path%>And then you should disable the Active Storage default routes with:
config.active_storage.draw_routes=falseto prevent files being accessed with the publicly accessible URLs.
7. Downloading Files
Sometimes you need to process a blob after it’s uploaded—for example, to convertit to a different format. Use the attachment'sdownload method to read a blob’sbinary data into memory:
binary=user.avatar.downloadYou might want to download a blob to a file on disk so an external program (e.g.a virus scanner or media transcoder) can operate on it. Use the attachment'sopen method to download a blob to a tempfile on disk:
message.video.opendo|file|system"/path/to/virus/scanner",file.path# ...endIt's important to know that the file is not yet available in theafter_create callback but in theafter_create_commit only.
8. Analyzing Files
Active Storage analyzes files once they've been uploaded by queuing a job in Active Job. Analyzed files will store additional information in the metadata hash, includinganalyzed: true. You can check whether a blob has been analyzed by callinganalyzed? on it.
Image analysis provideswidth andheight attributes. Video analysis provides these, as well asduration,angle,display_aspect_ratio, andvideo andaudio booleans to indicate the presence of those channels. Audio analysis providesduration andbit_rate attributes.
9. Displaying Images, Videos, and PDFs
Active Storage supports representing a variety of files. You can callrepresentation on an attachment to display an image variant, or apreview of a video or PDF. Before callingrepresentation, check if theattachment can be represented by callingrepresentable?. Some file formatscan't be previewed by Active Storage out of the box (e.g. Word documents); ifrepresentable? returns false you may want tolink tothe file instead.
<ul><%@message.files.eachdo|file|%><li><%iffile.representable?%><%=image_tagfile.representation(resize_to_limit:[100,100])%><%else%><%=link_torails_blob_path(file,disposition:"attachment")do%><%=image_tag"placeholder.png",alt:"Download file"%><%end%><%end%></li><%end%></ul>Internally,representation callsvariant for images, andpreview forpreviewable files. You can also call these methods directly.
9.1. Lazy vs Immediate Loading
By default, Active Storage will process representations lazily. This code:
image_tagfile.representation(resize_to_limit:[100,100])Will generate an<img> tag with thesrc pointing to theActiveStorage::Representations::RedirectController. The browser willmake a request to that controller, which will perform the following:
- Process file and upload the processed file if necessary.
- Return a
302redirect to the file either to- the remote service (e.g., S3).
- or
ActiveStorage::Blobs::ProxyControllerwhich will return the file contents ifproxy mode is enabled.
Loading the file lazily allows features likesingle use URLsto work without slowing down your initial page loads.
This works fine for most cases.
If you want to generate URLs for images immediately, you can call.processed.url:
image_tagfile.representation(resize_to_limit:[100,100]).processed.urlThe Active Storage variant tracker improves performance of this, by storing arecord in the database if the requested representation has been processed before.Thus, the above code will only make an API call to the remote service (e.g. S3)once, and once a variant is stored, will use that. The variant tracker runsautomatically, but can be disabled throughconfig.active_storage.track_variants.
If you're rendering lots of images on a page, the above example could resultin N+1 queries loading all the variant records. To avoid these N+1 queries,use the named scopes onActiveStorage::Attachment.
message.images.with_all_variant_records.eachdo|file|image_tagfile.representation(resize_to_limit:[100,100]).processed.urlend9.2. Transforming Images
Transforming images allows you to display the image at your choice of dimensions.To create a variation of an image, callvariant on the attachment. Youcan pass any transformation supported by the variant processor to the method.When the browser hits the variant URL, Active Storage will lazily transformthe original blob into the specified format and redirect to its new servicelocation.
<%=image_taguser.avatar.variant(resize_to_limit:[100,100])%>If a variant is requested, Active Storage will automatically applytransformations depending on the image's format:
Content types that are variable (as dictated by
config.active_storage.variable_content_types)and not considered web images (as dictated byconfig.active_storage.web_image_content_types),will be converted to PNG.If
qualityis not specified, the variant processor's default quality for the format will be used.
Active Storage can use eitherVips or MiniMagick as the variant processor.The default depends on yourconfig.load_defaults target version, and theprocessor can be changed by settingconfig.active_storage.variant_processor.
9.3. Previewing Files
Some non-image files can be previewed: that is, they can be presented as images.For example, a video file can be previewed by extracting its first frame. Out ofthe box, Active Storage supports previewing videos and PDF documents. To createa link to a lazily-generated preview, use the attachment'spreview method:
<%=image_tagmessage.video.preview(resize_to_limit:[100,100])%>To add support for another format, add your own previewer. See theActiveStorage::Preview documentation for more information.
10. Direct Uploads
Active Storage, with its included JavaScript library, supports uploadingdirectly from the client to the cloud.
10.1. Usage
Include the Active Storage JavaScript in your application's JavaScript bundle or reference it directly.
Requiring directly without bundling through the asset pipeline in the application HTML with autostart:
<%=javascript_include_tag"activestorage"%>Requiring via importmap-rails without bundling through the asset pipeline in the application HTML without autostart as ESM:
# config/importmap.rbpin"@rails/activestorage",to:"activestorage.esm.js"<scripttype="module-shim">import*asActiveStoragefrom"@rails/activestorage"ActiveStorage.start()</script>Using the asset pipeline:
//= require activestorageUsing the npm package:
import*asActiveStoragefrom"@rails/activestorage"ActiveStorage.start()Annotate file inputs with the direct upload URL using Rails'file field helper.
<%=form.file_field:attachments,multiple:true,direct_upload:true%>Or, if you aren't using a
FormBuilder, add the data attribute directly:<inputtype="file"data-direct-upload-url="<%=rails_direct_uploads_url%>"/>Configure CORS on third-party storage services to allow direct upload requests.
That's it! Uploads begin upon form submission.
10.2. Cross-Origin Resource Sharing (CORS) Configuration
To make direct uploads to a third-party service work, you’ll need to configure the service to allow cross-origin requests from your app. Consult the CORS documentation for your service:
Take care to allow:
- All origins from which your app is accessed
- The
PUTrequest method - The following headers:
Content-TypeContent-MD5Content-DispositionCache-Control(for GCS, only ifcache_controlis set)
No CORS configuration is required for the Disk service since it shares your app’s origin.
10.2.1. Example: S3 CORS Configuration
[{"AllowedHeaders":["Content-Type","Content-MD5","Content-Disposition"],"AllowedMethods":["PUT"],"AllowedOrigins":["https://www.example.com"],"MaxAgeSeconds":3600}]10.2.2. Example: Google Cloud Storage CORS Configuration
[{"origin":["https://www.example.com"],"method":["PUT"],"responseHeader":["Content-Type","Content-MD5","Content-Disposition"],"maxAgeSeconds":3600}]10.3. Direct Upload JavaScript Events
| Event name | Event target | Event data (event.detail) | Description |
|---|---|---|---|
direct-uploads:start | <form> | None | A form containing files for direct upload fields was submitted. |
direct-upload:initialize | <input> | {id, file} | Dispatched for every file after form submission. |
direct-upload:start | <input> | {id, file} | A direct upload is starting. |
direct-upload:before-blob-request | <input> | {id, file, xhr} | Before making a request to your application for direct upload metadata. |
direct-upload:before-storage-request | <input> | {id, file, xhr} | Before making a request to store a file. |
direct-upload:progress | <input> | {id, file, progress} | As requests to store files progress. |
direct-upload:error | <input> | {id, file, error} | An error occurred. Analert will display unless this event is canceled. |
direct-upload:end | <input> | {id, file} | A direct upload has ended. |
direct-uploads:end | <form> | None | All direct uploads have ended. |
10.4. Example
You can use these events to show the progress of an upload.

To show the uploaded files in a form:
// direct_uploads.jsaddEventListener("direct-upload:initialize",event=>{const{target,detail}=eventconst{id,file}=detailtarget.insertAdjacentHTML("beforebegin",` <divp">${id}"> <divp">${id}"></div> <span></span> </div> `)target.previousElementSibling.querySelector(`.direct-upload__filename`).textContent=file.name})addEventListener("direct-upload:start",event=>{const{id}=event.detailconstelement=document.getElementById(`direct-upload-${id}`)element.classList.remove("direct-upload--pending")})addEventListener("direct-upload:progress",event=>{const{id,progress}=event.detailconstprogressElement=document.getElementById(`direct-upload-progress-${id}`)progressElement.style.width=`${progress}%`})addEventListener("direct-upload:error",event=>{event.preventDefault()const{id,error}=event.detailconstelement=document.getElementById(`direct-upload-${id}`)element.classList.add("direct-upload--error")element.setAttribute("title",error)})addEventListener("direct-upload:end",event=>{const{id}=event.detailconstelement=document.getElementById(`direct-upload-${id}`)element.classList.add("direct-upload--complete")})Add styles:
/* direct_uploads.css */.direct-upload{display:inline-block;position:relative;padding:2px4px;margin:03px3px0;border:1pxsolidrgba(0,0,0,0.3);border-radius:3px;font-size:11px;line-height:13px;}.direct-upload--pending{opacity:0.6;}.direct-upload__progress{position:absolute;top:0;left:0;bottom:0;opacity:0.2;background:#0076ff;transition:width120msease-out,opacity60ms60msease-in;transform:translate3d(0,0,0);}.direct-upload--complete.direct-upload__progress{opacity:0.4;}.direct-upload--error{border-color:red;}input[type=file][data-direct-upload-url][disabled]{display:none;}10.5. Custom drag and drop solutions
You can use theDirectUpload class for this purpose. Upon receiving a file from your libraryof choice, instantiate a DirectUpload and call its create method. Create takesa callback to invoke when the upload completes.
import{DirectUpload}from"@rails/activestorage"constinput=document.querySelector('input[type=file]')// Bind to file drop - use the ondrop on a parent element or use a// library like DropzoneconstonDrop=(event)=>{event.preventDefault()constfiles=event.dataTransfer.files;Array.from(files).forEach(file=>uploadFile(file))}// Bind to normal file selectioninput.addEventListener('change',(event)=>{Array.from(input.files).forEach(file=>uploadFile(file))// you might clear the selected files from the inputinput.value=null})constuploadFile=(file)=>{// your form needs the file_field direct_upload: true, which// provides data-direct-upload-urlconsturl=input.dataset.directUploadUrlconstupload=newDirectUpload(file,url)upload.create((error,blob)=>{if(error){// Handle the error}else{// Add an appropriately-named hidden input to the form with a// value of blob.signed_id so that the blob ids will be// transmitted in the normal upload flowconsthiddenField=document.createElement('input')hiddenField.setAttribute("type","hidden");hiddenField.setAttribute("value",blob.signed_id);hiddenField.name=input.namedocument.querySelector('form').appendChild(hiddenField)}})}10.6. Track the progress of the file upload
When using theDirectUpload constructor, it is possible to include a third parameter.This will allow theDirectUpload object to invoke thedirectUploadWillStoreFileWithXHRmethod during the upload process.You can then attach your own progress handler to the XHR to suit your needs.
import{DirectUpload}from"@rails/activestorage"classUploader{constructor(file,url){this.upload=newDirectUpload(file,url,this)}uploadFile(file){this.upload.create((error,blob)=>{if(error){// Handle the error}else{// Add an appropriately-named hidden input to the form// with a value of blob.signed_id}})}directUploadWillStoreFileWithXHR(request){request.upload.addEventListener("progress",event=>this.directUploadDidProgress(event))}directUploadDidProgress(event){// Use event.loaded and event.total to update the progress bar}}10.7. Integrating with Libraries or Frameworks
Once you receive a file from the library you have selected, you need to createaDirectUpload instance and use its "create" method to initiate the upload process,adding any required additional headers as necessary. The "create" method also requiresa callback function to be provided that will be triggered once the upload has finished.
import{DirectUpload}from"@rails/activestorage"classUploader{constructor(file,url,token){constheaders={'Authentication':`Bearer${token}`}// INFO: Sending headers is an optional parameter. If you choose not to send headers,// authentication will be performed using cookies or session data.this.upload=newDirectUpload(file,url,this,headers)}uploadFile(file){this.upload.create((error,blob)=>{if(error){// Handle the error}else{// Use the with blob.signed_id as a file reference in next request}})}directUploadWillStoreFileWithXHR(request){request.upload.addEventListener("progress",event=>this.directUploadDidProgress(event))}directUploadDidProgress(event){// Use event.loaded and event.total to update the progress bar}}To implement customized authentication, a new controller must be created onthe Rails application, similar to the following:
classDirectUploadsController<ActiveStorage::DirectUploadsControllerskip_forgery_protectionbefore_action:authenticate!defauthenticate!@token=request.headers["Authorization"]&.split&.lasthead:unauthorizedunlessvalid_token?(@token)endendUsingDirect Uploads can sometimes result in a file that uploads, but never attaches to a record. Considerpurging unattached uploads.
11. Testing
Usefile_fixture_upload to test uploading a file in an integration or controller test.Rails handles files like any other parameter.
classSignupController<ActionDispatch::IntegrationTesttest"can sign up"dopostsignup_path,params:{name:"David",avatar:file_fixture_upload("david.png","image/png")}user=User.order(:created_at).lastassertuser.avatar.attached?endend11.1. Discarding Files Created During Tests
11.1.1. System Tests
System tests clean up test data by rolling back a transaction. Becausedestroyis never called on an object, the attached files are never cleaned up. If youwant to clear the files, you can do it in anafter_teardown callback. Doing ithere ensures that all connections created during the test are complete andyou won't receive an error from Active Storage saying it can't find a file.
classApplicationSystemTestCase<ActionDispatch::SystemTestCase# ...defafter_teardownsuperFileUtils.rm_rf(ActiveStorage::Blob.service.root)end# ...endIf you're usingparallel tests and theDiskService, you should configure each process to use its ownfolder for Active Storage. This way, theteardown callback will only delete files from the relevant process'tests.
classApplicationSystemTestCase<ActionDispatch::SystemTestCase# ...parallelize_setupdo|i|ActiveStorage::Blob.service.root="#{ActiveStorage::Blob.service.root}-#{i}"end# ...endIf your system tests verify the deletion of a model with attachments and you'reusing Active Job, set your test environment to use the inline queue adapter sothe purge job is executed immediately rather at an unknown time in the future.
# Use inline job processing to make things happen immediatelyconfig.active_job.queue_adapter=:inline11.1.2. Integration Tests
Similarly to System Tests, files uploaded during Integration Tests will not beautomatically cleaned up. If you want to clear the files, you can do it in anteardown callback.
classActionDispatch::IntegrationTestdefafter_teardownsuperFileUtils.rm_rf(ActiveStorage::Blob.service.root)endendIf you're usingparallel tests and the Disk service, you should configure each process to use its ownfolder for Active Storage. This way, theteardown callback will only delete files from the relevant process'tests.
classActionDispatch::IntegrationTestparallelize_setupdo|i|ActiveStorage::Blob.service.root="#{ActiveStorage::Blob.service.root}-#{i}"endend11.2. Adding Attachments to Fixtures
You can add attachments to your existingfixtures. First, you'll want to create a separate storage service:
# config/storage.ymltest_fixtures:service:Diskroot:<%= Rails.root.join("tmp/storage_fixtures") %>This tells Active Storage where to "upload" fixture files to, so it should be a temporary directory. By making ita different directory to your regulartest service, you can separate fixture files from files uploaded during atest.
Next, create fixture files for the Active Storage classes:
# test/fixtures/active_storage/attachments.ymldavid_avatar:name:avatarrecord:david (User)blob:david_avatar_blob# test/fixtures/active_storage/blobs.ymldavid_avatar_blob: <%= ActiveStorage::FixtureSet.blob filename:"david.png",service_name:"test_fixtures"%>Then put a file in your fixtures directory (the default path istest/fixtures/files) with the corresponding filename.See theActiveStorage::FixtureSet docs for more information.
Once everything is set up, you'll be able to access attachments in your tests:
classUserTest<ActiveSupport::TestCasedeftest_avataravatar=users(:david).avatarassertavatar.attached?assert_not_nilavatar.downloadassert_equal1000,avatar.byte_sizeendend11.2.1. Cleaning up Fixtures
While files uploaded in tests are cleaned upat the end of each test,you only need to clean up fixture files once: when all your tests complete.
If you're using parallel tests, callparallelize_teardown:
classActiveSupport::TestCase# ...parallelize_teardowndo|i|FileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)end# ...endIf you're not running parallel tests, useMinitest.after_run or the equivalent for your testframework (e.g.after(:suite) for RSpec):
# test_helper.rbMinitest.after_rundoFileUtils.rm_rf(ActiveStorage::Blob.services.fetch(:test_fixtures).root)end11.3. Configuring services
You can addconfig/storage/test.yml to configure services to be used in test environment.This is useful when theservice option is used.
classUser<ApplicationRecordhas_one_attached:avatar,service: :s3endWithoutconfig/storage/test.yml, thes3 service configured inconfig/storage.yml is used - even when running tests.
The default configuration would be used and files would be uploaded to the service provider configured inconfig/storage.yml.
In this case, you can addconfig/storage/test.yml and use Disk service fors3 service to prevent sending requests.
test:service:Diskroot:<%= Rails.root.join("tmp/storage") %>s3:service:Diskroot:<%= Rails.root.join("tmp/storage") %>12. Implementing Support for Other Cloud Services
If you need to support a cloud service other than these, you will need toimplement the Service. Each service extendsActiveStorage::Serviceby implementing the methods necessary to upload and download files to the cloud.
13. Purging Unattached Uploads
There are cases where a file is uploaded but never attached to a record. This can happen when usingDirect Uploads. You can query for unattached records using theunattached scope. Below is an example using acustom rake task.
namespace:active_storagedodesc"Purges unattached Active Storage blobs. Run regularly."taskpurge_unattached: :environmentdoActiveStorage::Blob.unattached.where(created_at:..2.days.ago).find_each(&:purge_later)endendThe query generated byActiveStorage::Blob.unattached can be slow and potentially disruptive on applications with larger databases.