[Rails] Active Storage Overview
keywords:active storage
, 檔案上傳
, 雲端儲存
- Active Storage 概要 @ Calvert's murmur
- Using Active Storage @ Andyyou devBlog
- Active Storage Overview @ RailsGuides
安裝
# 建立專案後
$ rails new <project_name> --webpack=stimulus --database=postgresql --skip-coffee --skip-test
$ rails active_storage:install
$ rails db:migrate
設定
設定 Active Storage 的服務
# ./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: ''
根據不同開發環境設定不同的儲存位置:
# ./config/environments/development.rb
# Store files locally.
config.active_storage.service = :local
# ./config/environments/production.rb
# Store files on Amazon S3.
config.active_storage.service = :amazon
上傳檔案
has_one_attached
has_one_attached
用來將資料和檔案做一對一的關聯,每一筆資料可以有一個檔案。
假設我們有一個 Model 是 User
:
# app/model/user.rb
class User < ApplicationRecord
has_one_attached :avatar
end
在資料庫不需要為 User 建立
:avatar
欄位即可使用has_one_attached
。
接著在 Controller 的部分把 :avatar
設定成可以使用的 params:
# 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 會自動儲存上傳的檔案:
<%= 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?
# 為使用者添加檔案
user.avatar.attach(params[:avatar])
# 檢查使用者是否有上傳檔案
user.avatar.attached?
❗ 透過
has_one_attached
上傳時,如果同樣欄位有新的檔案上傳,則會把舊的檔案刪掉,儲存新的檔案。
has_many_attached
透過 has_many_attached
可以用來設定資料和檔案間的一對多關係,每一筆資料可以附帶很多的檔案:
# app/model/user.rb
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :images
end
接著在 Controller 的部分把 :images
設定成可以使用的 params:
# 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 會自動儲存上傳的檔案:
<%= 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?
user.images.attach(params[:images])
# 檢驗 user 有無任何 images
user.images.attached?
❗️ 透過
has_many_attached
上傳時,如果同樣欄位有新的檔案上傳,則會保留舊的檔案,並把新的檔案附加上去。
移除檔案(purge)
如果要將檔案從 active storage 移除,可以使用 purge
:
# 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
❗ 使用
.purge
後會馬上刪除檔案,不需要.save
。 ❗ 針對has_many_attached:
的欄位如果直接使用.purge
會一次把所有檔案刪除。
或者我們也可以透過 id 刪除特定檔案:
# 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)
如果是圖檔的話可以使用:
<!-- 使用 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 分鐘後失效:
<!-- app/views/users/show.html.erb -->
<!-- 此方法會建立暫時性的轉址(預設 5 分鐘失效)-->
<%= url_for(@user.avatar) %>
若要建立下載用的連結可以使用 rails_blob_{path|url}
這個 helper:
<!-- app/views/users/show.html.erb -->
<%= rails_blob_path(user.avatar, disposition: "attachment") %>
如果需要從 View 以外的地方取用下載連結可以使用 rails_blob_url
或 rails_blob_path
:
# 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
:
<%= 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
:
/* ./app/assets/javascripts/application.js */
//= require activestorage
或者透過 npm 載入:
import * as ActiveStorage from 'activestorage';
ActiveStorage.start();
可用的事件
修改自官網:
// 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
/* 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;
}
進階使用
# 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)
資料來源
- Active Storage 概要 @ Calvert's murmur
- Active Storage Overview @ RailsGuides