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"
]
}
]
2.2.Salesforce側静的リソースにAWSのSDKをアップロードする
2.3.Salesforce側CSP 信頼済みサイトの設定
3.ソース構成図
lwc
├─fileuploadMock
├─fileuploadModal
├─progressbar
└─utils
fileuploadMock
fileuploadMock.css
.container {
background-color: #fff;
min-height:100%;
}
.wrapper {
background-color: #cecece;
overflow: scroll;
width: 100%;
}
fileuploadMock.html
<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>
fileuploadMock.js
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')
});
}
}
fileuploadMock.js-meta.xml
<?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>
fileuploadModal.html
<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>
fileuploadModal.js
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;
}
}
fileuploadModal.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
progressbar
progressbar.html
<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>
progressbar.js
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';
// const approval_flag = myResource + 'action-sprite/svg/symbols.svg#approval';
// const remove_flag = myResource + 'action-sprite/svg/symbols.svg#remove';
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;
}
}
progressbar.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
utils
utils.js
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 + 'バイト'
}
utils.js-meta.xml
<?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側動作確認
4.1.Salesforce側Lightning コンポーネントタブを作成
4.2.タブを開く
4.3.ファイル追加
4.4.ファイルダウンロード
4.5.ファイル削除
5.参考