2017-11-02 19:52:42 -04:00
|
|
|
|
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
|
|
|
|
|
|
2017-12-15 00:17:19 -05:00
|
|
|
|
Active Storage Overview
|
|
|
|
|
=======================
|
2017-11-02 19:52:42 -04:00
|
|
|
|
|
2017-11-18 20:24:37 -05:00
|
|
|
|
This guide covers how to attach files to your Active Record models.
|
2017-11-02 19:52:42 -04:00
|
|
|
|
|
|
|
|
|
After reading this guide, you will know:
|
|
|
|
|
|
2017-11-18 20:24:37 -05:00
|
|
|
|
* 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.
|
2017-11-16 22:07:10 -05:00
|
|
|
|
* How to clean up files stored during testing.
|
2017-12-14 22:08:33 -05:00
|
|
|
|
* How to implement support for additional storage services.
|
2017-11-02 19:52:42 -04:00
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
2017-11-18 20:24:37 -05:00
|
|
|
|
What is Active Storage?
|
|
|
|
|
-----------------------
|
2017-11-02 19:52:42 -04:00
|
|
|
|
|
2017-11-18 20:24:37 -05:00
|
|
|
|
Active Storage facilitates uploading files to a cloud storage service like
|
|
|
|
|
Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching those
|
|
|
|
|
files to Active Record objects. It comes with a local disk-based service for
|
|
|
|
|
development and testing and supports mirroring files to subordinate services for
|
|
|
|
|
backups and migrations.
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-11-18 20:24:37 -05:00
|
|
|
|
Using Active Storage, an application can transform image uploads with
|
|
|
|
|
[ImageMagick](https://www.imagemagick.org), generate image representations of
|
|
|
|
|
non-image uploads like PDFs and videos, and extract metadata from arbitrary
|
|
|
|
|
files.
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-11-18 20:24:37 -05:00
|
|
|
|
## Setup
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-11-30 11:42:04 -05:00
|
|
|
|
Active Storage uses two tables in your application’s database named
|
|
|
|
|
`active_storage_blobs` and `active_storage_attachments`. After upgrading your
|
|
|
|
|
application to Rails 5.2, run `rails active_storage:install` to generate a
|
|
|
|
|
migration that creates these tables. Use `rails db:migrate` to run the
|
|
|
|
|
migration.
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-11-30 11:42:04 -05:00
|
|
|
|
You need not run `rails active_storage:install` in a new Rails 5.2 application:
|
|
|
|
|
the migration is generated automatically.
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-11-30 11:42:04 -05:00
|
|
|
|
Declare Active Storage services in `config/storage.yml`. For each service your
|
|
|
|
|
application uses, provide a name and the requisite configuration. The example
|
2017-12-15 18:49:18 -05:00
|
|
|
|
below declares three services named `local`, `test`, and `amazon`:
|
2017-11-30 11:42:04 -05:00
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
local:
|
|
|
|
|
service: Disk
|
|
|
|
|
root: <%= Rails.root.join("storage") %>
|
|
|
|
|
|
|
|
|
|
test:
|
|
|
|
|
service: Disk
|
|
|
|
|
root: <%= Rails.root.join("tmp/storage") %>
|
|
|
|
|
|
2017-12-15 18:49:18 -05:00
|
|
|
|
amazon:
|
2017-11-30 11:42:04 -05:00
|
|
|
|
service: S3
|
|
|
|
|
access_key_id: ""
|
|
|
|
|
secret_access_key: ""
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Tell Active Storage which service to use by setting
|
|
|
|
|
`Rails.application.config.active_storage.service`. Because each environment will
|
|
|
|
|
likely use a different service, it is recommended to do this on a
|
|
|
|
|
per-environment basis. To use the disk service from the previous example in the
|
|
|
|
|
development environment, you would add the following to
|
2017-12-15 02:33:38 -05:00
|
|
|
|
`config/environments/development.rb`:
|
2017-11-30 11:42:04 -05:00
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
# Store files locally.
|
|
|
|
|
config.active_storage.service = :local
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-15 18:49:18 -05:00
|
|
|
|
To use the Amazon S3 service in production, you add the following to
|
2017-11-30 11:42:04 -05:00
|
|
|
|
`config/environments/production.rb`:
|
|
|
|
|
|
|
|
|
|
```ruby
|
2017-12-15 18:49:18 -05:00
|
|
|
|
# Store files on Amazon S3.
|
|
|
|
|
config.active_storage.service = :amazon
|
2017-11-18 20:24:37 -05:00
|
|
|
|
```
|
2017-11-16 22:07:10 -05:00
|
|
|
|
|
2017-11-30 11:42:04 -05:00
|
|
|
|
Continue reading for more information on the built-in service adapters (e.g.
|
|
|
|
|
`Disk` and `S3`) and the configuration they require.
|
2017-11-18 20:24:37 -05:00
|
|
|
|
|
|
|
|
|
### Disk Service
|
2017-12-14 22:08:33 -05:00
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
Declare a Disk service in `config/storage.yml`:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
|
|
|
|
|
``` yaml
|
|
|
|
|
local:
|
|
|
|
|
service: Disk
|
|
|
|
|
root: <%= Rails.root.join("storage") %>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Amazon S3 Service
|
2017-12-14 22:08:33 -05:00
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
Declare an S3 service in `config/storage.yml`:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
|
|
|
|
|
``` yaml
|
2017-12-15 18:49:18 -05:00
|
|
|
|
amazon:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
service: S3
|
|
|
|
|
access_key_id: ""
|
|
|
|
|
secret_access_key: ""
|
|
|
|
|
region: ""
|
|
|
|
|
bucket: ""
|
|
|
|
|
```
|
2017-12-15 02:33:38 -05:00
|
|
|
|
Also, add the S3 client gem to your `Gemfile`:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
|
|
|
|
|
``` ruby
|
|
|
|
|
gem "aws-sdk-s3", require: false
|
|
|
|
|
```
|
2017-12-14 22:09:48 -05:00
|
|
|
|
|
2017-11-18 20:24:37 -05:00
|
|
|
|
### Microsoft Azure Storage Service
|
2017-12-14 22:08:33 -05:00
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
Declare an Azure Storage service in `config/storage.yml`:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
|
|
|
|
|
``` yaml
|
2017-11-30 11:42:04 -05:00
|
|
|
|
azure:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
service: AzureStorage
|
|
|
|
|
path: ""
|
|
|
|
|
storage_account_name: ""
|
|
|
|
|
storage_access_key: ""
|
|
|
|
|
container: ""
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-15 02:33:38 -05:00
|
|
|
|
Also, add the Microsoft Azure Storage client gem to your `Gemfile`:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
|
|
|
|
|
``` ruby
|
|
|
|
|
gem "azure-storage", require: false
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Google Cloud Storage Service
|
2017-12-14 22:08:33 -05:00
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
Declare a Google Cloud Storage service in `config/storage.yml`:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
|
|
|
|
|
``` yaml
|
2017-11-30 11:42:04 -05:00
|
|
|
|
google:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
service: GCS
|
|
|
|
|
keyfile: {
|
|
|
|
|
type: "service_account",
|
|
|
|
|
project_id: "",
|
|
|
|
|
private_key_id: "",
|
|
|
|
|
private_key: "",
|
|
|
|
|
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: ""
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-15 02:33:38 -05:00
|
|
|
|
Also, add the Google Cloud Storage client gem to your `Gemfile`:
|
2017-11-18 20:24:37 -05:00
|
|
|
|
|
|
|
|
|
``` ruby
|
|
|
|
|
gem "google-cloud-storage", "~> 1.3", require: false
|
|
|
|
|
```
|
2017-11-16 22:07:10 -05:00
|
|
|
|
|
2017-11-18 20:24:37 -05:00
|
|
|
|
### Mirror Service
|
2017-11-16 22:07:10 -05:00
|
|
|
|
|
2017-12-08 16:44:59 -05:00
|
|
|
|
You can keep multiple services in sync by defining a mirror service. When a file
|
|
|
|
|
is uploaded or deleted, it's done across all the mirrored services. Mirrored
|
|
|
|
|
services can be used to facilitate a migration between services in production.
|
|
|
|
|
You can start mirroring to the new service, copy existing files from the old
|
|
|
|
|
service to the new, then go all-in on the new service. Define each of the
|
|
|
|
|
services you'd like to use as described above and reference them from a mirrored
|
|
|
|
|
service.
|
2017-11-16 22:07:10 -05:00
|
|
|
|
|
|
|
|
|
``` yaml
|
2017-11-18 20:24:37 -05:00
|
|
|
|
s3_west_coast:
|
|
|
|
|
service: S3
|
|
|
|
|
access_key_id: ""
|
|
|
|
|
secret_access_key: ""
|
|
|
|
|
region: ""
|
|
|
|
|
bucket: ""
|
|
|
|
|
|
|
|
|
|
s3_east_coast:
|
|
|
|
|
service: S3
|
|
|
|
|
access_key_id: ""
|
|
|
|
|
secret_access_key: ""
|
|
|
|
|
region: ""
|
|
|
|
|
bucket: ""
|
|
|
|
|
|
|
|
|
|
production:
|
|
|
|
|
service: Mirror
|
|
|
|
|
primary: s3_east_coast
|
|
|
|
|
mirrors:
|
|
|
|
|
- s3_west_coast
|
2017-11-16 22:07:10 -05:00
|
|
|
|
```
|
|
|
|
|
|
2017-11-30 12:19:57 -05:00
|
|
|
|
NOTE: Files are served from the primary service.
|
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
Attach Files to a Model
|
2017-12-14 22:08:33 -05:00
|
|
|
|
-----------------------
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
### `has_one_attached`
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
The `has_one_attached` macro sets up a one-to-one mapping between records and
|
|
|
|
|
files. Each record can have one file attached to it.
|
|
|
|
|
|
|
|
|
|
For example, suppose your application has a User model. If you want each user to
|
|
|
|
|
have an avatar, define the `User` model like this:
|
|
|
|
|
|
|
|
|
|
``` ruby
|
2017-11-09 11:51:14 -05:00
|
|
|
|
class User < ApplicationRecord
|
|
|
|
|
has_one_attached :avatar
|
|
|
|
|
end
|
2017-12-04 23:56:16 -05:00
|
|
|
|
```
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
You can create a user with an avatar:
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
``` ruby
|
|
|
|
|
class SignupController < ApplicationController
|
|
|
|
|
def create
|
|
|
|
|
user = Users.create!(user_params)
|
|
|
|
|
session[:user_id] = user.id
|
|
|
|
|
redirect_to root_path
|
2017-11-09 11:51:14 -05:00
|
|
|
|
end
|
2017-12-04 23:56:16 -05:00
|
|
|
|
|
|
|
|
|
private
|
|
|
|
|
def user_params
|
|
|
|
|
params.require(:user).permit(:email_address, :password, :avatar)
|
|
|
|
|
end
|
2017-11-09 11:51:14 -05:00
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
Call `avatar.attach` to attach an avatar to an existing user:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
Current.user.avatar.attach(params[:avatar])
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Call `avatar.attached?` to determine whether a particular user has an avatar:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
Current.user.avatar.attached?
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `has_many_attached`
|
|
|
|
|
|
|
|
|
|
The `has_many_attached` macro sets up a one-to-many relationship between records
|
|
|
|
|
and files. Each record can have many files attached to it.
|
|
|
|
|
|
|
|
|
|
For example, suppose your application has a `Message` model. If you want each
|
|
|
|
|
message to have many images, define the Message model like this:
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
class Message < ApplicationRecord
|
|
|
|
|
has_many_attached :images
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
You can create a message with images:
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
class MessagesController < ApplicationController
|
|
|
|
|
def create
|
2017-11-21 13:27:02 -05:00
|
|
|
|
message = Message.create!(message_params)
|
2017-11-09 11:51:14 -05:00
|
|
|
|
redirect_to message
|
|
|
|
|
end
|
|
|
|
|
|
2017-11-21 13:27:02 -05:00
|
|
|
|
private
|
|
|
|
|
def message_params
|
|
|
|
|
params.require(:message).permit(:title, :content, images: [])
|
|
|
|
|
end
|
2017-11-09 11:51:14 -05:00
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-04 23:56:16 -05:00
|
|
|
|
Call `images.attach` to add new images to an existing message:
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
@message.images.attach(params[:images])
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-08 16:44:59 -05:00
|
|
|
|
Call `images.attached?` to determine whether a particular message has any images:
|
2017-12-04 23:56:16 -05:00
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
@message.images.attached?
|
|
|
|
|
```
|
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
Remove File Attached to Model
|
2017-12-14 22:08:33 -05:00
|
|
|
|
-----------------------------
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-11-16 22:07:10 -05:00
|
|
|
|
To remove an attachment from a model, call `purge` on the attachment. Removal
|
2017-11-21 13:29:41 -05:00
|
|
|
|
can be done in the background if your application is setup to use Active Job.
|
2017-11-18 20:24:37 -05:00
|
|
|
|
Purging deletes the blob and the file from the storage service.
|
2017-11-16 22:07:10 -05:00
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
```ruby
|
|
|
|
|
# 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_later
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Link to Attachments
|
2017-12-14 22:08:33 -05:00
|
|
|
|
-------------------
|
2017-11-16 22:07:10 -05:00
|
|
|
|
|
|
|
|
|
Generate a permanent URL for the blob that points to the application. Upon
|
|
|
|
|
access, a redirect to the actual service endpoint is returned. This indirection
|
2017-11-20 20:01:00 -05:00
|
|
|
|
decouples the public URL from the actual one, and allows, for example, mirroring
|
2017-11-16 22:07:10 -05:00
|
|
|
|
attachments in different services for high-availability. The redirection has an
|
|
|
|
|
HTTP expiration of 5 min.
|
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
```ruby
|
|
|
|
|
url_for(user.avatar)
|
|
|
|
|
```
|
|
|
|
|
|
2017-11-20 20:01:00 -05:00
|
|
|
|
To create a download link, use the `rails_blob_{path|url}` helper. Using this
|
2017-11-21 13:27:02 -05:00
|
|
|
|
helper allows you to set the disposition.
|
2017-11-20 20:01:00 -05:00
|
|
|
|
|
|
|
|
|
```ruby
|
2017-11-20 20:09:28 -05:00
|
|
|
|
rails_blob_path(user.avatar, disposition: "attachment")
|
2017-11-20 20:01:00 -05:00
|
|
|
|
```
|
|
|
|
|
|
2017-12-14 22:08:33 -05:00
|
|
|
|
Transform Images
|
|
|
|
|
----------------
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
2017-12-14 22:08:33 -05:00
|
|
|
|
To create variation of the image, call `variant` on the Blob.
|
2017-11-16 22:07:10 -05:00
|
|
|
|
You can pass any [MiniMagick](https://github.com/minimagick/minimagick)
|
2017-12-08 16:44:59 -05:00
|
|
|
|
supported transformation to the method.
|
|
|
|
|
|
2017-12-15 02:33:38 -05:00
|
|
|
|
To enable variants, add `mini_magick` to your `Gemfile`:
|
2017-12-08 16:44:59 -05:00
|
|
|
|
|
|
|
|
|
``` ruby
|
|
|
|
|
gem 'mini_magick'
|
|
|
|
|
```
|
2017-11-16 22:07:10 -05:00
|
|
|
|
|
|
|
|
|
When the browser hits the variant URL, ActiveStorage will lazy transform the
|
|
|
|
|
original blob into the format you specified and redirect to its new service
|
|
|
|
|
location.
|
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
```erb
|
|
|
|
|
<%= image_tag user.avatar.variant(resize: "100x100") %>
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-14 22:08:33 -05:00
|
|
|
|
Preview Non-image 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 of
|
|
|
|
|
the box, Active Storage supports previewing videos and PDF documents.
|
2017-11-09 11:51:14 -05:00
|
|
|
|
|
|
|
|
|
```erb
|
|
|
|
|
<ul>
|
|
|
|
|
<% @message.files.each do |file| %>
|
|
|
|
|
<li>
|
|
|
|
|
<%= image_tag file.preview(resize: "100x100>") %>
|
|
|
|
|
</li>
|
|
|
|
|
<% end %>
|
|
|
|
|
</ul>
|
|
|
|
|
```
|
|
|
|
|
|
2017-11-20 21:11:04 -05:00
|
|
|
|
WARNING: Extracting previews requires third-party applications, `ffmpeg` for
|
|
|
|
|
video and `mutool` for PDFs. These libraries are not provided by Rails. You must
|
|
|
|
|
install them yourself to use the built-in previewers. Before you install and use
|
|
|
|
|
third-party software, make sure you understand the licensing implications of
|
|
|
|
|
doing so.
|
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
Upload Directly to Service
|
|
|
|
|
--------------------------
|
|
|
|
|
|
|
|
|
|
Active Storage, with its included JavaScript library, supports uploading
|
|
|
|
|
directly from the client to the cloud.
|
|
|
|
|
|
|
|
|
|
### Direct upload installation
|
|
|
|
|
|
|
|
|
|
1. Include `activestorage.js` in your application's JavaScript bundle.
|
|
|
|
|
|
|
|
|
|
Using the asset pipeline:
|
2017-11-30 12:19:57 -05:00
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
```js
|
|
|
|
|
//= require activestorage
|
2017-11-30 12:19:57 -05:00
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
```
|
2017-11-30 12:19:57 -05:00
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
Using the npm package:
|
2017-11-30 12:19:57 -05:00
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
```js
|
|
|
|
|
import * as ActiveStorage from "activestorage"
|
|
|
|
|
ActiveStorage.start()
|
|
|
|
|
```
|
2017-11-30 12:19:57 -05:00
|
|
|
|
|
2017-11-09 11:51:14 -05:00
|
|
|
|
2. Annotate file inputs with the direct upload URL.
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
<%= form.file_field :attachments, multiple: true, direct_upload: true %>
|
|
|
|
|
```
|
|
|
|
|
3. That's it! Uploads begin upon form submission.
|
|
|
|
|
|
|
|
|
|
### 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. An `alert` 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. |
|
|
|
|
|
|
2017-11-30 12:01:01 -05:00
|
|
|
|
### Example
|
|
|
|
|
|
|
|
|
|
You can use these events to show the progress of an upload.
|
|
|
|
|
|
|
|
|
|
![direct-uploads](https://user-images.githubusercontent.com/5355/28694528-16e69d0c-72f8-11e7-91a7-c0b8cfc90391.gif)
|
|
|
|
|
|
|
|
|
|
To show the uploaded files in a form:
|
2017-11-30 12:19:57 -05:00
|
|
|
|
|
2017-11-30 12:01:01 -05:00
|
|
|
|
```js
|
|
|
|
|
// direct_uploads.js
|
|
|
|
|
|
|
|
|
|
addEventListener("direct-upload:initialize", event => {
|
|
|
|
|
const { target, detail } = event
|
|
|
|
|
const { id, file } = detail
|
|
|
|
|
target.insertAdjacentHTML("beforebegin", `
|
|
|
|
|
<div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
|
|
|
|
|
<div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
|
|
|
|
|
<span class="direct-upload__filename">${file.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
addEventListener("direct-upload:start", event => {
|
|
|
|
|
const { id } = event.detail
|
|
|
|
|
const element = document.getElementById(`direct-upload-${id}`)
|
|
|
|
|
element.classList.remove("direct-upload--pending")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
addEventListener("direct-upload:progress", event => {
|
|
|
|
|
const { id, progress } = event.detail
|
|
|
|
|
const progressElement = document.getElementById(`direct-upload-progress-${id}`)
|
|
|
|
|
progressElement.style.width = `${progress}%`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
addEventListener("direct-upload:error", event => {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
const { id, error } = event.detail
|
|
|
|
|
const element = document.getElementById(`direct-upload-${id}`)
|
|
|
|
|
element.classList.add("direct-upload--error")
|
|
|
|
|
element.setAttribute("title", error)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
addEventListener("direct-upload:end", event => {
|
|
|
|
|
const { id } = event.detail
|
|
|
|
|
const element = document.getElementById(`direct-upload-${id}`)
|
|
|
|
|
element.classList.add("direct-upload--complete")
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
2017-11-30 12:19:57 -05:00
|
|
|
|
Add styles:
|
2017-11-30 12:01:01 -05:00
|
|
|
|
|
|
|
|
|
```css
|
|
|
|
|
/* direct_uploads.css */
|
|
|
|
|
|
|
|
|
|
.direct-upload {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
position: relative;
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
margin: 0 3px 3px 0;
|
|
|
|
|
border: 1px solid rgba(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: width 120ms ease-out, opacity 60ms 60ms ease-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;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2017-11-16 22:20:31 -05:00
|
|
|
|
Clean up Stored Files Store During System Tests
|
|
|
|
|
-----------------------------------------------
|
|
|
|
|
|
|
|
|
|
System tests clean up test data by rolling back a transaction. Because destroy
|
|
|
|
|
is never called on an object, the attached files are never cleaned up. If you
|
|
|
|
|
want to clear the files, you can do it in an `after_teardown` callback. Doing it
|
2017-11-18 20:30:26 -05:00
|
|
|
|
here ensures that all connections created during the test are complete and
|
|
|
|
|
you won't receive an error from ActiveStorage saying it can't find a file.
|
2017-11-16 22:20:31 -05:00
|
|
|
|
|
|
|
|
|
``` ruby
|
|
|
|
|
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
|
|
|
|
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
|
|
|
|
|
|
|
|
|
|
def remove_uploaded_files
|
|
|
|
|
FileUtils.rm_rf("#{Rails.root}/storage_test")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def after_teardown
|
|
|
|
|
super
|
|
|
|
|
remove_uploaded_files
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-08 16:44:59 -05:00
|
|
|
|
If your system tests verify the deletion of a model with attachments and you're
|
2017-11-21 13:29:41 -05:00
|
|
|
|
using Active Job, set your test environment to use the inline queue adapter so
|
2017-11-16 22:20:31 -05:00
|
|
|
|
the purge job is executed immediately rather at an unknown time in the future.
|
|
|
|
|
|
|
|
|
|
You may also want to use a separate service definition for the test environment
|
|
|
|
|
so your tests don't delete the files you create during development.
|
|
|
|
|
|
|
|
|
|
``` ruby
|
|
|
|
|
# Use inline job processing to make things happen immediately
|
|
|
|
|
config.active_job.queue_adapter = :inline
|
|
|
|
|
|
|
|
|
|
# Separate file storage in the test environment
|
|
|
|
|
config.active_storage.service = :local_test
|
|
|
|
|
```
|
|
|
|
|
|
2017-12-14 22:08:33 -05:00
|
|
|
|
Support Additional Cloud Services
|
|
|
|
|
---------------------------------
|
2017-11-16 22:07:10 -05:00
|
|
|
|
|
2017-12-08 16:44:59 -05:00
|
|
|
|
If you need to support a cloud service other than these, you will need to
|
|
|
|
|
implement the Service. Each service extends
|
2017-11-16 22:07:10 -05:00
|
|
|
|
[`ActiveStorage::Service`](https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/service.rb)
|
|
|
|
|
by implementing the methods necessary to upload and download files to the cloud.
|