使用 Google Apps Script 實現網頁錄音、多檔案與文字上傳至 Google Drive

5 min read
目標
使用者經常需要在網頁上進行錄音、上傳多個檔案(例如圖片、文件),或者上傳一段文字,並希望將這些資料直接儲存到 Google Drive。傳統方法可能需要複雜的後端設定或第三方服務。本筆記旨在提供一個基於 Google Apps Script 的解決方案,讓使用者無需複雜設定,即可透過網頁介面直接完成錄音、檔案和文字上傳,並將資料儲存至指定的 Google Drive 資料夾。
核心步驟
- 建立 Google Apps Script 專案: 建立一個新的 Google Apps Script 專案,用於處理網頁前端和 Google Drive API 之間的互動。
- 部署網頁應用程式: 編寫 HTML、CSS 和 JavaScript 程式碼,建立使用者介面,包含錄音、檔案拖曳/選擇、文字輸入等功能,並將其部署為網頁應用程式。
- 取得使用者授權: 首次使用時,需要取得使用者授權,允許 Apps Script 存取其 Google Drive。
- 前端錄音與檔案處理: 使用 JavaScript 的
MediaRecorder
API 進行錄音,並將錄音資料轉換為 Blob 格式。使用FileReader
API 讀取使用者選擇的檔案,並將其轉換為 Data URL 格式。 - 前端文字處理: 取得使用者在
textarea
中輸入的文字內容。 - 後端資料處理與上傳: 將前端取得的錄音 Data URL、檔案 Data URL 和文字內容傳送至 Google Apps Script 後端。後端將 Data URL 解碼為二進位資料,並使用 Google Drive API 將其建立為檔案,儲存至指定的資料夾。 建立以時間戳記為名稱的子資料夾,以組織每次上傳。
- 顯示上傳狀態: 前端即時顯示上傳狀態和訊息,讓使用者了解目前進度。
步驟實現
1. 建立 Google Apps Script 專案
- 前往 Google Drive (drive.google.com)。
- 點擊「新增」>「更多」>「Google Apps Script」。
- 將專案重新命名 (例如:"WebRecorderUploader")。
2. 部署網頁應用程式
在 Apps Script 編輯器中,建立兩個檔案:
Code.gs
(後端程式碼) 和index.html
(前端程式碼)。Code.gs:
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('index');
}
// 建立以時間戳記為名稱的子資料夾
function createSessionFolder() {
var parentFolderId = "YOUR_PARENT_FOLDER_ID"; // 替換成你的 Google Drive 父資料夾 ID
var parentFolder = DriveApp.getFolderById(parentFolderId);
var timestamp = new Date().toISOString();
var sessionFolder = parentFolder.createFolder(timestamp);
return sessionFolder.getId();
}
function uploadAudio(dataUrl, folderId) {
try {
// dataUrl 格式類似 data:audio/webm;base64,xxxx...
var parts = dataUrl.split(',');
if (parts.length !== 2) {
throw new Error('Invalid data URL format.');
}
// 解析 MIME 類型,例如 audio/webm
var contentType = parts[0].match(/:(.*?);/)[1];
var decoded = Utilities.base64Decode(parts[1]);
// 產生 Blob 物件,並以 timestamp 當作檔名
var blob = Utilities.newBlob(decoded, contentType, new Date().toISOString() + ".webm");
var folder = DriveApp.getFolderById(folderId);
var file = folder.createFile(blob);
return 'Audio File uploaded successfully: ' + file.getUrl();
} catch (error) {
return 'Error uploading audio file: ' + error.toString();
}
}
function uploadFile(dataUrl, filename, folderId) {
try {
var parts = dataUrl.split(',');
if (parts.length !== 2) {
throw new Error('Invalid data URL format.');
}
var contentType = parts[0].match(/:(.*?);/)[1];
var decoded = Utilities.base64Decode(parts[1]);
var blob = Utilities.newBlob(decoded, contentType, filename);
var folder = DriveApp.getFolderById(folderId);
var file = folder.createFile(blob);
return 'File uploaded successfully: ' + file.getUrl();
} catch (error) {
return 'Error uploading file: ' + error.toString();
}
}
function uploadText(text, folderId) {
try {
var folder = DriveApp.getFolderById(folderId);
var fileName = new Date().toISOString() + ".txt"
var file = folder.createFile(fileName, text, MimeType.PLAIN_TEXT);
return 'Text uploaded successfully: ' + file.getUrl();
}
catch (error){
return 'Error uploading text file: ' + error.toString();
}
}
index.html:
<!DOCTYPE html>
<html>
<head>
<title>Record, Upload Files & Text to Google Drive</title>
<style>
/* 訊息面板樣式 */
#log {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: #f0f0f0;
max-height: 150px;
overflow-y: auto;
padding: 10px;
font-size: 14px;
border-top: 1px solid #ccc;
}
/* 按鈕樣式 */
button {
padding: 10px 20px;
margin: 5px;
font-size: 16px;
}
/* Drag & drop 區域樣式 */
#drop-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
margin-top: 20px;
}
/* Highlight state for drag & drop */
#drop-area.highlight {
border-color: #333;
background-color: #f9f9f9;
}
/* Text upload 區域樣式 */
#text-upload {
margin-top: 20px;
}
textarea {
width: 100%;
height: 100px;
font-size: 16px;
}
</style>
</head>
<body>
<h1>Record, Upload Files & Text to Google Drive</h1>
<!-- Audio recording buttons -->
<button id="start">Start recording</button>
<button id="stop" disabled>Stop recording</button>
<!-- File upload area -->
<div id="drop-area">
<p>Drag and drop files here</p>
<p>or</p>
<input type="file" id="file-input" multiple style="display: none;">
<button id="select-files">Select Files</button>
</div>
<!-- Text upload area -->
<div id="text-upload">
<textarea id="text-input" placeholder="Enter text here..."></textarea>
<button id="upload-text">Upload Text</button>
</div>
<!-- 狀態訊息面板 -->
<div id="log">
<p>Status Message: </p>
</div>
<script>
let folderId;
// 建立上傳資料夾
google.script.run.withSuccessHandler(
id => {
folderId = id;
logMessage('Session folder created with ID: ' + folderId);
}
).createSessionFolder();
let mediaRecorder;
let recordedChunks = [];
// 在頁面上顯示狀態訊息
function logMessage(message) {
const logDiv = document.getElementById('log');
const p = document.createElement('p');
p.textContent = message;
logDiv.appendChild(p);
logDiv.scrollTop = logDiv.scrollHeight;
}
// Audio recording logic
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = function(e) {
if (e.data && e.data.size > 0) {
recordedChunks.push(e.data);
}
};
mediaRecorder.onstop = function() {
let blob = new Blob(recordedChunks, { type: 'audio/webm' });
recordedChunks = [];
logMessage('Recording stopped. Preparing to upload...');
uploadAudio(blob);
};
document.getElementById('start').onclick = function() {
mediaRecorder.start();
logMessage('Recording started...');
this.disabled = true;
document.getElementById('stop').disabled = false;
};
document.getElementById('stop').onclick = function() {
mediaRecorder.stop();
logMessage('Stopping recording...');
this.disabled = true;
document.getElementById('start').disabled = false;
};
})
.catch(err => {
logMessage('Error accessing microphone: ' + err);
});
} else {
logMessage('getUserMedia not supported.');
}
// 將錄音 Blob 轉成 Data URL 並呼叫伺服器端上傳
function uploadAudio(blob) {
let reader = new FileReader();
reader.onloadend = function() {
let dataUrl = reader.result;
logMessage('Uploading audio...');
google.script.run.withSuccessHandler(logMessage).uploadAudio(dataUrl, folderId);
};
reader.readAsDataURL(blob);
}
// File upload logic
document.getElementById('select-files').addEventListener('click', () => {
document.getElementById('file-input').click();
});
const dropArea = document.getElementById('drop-area');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
dropArea.classList.add('highlight');
}
function unhighlight(e) {
dropArea.classList.remove('highlight');
}
dropArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
let dt = e.dataTransfer;
let files = dt.files;
handleFiles(files);
}
document.getElementById('file-input').addEventListener('change', function(e) {
handleFiles(this.files);
});
function handleFiles(files) {
([...files]).forEach(uploadFile);
}
function uploadFile(file) {
let reader = new FileReader();
reader.onloadend = function() {
let dataUrl = reader.result;
logMessage('Uploading file: ' + file.name);
google.script.run.withSuccessHandler(logMessage).uploadFile(dataUrl, file.name, folderId);
}
reader.readAsDataURL(file);
}
// Text upload logic
document.getElementById('upload-text').addEventListener('click', () => {
const text = document.getElementById('text-input').value;
if (text.trim() !== '') {
logMessage('Uploading text...');
google.script.run.withSuccessHandler(logMessage).uploadText(text, folderId);
} else {
logMessage('Please enter some text to upload.');
}
});
</script>
</body>
</html>
重要: 將
Code.gs
中的"YOUR_PARENT_FOLDER_ID"
替換成你 Google Drive 中想要存放上傳檔案的父資料夾 ID。 (取得資料夾 ID 的方法:開啟該資料夾,網址列中folders/
後面的那一串字元即為資料夾 ID。)點擊「部署」>「新增部署作業」。
- 在「設定」中,選擇「網頁應用程式」。
- 「網頁應用程式」設定:
- 「執行身分」:選擇「我」。
- 「誰可以存取」:選擇「任何人」。
- 點擊「部署」。
- 複製「網頁應用程式」的網址,這就是你的網頁應用程式的網址。
3. 取得使用者授權
- 首次在瀏覽器中開啟網頁應用程式網址時,系統會要求你授權該應用程式存取你的 Google Drive。請按照指示完成授權。
4. 前端錄音與檔案處理 (已在 index.html
程式碼中)
- 錄音: 使用
navigator.mediaDevices.getUserMedia
取得麥克風權限,並建立MediaRecorder
物件。點擊「Start recording」按鈕開始錄音,點擊「Stop recording」按鈕停止錄音。錄音資料會以audio/webm
格式的 Blob 儲存在recordedChunks
陣列中。停止錄音後,會將recordedChunks
中的 Blob 合併成一個 Blob,並呼叫uploadAudio
函式。 - 檔案上傳: 使用者可以透過拖曳檔案到指定區域,或點擊「Select Files」按鈕選擇檔案。
handleFiles
函式會處理選擇的檔案,並對每個檔案呼叫uploadFile
函式。 - Data URL 轉換:
uploadAudio
和uploadFile
函式中使用FileReader
的readAsDataURL
方法將 Blob 或 File 物件轉換為 Data URL。
5. 前端文字處理 (已在 index.html
程式碼中)
- 使用者在
<textarea>
中輸入文字。 - 點擊 "Upload Text" 按鈕後,會取得
textarea
的值,並傳送至uploadText
。
6. 後端資料處理與上傳 (已在 Code.gs
程式碼中)
createSessionFolder
函式: 建立一個以目前時間戳記為名稱的子資料夾,用於存放本次上傳的所有檔案。uploadAudio
函式: 接收前端傳來的 Data URL,將其解碼為二進位資料,建立audio/webm
格式的 Blob 物件,並使用DriveApp.getFolderById(folderId).createFile(blob)
將其上傳到 Google Drive 指定的資料夾。uploadFile
函式: 接收前端傳來的 Data URL 和檔名,將其解碼為二進位資料,建立 Blob 物件,並使用DriveApp.getFolderById(folderId).createFile(blob)
將其上傳到 Google Drive 指定的資料夾。uploadText
函式: 接收前端傳來的文字內容和資料夾 ID,並使用DriveApp.getFolderById(folderId).createFile(filename, text, MimeType.PLAIN_TEXT)
將其上傳到 Google Drive 指定的資料夾,檔案類型為純文字。
7. 顯示上傳狀態 (已在 index.html
程式碼中)
logMessage
函式用於在網頁下方的訊息面板中顯示狀態訊息。- 前端和後端的各個步驟都會呼叫
logMessage
函式,顯示目前的操作狀態 (例如:「Recording started...」、「Uploading audio...」、「Audio File uploaded successfully: ...」)。 - 使用
google.script.run.withSuccessHandler(logMessage)
將後端函式的回傳值 (上傳成功訊息或錯誤訊息) 顯示在前端。
結論
本筆記提供了一個完整的解決方案,讓使用者可以透過簡單的網頁介面,直接進行錄音、上傳多個檔案和文字,並將資料儲存到 Google Drive。此方案基於 Google Apps Script,無需複雜的後端設定,易於部署和使用。
優點:
- 簡單易用: 使用者只需透過網頁介面即可操作,無需安裝任何軟體或插件。
- 無需後端設定: 所有程式碼都在 Google Apps Script 中執行,無需架設伺服器。
- 直接整合 Google Drive: 資料直接儲存到 Google Drive,方便管理和分享。
- 支援多檔案上傳: 可以一次上傳多個檔案,提高效率。
0
Subscribe to my newsletter
Read articles from Aldo Yang directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
