こちらの記事の続きです。
Google DriveとSalesforceの相互連携を行うためのシリーズです。今回は最終回、ApexとLWCで、SalesforceからGoogle Driveへファイルをアップロードを実現します。ついでにフォルダ作成も行います。
ファイルまとめ
class
public with sharing class GoogleDriveController {
@AuraEnabled
public static Map<String, Object> uploadFileToGoogleDrive(String fileName, String fileContentBase64, String folderId) {
try {
String fileExtension = getFileExtension(fileName);
String mimeType = getMimeType(fileExtension);
String metadata = createFileMetadata(fileName, mimeType, folderId);
String boundary = '-------------' + String.valueOf(DateTime.now().getTime());
String requestBody = createMultipartBody(metadata, mimeType, fileContentBase64, boundary);
HttpResponse response = makeUploadRequest(requestBody, boundary);
return handleUploadResponse(response);
} catch (Exception e) {
System.debug('Error during upload: ' + e.getMessage());
throw new AuraHandledException(e.getMessage());
}
}
private static final Map<String, String> MIME_TYPES = new Map<String, String>{
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'pdf' => 'application/pdf',
'txt' => 'text/plain'
};
private static String createFileMetadata(String fileName, String mimeType, String folderId) {
return '{"name": "' + fileName +
'", "mimeType": "' + mimeType +
'", "parents": ["' + folderId + '"]}';
}
private static String createMultipartBody(String metadata, String mimeType, String fileContentBase64, String boundary) {
List<String> lines = new List<String>();
// Metadata part
lines.add('--' + boundary);
lines.add('Content-Type: application/json; charset=UTF-8');
lines.add('');
lines.add(metadata);
// File content part
lines.add('--' + boundary);
lines.add('Content-Type: ' + mimeType);
lines.add('Content-Transfer-Encoding: base64');
lines.add('');
lines.add(fileContentBase64);
// Final boundary
lines.add('--' + boundary + '--');
return String.join(lines, '\r\n');
}
private static HttpResponse makeUploadRequest(String requestBody, String boundary) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:GoogleDriveUpload/files?uploadType=multipart');
req.setMethod('POST');
req.setHeader('Content-Type', 'multipart/related; boundary=' + boundary);
req.setTimeout(120000); // 2 minutes timeout
req.setBody(requestBody);
Http http = new Http();
return http.send(req);
}
private static Map<String, Object> handleUploadResponse(HttpResponse response) {
System.debug('Response status: ' + response.getStatusCode());
System.debug('Response body: ' + response.getBody());
if (response.getStatusCode() == 200 || response.getStatusCode() == 201) {
Map<String, Object> responseMap = (Map<String, Object>)JSON.deserializeUntyped(response.getBody());
return responseMap;
//return response.getBody();
} else {
throw new AuraHandledException('Upload failed with status ' +
response.getStatusCode() + ': ' + response.getBody());
}
}
private static String getFileExtension(String fileName) {
system.debug('fileName: ' + fileName);
if (String.isBlank(fileName) || !fileName.contains('.')) {
throw new AuraHandledException('Invalid file name: ' + fileName);
}
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
}
private static String getMimeType(String fileExtension) {
String mimeType = MIME_TYPES.get(fileExtension.toLowerCase());
if (mimeType == null) {
throw new AuraHandledException('Unsupported file type: ' + fileExtension +
'. Supported types are: ' + String.join(new List<String>(MIME_TYPES.keySet()), ', '));
}
return mimeType;
}
@AuraEnabled
public static ContentVersion createContentVersionFromGoogleFile(Map<String, Object> googleFileInfo, String linkedEntityId, String dataSourceName) {
String fileId = (String) googleFileInfo.get('id');
String fileName = (String) googleFileInfo.get('name');
ContentVersion contentVersion = new ContentVersion();
contentVersion.Title = fileName;
contentVersion.PathOnClient = fileName;
contentVersion.ContentLocation = 'E';
contentVersion.Origin = 'H';
contentVersion.ExternalDocumentInfo1 = 'https://drive.google.com/file/d/' + fileId + '/view?usp=drivesdk';
contentVersion.ExternalDocumentInfo2 = 'file:' + fileId;
contentVersion.ExternalDataSourceId = getExternalDataSourceId(dataSourceName); // 重要
insert contentVersion;
ContentVersion currentContentVersion = [SELECT Id, ContentDocumentId FROM ContentVersion WHERE Id = :contentVersion.Id WITH SYSTEM_MODE LIMIT 1];
system.debug('currentContentVersion: ' + currentContentVersion);
ContentDocumentLink contentDocumentLink = new ContentDocumentLink();
contentDocumentLink.LinkedEntityId = linkedEntityId;
contentDocumentLink.ContentDocumentId = currentContentVersion.ContentDocumentId;
contentDocumentLink.ShareType = 'V';
contentDocumentLink.Visibility = 'AllUsers';
system.debug('contentDocumentLink: ' + contentDocumentLink);
insert contentDocumentLink;
return currentContentVersion;
}
private static Id getExternalDataSourceId(String name) {
ExternalDataSource externalDataSource = [SELECT Id FROM ExternalDataSource WHERE DeveloperName = :name LIMIT 1];
return externalDataSource.Id;
}
@AuraEnabled
public static String upsertFolder(String folderName, String parentFolderName) {
String parentFolderId = getFolderMetadata(parentFolderName).Folder_Id__c;
String folderId = findFolder(folderName, parentFolderId);
if (folderId != null) {
return folderId;
}
return createFolder(folderName, parentFolderId);
}
public static String findFolder(String folderName, String parentFolderId) {
String query = 'name = \'' + folderName + '\' and mimeType = \'application/vnd.google-apps.folder\' and trashed = false and \'' + parentFolderId + '\' in parents';
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:GoogleDrive/files?q=' + EncodingUtil.urlEncode(query, 'UTF-8'));
req.setMethod('GET');
req.setHeader('Content-Type', 'application/json');
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> jsonResponse = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
List<Object> files = (List<Object>) jsonResponse.get('files');
if (!files.isEmpty()) {
return (String)((Map<String, Object>) files[0]).get('id'); // Return the folder ID
}
} else {
System.debug('Error finding folder: ' + res.getBody());
}
return null; // Folder not found
}
public static String createFolder(String folderName, String parentFolderId) {
Map<String, Object> requestBody = new Map<String, Object>{
'name' => folderName,
'mimeType' => 'application/vnd.google-apps.folder',
'parents' => new List<String>{ parentFolderId }
};
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:GoogleDrive/files');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(requestBody));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200 || res.getStatusCode() == 201) {
Map<String, Object> jsonResponse = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
return (String) jsonResponse.get('id'); // Return the new folder ID
} else {
System.debug('Error creating folder: ' + res.getBody());
}
return null; // Folder creation failed
}
@AuraEnabled
public static GoogleDrive__mdt getFolderMetadata(String parentFolderName) {
// Query to get up to 200 metadata records
GoogleDrive__mdt folderMetadata = [SELECT DeveloperName, Folder_Id__c FROM GoogleDrive__mdt WHERE DeveloperName =:parentFolderName LIMIT 1];
return folderMetadata;
}
}
html
<template>
<lightning-card hide-header="true">
<div
class="slds-grid slds-wrap slds-grid_vertical-align-center slds-page-header forceRelatedListCardHeader slds-p-vertical_small">
<lightning-button lwc:if={isBlankFolder} label="Create Google Drive Folder" onclick={upsertFolder}
class={buttonUploadClass}>
</lightning-button>
<template lwc:if={isCreating}>
<lightning-spinner alternative-text="Creating..." size="x-small"></lightning-spinner>
</template>
<div lwc:if={isNotBlankFolder} class="slds-grid slds-grid_vertical-align-center slds-grid_align-spread">
<img src=" /img/icon/contentHubGDrive_32.png" width="24px" alt="Google Drive" />
<lightning-formatted-url value={driveFolderUrl} label={driveFolderName}
class="slds-m-horizontal_x-small slds-card__header-title" target="_blank"></lightning-formatted-url>
<div lwc:if={copyAvailable}>
<a href="javascript:void(0);" onclick={copyFolderUrl} class="slds-m-left_x-small">
<lightning-icon icon-name="utility:copy_to_clipboard" size="x-small" alternative-text="Copy"
variant={copyIconVariant} class="copy-icon"></lightning-icon>
</a>
<span class="slds-m-left_x-small">{copyText}</span>
</div>
<div lwc:else
class="slds-theme_error slds-p-horizontal_x-small slds-p-vertical_xx-small slds-page-header">
<h2 class="slds-text-color_inverse">外部共有禁止</h2>
</div>
</div>
</div>
<div class="slds-grid slds-grid_vertical-align-center slds-p-around_small slds-border_top">
<lightning-input multiple disabled={isBlankFolder} type="file" variant="label-hidden"
onchange={handleFileChange}></lightning-input>
<lightning-button disabled={isUploadDisabled} label="Upload to Google" variant="brand"
class="slds-m-left_x-small" onclick={handleUpload}></lightning-button>
</div>
<div class="slds-grid slds-wrap slds-grid_vertical-align-center slds-p-horizontal_x-small">
<template lwc:if={files}>
<template for:each={selectedFiles} for:item="file">
<lightning-pill key={file.name} name={file.name} label={file.name} onremove={handleRemoveFile}
class={selectedItemClass}>
<lightning-icon icon-name={file.iconName} alternative-text={file.iconName}>
</lightning-icon>
</lightning-pill>
</template>
<template lwc:if={isUploading}>
<lightning-spinner alternative-text="Uploading..." size="x-small"></lightning-spinner>
</template>
</template>
</div>
</lightning-card>
</template>
js
import { LightningElement, wire, api } from 'lwc';
import uploadFileToGoogleDrive from '@salesforce/apex/GoogleDriveController.uploadFileToGoogleDrive';
import createContentVersionFromGoogleFile from '@salesforce/apex/GoogleDriveController.createContentVersionFromGoogleFile';
import upsertFolder from '@salesforce/apex/GoogleDriveController.upsertFolder';
import ACCOUNT_UNIQUE_NUMBER_FIELD from '@salesforce/schema/Account.AccountNumber';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';
import ACCOUNT_DRIVE_FOLDER_ID_FIELD from '@salesforce/schema/Account.Drive_Folder_Id__c';
import ACCOUNT_DRIVE_FOLDER_NAME_FIELD from '@salesforce/schema/Account.Drive_Folder_Name__c';
import { getRecord, updateRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { RefreshEvent } from "lightning/refresh";
export default class GoogleDrive extends LightningElement {
@api recordId;
@api flexipageRegionWidth;
@api objectApiName;
files = [];
selectedFiles = [];
isUploading = false;
isCreating = false;
driveFolderId;
driveFolderName = '';
driveFolderUrl = '';
copyAvailable = false;
copyIconVariant = 'utility:copy';
uniqueNumber;
uniqueName;
copyText = '';
copyIconVariant = 'brand';
get isUploadDisabled() {
return this.files.length === 0;
}
get isBlankFolder() {
return !this.driveFolderId;
}
get isNotBlankFolder() {
return this.driveFolderId;
}
get buttonUploadClass() {
return this.flexipageRegionWidth === 'SMALL' ? 'slds-p-right_x-small' : 'slds-p-left_small slds-m-around_xx-small';
}
get folderSelectClass() {
return this.flexipageRegionWidth === 'SMALL' ? 'slds-p-right_x-small slds-size_1-of-2' : 'slds-p-left_small slds-m-around_xx-small slds-size_1-of-6';
}
get selectedItemClass() {
return this.flexipageRegionWidth === 'SMALL' ? 'slds-p-right_x-small slds-p-bottom_x-small' : 'slds-m-around_xx-small';
}
get fields() {
return [
ACCOUNT_UNIQUE_NUMBER_FIELD,
ACCOUNT_NAME_FIELD,
ACCOUNT_DRIVE_FOLDER_ID_FIELD,
ACCOUNT_DRIVE_FOLDER_NAME_FIELD
];
}
@wire(getRecord, { recordId: '$recordId', fields: '$fields' })
wiredRecord({ error, data }) {
if (data) {
const baseUrl = 'https://drive.google.com/drive/folders/';
this.uniqueNumber = data.fields[ACCOUNT_UNIQUE_NUMBER_FIELD.fieldApiName].value;
this.uniqueName = data.fields[ACCOUNT_NAME_FIELD.fieldApiName].value;
this.driveFolderName = data.fields[ACCOUNT_DRIVE_FOLDER_NAME_FIELD.fieldApiName].value;
this.driveFolderId = data.fields[ACCOUNT_DRIVE_FOLDER_ID_FIELD.fieldApiName].value;
this.driveFolderUrl = baseUrl + this.driveFolderId;
this.copyAvailable = true;
this.copyIconVariant = 'utility:copy';
}
if (error) {
console.error('Error loading record:', error);
}
}
handleFileChange(event) {
this.files = Array.from(event.target.files);
this.selectedFiles = this.files.map(file => ({
name: file.name,
iconName: this.getFileIcon(file.type)
}));
}
getFileIcon(fileType) {
const icons = {
'application/pdf': 'doctype:pdf',
'image/jpeg': 'doctype:image',
'image/jpg': 'doctype:image',
'image/png': 'doctype:image',
'application/msword': 'doctype:word',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'doctype:word',
'application/vnd.ms-excel': 'doctype:excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'doctype:excel',
'text/plain': 'doctype:txt'
};
return icons[fileType] || 'doctype:unknown';
}
handleUpload() {
this.isUploading = true;
Promise.all(this.files.map(file => this.uploadFile(file)))
.then((result) => {
console.log('result: ', result);
const createContentVersionPromises = result.map(file => this.createContentVersion(file));
return Promise.all(createContentVersionPromises);
})
.catch(error => {
console.error('Error uploading files: ', error);
this.dispatchEvent(
new ShowToastEvent({
title: 'Error uploading files',
message: error.body.message,
variant: 'error'
})
);
})
.finally(() => {
this.finishUploading();
});
}
async uploadFile(file) {
const base64Content = await this.convertFileToBase64(file);
console.log('Base64 content: ', base64Content);
const params = {
fileName: file.name,
fileContentBase64: base64Content,
folderId: this.driveFolderId
};
console.log('params: ', params);
return uploadFileToGoogleDrive(params);
}
convertFileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
handleRemoveFile(event) {
const fileName = event.detail.name;
this.selectedFiles = this.selectedFiles.filter(file => file.name !== fileName);
this.files = this.files.filter(file => file.name !== fileName);
}
finishUploading() {
this.isUploading = false;
this.files = [];
this.selectedFiles = [];
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Files uploaded successfully',
variant: 'success'
})
);
console.log('RefreshEvent');
this.dispatchEvent(new RefreshEvent());
}
createContentVersion(file) {
const googleFileInfo = {
'id': file.id,
'name': file.name,
'mimeType': file.mimeType
};
const params = {
googleFileInfo: googleFileInfo,
linkedEntityId: this.recordId,
dataSourceName: 'GoogleDrive'
};
return createContentVersionFromGoogleFile(params);
}
upsertFolder() {
this.isCreating = true;
if (this.objectApiName !== '') {
const folderName = `${this.uniqueNumber} ${this.uniqueName}`;
upsertFolder({ folderName: folderName, parentFolderName: this.objectApiName })
.then(result => {
const fields = {};
switch (this.objectApiName) {
case 'Account':
fields[ACCOUNT_DRIVE_FOLDER_ID_FIELD.fieldApiName] = result;
fields[ACCOUNT_DRIVE_FOLDER_NAME_FIELD.fieldApiName] = folderName;
break;
default:
}
fields['Id'] = this.recordId;
this.isCreating = false; // finish spinner while creating
const recordInput = { fields };
updateRecord(recordInput)
.then(() => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Folder upserted and the record updated successfully!',
variant: 'success',
}),
);
})
.catch(error => {
console.error('Error updating Case record:', error);
this.dispatchEvent(
new ShowToastEvent({
title: 'Error',
message: 'Folder upserted but Case record update failed: ' + error.body.message,
variant: 'error',
}),
);
});
})
.catch(error => {
console.error('Error upserting folder:', error);
this.dispatchEvent(
new ShowToastEvent({
title: 'Error',
message: 'Folder upsert failed: ' + error.body.message,
variant: 'error',
}),
);
});
}
}
async copyFolderUrl() {
try {
await navigator.clipboard.writeText(this.driveFolderUrl);
this.copyText = 'Copied!';
this.copyIconVariant = 'inverse';
} catch (err) {
this.copyText = 'Failed to copy - please check permissions';
console.error('Copy failed:', err);
}
}
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
css
:host {
--slds-c-card-header-spacing-block-start: 0;
--slds-c-card-header-spacing-block-end: 0;
--slds-c-card-body-spacing-block-start: 0;
--slds-c-card-body-spacing-block-end: 0;
}
.copy-icon:hover {
--sds-c-icon-color-foreground-default: var(
--sds-c-icon-color-foreground-hover
);
}
新規項目まとめ
New Fields
Account.Drive_Folder_Id__c
Account.Drive_Folder_Name__c
Custom Metadata
Custom Metadata Type:
GoogleDrive
Custom Metadata Field:
Folder_Id__c
手順
-
Class
の作成-
uploadFileToGoogleDrive
メソッドの作成 - アップロードの検証
-
-
LWC
を作成-
Upload Files
ボタンのアクションを追加 -
pill
の削除のアクションを追加 -
Upload to Google
ボタンのアクションを追加 -
Files
の追加
-
-
フォルダ
を作成-
External Data Source
のCustom MetaData管理 -
driveFolderId
の動的処理
-
Classの作成
まずGoogleDriveController
classを作成してみます。その中で、uploadFileToGoogleDrive
のメソッドを作成します。
ApexでuploadFileToGoogleDrive
のメソッドの作成
uploadFileToGoogleDrive
@AuraEnabled
public static Map<String, Object> uploadFileToGoogleDrive(String fileName, String fileContentBase64, String folderId) {
try {
String fileExtension = getFileExtension(fileName);
String mimeType = getMimeType(fileExtension);
String metadata = createFileMetadata(fileName, mimeType, folderId);
String boundary = '-------------' + String.valueOf(DateTime.now().getTime());
String requestBody = createMultipartBody(metadata, mimeType, fileContentBase64, boundary);
HttpResponse response = makeUploadRequest(requestBody, boundary);
return handleUploadResponse(response);
} catch (Exception e) {
System.debug('Error during upload: ' + e.getMessage());
throw new AuraHandledException(e.getMessage());
}
}
最終的にはhttpResponseにmakeUploadRequest
からフォーマットされたrequestBodyとboundaryを渡すのを目的に作成します。ただし、これにはprivate methodが以下のように複数含まれているのでそちらも同時に作成します。
makeUploadRequest
getFileExtension
getMineType
createFileMetadata
createMultipartBody
handleUploadResponse
makeUploadRequest
private static HttpResponse makeUploadRequest(String requestBody, String boundary) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:GoogleDriveUpload/files?uploadType=multipart');
req.setMethod('POST');
req.setHeader('Content-Type', 'multipart/related; boundary=' + boundary);
req.setTimeout(120000); // 2 minutes timeout
req.setBody(requestBody);
Http http = new Http();
return http.send(req);
}
httpRequestのEndPointは callout:GoogleDriveUpload/files?uploadType=multipart'
です。CalloutはGoogleDriveUpload
を使用します。こちらはurlに/upload/drive/v3
が指定しているので、/files
を続けて、?
で変数を当てていけます。
Named Credential(Upload用)
また、多くのメタデータを渡すのでuploadType=multipart
を利用します。このuploadTypeを利用することによって、親フォルダの指定をしたり、ファイルのname, mimeType, descriptionなどを追加してアップロードできます。
参照: uploadType=media
参照: uploadType=multipart
参照: uploadType=resumable
getFileExtension
private static String getFileExtension(String fileName) {
if (String.isBlank(fileName) || !fileName.contains('.')) {
throw new AuraHandledException('Invalid file name: ' + fileName);
}
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
}
getMimeType
private static String getMimeType(String fileExtension) {
String mimeType = MIME_TYPES.get(fileExtension.toLowerCase());
if (mimeType == null) {
throw new AuraHandledException('Unsupported file type: ' + fileExtension +
'. Supported types are: ' + String.join(new List<String>(MIME_TYPES.keySet()), ', '));
}
return mimeType;
}
createFileMetadata
private static String createFileMetadata(String fileName, String mimeType, String folderId) {
return '{"name": "' + fileName +
'", "mimeType": "' + mimeType +
'", "parents": ["' + folderId + '"]}';
}
createMultipartBody
private static String createMultipartBody(String metadata, String mimeType, String fileContentBase64, String boundary) {
List<String> lines = new List<String>();
// Metadata part
lines.add('--' + boundary);
lines.add('Content-Type: application/json; charset=UTF-8');
lines.add('');
lines.add(metadata);
// File content part
lines.add('--' + boundary);
lines.add('Content-Type: ' + mimeType);
lines.add('Content-Transfer-Encoding: base64');
lines.add('');
lines.add(fileContentBase64);
// Final boundary
lines.add('--' + boundary + '--');
return String.join(lines, '\r\n');
}
handleUploadResponse
private static Map<String, Object> handleUploadResponse(HttpResponse response) {
System.debug('Response status: ' + response.getStatusCode());
System.debug('Response body: ' + response.getBody());
if (response.getStatusCode() == 200 || response.getStatusCode() == 201) {
Map<String, Object> responseMap = (Map<String, Object>)JSON.deserializeUntyped(response.getBody());
return responseMap;
} else {
throw new AuraHandledException('Upload failed with status ' +
response.getStatusCode() + ': ' + response.getBody());
}
}
これにより最終的にuploadFileToGoogleDrive
メソッドが完成します。のちにLWC側でimportして利用します。
アップロードの検証
GoogleDriveController
をデプロイしたのち、Anonymous Windowで同クラスのメソッドを呼び出してアップロードが可能か検証します。
検証Apex
String fileName = 'Text Plain.txt';
String fileContent = 'Test Content Encoded';
String fileContentBase64 = EncodingUtil.base64Encode(Blob.valueOf(fileContent));
String folderId = 'root';
GoogleDriveController.uploadFileToGoogleDrive(fileName, fileContentBase64, folderId);
こちらでuploadFileToGoogleDriveに渡すと、txtファイルが作成されているのが確認できたら成功です。folderIdはrootを指定しているので、MyDrive配下にできます。
folderIdありで検証
次に、Google Drive内に新規のフォルダを作成してみましょう。名前は任意ですのでここではDev
とつけて作成、URLからフォルダのid
をコピペして、フォルダIdに当てて呼んでみましょう。
検証Apex (folderIdあり)
String fileName = 'TestFile.txt';
String fileContent = 'Test Content Encoded';
String fileContentBase64 = EncodingUtil.base64Encode(Blob.valueOf(fileContent));
String folderId = '1eE6*****************MO3'; // 実際のフォルダIdを入れてください
GoogleDriveController.uploadFileToGoogleDrive(fileName, fileContentBase64, folderId);
実行後、Dev
フォルダ配下にファイルが作成されていれば、GoogleDriveController.uploadFileToGoogleDriveのメソッドの検証は成功です。
LWCを作成
以上でControllerの基本的fileUploadメソッドの作成は終わりました。それでは続いてLWCで以下の単純なボタンのみあるコンポートネントを作成します。
-
ファイルをアップロードするアクション
(lightning-input) -
Googleへアップロードするボタン
(lightning-button)
構成
htmlファイルの作成
構成はとりあえず最低限で lightning-input
と lightning-button
をdiv
で囲い slds-grid
で縦方向中央揃えします。
html
<template>
<lightning-card>
<div class="slds-grid slds-grid_vertical-align-center slds-p-around_small">
<lightning-input multiple type="file" variant="label-hidden" onchange={handleFileChange}></lightning-input>
<lightning-button disabled={isUploadDisabled} label="Upload to Google" variant="brand"
class="slds-m-left_x-small" onclick={handleUpload}></lightning-button>
</div>
</lightning-card>
</template>
lightning-card
今後テストでいくつかボタンを追加していきます。それをlightning-card
で囲って、パーツぽくまとめます。
lightning-input
lightning-input
は面白くて、type=file
にすることでファイルアップロードモジュールになります。扱うファイルはmultiple
パラメーターの追加で複数ファイルを扱うようになります。
参照: lightning-card / lightning-input / lightning-button / slds-grid
jsファイルの作成
今はほぼ空で。次の工程で複数のメソッドを追加していきます。
js
import { LightningElement, api } from 'lwc';
import uploadFileToGoogleDrive from '@salesforce/apex/GoogleDriveController.uploadFileToGoogleDrive';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class GoogleDrive extends LightningElement {
@api recordId;
@api flexipageRegionWidth;
}
api decorator
とりあえず LightningElementの後にカンマでapiを追加して@api
decoratorを有効にしてrecordId
とflexipageRegionWidth
を取得します。どちらも後の工程で利用します。
import method
前工程で作成してあるuploadFileToGoogleDrive
メソッドもimportしておきましょう。
import toast
またお馴染のShowToastEvent
も処理完了の通知で利用するのでimportしておきましょう。
js-meta.xml
exposedをtrue
にするのとでlightning__RecordPage
のみ追加します。これでレコードページにLWCを配置できるようになります。
js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__RecordPage</target>
</targets>
</LightningComponentBundle>
添付ファイルの表示(onremoveアクションあり)
さらに、追加のhtmlです。添付ファイルの存在を表示させて、アップロードに準備されているファイルがいくつあるのか、以下のhtmlを追加します。Upload Filesに渡されたfilesをわかりやすいようにpill表示します。(onremoveアクションあり)
files表示
<div class="slds-grid slds-wrap slds-grid_vertical-align-center slds-p-around_x-small">
<template lwc:if={files}>
<template for:each={selectedFiles} for:item="file">
<lightning-pill key={file.name} name={file.name} label={file.name} onremove={handleRemoveFile}
class={selectedItemClass}>
<lightning-icon icon-name={file.iconName} alternative-text={file.iconName}>
</lightning-icon>
</lightning-pill>
</template>
<template lwc:if={isUploading}>
<lightning-spinner alternative-text="Uploading..." size="x-small"></lightning-spinner>
</template>
</template>
</div>
Upload Filesボタンのアクションを追加
とりあえずコンポーネントは配置できたのを確認したのち、Upload Files
ボタンにファイルをドラッグ&ドロップ(もしくはクリックしてファイル選択)をした際の挙動として、ファイルをアップロードした後の処理を追加していきます。
jsにhandleFileChangeとgetFileIconを追加
import { LightningElement, api } from 'lwc';
import uploadFileToGoogleDrive from '@salesforce/apex/GoogleDriveController.uploadFileToGoogleDrive';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class GoogleDrive extends LightningElement {
@api recordId;
@api flexipageRegionWidth;
files = [];
selectedFiles = [];
isUploading = false;
handleFileChange(event) {
this.files = Array.from(event.target.files);
this.selectedFiles = this.files.map(file => ({
name: file.name,
iconName: this.getFileIcon(file.type)
}));
}
getFileIcon(fileType) {
const icons = {
'application/pdf': 'doctype:pdf',
'image/jpeg': 'doctype:image',
'image/jpg': 'doctype:image',
'image/png': 'doctype:image',
'application/msword': 'doctype:word',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'doctype:word',
'application/vnd.ms-excel': 'doctype:excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'doctype:excel',
'text/plain': 'doctype:txt'
};
return icons[fileType] || 'doctype:unknown';
}
}
handleFileChange
コンポーネントにファイルを追加するたびにこちらのメソッドが呼ばれるのでその処理をします。
files
とselectedFiles
の配列にgetFileIconでdoctypeアイコン形式に変換した配列として格納していきます。
getFileIcon
linghting-inputで渡されるデータは基本的に以下のようになります。
key |
---|
lastModified |
lastModifiedDate |
name |
size |
type (こちらを使用) |
webkitRelativePath |
lightning-iconに渡してslds iconsのdoctypeで使用可能な文字列に変換しておきます。メジャーなものだけを追加したので必要に応じてcaseを増やしてみてください。
slds doctype
Upload to Googleボタンを有効化する
filesが0以上の時にのみUpload to Googleを有効化します。
isUploadDisabledのgetterを設定
get isUploadDisabled() {
return this.files.length === 0;
}
lightning-button disabledを設定
<lightning-button disabled={isUploadDisabled} label="Upload to Google"
variant="label-hidden" class="slds-m-left_x-small" onclick={handleUpload}>
</lightning-button>
したがって、Upload to Googleのボタンのdisabledは、disabled={isUplaodDisabled}
となります。こうすることによってファイルがアップロードされたらクリックできるようなボタンの完成です。
pillの削除のアクションを追加
onremoveのhandleRemoveFileアクションを追加
handleRemoveFile(event) {
const fileName = event.detail.name;
this.selectedFiles = this.selectedFiles.filter(file => file.name !== fileName);
this.files = this.files.filter(file => file.name !== fileName);
}
get selectedItemClass() {
return this.flexipageRegionWidth === 'SMALL' ? 'slds-p-right_x-small slds-p-top_x-small' : 'slds-m-around_xx-small';
}
handleRemoveFile
pill要素にはhandleRemoveFile
がonremoveで呼べるので、こちらのメソッドを用意します。クリックするたびに、一度Upload Filesであげたものをキャンセルすることができます。
selectedItemClass
また、getterで、selectedItemClass
を作成します。this.flexipageRegionWidth
はとても便利でコンポーネントが置かれた領域の大きさが取れます。それを利用してコンポーネントが大きい領域に配置された時と小さい領域に配置された時で、スペーシングが異なるような挙動も入れます。必須ではないですが見た目が整います。
参照: flexipageRegionWidth
Upload to Google ボタンのアクションを追加
上記でファイルは取得できているので、次はそれをGoogle Driveへアップロードするアクションを追加していきます。
handleUploadによるfilesの処理
handleUpload() {
this.isUploading = true;
Promise.all(this.files.map(file => this.uploadFile(file)))
.then((result) => {
console.log('result: ', result);
})
.finally(() => {
this.finishUploading();
});
}
async uploadFile(file) {
const base64Content = await this.convertFileToBase64(file);
const params = {
fileName: file.name,
fileContentBase64: base64Content,
folderId: 'root'// とりあえずroot 指定します。MyDriveへ入ります
};
return uploadFileToGoogleDrive(params);
}
convertFileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
finishUploading() {
this.isUploading = false;
this.files = [];
this.selectedFiles = [];
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Files uploaded successfully',
variant: 'success'
})
);
}
handleUpload
handleUpload
は単数もしくは複数のファイルをPromiseAllで処理していきます。処理の工程の中でuploadFile
としてファイルデータをbase64に変換してparamとして保管し、Apexのメソッドを呼び出します。
uploadFile
foldconverFileToBase64で帰ってきたfile情報を、folderIdに向けて上げていくアクションでです。とりあえずfolderIdをベタ打ちで設定しましょう。後ほどセキュアで動的なfolderIdの設定にアップデートします。
convertFileToBase64
またconvertFileToBase64
によりbase64への変換もPromiseとして返します。 async awaitでその処理をつど待つことになります。処理が終わればPromiseAllの最終処理が呼ばれます。
finishUploading
こちらは、処理の最初にisUploading変数をtrueへ設定し、spinnerを回す状態にして、PromiseAllが終了したのちの処理をまとめたものです。filesやselectedFilesを空に戻したりSpinnerを止めたりします。ToastEventもあると明示的にアップロードの終了がわかるのでユーザーにとって丁寧な通知になります。
base64について
LWCからApexにbinary data(0101...で表現されたデータ) を渡す方法がないです。またセキュリティ上ファイルデータを安全に渡すためにバイナリデータからbase64文字列 ([0-9a-zA-Z+/]*)に変換する必要があり、JavaScriptとApexどちらともにその機能はあります。今回のLWCでは、JavaScriptで変換を行なっています。一方Anonymous Windowでのテストでは、ApexにあるEncodingUtil.base64Encode()を利用して行なっています。
参照: JavaScriptでBase64エンコード・デコード
参照: base64ってなんぞ??理解のために実装してみた
JavaScriptにおけるbase64へのencodingの動き
convertFileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
reader.readAsDataURL(file) を呼び出した後、reader.onload イベントハンドラ内で base64 変換が行われています。具体的には、reader.result.split(',')[1] の部分で base64 文字列が抽出されます。
- new FileReader() で FileReader オブジェクトを作成
- reader.readAsDataURL(file) メソッドを呼び出すと、ファイルが base64 エンコードされたデータ URL に変換
- 変換が完了すると onload イベントが発生
- reader.result には "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD..." のようなフォーマットの文字列が含まれる
- split(',')[1] で base64 部分(/9j/4AAQSkZJRgABAQEAYABgAAD...)だけを抽出
- resolve() メソッドで Promise を解決し、base64 文字列を返す
この実装により、渡されたファイルが効率的に base64 文字列に変換されます。
次は、return uploadFileToGoogleDrive(params);
こちらでファイル作成のメソッドを呼び出し、Google Driveにファイルが作成されていることと、結果作成されたファイルのアドレスとIdが戻り値としてきているのを確認します。
Filesに追加
handleUpload
でPromiseAllによりアップロードを終えたのち、ファイルのGoogle Driveのデータが戻ってきます。それを利用してContentDocumentLinkを作成します。Google Driveにあるファイルの参照リンクはExternalDocumentInfo1
とExternalDocumentInfo2
に保存されてReference
としてのみ機能します。
戻り値を確認する
複数のファイルをアップロードすれば複数の戻り値が配列で戻ってきます。これを利用して次でContentDocumentLinkを作成します。
uploadFileの戻り値の確認
handleUpload() {
this.isUploading = true;
Promise.all(this.files.map(file => this.uploadFile(file)))
.then((result) => {
console.log('result: ', result);
})
.finally(() => {
this.finishUploading();
});
}
uploadFileの戻り値
[
{
kind: 'drive#file',
id: '104*********************S_hcQ', // 実際は別のfileId
name: 'cat-king-craddle.png',
mimeType: 'image/png'
}
];
key | value |
---|---|
kind | drive#file |
id | 104*********************S_hcQ |
name | cat-king-craddle.png |
mimeType | image/png |
こちらは配列なので、そのまま次の項目のApexメソッドへ投げ込みます。
ApexでContentVersionとDocumentLinkを作成
配列を高階関数で渡す先のメソッドですが、受け取った情報からまずContentVersionを作成しContentDocumentLinkを追加することでGoogle DriveへあげたFileを既存のFilesリストへ追加することができます。自作することもできますが、既存のFilesリストはPreviewもできるので便利です。
Apex (ContentVersionとDocumentLink)
@AuraEnabled
public static ContentVersion createContentVersionFromGoogleFile(Map<String, Object> googleFileInfo, String linkedEntityId, String dataSourceName) {
String fileId = (String) googleFileInfo.get('id');
String fileName = (String) googleFileInfo.get('name');
ContentVersion contentVersion = new ContentVersion();
contentVersion.Title = fileName;
contentVersion.PathOnClient = fileName;
contentVersion.ContentLocation = 'E';
contentVersion.Origin = 'H';
contentVersion.ExternalDocumentInfo1 = 'https://drive.google.com/file/d/' + fileId + '/view?usp=drivesdk';
contentVersion.ExternalDocumentInfo2 = 'file:' + fileId;
contentVersion.ExternalDataSourceId = getExternalDataSourceId(dataSourceName); // 重要
insert contentVersion;
ContentVersion currentContentVersion = [SELECT Id, ContentDocumentId FROM ContentVersion WHERE Id = :contentVersion.Id WITH SYSTEM_MODE LIMIT 1];
system.debug('currentContentVersion: ' + currentContentVersion);
ContentDocumentLink contentDocumentLink = new ContentDocumentLink();
contentDocumentLink.LinkedEntityId = linkedEntityId;
contentDocumentLink.ContentDocumentId = currentContentVersion.ContentDocumentId;
contentDocumentLink.ShareType = 'V';
contentDocumentLink.Visibility = 'AllUsers';
system.debug('contentDocumentLink: ' + contentDocumentLink);
insert contentDocumentLink;
return currentContentVersion;
}
private static Id getExternalDataSourceId(String name) {
ExternalDataSource externalDataSource = [SELECT Id FROM ExternalDataSource WHERE DeveloperName = :name LIMIT 1];
return externalDataSource.Id;
}
createContentVersionFromGoogleFile
こちらはGoogleDriveからの戻り値をもとに、ContentVersionとContentDocumentLinkを作成します。通常のContentVersionとは異なり以下の点が必要です。
-
ContentLocation
='E' // Externalの意味 -
ExternalDocumentInfo1
// 外部ファイルのViewアドレス -
ExternalDocumentInfo2
// file:Id情報 -
ExternalDataSourceId
// External Data Sourceに登録してある外部ソースのId
externalDataSourceId
ここで最重要なのが、externalDataSourceId
です。こちらは前回FilesConnectで作成したExternal Data Source
のGoogle Driveは実はIdが設定されていて、これをContentVersionのExternalDataSourceId
に設定する必要があります。getExternalDataSourceId
のパラメータにGoogleDrive
と入れて取得してきます。
参照:8. Google ドライブのExternal Data Sourceの定義
PromiseAllが完了したら呼ぶメソッド
上記で作成したメソッドを呼び出しパラメータを渡します。
getExternalDataSourceId
/* 略 */
contentVersion.ExternalDataSourceId = getExternalDataSourceId(dataSourceName); // 重要
/* 略 */
private static Id getExternalDataSourceId(String name) {
ExternalDataSource externalDataSource = [SELECT Id FROM ExternalDataSource WHERE DeveloperName = :name LIMIT 1];
return externalDataSource.Id;
}
createContentVersionFromGoogleFile
handleUpload() {
this.isUploading = true;
Promise.all(this.files.map(file => this.uploadFile(file)))
.then((result) => {
console.log('result: ', result);
const createContentVersionPromises = result.map(file => this.createContentVersion(file));
return Promise.all(createContentVersionPromises);
})
.catch(error => {
console.error('Error uploading files: ', error);
this.dispatchEvent(
new ShowToastEvent({
title: 'Error uploading files',
message: error.body.message,
variant: 'error'
})
);
})
.finally(() => {
this.finishUploading();
});
}
/*
(省略)
*/
createContentVersion(file) {
const googleFileInfo = {
'id': file.id,
'name': file.name,
'mimeType': file.mimeType
};
const params = {
googleFileInfo: googleFileInfo,
linkedEntityId: this.recordId,
dataSourceName: 'GoogleDrive'
};
return createContentVersionFromGoogleFile(params);
}
Google Driveから帰ってきた戻り値をid
, name
, mimeType
のオブジェクトとしてまとめます。追加でLinkedEntityIdは、ファイルが紐づくsObjectを表すためrecordId
です。
最後はdataSourceName
ですが、'GoogleDrive'をパラメータで指定することによって、Apex内でprivate メソッドによる組織のExternalDataSourceで、名前が'GoogleDrive'のIdを取得してくれます。
Files追加後の関連リスト自動更新
ContentDocumentLink追加後の関連リスト自動更新
また、ContentDocumentLinkが追加されたのち、関連リストを自動更新するために lightning/refresh
をimportしておき、finishUploadingで呼びましょう。そうすると、追加された関連FilesがGoogle DriveへのReferenceとして追加されたものを反映したリストになります。
import { RefreshEvent } from "lightning/refresh";
/*
(省略)
*/
finishUploading() {
this.isUploading = false;
this.files = [];
this.selectedFiles = [];
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Files uploaded successfully',
variant: 'success'
})
);
this.dispatchEvent(new RefreshEvent());
}
ここまでできればjs側のファイルの準備は完了です。
Folder作成
fileをDevフォルダにアップロードするのはできましたが、より整理されたフォルダ管理をしたいので、オブジェクト毎でまず大枠をフォルダで括り、その中に個別のフォルダを作成し、ファイルアップロードを行えれば、外部共有などでも安全にスコープを限定できます。
例:Dev
> Account
> United Oil & Gas Corp.
のような構造です。
United Oil & Gas Corp.
を子フォルダとすると、Account
が親フォルダになるという構造で整理していきます。
フォルダ作成手順
-
United Oil & Gas Corp.
というAcountのレコードページにボタンを配置 - Google Driveに同会社名のフォルダが
Account
配下にあるか探す- あれば、そのフォルダへファイルをアップロード
- なければ、
Account
配下にUnited Oil & Gas Corp.
のフォルダを作成- そのフォルダにファイルをアップロードする、というロジックを組みます。
フォルダの新規作成ボタン
Create Google Drive Folder
<lightning-button lwc:if={isBlankFolder} label="Create Google Drive Folder"
onclick={upsertFolder} class={buttonUploadClass}></lightning-button>
<template lwc:if={isCreating}>
<lightning-spinner alternative-text="Creating..." size="x-small"></lightning-spinner>
</template>
フォルダを新規作成または更新
upsertFolder
@AuraEnabled
public static String upsertFolder(String folderName, String parentFolderName) {
String parentFolderId = getFolderMetadata(parentFolderName).Folder_Id__c;
String folderId = findFolder(folderName, parentFolderId);
if (folderId != null) {
return folderId;
}
return createFolder(folderName, parentFolderId);
}
フォルダを探す
findFolder
public static String findFolder(String folderName, String parentFolderId) {
String query = 'name = \'' + folderName + '\' and mimeType = \'application/vnd.google-apps.folder\' and trashed = false and \'' + parentFolderId + '\' in parents';
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:GoogleDrive/files?q=' + EncodingUtil.urlEncode(query, 'UTF-8'));
req.setMethod('GET');
req.setHeader('Content-Type', 'application/json');
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> jsonResponse = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
List<Object> files = (List<Object>) jsonResponse.get('files');
if (!files.isEmpty()) {
return (String)((Map<String, Object>) files[0]).get('id');
}
} else {
System.debug('Error finding folder: ' + res.getBody());
}
return null; // Folder not found
}
フォルダを新規作成
createFolder
public static String createFolder(String folderName, String parentFolderId) {
Map<String, Object> requestBody = new Map<String, Object>{
'name' => folderName,
'mimeType' => 'application/vnd.google-apps.folder',
'parents' => new List<String>{ parentFolderId }
};
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:GoogleDrive/files');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(requestBody));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200 || res.getStatusCode() == 201) {
Map<String, Object> jsonResponse = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
return (String) jsonResponse.get('id');
} else {
System.debug('Error creating folder: ' + res.getBody());
}
return null; // Folder creation failed
}
parentFolderIdをCustom MetaDataで管理
上記のメソッドはともにparentFolderが何であるかを伝えてそこに格納するのですが、parentFolderIdをベタ打ちするもは避けたいので、Custom Metadataで管理します。LWCをAccountに配置すれば、Google DriveのAccountフォルダをparentFolderIdに、Opportunityに配置すればGoogle DriveのOpportunityフォルダをparentFolderIdに、という具合です。
Custom Metadataの作成
Setup > Custom Metadata Types > New へ行き、以下のように設定する。
Field | Value |
---|---|
Label | GoogleDrive (任意) |
Object Name | GoogleDrive (任意) |
Description | 説明 (任意) |
Visibility | All Apex code and APIs can use the types, and its visible in Setup |
Filder Id 項目を作成
Folder_Id__cという項目を追加する。これとDeveloperNameを後述のApexで利用する。
Google DriveのAcount フォルダのIdを取得
Dev > Accountと行くとURLにfolders/以下の文字列がfolder Idです。
AccountというDeveloperNameに、Google DriveのフォルダIdを格納しておきます。
これで、Google DriveのAccountフォルダId(parentFolderId)は、以下のapexでDeveloperNameで取得できようになります。
getFolderMetadata
@AuraEnabled
public static GoogleDrive__mdt getFolderMetadata(String parentFolderName) {
// Query to get up to 200 metadata records
GoogleDrive__mdt folderMetadata = [SELECT DeveloperName, Folder_Id__c FROM GoogleDrive__mdt WHERE DeveloperName =:parentFolderName LIMIT 1];
return folderMetadata;
}
driveFolderIdの動的処理
各SObjectにDrive_Folder_Id__c
とDrive_Folder_Name__c
項目を作成して、一度作成したGoogle Driveのフォルダ名、フォルダIdを格納しておきます。すでに作成されていれば@wire
で取得してきますし、なければCreate Drive Folder
ボタンを押すことでupsertFolder
が呼ばれ、新規のフォルダをGoogle Driveで作成したのちupdateRecordで、該当レコード内のDrive_Folder_Id__c
とDrive_Folder_Name__c
項目に更新しておきます。
js
import { getRecord, updateRecord } from 'lightning/uiRecordApi';
import ACCOUNT_UNIQUE_NUMBER_FIELD from '@salesforce/schema/Account.AccountNumber';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';
import ACCOUNT_DRIVE_FOLDER_ID_FIELD from '@salesforce/schema/Account.Drive_Folder_Id__c';
import ACCOUNT_DRIVE_FOLDER_NAME_FIELD from '@salesforce/schema/Account.Drive_Folder_Name__c';
/*
略
*/
get fields() {
return [
ACCOUNT_UNIQUE_NUMBER_FIELD,
ACCOUNT_NAME_FIELD,
ACCOUNT_DRIVE_FOLDER_ID_FIELD,
ACCOUNT_DRIVE_FOLDER_NAME_FIELD
];
}
@wire(getRecord, { recordId: '$recordId', fields: '$fields' })
wiredRecord({ error, data }) {
if (data) {
this.uniqueNumber = data.fields[ACCOUNT_UNIQUE_NUMBER_FIELD.fieldApiName].value;
this.uniqueName = data.fields[ACCOUNT_NAME_FIELD.fieldApiName].value;
this.driveFolderName = data.fields[ACCOUNT_DRIVE_FOLDER_NAME_FIELD.fieldApiName].value;
this.driveFolderId = data.fields[ACCOUNT_DRIVE_FOLDER_ID_FIELD.fieldApiName].value;
this.driveFolderUrl = `https://drive.google.com/drive/folders/${this.driveFolderId}`;
this.copyAvailable = true;
this.copyIconVariant = 'utility:copy';
}
if (error) {
console.error('Error loading record:', error);
}
}
wire
@wire
でコンポーネントを配置したレコードの情報を動的に取得します。Drive_Folder_Id__c
があれば、それをthis.driveFolderId
に当てて、なければ、新規作成のボタンを表示にします。新規作成ボタンを押せば、Google Driveに一意のフォルダが作成され戻り値をDrive_Folder_Id__c
に格納します。
注意点
今回カバーしきれていない点をいかにまとめておきます。今後徐々に対応したバージョンをおりを見て投稿していきます。
他のオブジェクトで利用する場合
Opportunity, Caseそれぞれのフォルダを作成する場合、Custom MetadataにそれぞれのFolderIdを作成しておきましょう。DeveloperNameがObjectApiNameと一致するように、特にカスタムオブジェクトは__cが必要です。
objectApiNameでの条件分岐
また、それぞれのオブジェクトにDrive_Folder_Id__cとDrive_Folder_Name__cを作成する必要があります。upsertFolderでもwireでも、importとそれぞれのobjectApiNameによる条件分岐が必要です。
共有フォルダのアクセス権限設定
共有フォルダへのアクセスはGoogle Driveのフォルダレベルで管理
ファイル削除の際の連動
ファイルをSalesforce側で削除した際、GoogleDriveのフォルダ内のファイルも削除するかどうか (一貫性を保つためには削除した方が良い、fileIdがあるので一意性は保たれる)
外部共有の仕方
あくまでReference
なのでメールでの添付などで外部と共有するのはやはり実物をLoadするか、共有ファイルのリンクを共有する必要がある
比較的大きいファイルのアップロード
4Mを超えるファイルのアップロードは、Google Drive APIのresumableを利用する必要があります
参照: uploadType=resumable
バージョン管理
Google Drive APIは現在v3ですが、今後バージョンが上がって新規機能や設定変更など常にアップデート情報を得て定期的にメンテする必要
参考
Develop Google Drive solutions
最後に
後ほどgithubに上げます。好評であれば来年はPackageで配布するのに挑戦したいと思います。質問があればxからお気軽にどうぞ。
@masahiro_sf