PJCHENder 未整理筆記

[Rails] Active Storage Overview

2018-05-25

@(Ruby on Rails)[Ruby,RailsGuides]

[Rails] Active Storage Overview

keywords:active storage, 檔案上傳, 雲端儲存

安裝

1
2
3
4
# 建立專案後
$ rails new <project_name> --webpack=stimulus --database=postgresql --skip-coffee --skip-test
$ rails active_storage:install
$ rails db:migrate

設定

設定 Active Storage 的服務

1
2
3
4
5
6
7
8
9
10
11
12
13
# ./config/storage.yml
local:
service: Disk
root: <%= Rails.root.join("storage") %>

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

amazon:
service: S3
access_key_id: ""
secret_access_key: ""

根據不同開發環境設定不同的儲存位置:

1
2
3
# ./config/environments/development.rb
# Store files locally.
config.active_storage.service = :local
1
2
3
# ./config/environments/production.rb
# Store files on Amazon S3.
config.active_storage.service = :amazon

上傳檔案

has_one_attached

has_one_attached 用來將資料和檔案做一對一的關聯,每一筆資料可以有一個檔案。

假設我們有一個 Model 是 User

1
2
3
4
# app/model/user.rb
class User < ApplicationRecord
has_one_attached :avatar
end

在資料庫不需要為 User 建立 :avatar 欄位即可使用 has_one_attached

接著在 Controller 的部分把 :avatar 設定成可以使用的 params:

1
2
3
4
5
6
7
8
9
10
11
# app/controller/users_controller.rb

class UsersController < ApplicationController
#...

private
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(:name, :avatar)
end
end

在 View 的地方使用 file_field 上傳圖檔,如此 Active Storage 會自動儲存上傳的檔案:

1
2
3
4
5
6
7
8
9
10
11
<%= form_with(model: user, local: true) do |form| %>
<!-- has_one_attached -->
<div class="field">
<%= form.label :avatar %>
<%= form.file_field :avatar %>
</div>

<div class="actions">
<%= form.submit %>
</div>
<% end %>

其他可用方法:avatar.attach, avatar.attached?

1
2
3
4
5
# 為使用者添加檔案
user.avatar.attach(params[:avatar])

# 檢查使用者是否有上傳檔案
user.avatar.attached?

:exclamation: 透過 has_one_attached 上傳時,如果同樣欄位有新的檔案上傳,則會把舊的檔案刪掉,儲存新的檔案。

has_many_attached

透過 has_many_attached 可以用來設定資料和檔案間的一對多關係,每一筆資料可以附帶很多的檔案:

1
2
3
4
5
# app/model/user.rb
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :images
end

接著在 Controller 的部分把 :images 設定成可以使用的 params:

1
2
3
4
5
6
7
8
9
10
11
# app/controller/users_controller.rb

class UsersController < ApplicationController
#...

private
# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(:name, :avatar, images: [])
end
end

在 View 的地方使用 file_field 搭配 multiple: true 上傳圖檔,如此 Active Storage 會自動儲存上傳的檔案:

1
2
3
4
5
6
7
8
9
10
11
12
13
<%= form_with(model: user, local: true) do |form| %>

<!-- has_many_attached -->
<div class="field">
<%= form.label :images %>
<%= form.file_field :images, multiple: true %>
</div>

<div class="actions">
<%= form.submit %>
</div>

<% end %>

其他可用方法:images.attach, images.attached?

1
2
3
4
user.images.attach(params[:images])

# 檢驗 user 有無任何 images
user.images.attached?

❗️ 透過 has_many_attached 上傳時,如果同樣欄位有新的檔案上傳,則會保留舊的檔案,並把新的檔案附加上去。

移除檔案(purge)

如果要將檔案從 active storage 移除,可以使用 purge

1
2
3
4
5
# 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

:exclamation: 使用 .purge 後會馬上刪除檔案,不需要 .save
:exclamation: 針對 has_many_attached: 的欄位如果直接使用 .purge 會一次把所有檔案刪除。

或者我們也可以透過 id 刪除特定檔案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/controllers/user_controllers.rb

# has_one_attached
def destroy_avatar
if @user.avatar.id == params[:blob][:id].to_i
@user.avatar.purge
render json: { status:: ok }
else
render json: { status:: fail }
end
end

# has_many_attached
def remove_images
@user.images.each do |image|
image.purge if image.id == params[:image_id].to_i
end

render json: { status: :ok }
end

連結檔案(Linking to Files)

如果是圖檔的話可以使用:

1
2
3
4
5
6
7
8
9
10
11
<!-- 使用 variant 前須先安裝其它套件 -->

<!-- has_one_attached -->
<%= image_tag @user.avatar if @user.avatar.attached? %>

<!-- has_many_attached -->
<% if @user.images.attached? %>
<% @user.images.each do |image| %>
<%= image_tag image %>
<% end %>
<% end %>

透過 url_for 會建立一個暫時性的轉址連結,這個連結會從你的 application 轉到檔案存在於外部服務的位置(例如,Google Cloud, Amazon S3),這個網址預設會於 5 分鐘後失效:

1
2
3
<!-- app/views/users/show.html.erb -->
<!-- 此方法會建立暫時性的轉址(預設 5 分鐘失效)-->
<%= url_for(@user.avatar) %>

若要建立下載用的連結可以使用 rails_blob_{path|url} 這個 helper:

1
2
<!-- app/views/users/show.html.erb -->
<%= rails_blob_path(user.avatar, disposition: "attachment") %>

如果需要從 View 以外的地方取用下載連結可以使用 rails_blob_urlrails_blob_path

1
2
3
# in controller
avatar_url = Rails.application.routes.url_helpers.rails_blob_url(user.avatar)
avatar_url = Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)

直接上傳(Direct Upload)

在 View 中上傳的 input 多加上 direct_upload: true

1
2
3
4
5
6
7
8
9
10
11
<%= form_with(model: user, local: true) do |form| %>
<!-- has_many_attached -->
<div class="field">
<%= form.label :images %>
<%= form.file_field :images, multiple: true, direct_upload: true %>
</div>

<div class="actions">
<%= form.submit %>
</div>
<% end %>

載入 JS 檔

首先在 Assets 中載入 activestorage.js

1
2
3
/* ./app/assets/javascripts/application.js */

//= require activestorage

或者透過 npm 載入:

1
2
import * as ActiveStorage from "activestorage"
ActiveStorage.start()

可用的事件

修改自官網:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// direct_uploads.js

addEventListener('direct-uploads:start', event => {
console.log('direct-uploads:start', event.detail);
})

addEventListener("direct-upload:initialize", event => {
console.log('direct-upload:initialize', event.detail);
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 => {
console.log('direct-upload:start', event.detail);
const { target, detail } = event;
const { id, file } = detail;
const element = document.getElementById(`direct-upload-${id}`);
element.classList.remove("direct-upload--pending");
})

addEventListener('direct-upload:before-blob-request', event => {
console.log('direct-upload:before-blob-request', event.detail);
const { target, detail } = event;
const { id, file, xhr } = detail;
})

addEventListener("direct-upload:progress", event => {
console.log('direct-upload:progress', event.detail);
const { target, detail } = event;
const { id, file, progress } = detail;
const progressElement = document.getElementById(`direct-upload-progress-${id}`);
progressElement.style.width = `${progress}%`;
})

addEventListener("direct-upload:error", event => {
console.log('direct-upload:error', event.detail);
event.preventDefault();
const { target, detail } = event;
const { id, file, error } = detail;
const element = document.getElementById(`direct-upload-${id}`);
element.classList.add("direct-upload--error");
element.setAttribute("title", error);
})

addEventListener("direct-upload:end", event => {
console.log('direct-upload:end', event.detail);
const { id, file } = event.detail;
const element = document.getElementById(`direct-upload-${id}`);
element.classList.add("direct-upload--complete");
})

addEventListener('direct-uploads:end', event => {
console.log('direct-uploads:end', event.detail);
})

可套用的 CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 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;
}

進階使用

1
2
3
4
5
6
7
# project_controllers

# 從 signed_id 可以利用 find_signed(<signed_id>) 找到該圖片
image = ActiveStorage::Blob.find_signed(params[:signed_id])

# 取得該圖片後可以利用 routes.url_helpers.rails_representation_url 取得該圖片的 URL
image_url = Rails.application.routes.url_helpers.rails_representation_url(image.variant(resize: '100x100'), only_path: true)

資料來源

Tags: rails

掃描二維條碼,分享此文章