1.目的
今回 LWC 中に AWS の SDK for javascript で S3 と連携する方法を共有します。
2.前提
- 2.1.AWS S3 バケット Cross-Origin Resource Sharing (CORS)の設定
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["HEAD", "GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["*"],
"ExposeHeaders": ["ETag"]
}
]
3.ソース構成図
lwc
├─fileuploadMock
├─fileuploadModal
├─progressbar
└─utils
.container {
background-color: #fff;
min-height: 100%;
}
.wrapper {
background-color: #cecece;
overflow: scroll;
width: 100%;
}
<template>
<template if:true="{loading}">
<lightning-spinner
alternative-text="Loading"
size="medium"
></lightning-spinner>
</template>
<c-progressbar
upload-file-name="{uploadFileName}"
progress="{progress}"
onabort="{Abort}"
>
</c-progressbar>
<c-fileupload-modal
title="ファイル追加"
onselect="{uploadHandler}"
onfolderchange="{folderchangeHandler}"
></c-fileupload-modal>
<lightning-card>
<div class="slds-p-horizontal_small">
<div class="slds-form">
<div class="slds-form__row">
<div class="slds-form__item">
<button
class="slds-button slds-button_brand"
onclick="{fileSelectorHandler}"
>
ファイル追加
</button>
</div>
</div>
</div>
</div>
<div slot="footer">
<lightning-datatable
hide-checkbox-column
key-field="key"
columns="{columns}"
data="{objectlist}"
onrowaction="{handleRowAction}"
>
</lightning-datatable>
</div>
</lightning-card>
</template>
import { LightningElement, track } from "lwc";
import { NavigationMixin } from "lightning/navigation";
import { loadScript } from "lightning/platformResourceLoader";
import AWS_SDK from "@salesforce/resourceUrl/aws_sdk";
import { showToast, dateFormat, fileSizeUnit } from "c/utils";
const bucketName = "bucket-name"; //バケット名
const region = "ap-northeast-1"; //地域
const accessKeyId = "accessKeyId "; //アクセスID
const secretAccessKey = "secretAccessKey"; //アクセスキー
export default class FileuploadMock extends NavigationMixin(LightningElement) {
//ファイル非活性
@track fileDisable;
@track datas;
@track objectlist = [];
@track folder;
@track loading;
@track uploadFileName;
@track progress;
/**
* 初期化AWS
*/
async initAWS() {
AWS.config.update({
region: region,
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
});
this.s3 = new AWS.S3({
apiVersion: "2006-03-01",
params: { Bucket: bucketName },
});
await this.listObjects();
}
/**
* ファイルアップロード
* @param {*} event
*/
async uploadHandler(event) {
let input = event.detail;
let files = input.files;
if (files.length > 0) {
try {
// let result = await this.upload(files[0]);
let result = await this.managedUpload(files[0], (progress) => {
this.uploadFileName = files[0].name;
this.progress = Math.floor((progress.loaded / progress.total) * 100);
this.template.querySelector("c-progressbar").open();
});
// console.log(result);
this.template.querySelector("c-progressbar").close();
await this.listObjects();
showToast(this, "", "成功にアップロードしました", "success");
} catch (err) {
showToast(this, "", err.message, "error");
console.error("Error:", err);
}
}
}
/**
* ファイルアップロードキャンセル
*/
async Abort() {
await this.request.abort();
}
/**
* ファイルダウンロード
* @param {*} event
*/
async fileDownload(fileKey) {
try {
this.loading = true;
await this.downloadFile(fileKey);
} catch (err) {
showToast(this, "", err.message, "error");
console.error("Error:", err);
} finally {
this.loading = false;
}
}
/**
* ファイル削除
* @param {string} fileKey
*/
async deleteFile(fileKey) {
try {
this.loading = true;
await this.deleteObject(fileKey);
await this.listObjects();
showToast(this, "", "成功に削除しました", "success");
} catch (err) {
showToast(this, "", err.message, "error");
console.error("Error:", err);
} finally {
this.loading = false;
}
}
/**
* ファイルリスト取得
*/
async listObjects() {
let data = await this.s3.listObjects().promise();
console.log(data);
this.objectlist = [];
data.Contents.forEach((e) => {
let key = e.Key;
let folder;
let fileName;
if (e.Size === 0) return;
if (key.lastIndexOf("/") > -1) {
fileName = key.substring(key.lastIndexOf("/") + 1, key.length);
folder = "./" + key.replace(fileName, "");
} else {
folder = "./";
fileName = key;
}
let fileType = fileName.split(".")[1];
this.objectlist.push({
key: key,
folder: folder,
fileName: fileName,
fileType: fileType,
LastModified: dateFormat(e.LastModified, "YYYY/mm/dd HH:MM:SS"),
Owner: e.Owner.DisplayName,
Size: fileSizeUnit(e.Size),
StorageClass: e.StorageClass,
});
});
}
/**
* ファイル取得処理
* @param {string} fileKey キー
* @param {string} fileName ファイル名
*/
async downloadFile(fileKey) {
let url = await this.getSignedUrlPromise("getObject", {
Bucket: bucketName,
Key: fileKey,
Expires: 1,
});
console.log(url);
window.location.href = url;
}
/**
* オブジェクト取得
* @param {*} fileKey キー
*/
getObject(fileKey) {
return this.s3.getObject({ Key: fileKey }).promise();
}
/**
* ファイル保存処理
* @param {File(blob)} file ファイル
*/
putObject(file) {
const { folder } = this;
let fileName = file.name;
let fileKey;
if (folder || folder === 0) fileKey = `${folder}/${fileName}`;
else fileKey = fileName;
return this.s3.putObject({ Key: fileKey, Body: file }).promise();
}
/**
* ファイル削除処理
* @param {string} fileKey ファイルキー
*/
deleteObject(fileKey) {
return this.s3.deleteObject({ Key: fileKey }).promise();
}
/**
* ファイル保存処理(ビッグサイズ用)
* @param {File(blob)} file ファイル
*/
upload(file) {
const { folder } = this;
let fileName = file.name;
let fileKey;
if (folder || folder === 0) fileKey = `${folder}/${fileName}`;
else fileKey = fileName;
return this.s3.upload({ Key: fileKey, Body: file }).promise();
}
/**
* URL発行
* @param {*} action アクション:getObject,putObject,deleteObject
* @param {*} fileKey ファイルキー
*/
getSignedUrlPromise(action, params) {
return this.s3.getSignedUrlPromise(action, params);
}
/**
* マルチファイルアップロード管理
* @param {*} file
*/
managedUpload(file, progressCallBack) {
const { folder } = this;
let fileName = file.name;
let fileKey;
if (folder || folder === 0) fileKey = `${folder}/${fileName}`;
else fileKey = fileName;
this.request = new AWS.S3.ManagedUpload({
partSize: 100 * 1024 * 1024,
queueSize: 1,
params: { Bucket: bucketName, Key: fileKey, Body: file },
});
this.request.on("httpUploadProgress", (progress) => {
if (progressCallBack) progressCallBack(progress);
else
console.log(
"progress:",
Math.floor((progress.loaded / progress.total) * 100)
);
});
this.request.send((err, data) => {
if (err) console.error(err);
console.info(data);
});
return this.request.promise();
}
/**
* ファイル選択
* @param {*} event
*/
fileSelectorHandler(event) {
event.preventDefault();
this.template.querySelector("c-fileupload-modal").open();
}
/**
* フォルダ選択
* @param {*} event
*/
folderchangeHandler(event) {
this.folder = event.detail;
}
/**
* RowAction
* @param {*} event
*/
async handleRowAction(event) {
const action = event.detail.action;
const row = event.detail.row;
switch (action.name) {
case "download":
await this.fileDownload(row.key);
break;
case "delete":
await this.deleteFile(row.key);
break;
}
}
/**
* 初期化
*/
connectedCallback() {
this.columns = [
{ label: "フォルダー名", fieldName: "folder" },
{ label: "ファイル名", fieldName: "fileName" },
{ label: "タイプ", fieldName: "fileType" },
{ label: "最新更新日", fieldName: "LastModified" },
{ label: "所有者", fieldName: "Owner" },
{ label: "サイズ", fieldName: "Size" },
{ label: "ストレージクラス", fieldName: "StorageClass" },
{
type: "action",
typeAttributes: {
rowActions: [
{ label: "ダウンロード", name: "download" },
{ label: "削除", name: "delete" },
],
menuAlignment: "auto",
},
},
];
}
/**
* aws-sdkロード
*/
renderedCallback() {
if (this.jsinit) return;
this.jsinit = true;
Promise.all([loadScript(this, AWS_SDK)])
.then(async () => {
await this.initAWS();
})
.catch((error) => {
showToast(
this,
"JSライブラリロードに失敗しました",
error.message,
"error"
);
});
}
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__Tab</target>
</targets>
</LightningComponentBundle>
<template>
<!--Use template if:true to display/hide popup based on isModalOpen value-->
<template if:true="{_isModalOpen}">
<!-- Modal/Popup Box LWC starts here -->
<section
role="dialog"
tabindex="-1"
aria-modal="true"
class="slds-modal slds-fade-in-open"
style="z-index:9001"
>
<div
class="slds-modal__container"
style="width: auto;max-width: fit-content;"
>
<!-- Modal/Popup Box LWC header here -->
<header class="slds-modal__header">
<button
class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse"
title="Close"
onclick="{close}"
>
<lightning-icon
icon-name="utility:close"
alternative-text="close"
variant="inverse"
size="small"
>
</lightning-icon>
<span class="slds-assistive-text">Close</span>
</button>
<h2 class="slds-text-heading_medium slds-hyphenate">{title}</h2>
</header>
<!-- Modal/Popup Box LWC body starts here -->
<div
class="slds-modal__content slds-p-around_medium"
style="height:50%"
>
<lightning-input
label="パス"
name="path"
onchange="{commonChange}"
></lightning-input>
<lightning-input
type="file"
label="ファイルアップロード"
onchange="{uploadHandler}"
>
</lightning-input>
</div>
<!-- Modal/Popup Box LWC footer starts here -->
<footer class="slds-modal__footer">
<button class="slds-button slds-button_neutral" onclick="{close}">
キャンセル
</button>
<!-- <button class="slds-button slds-button_brand" onclick={confirmHandle}>ファイル追加</button> -->
</footer>
</div>
</section>
<div class="slds-backdrop slds-backdrop_open"></div>
</template>
</template>
import { LightningElement, track, api } from "lwc";
export default class FileUploadModal extends LightningElement {
@api name;
@track path;
//表示フラグ
@track _isModalOpen;
/**
* 共通Change処理
* @param {*} event
*/
commonChange(event) {
let name = event.target.name;
let value = event.target.value;
this[name] = value;
this.dispatchEvent(
new CustomEvent("folderchange", {
detail: value,
composed: true,
bubbles: true,
cancelable: true,
})
);
}
/**
* ファイルアップロード
* @param {*} event
*/
uploadHandler(event) {
let changenEvent = new CustomEvent("select", {
detail: event.target,
composed: true,
bubbles: true,
cancelable: true,
});
this.dispatchEvent(changenEvent);
}
/**
* モーダル開く
*/
@api
open() {
this._isModalOpen = true;
}
/**
* モーダル閉じる
*/
close(e) {
e.preventDefault();
this._isModalOpen = false;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
<template>
<template if:true="{isShow}">
<section
role="dialog"
tabindex="-1"
class="slds-modal slds-fade-in-open"
aria-modal="true"
style="z-index:9002"
>
<div class="slds-modal__container" style="width: auto;max-width: 50rem;">
<header class="slds-modal__header">
<button
class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse"
title="Close"
onclick="{cancel}"
>
<lightning-icon
icon-name="utility:close"
alternative-text="close"
variant="inverse"
size="small"
>
</lightning-icon>
<span class="slds-assistive-text">Close</span>
</button>
<h2 class="slds-modal__title slds-hyphenate">
ファイルをアップロード
</h2>
</header>
<div
class="slds-modal__content slds-p-around_medium"
style="display: grid;grid-template-columns: 0.5fr 3fr 3fr 0.5fr; height: 5rem;overflow-y: hidden;"
>
<div style="align-self: center;">
<span class="slds-icon_container slds-icon-doctype-xml"> </span>
</div>
<div style="align-self: center;">
<span>{uploadFileName} <br /><b>{progress}%</b></span>
</div>
<div
class="slds-progress-bar slds-progress-bar_circular"
style="align-self: center;height: 0.6rem;width: 20rem;"
>
<span class="slds-progress-bar__value" style="{barStyle}"></span>
</div>
<div style="align-self: center;margin-left: 0.2rem;">
<span class="{barClass}"> </span>
</div>
</div>
<footer class="slds-modal__footer">
<span style="float: left;"></span>
<button class="slds-button slds-button_brand" onclick="{cancel}">
キャンセル
</button>
</footer>
</div>
</section>
<div class="slds-backdrop slds-backdrop_open"></div>
</template>
</template>
import { LightningElement, track, api } from "lwc";
const bar_cancel_class =
"slds-icon_container slds-icon_container_circle slds-icon-action-description slds-icon-standard-password";
const bar_success_class =
"slds-icon_container slds-icon_container_circle slds-icon-action-description slds-icon-text-success";
export default class Fileupload extends LightningElement {
@api uploadFileName;
@api progress = 0;
@track isShow;
/**
* ProgressBar
*/
get barStyle() {
return `width:${this.progress}%`;
}
get barClass() {
return this.progress >= 100 ? bar_success_class : bar_cancel_class;
}
/**
* キャンセル
*/
cancel(e) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent("abort", {
detail: true,
})
);
this.close();
}
@api
close() {
this.isShow = false;
}
@api
open() {
this.isShow = true;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
import { ShowToastEvent } from "lightning/platformShowToastEvent";
/**
* メッセージ表示
* @param {window} that
* @param {string} title タイトール
* @param {string} message メッセージ
* @param {string} variant タイプ info、success、warning、error
*/
export const showToast = (that, title, message, variant) => {
const event = new ShowToastEvent({
title: title,
message: message,
variant: variant,
});
that.dispatchEvent(event);
};
/**
* デートフォマート
* @param {Date} date date
* @param {string} fmt format
* @returns {string} StringDate
*/
export const dateFormat = (date, fmt = "YYYY/mm/dd") => {
let ret;
const opt = {
"Y+": date.getFullYear().toString(), // 年
"m+": (date.getMonth() + 1).toString(), // 月
"d+": date.getDate().toString(), // 日
"H+": date.getHours().toString(), // 時
"M+": date.getMinutes().toString(), // 分
"S+": date.getSeconds().toString(), // 秒
};
for (let k in opt) {
ret = new RegExp("(" + k + ")").exec(fmt);
if (ret) {
fmt = fmt.replace(
ret[1],
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0")
);
}
}
return fmt;
};
/**
* YYYY/MM/DD ⇒ Mon Nov 27 2017 20:30:00 GMT+0900 (JST)に変換
* @param {string} dataStr stringDate
* @returns {Date} Date
*/
export const datePrase = (dataStr) => {
return new Date(dataStr);
};
/**
* デートフォマート
* @param {string} date strData
* @param {string} fmt format
* @returns {string} StringDate
*/
export const strDateFormat = (strData, fmt = "YYYY/mm/dd HH:MM:SS") => {
return dateFormat(datePrase(strData), fmt);
};
/**
* ファイルサイズ変換
* @param {*} size バイト
* @returns 変換後のサイズ
*/
export const fileSizeUnit = (size) => {
// 1 KB = 1024 Byte
const kb = 1024;
const mb = Math.pow(kb, 2);
const gb = Math.pow(kb, 3);
const tb = Math.pow(kb, 4);
const pb = Math.pow(kb, 5);
const round = (size, unit) => {
return Math.round((size / unit) * 100.0) / 100.0;
};
if (size >= pb) {
return round(size, pb) + "PB";
} else if (size >= tb) {
return round(size, tb) + "TB";
} else if (size >= gb) {
return round(size, gb) + "GB";
} else if (size >= mb) {
return round(size, mb) + "MB";
} else if (size >= kb) {
return round(size, kb) + "KB";
}
return size + "バイト";
};
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
4.Salesforce 側動作確認
5.参考
GitHub - aws/aws-sdk-js: AWS SDK for JavaScript in the browser and Node.js
Get started in the browser - AWS SDK for JavaScript
CORS 設定のエレメント - Amazon Simple Storage Service
Component Library
Class: AWS.S3
— AWS SDK for JavaScript