星期五, 12月 26, 2025

[GAS] 上傳檔案至 Google Drive - Excel to Sheet

在 Google Apps Script 內操作檔案有兩種方式,分別為 DriveApp 和 Drive API,要把 Excel 轉為 Sheet 必須使用 Drive API 才能進行轉換

Drvie API 服務

要使用 Drive API 必須先在服務內加入,該筆記是加入 Drive API V3,該筆記記錄當下 V2 還是存在的




index.html
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <?!= include('css') ?>
</head>

<body>

  <div class="app-container">
      <!-- return false:避免 form onsubmit 預設 redirect 行為 -->
      <form onsubmit="handleFormSubmit(this); return false;">

      <label class="field-label">電子郵件:</label>
      <input type="email" name="userEmail" class="input-field" placeholder="example@mail.com" required>

      <label class="field-label">選擇檔案:</label>
      <!-- 有限定檔案為 xls 和 xlsx -->
      <input type="file" name="myFile" class="input-field"
                  accept=".xls,.xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                  required>

      <button type="submit" class="btn-submit">上傳</button>
    </form>

    <div class="result-message">等待操作</div>
  </div>

  <script>
    function onSuccessHandler(response) {
        const display = document.querySelector('.result-message');
        display.innerText = "【成功】\n" + response;
        display.className = "result-message text-success";
    }

    function onFailureHandler(error) {
        const display = document.querySelector('.result-message');
        display.innerText = "【失敗】\n" + error.message;
        display.className = "result-message text-failure";
    }

    function handleFormSubmit(formObject) {

        let display = document.querySelector('.result-message');
        
        let fileInput = formObject.myFile; 
                
        // 透過 .files 屬性取得檔案
        let file = fileInput.files[0];
        let fileName = file.name.toLowerCase();

        // 檢查副檔名
        if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
            display.innerText = "【格式錯誤】\n檔案格式不正確。\n請只上傳 Excel 檔案 (.xls, .xlsx)";
            display.className = "result-message text-failure";
            return;
        }

        display.innerText = "正在處理檔案,請勿重整頁面";
        display.className = "result-message";

        // 直接傳遞 formObject 
        google.script.run 
          .withSuccessHandler(onSuccessHandler)
          .withFailureHandler(onFailureHandler)
          .processForm(formObject);
      }

  </script>
</body>

</html>
code.gs
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('[GAS] 上傳檔案至 Google Drive - Excel to Sheet');
}

function include(fileName) {
  return HtmlService.createHtmlOutputFromFile(fileName).getContent();
}

function processForm(formObject) {

  try {
    let parentFoldID = getFoldID(); 
    let parentFolder = DriveApp.getFolderById(parentFoldID);

    let timestamp = Utilities.formatDate(new Date(), "GMT+8", "yyyyMMdd_HHmmss");
    let subFolder = parentFolder.createFolder(timestamp);
    let subFolderId = subFolder.getId(); // 取得 ID 供後續 Drive API 使用

    // 儲存 Excel 檔案
    let excelFileBlob = formObject.myFile; 
    subFolder.createFile(excelFileBlob);

    let fileMetaData = {
      name: excelFileBlob.getName(),
      mimeType: MimeType.GOOGLE_SHEETS, // 指定檔案類型
      parents: [subFolderId] // 指定父資料夾,必須是資料夾ID (FoldID) 的陣列格式
    };

    // 透過 Drvie API 把 Excel 轉換為 Sheet
    let sheetFileByAPI = Drive.Files.create(fileMetaData, excelFileBlob);
    
    // 檔案 ID 是相同,單純方便操作檔案,才透過 DriveApp.getFileById() 再轉回 File Class 來使用
    let sheetFileByApp = DriveApp.getFileById(sheetFileByAPI.id);
    let url = sheetFileByApp.getUrl();
    sheetFileByApp.setDescription("透過 DriveApp 設定:上傳者為 " + formObject.userEmail)

    let message = `
      上傳時間:${timestamp}
      Email:${formObject.userEmail}
      上傳檔案 URL:${url}`;

    return message;    

  } catch (error) {
    throw new Error("系統錯誤:" + error.toString());
  }
}
CSS
<style>
  .app-container {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    padding: 30px;
    line-height: 1.6;
  }

  .field-label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold; 
  }

  .input-field {
    width: 280px;
    padding: 8px;
    margin-bottom: 15px;
    border: 1px solid #ccc; 
    border-radius: 4px;
    display: block;
  }

  .btn-submit {
    display: block;
    padding: 8px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .btn-submit:hover {
    background-color: #0056b3;
  }

  .result-message {
    margin-top: 20px;
    padding: 15px; 
    border-left: 5px solid #eee;
    background-color: #f9f9f9;
    white-space: pre-wrap;
  }

  .text-success {
    border-left-color: #28a745;
    color: #155724;
  }

  .text-failure {
    border-left-color: #dc3545;
    color: #721c24;
  }
</style>
執行結果
上傳一個 Excel 和轉換後的 Sheet 並列呈現

星期五, 12月 19, 2025

[GAS] 上傳檔案至 Google Drive

在官方文件 在 HTML 服務:與伺服器函式通訊 - 表單 上有特別提到,當把 html 上的 form 當成參數傳遞至 gs code 內的函數時,form 會變成一個 key-value object,form field names 為 key、form field values 為 value,值會轉成 string,只有 file 會轉成 blob,該筆記紀錄該 GAS 特色功能

index.html
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <?!= include('css') ?>
</head>

<body>

  <div class="app-container">
    <form onsubmit="handleFormSubmit(event)">

      <label class="field-label">電子郵件:</label>
      <input type="email" name="userEmail" class="input-field" placeholder="example@mail.com" required>

      <label class="field-label">選擇檔案:</label>
      <input type="file" name="myFile" class="input-field" required>

      <button type="submit" class="btn-submit">上傳</button>
    </form>

    <div class="result-message">等待操作</div>
  </div>

  <script>
    function onSuccessHandler(response) {
        const display = document.querySelector('.result-message');
        display.innerText = "【成功】\n" + response;
        display.className = "result-message text-success";
    }

    function onFailureHandler(error) {
        const display = document.querySelector('.result-message');
        display.innerText = "【失敗】\n" + error.message;
        display.className = "result-message text-failure";
    }

    function handleFormSubmit(event) {

        // 避免 form onsubmit 預設 redirect 行為
        event.preventDefault(); 

        // 取得 form
        let formObject = event.target;
        
        let display = document.querySelector('.result-message');    
        display.innerText = "正在上傳至 Google Drive";
        display.className = "result-message";

        // 呼叫 gs Code 內的 processForm() 並把 form 當成參數
        google.script.run 
          .withSuccessHandler(onSuccessHandler)
          .withFailureHandler(onFailureHandler)
          .processForm(formObject);
    }

  </script>
</body>

</html>

css.html
<style>
  .app-container {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    padding: 30px;
    line-height: 1.6;
  }

  .field-label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold; 
  }

  .input-field {
    width: 280px;
    padding: 8px;
    margin-bottom: 15px;
    border: 1px solid #ccc; 
    border-radius: 4px;
    display: block;
  }

  .btn-submit {
    display: block;
    padding: 8px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .btn-submit:hover {
    background-color: #0056b3;
  }

  .result-message {
    margin-top: 20px;
    padding: 15px; 
    border-left: 5px solid #eee;
    background-color: #f9f9f9;
    white-space: pre-wrap;
  }

  .text-success {
    border-left-color: #28a745;
    color: #155724;
  }

  .text-failure {
    border-left-color: #dc3545;
    color: #721c24;
  }
</style>

gs code
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('[GAS] 上傳檔案至 Google Drive');
}

function include(fileName) {
  return HtmlService.createHtmlOutputFromFile(fileName).getContent();
}

/**
 * 處理表單提交
 */
function processForm(formObject) {

  try {
    
    let parentFoldID = getFoldID();
    let parentFolder = DriveApp.getFolderById(parentFoldID);

    // 建立時間戳記子資料夾
    let timestamp = Utilities.formatDate(new Date(), "GMT+8", "yyyyMMdd_HHmmss");
    let subFolder = parentFolder.createFolder(timestamp);

    // 儲存檔案 (formObject.myFile 會自動被 GAS 轉為 Blob)
    let fileBlob = formObject.myFile; 
    let file = subFolder.createFile(fileBlob);
    file.setDescription("上傳者:" + formObject.userEmail)

    let message = `
      上傳時間:${timestamp}
      Email:${formObject.userEmail}
      上傳檔案 URL:${file.getUrl()}`;

    return message;

  } catch (error) {

    throw new Error("系統錯誤:" + error.toString());

  }

}

執行結果

星期四, 12月 18, 2025

[GAS] google.script.run

google.script.run 是非同步用戶端 JavaScript API,可呼叫伺服器端 Apps Script 內函式,當呼叫 Apps Script 內函式時並不會直接傳回值,而是透過 withSuccessHandler、withFailureHandler 搭配對應 callback function 來處理回傳值

範例 Code 說明
 
使用 google.script.run 非同步呼叫 .gs Code 內的 processData(),processData() 執行
  • 成功:透過 withSuccessHandler 執行自訂函數 onSuccessHandler
  • 失敗:透過 withFailureHandler 執行自訂函數 onFailureHandler
google.script.run
	.withSuccessHandler(onSuccessHandler)
	.withFailureHandler(onFailureHandler)
	.processData(emailValue);
index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= include('css') ?>
  </head>
  <body>

    <div class="app-container">

      <label class="field-label">電子郵件:</label>

      <input type="email" class="input-email" placeholder="example@mail.com">
      
      <button class="btn-submit" onclick="runUpload()">提交測試</button>
      
      <div class="result-message">等待操作</div>
    </div>

    <script>
      function onSuccessHandler(response) {
        const display = document.querySelector('.result-message');
        display.innerText = "【SuccessHandler 觸發】\n" + response;
        display.className = "result-message text-success";
      }

      // withFailureHandler 回傳值為 Error 物件
      function onFailureHandler(error) {
        const display = document.querySelector('.result-message');
        display.innerText = "【FailureHandler 觸發】\n" + error.message;
        display.className = "result-message text-failure";
      }

      function runUpload() {
        let emailValue = document.querySelector('.input-email').value;
        let display = document.querySelector('.result-message');
        
        display.innerText = "呼叫 .gs Code 中,請稍後";

        // 使用 google.script.run 呼叫 .gs Code 內的 processData()
        google.script.run
          .withSuccessHandler(onSuccessHandler)
          .withFailureHandler(onFailureHandler)
          .processData(emailValue);
      }
    </script>
  </body>
</html>
Code.gs

奇數分鐘回傳成功,偶數分數回傳失敗
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('[GAS] google.script.run');
}

function include(fileName)
{
  return HtmlService.createHtmlOutputFromFile(fileName).getContent();
}

function processData(email) {

  let now = new Date();
  let minutes = now.getMinutes();

  // 奇數分鐘:正常回傳
  if (minutes % 2 !== 0) {
    return "成功!Email 為:" + email;
  }

  // 偶數分鐘:拋出錯誤
  throw new Error("失敗!Email 為:" + email);
}
css
<style>
  .app-container {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    padding: 30px;
    line-height: 1.6;
  }

  .field-label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
  }

  .input-email {
    width: 250px;
    padding: 8px;
    margin-bottom: 15px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  .btn-submit {
    display: block;
    padding: 8px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .btn-submit:hover {
    background-color: #0056b3;
  }

  .result-message {
    margin-top: 20px;
    padding: 15px;
    border-left: 5px solid #eee;
    background-color: #f9f9f9;
    white-space: pre-wrap;
  }

  .text-success {
    border-left-color: #28a745;
    color: #155724;
  }

  .text-failure {
    border-left-color: #dc3545;
    color: #721c24;
  }
</style>
執行結果

星期六, 12月 13, 2025

[GAS] 引用 css、javascript

學習前端開發時,習慣會把 html、css、javascript 檔案拆開來維護,但 Vibe Coding 產生的 Code 都是全部放在一起,該筆記是要記錄如何拆分檔案且能正常執行,基本上是透過 HtmlService.createTemplateFromFile() 來達成該需求

html、css、javascript 全部在一起
.gs Code

使用 HtmlService.createHtmlOutputFromFile 直接呼叫 index.html
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
       .setTitle('Google Apps Script 引用 css、javascript 檔案');     
}
index.html
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <style>
    h2 {
      background-color: gray;
      text-align: left;
      padding: 10px;
    }
  </style>
</head>

<body>

  <h2>Google Apps Script 引用 css、javascript 檔案</h2>

  <button id="btnUpload" onclick="disableButton()">
    上傳檔案
  </button>

  <script>
    function disableButton() {
      const button = document.getElementById('btnUpload');

      if (button) { 
        button.innerText = "上傳處理中,請稍候... (From index.html)";

        button.disabled = true;

        button.style.backgroundColor = '#cccccc';
        button.style.color = '#666666';
        button.style.cursor = 'not-allowed';
      }      

    }
  </script>
</body>

</html>
執行結果

h2 以灰色背景呈現,按鈕按下去文字會變成 [上傳處理中,請稍候... (From index.html)]
拆分檔案

Google Apps Script 內只有 .html 和 .gs 兩種檔案格式,css 和 javascript 內容也是放在 html 檔案格式內,拆分後如下圖

.gs Code

因為 index.html 會呼叫 include() 函數來抓取 css.html 和 js.html 檔案,所以必須使用 HtmlService.createTemplateFromFile() 來呼叫 index.html
function doGet() {
  return HtmlService.createTemplateFromFile('index') // 回傳 HtmlTemplate
    .evaluate() // 把 HtmlTemplate 轉回 HtmlOuput
    .setTitle('Google Apps Script 引用 css、javascript 檔案');       
}

/**
 * index.html 內會透過呼叫 include() 來把 css.html 和 js.html 內容塞進 index.html 
 */
function include(fileName)
{
  return HtmlService.createHtmlOutputFromFile(fileName).getContent();
}
index.html

<?!= include("css") ?> 意思分兩個部分解釋
  • <? 函式 ?>:呼叫 .gs 檔案內的函式,即為 include()
  • !=:不進行 HTML 跳脫 (no escaping),會當成 html 語法的意思
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
  <!-- 載入 css -->
  <?!= include("css") ?>
</head>

<body>

  <h2>Google Apps Script 引用 css、javascript 檔案</h2>

  <button id="btnUpload" onclick="disableButton()">
    上傳檔案
  </button>

  <!-- 載入 Javascript -->
  <?!= include("js") ?>
</body>

</html>
css.html

css 語法必須用 style 標籤包起來
<style>
h2 {
  background-color: green;
  text-align: left;
  padding: 10px;
}
</style>
js.html

javascript 語法必須用 script 標籤包起來
<script>
function disableButton() {
  const button = document.getElementById('btnUpload');

  if (button) { 

    button.innerText = "上傳處理中,請稍候... (From js html)";
    
    button.disabled = true;
    
    button.style.backgroundColor = '#cccccc'; 
    button.style.color = '#666666';
    button.style.cursor = 'not-allowed';
  }      
}
</script>
執行結果

h2 以綠色背景呈現,按鈕按下去文字會變成 [上傳處理中,請稍候... (From js.html)]

星期五, 12月 12, 2025

GodexRT730x-紙張偵測 sensor

公司內有 [一 Row 兩張] 的標籤紙,廠商來公司進行維修時請教對方,這類標籤紙在偵測結束後,按 [出紙鍵] 不會作動,直覺是偵測異常,經對方解釋後才理解,標籤機內 [紙張偵測 sensor] 是可以調整的,sensor 不要剛好置中就可以提高紙張偵測成功度

[一 Row 兩張] 標籤紙,不知道正式名稱該怎麼稱呼它,就隨興取個名字
下圖紅色箭頭為 [紙張偵測 sensor] 可以左右移動,[一 Row 兩張] 正中間剛好是空白,導致偵測成功率降低,往兩邊移動偵測,讓 sensor 可以實際偵測到紙張大小就是