active_storage_overview.md 16.5 KB
Newer Older
J
Jeffrey Guenther 已提交
1 2
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**

3 4
Active Storage Overview
=======================
J
Jeffrey Guenther 已提交
5

J
Jeffrey Guenther 已提交
6
This guide covers how to attach files to your Active Record models.
J
Jeffrey Guenther 已提交
7 8 9

After reading this guide, you will know:

J
Jeffrey Guenther 已提交
10 11 12 13 14 15 16
* 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.
17
* How to clean up files stored during testing.
J
Jeffrey Guenther 已提交
18
* How to implement support for additional storage services.
J
Jeffrey Guenther 已提交
19 20 21

--------------------------------------------------------------------------------

J
Jeffrey Guenther 已提交
22 23
What is Active Storage?
-----------------------
J
Jeffrey Guenther 已提交
24

J
Jeffrey Guenther 已提交
25 26 27 28 29
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.
30

J
Jeffrey Guenther 已提交
31 32 33 34
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.
35

J
Jeffrey Guenther 已提交
36
## Setup
37

38 39 40 41 42
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.
43

44 45
Declare Active Storage services in `config/storage.yml`. For each service your
application uses, provide a name and the requisite configuration. The example
46
below declares three services named `local`, `test`, and `amazon`:
47 48 49 50 51 52 53 54 55 56

```yaml
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

57
amazon:
58 59 60 61 62 63 64 65 66 67
  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
Y
Yauheni Dakuka 已提交
68
`config/environments/development.rb`:
69 70 71 72 73 74

```ruby
# Store files locally.
config.active_storage.service = :local
```

75
To use the Amazon S3 service in production, you add the following to
76 77 78
`config/environments/production.rb`:

```ruby
79 80
# Store files on Amazon S3.
config.active_storage.service = :amazon
J
Jeffrey Guenther 已提交
81
```
82

83 84
Continue reading for more information on the built-in service adapters (e.g.
`Disk` and `S3`) and the configuration they require.
J
Jeffrey Guenther 已提交
85 86

### Disk Service
J
Jeffrey Guenther 已提交
87

88
Declare a Disk service in `config/storage.yml`:
J
Jeffrey Guenther 已提交
89

90
```yaml
J
Jeffrey Guenther 已提交
91 92 93
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>
94 95 96 97 98 99 100 101 102
```

Optionally specify a host for generating URLs (the default is `http://localhost:3000`):

```yaml
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>
  host: http://myapp.test
J
Jeffrey Guenther 已提交
103 104 105
```

### Amazon S3 Service
J
Jeffrey Guenther 已提交
106

107
Declare an S3 service in `config/storage.yml`:
J
Jeffrey Guenther 已提交
108

109
```yaml
110
amazon:
J
Jeffrey Guenther 已提交
111 112 113 114 115 116
  service: S3
  access_key_id: ""
  secret_access_key: ""
  region: ""
  bucket: ""
```
117 118

Add the [`aws-sdk-s3`](https://github.com/aws/aws-sdk-ruby) gem to your `Gemfile`:
J
Jeffrey Guenther 已提交
119

120
```ruby
J
Jeffrey Guenther 已提交
121 122
gem "aws-sdk-s3", require: false
```
J
Jeffrey Guenther 已提交
123

124 125
NOTE: The core features of Active Storage require the following permissions: `s3:ListBucket`, `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject`. If you have additional upload options configured such as setting ACLs then additional permissions may be required.

J
Jeffrey Guenther 已提交
126
### Microsoft Azure Storage Service
J
Jeffrey Guenther 已提交
127

128
Declare an Azure Storage service in `config/storage.yml`:
J
Jeffrey Guenther 已提交
129

130
```yaml
131
azure:
J
Jeffrey Guenther 已提交
132 133 134 135 136 137 138
  service: AzureStorage
  path: ""
  storage_account_name: ""
  storage_access_key: ""
  container: ""
```

139
Add the [`azure-storage`](https://github.com/Azure/azure-storage-ruby) gem to your `Gemfile`:
J
Jeffrey Guenther 已提交
140

141
```ruby
J
Jeffrey Guenther 已提交
142 143 144 145
gem "azure-storage", require: false
```

### Google Cloud Storage Service
J
Jeffrey Guenther 已提交
146

147
Declare a Google Cloud Storage service in `config/storage.yml`:
J
Jeffrey Guenther 已提交
148

149
```yaml
150
google:
J
Jeffrey Guenther 已提交
151
  service: GCS
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
  credentials: <%= Rails.root.join("path/to/keyfile.json") %>
  project: ""
  bucket: ""
```

Optionally provide a Hash of credentials instead of a keyfile path:

```yaml
google:
  service: GCS
  credentials:
    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) %>
    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"
J
Jeffrey Guenther 已提交
172 173 174 175 176
    client_x509_cert_url: ""
  project: ""
  bucket: ""
```

177
Add the [`google-cloud-storage`](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-storage) gem to your `Gemfile`:
J
Jeffrey Guenther 已提交
178

179
```ruby
180
gem "google-cloud-storage", "~> 1.8", require: false
J
Jeffrey Guenther 已提交
181
```
182

J
Jeffrey Guenther 已提交
183
### Mirror Service
184

J
Jeffrey Guenther 已提交
185 186 187 188 189 190 191
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.
192

193
```yaml
J
Jeffrey Guenther 已提交
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
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
213 214
```

J
Jeffrey Guenther 已提交
215 216
NOTE: Files are served from the primary service.

G
George Claghorn 已提交
217 218
Attaching Files to Records
--------------------------
219

220
### `has_one_attached`
221

222 223 224
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.

225
For example, suppose your application has a `User` model. If you want each user to
226 227
have an avatar, define the `User` model like this:

228
```ruby
229 230 231
class User < ApplicationRecord
  has_one_attached :avatar
end
232
```
233

234
You can create a user with an avatar:
235

236
```ruby
237 238
class SignupController < ApplicationController
  def create
239
    user = User.create!(user_params)
240 241
    session[:user_id] = user.id
    redirect_to root_path
242
  end
243 244 245 246 247

  private
    def user_params
      params.require(:user).permit(:email_address, :password, :avatar)
    end
248 249 250
end
```

251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
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
269
message to have many images, define the `Message` model like this:
270 271 272 273 274 275 276

```ruby
class Message < ApplicationRecord
  has_many_attached :images
end
```

277
You can create a message with images:
278 279 280 281

```ruby
class MessagesController < ApplicationController
  def create
282
    message = Message.create!(message_params)
283 284 285
    redirect_to message
  end

286 287 288 289
  private
    def message_params
      params.require(:message).permit(:title, :content, images: [])
    end
290 291 292
end
```

293 294 295 296 297 298
Call `images.attach` to add new images to an existing message:

```ruby
@message.images.attach(params[:images])
```

J
Jeffrey Guenther 已提交
299
Call `images.attached?` to determine whether a particular message has any images:
300 301 302 303 304

```ruby
@message.images.attached?
```

G
George Claghorn 已提交
305 306
Removing Files
--------------
307

308
To remove an attachment from a model, call `purge` on the attachment. Removal
J
Jeffrey Guenther 已提交
309
can be done in the background if your application is setup to use Active Job.
J
Jeffrey Guenther 已提交
310
Purging deletes the blob and the file from the storage service.
311

312 313 314 315 316 317 318 319
```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
```

G
George Claghorn 已提交
320 321
Linking to Files
----------------
322 323 324

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
325
decouples the public URL from the actual one, and allows, for example, mirroring
326 327 328
attachments in different services for high-availability. The redirection has an
HTTP expiration of 5 min.

329 330 331 332
```ruby
url_for(user.avatar)
```

333
To create a download link, use the `rails_blob_{path|url}` helper. Using this
334
helper allows you to set the disposition.
335 336

```ruby
337
rails_blob_path(user.avatar, disposition: "attachment")
338 339
```

G
George Claghorn 已提交
340 341
Transforming Images
-------------------
342

J
Jeffrey Guenther 已提交
343
To create variation of the image, call `variant` on the Blob.
344
You can pass any [MiniMagick](https://github.com/minimagick/minimagick)
J
Jeffrey Guenther 已提交
345 346
supported transformation to the method.

Y
Yauheni Dakuka 已提交
347
To enable variants, add `mini_magick` to your `Gemfile`:
J
Jeffrey Guenther 已提交
348

349
```ruby
J
Jeffrey Guenther 已提交
350 351
gem 'mini_magick'
```
352

353
When the browser hits the variant URL, Active Storage will lazy transform the
354 355 356
original blob into the format you specified and redirect to its new service
location.

357 358 359 360
```erb
<%= image_tag user.avatar.variant(resize: "100x100") %>
```

G
George Claghorn 已提交
361 362
Previewing Files
----------------
J
Jeffrey Guenther 已提交
363 364 365 366

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.
367 368 369 370 371 372 373 374 375 376 377

```erb
<ul>
  <% @message.files.each do |file| %>
    <li>
      <%= image_tag file.preview(resize: "100x100>") %>
    </li>
  <% end %>
</ul>
```

378 379 380 381 382 383
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.

G
George Claghorn 已提交
384 385
Direct Uploads
--------------
386 387 388 389 390 391 392 393 394

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:
J
Jeffrey Guenther 已提交
395

396 397
    ```js
    //= require activestorage
J
Jeffrey Guenther 已提交
398

399
    ```
J
Jeffrey Guenther 已提交
400

401
    Using the npm package:
J
Jeffrey Guenther 已提交
402

403 404 405 406
    ```js
    import * as ActiveStorage from "activestorage"
    ActiveStorage.start()
    ```
J
Jeffrey Guenther 已提交
407

408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
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. |

J
Jeffrey Guenther 已提交
429 430 431 432 433 434 435
### 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:
J
Jeffrey Guenther 已提交
436

J
Jeffrey Guenther 已提交
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
```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")
})
```

J
Jeffrey Guenther 已提交
478
Add styles:
J
Jeffrey Guenther 已提交
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521

```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;
}
```

G
George Claghorn 已提交
522 523
Discarding Files Stored During System Tests
-------------------------------------------
J
Jeffrey Guenther 已提交
524 525 526 527

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
J
Jeffrey Guenther 已提交
528
here ensures that all connections created during the test are complete and
529
you won't receive an error from Active Storage saying it can't find a file.
J
Jeffrey Guenther 已提交
530

531
```ruby
J
Jeffrey Guenther 已提交
532 533 534 535 536 537 538 539 540 541 542 543 544 545
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
```

J
Jeffrey Guenther 已提交
546
If your system tests verify the deletion of a model with attachments and you're
J
Jeffrey Guenther 已提交
547
using Active Job, set your test environment to use the inline queue adapter so
J
Jeffrey Guenther 已提交
548 549 550 551 552
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.

553
```ruby
J
Jeffrey Guenther 已提交
554 555 556 557 558 559 560
# 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
```

561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
Discarding Files Stored During Integration Tests
-------------------------------------------

Similarly to System Tests, files uploaded during Integration Tests will not be
automatically cleaned up. If you want to clear the files, you can do it in an
`after_teardown` callback. Doing it here ensures that all connections created
during the test are complete and you won't receive an error from Active Storage
saying it can't find a file.

```ruby
module ActionDispatch
  class IntegrationTest
    def remove_uploaded_files
      FileUtils.rm_rf(Rails.root.join('tmp', 'storage'))
    end

    def after_teardown
      super
      remove_uploaded_files
    end
  end
end
```

G
George Claghorn 已提交
585 586
Implementing Support for Other Cloud Services
---------------------------------------------
587

J
Jeffrey Guenther 已提交
588 589
If you need to support a cloud service other than these, you will need to
implement the Service. Each service extends
590 591
[`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.