LoginSignup
9
9

More than 1 year has passed since last update.

LWCでAWS S3と連携する方法

Last updated at Posted at 2021-07-28

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をアップロードする

image.png

2.3.Salesforce側CSP 信頼済みサイトの設定

image.png

3.ソース構成図

lwc
 ├─fileuploadMock
 ├─fileuploadModal
 ├─progressbar
 └─utils

fileuploadMock

image.png

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

image.png

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

image.png

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

image.png

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 コンポーネントタブを作成

image.png

4.2.タブを開く

image.png

4.3.ファイル追加

スクリーンショット 2021-07-28 121057.png

4.4.ファイルダウンロード

スクリーンショット 2021-07-28 121134.png

4.5.ファイル削除

image.png

5.参考

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9