[Rails]ドラッグ&ドロップで画像をアップロードする
はじめに
Active StorageとS3を使って、ログの投稿に画像のドラッグ&ドロップ機能を追加していきます。
Active StorageとS3の設定について
Active Storageのダイレクトアップロードを参考にしながら進めます。
環境
Rails 7.0.4.3
ruby 3.2.1
stimulusコントローラーを作成する
フォーム内のドラッグ&ドロップに対応するコントローラーを作成します。
bin/rails g stimulus markdown_upload
create app/javascript/controllers/markdown_upload_controller.js
コントローラーを読み込む
作成したコントローラーを読み込みます。
import MarkdownUploadController from './markdown_upload_controller'
application.register('markdown-upload', MarkdownUploadController)
data-controller
を追加する
テキストエリア内...
<%= form.text_area :content, rows: 20,
data: {
controller: "markdown-upload",
markdown_upload_url_value: rails_direct_uploads_url,
action: "drop->markdown-upload#dropUpload"
}
class: 'block w-full...'
%>
drop
動作をリスニングし、markdown-upload
コントローラとdropUpload
アクションがドラッグ&ドロップの処理を行う流れになります。
rails_direct_uploads_url
はActive StorageのデフォルトのURLヘルパーであり、アップロードURLを提供します。Active Storageを使用して直接アップロードするためのURLを提供する必要があります。
// (これはdata-direct-upload-urlを提供する)
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
DirectUploadモジュールを導入する
ダイレクトアップロード機能をJavaScriptフレームワークから利用したい場合や、ドラッグアンドドロップをカスタマイズしたい場合は、DirectUploadクラスを利用できます。
@rails/activestorage
をダウンロードしてコントローラーに読み込みます。
bin/importmap pin @rails/activestorage
Pinning "@rails/activestorage" to https://ga.jspm.io/npm:@rails/activestorage@7.0.7/app/assets/javascripts/activestorage.esm.js
import { DirectUpload } from "@rails/activestorage"
dropUpload
アクションを作成する
ドロップ→ファイルを取得→ファイルにsigned_id
を与え→Active Storage用URLを生成する→マークダウン形式のリンクとして挿入する→アップロード
このような流れになります。
ファイルを取得する
dropUpload(e){
e.preventDefault();
Array.from(e.dataTransfer.files).forEach(file => this.uploadFile(file));
}
blobに保存させる
uploadFile(file){
const upload = new DirectUpload(file, this.urlValue);
upload.create((error, blob) => {
if (error) {
alert(error);
} else {
console.dir(blob);
const text = this.markdownLink(blob);
}
})
}
signed_id
は、ファイルのアップロード中に一時的に生成される一意の識別子です。
ファイルがアップロードされると、そのファイルに対して一意のsigned_id
が生成されます。
署名付きID(Signed ID)を使ったURLは次のような形になります。
/rails/active_storage/blobs/signed_id/file.jpg
アップロードされたファイルの情報は、Active StorageのBlobモデルに保存されます。
マークダウンURLを生成する
markdownLink(blob) {
const filename = blob.filename;
const url = `/rails/active_storage/blobs/${blob.signed_id}/${filename}`;
const prefix = (this.isImage(blob.content_type) ? '!' : '');
return `${prefix}[${filename}](${url})\n`;
}
const prefix = (this.isImage(blob.content_type) ? '!' : '');
: アップロードされたファイルが画像の場合は ! をプレフィックスとして付け加え、それ以外の場合は空文字列を設定します。これにより、画像ファイルの場合はMarkdown内で画像として表示される形式のリンクになります。
マークダウンURLをテキストエリア内に挿入する
uploadFile(file) {
const upload = new DirectUpload(file, this.urlValue);
upload.create((error, blob) => {
if (error) {
console.log("Error");
} else {
const text = this.markdownLink(blob);
const start = this.element.selectionStart;
const end = this.element.selectionEnd;
this.element.setRangeText(text, start, end);
}
});
}
const start = this.element.selectionStart;
とconst end = this.element.selectionEnd;
: 現在のテキストエリア内で選択されているテキストの開始位置と終了位置を取得します。
this.element.setRangeText(text, start, end);
: テキストエリア内で選択された範囲に、生成されたURLを挿入します。
バリデーションを追加する
if (!this.isValidFileType(file)) {
alert("アップロード可能の画像形式がJPEG, PNG, GIFです。ファイル形式をご確認ください。");
return;
}
if (!this.isValidFileSize(file)) {
alert("1MB以下の画像をアップロードしてください。");
return;
}
isValidFileType(file) {
return this.isImage(file.type);
}
isValidFileSize(file) {
const maxSize = 1 * 1024 * 1024;
return file.size <= maxSize;
}
画像でないファイルや1MB以上のファイルをアップロードし、アラートが表示されることを確認します。
完成形
import { Controller } from "@hotwired/stimulus"
import { DirectUpload } from "@rails/activestorage"
// Connects to data-controller="markdown-upload"
export default class extends Controller {
static values = { url: String };
connect() {
}
dropUpload(e){
e.preventDefault();
Array.from(e.dataTransfer.files).forEach(file => this.uploadFile(file));
}
uploadFile(file){
if (!this.isValidFileType(file)) {
alert("アップロード可能の画像形式がJPEG, PNG, GIFです。ファイル形式をご確認ください。");
return;
}
if (!this.isValidFileSize(file)) {
alert("1MB以下の画像をアップロードしてください。");
return;
}
const upload = new DirectUpload(file, this.urlValue);
upload.create((error, blob) => {
if (error) {
console.log(error);
} else {
const text = this.markdownUrl(blob);
const start = this.element.selectionStart;
const end = this.element.selectionEnd;
this.element.setRangeText(text,start,end)
}
})
}
markdownUrl(blob){
const filename = blob.filename
const url = `/rails/active_storage/blobs/${blob.signed_id}/${blob.filename}`;
const prefix = (this.isImage(blob.content_type) ? '!' : '');
return `${prefix}[${filename}](${url})\n`;
}
isImage(contentType){
return ["image/jpeg", "image/gif", "image/png"].includes(contentType);
}
isValidFileType(file) {
return this.isImage(file.type);
}
isValidFileSize(file) {
const maxSize = 1 * 1024 * 1024; // 3MB in bytes
return file.size <= maxSize;
}
}
S3にアップロードされたことを確認します。
21:43:08 web.1 | Started POST "/rails/active_storage/direct_uploads" for ::1 at 2023-08-17 21:43:08 +0900
21:43:08 web.1 | ActiveRecord::SchemaMigration Pluck (0.9ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
21:43:08 web.1 | Processing by ActiveStorage::DirectUploadsController#create as JSON
21:43:08 web.1 | Parameters: {"blob"=>{"filename"=>"dev-log.png", "content_type"=>"image/png", "byte_size"=>4608, "checksum"=>"wW3nC7jrg+AcfF4pnVWAbA=="}, "direct_upload"=>{"blob"=>{"filename"=>"dev-log.png", "content_type"=>"image/png", "byte_size"=>4608, "checksum"=>"wW3nC7jrg+AcfF4pnVWAbA=="}}}
21:43:08 web.1 | TRANSACTION (0.2ms) BEGIN
21:43:08 web.1 | ActiveStorage::Blob Create (11.8ms) INSERT INTO "active_storage_blobs" ("key", "filename", "content_type", "metadata", "service_name", "byte_size", "checksum", "created_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" [["key", "ekl43w84hcge5zrn5meu2g9v7e4m"], ["filename", "dev-log.png"], ["content_type", "image/png"], ["metadata", nil], ["service_name", "amazon"], ["byte_size", 4608], ["checksum", "wW3nC7jrg+AcfF4pnVWAbA=="], ["created_at", "2023-08-17 21:43:08.871993"]]
21:43:08 web.1 | TRANSACTION (1.0ms) COMMIT
21:43:08 web.1 | S3 Storage (41.5ms) Generated URL for file at key: **************** (https://***********.s3.ap-northeast-1.amazonaws.com/ekl43w84hcge5zrn5meu2g9v7e4m?x-amz-acl=public-read&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARVCZOLN47DOARCXF%2F20230817%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20230817T124308Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bcontent-type%3Bhost&X-Amz-Signature=8531fd67df840d9536e2058e8bed9de13c43e204fa9a6ed593c0548fea51c0e6)
21:43:09 web.1 | Completed 200 OK in 544ms (Views: 0.3ms | ActiveRecord: 17.4ms | Allocations: 317670)
21:43:09 web.1 |
21:43:09 web.1 |
終わりに
ドラッグ&ドロップで画像をアップロードできるようになりました。
参考した記事にドロップ以外に画像のペーストについての記述もあります。
Discussion