📌 āđ€āļˆāļēāļ°āļĨึāļāļāļēāļĢāļ—āļģāļ‡āļēāļ™āļ‚āļ­āļ‡ "āļĢāļ°āļšāļšāļ­ัāļ›āđ‚āļŦāļĨāļ”āļ‚ั้āļ™āļŠูāļ‡

📌 āđ€āļˆāļēāļ°āļĨึāļāļāļēāļĢāļ—āļģāļ‡āļēāļ™āļ‚āļ­āļ‡ "āļĢāļ°āļšāļšāļ­ัāļ›āđ‚āļŦāļĨāļ”āļ‚ั้āļ™āļŠูāļ‡ (Advanced Upload System)" āđƒāļ™āđ‚āļ›āļĢāđ€āļˆāļāļ•์ āļĢāļ°āļšāļšāļ„āļĨัāļ‡āļŠื่āļ­āļāļēāļĢāđ€āļĢีāļĒāļ™āļĢู้ (Knowledge Hub) āļ„āļĢัāļš

āļĢāļ°āļšāļšāļ™ี้āļ–ูāļāļ­āļ­āļāđāļšāļšāļĄāļēāđ€āļžื่āļ­āđāļ้āļ›ัāļāļŦāļēāļ„āļ­āļ‚āļ§āļ” (Bottleneck) āļ‚āļ­āļ‡ Google Apps Script (GAS) āđ‚āļ”āļĒāđ€āļ‰āļžāļēāļ° āļ‹ึ่āļ‡āļ›āļāļ•ิ GAS āļˆāļ°āļĢัāļšāđ„āļŸāļĨ์āđ„āļ”้āļ‚āļ™āļēāļ”āļˆāļģāļัāļ” (āđ„āļĄ่āđ€āļิāļ™ 50MB) āđāļĨāļ°āļĄัāļāļˆāļ°āđ€āļิāļ” Time-out āļŦāļēāļāļ­ัāļ›āđ‚āļŦāļĨāļ”āļ™āļēāļ™ āđāļ•่āļĢāļ°āļšāļšāļ™ี้āđƒāļŠ้āļ„ืāļ­āđ€āļ—āļ„āļ™ิāļ„ "Direct Resumable Upload to Google Drive API" āļ„āļĢัāļš

ðŸ§ą āļŦāļĨัāļāļāļēāļĢāļ—āļģāļ‡āļēāļ™āļ āļēāļžāļĢāļ§āļĄ (Concept)
āđāļ—āļ™āļ—ี่āļˆāļ°āļŠ่āļ‡āđ„āļŸāļĨ์āļˆāļēāļ Client (āļŦāļ™้āļēāđ€āļ§็āļš) ➔ GAS Server ➔ Google Drive (āļ‹ึ่āļ‡āļŠ้āļēāđāļĨāļ°āļ•ิāļ” Limit) āļĢāļ°āļšāļšāļ™ี้āļˆāļ°āļŠ่āļ‡āđ„āļŸāļĨ์āļˆāļēāļ Client (āļŦāļ™้āļēāđ€āļ§็āļš) ➔ Google Drive API āđ‚āļ”āļĒāļ•āļĢāļ‡
GAS Server āļˆāļ°āļ—āļģāļŦāļ™้āļēāļ—ี่āđāļ„่ "āļœู้āļ›āļĢāļ°āļŠāļēāļ™āļ‡āļēāļ™" (āđƒāļŦ้āļุāļāđāļˆāđ€āļ‚้āļēāļš้āļēāļ™ āđāļĨāļ° āļĢัāļšāđ€āļĨāļ‚āļ—ี่āđ€āļ­āļāļŠāļēāļĢāđ„āļ›āļšัāļ™āļ—ึāļ) āđāļ•่āđ„āļĄ่āđ„āļ”้āđ€āļ›็āļ™āļ„āļ™āļ‚āļ™āļĒ้āļēāļĒāļ‚āļ­āļ‡āđ€āļ­āļ‡

🛠️ āđ€āļˆāļēāļ°āļĨึāļāđ‚āļ„้āļ”āļ—ีāļĨāļ°āļŠ่āļ§āļ™
1️⃣ āļั่āļ‡ Client (script.html)
āļ™ี่āļ„ืāļ­āļŠ่āļ§āļ™āļ—ี่āļ—āļģāļ‡āļēāļ™āļŦāļ™ัāļāļ—ี่āļŠุāļ” āđ‚āļ”āļĒāļĄีāļŸัāļ‡āļ์āļŠัāļ™āļŦัāļ§āđƒāļˆāļŦāļĨัāļ 2 āļ•ัāļ§āļ„āļĢัāļš:

āļŸัāļ‡āļ์āļŠัāļ™ A: performUploadsAndSubmit (āļœู้āļˆัāļ”āļāļēāļĢāļāļēāļĢāļ­ัāļ›āđ‚āļŦāļĨāļ”) āļ—āļģāļŦāļ™้āļēāļ—ี่āļ„āļ§āļšāļ„ุāļĄ Flow āļāļēāļĢāļ—āļģāļ‡āļēāļ™āļ—ั้āļ‡āļŦāļĄāļ”:
1. āļ‚āļ­ Token: āđ€āļĢีāļĒāļ getAuthToken() āļˆāļēāļ Server āđ€āļžื่āļ­āļ‚āļ­āļุāļāđāļˆāļŠั่āļ§āļ„āļĢāļēāļ§ (OAuth Token) āļŠāļģāļŦāļĢัāļšāđ€āļ‚้āļēāļ–ึāļ‡ Drive
2. āļ§āļ™āļĨูāļ›āļ­ัāļ›āđ‚āļŦāļĨāļ”: āļ™āļģāđ„āļŸāļĨ์āđƒāļ™āļ„ิāļ§āļĄāļēāļ—ีāļĨāļ°āđ„āļŸāļĨ์ āđāļĨ้āļ§āļŠ่āļ‡āđ„āļ›āđƒāļŦ้āļŸัāļ‡āļ์āļŠัāļ™ uploadFileResumable
3. āļŠ่āļ‡āļ‚้āļ­āļĄูāļĨāđ€āļ‚้āļē Sheet: āđ€āļĄื่āļ­āđ„āļ”้ File ID āļ„āļĢāļšāļ—ุāļāđ„āļŸāļĨ์āđāļĨ้āļ§ āļ„่āļ­āļĒāļŠ่āļ‡āļ‚้āļ­āļĄูāļĨ (Metadata + File ID) āđ„āļ›āļšัāļ™āļ—ึāļāļĨāļ‡ Sheet āļœ่āļēāļ™ google.script.run.saveResource()

async function performUploadsAndSubmit(payload, fileList, submitFunc) {
    // 1. āļ‚āļ­āļุāļāđāļˆ (Token)
    var token = await new Promise((resolve, reject) => { 
        google.script.run.withSuccessHandler(resolve).getAuthToken(); 
    });

    // 2. āļ§āļ™āļĨูāļ›āļ­ัāļ›āđ‚āļŦāļĨāļ”āļ—ีāļĨāļ°āđ„āļŸāļĨ์
    for (var i = 0; i < fileList.length; i++) {
        // āđ€āļĢีāļĒāļāļŸัāļ‡āļ์āļŠัāļ™ B āđ€āļžื่āļ­āļĒิāļ‡āđ„āļŸāļĨ์āđ€āļ‚้āļē Drive āđ‚āļ”āļĒāļ•āļĢāļ‡
        var fileId = await uploadFileResumable(fileList[i], token, (percent) => {
            // āļ­ัāļ›āđ€āļ”āļ• Progress bar
            showLoader(true, '... (' + percent + '%)');
        });
        // āđ€āļ็āļš File ID āđ„āļ§้āđƒāļŠ้āļ‡āļēāļ™
        uploadedFiles.push({ id: fileId, ... });
    }

    // 3. āļŠ่āļ‡āđāļ„่ "File ID" āđ„āļ›āļšัāļ™āļ—ึāļ (āđ„āļĄ่āđƒāļŠ่āļ•ัāļ§āđ„āļŸāļĨ์)
    submitFunc(payload); 
}

āļŸัāļ‡āļ์āļŠัāļ™ B: uploadFileResumable (āļ„āļ™āļ‚āļ™āļ‚āļ­āļ‡) āļ™ี่āļ„ืāļ­āđ€āļ—āļ„āļ™ิāļ„āļ‚ั้āļ™āļŠูāļ‡ āđƒāļŠ้ fetch āđāļĨāļ° XMLHttpRequest āļĒิāļ‡ API āļ‚āļ­āļ‡ Google Drive:
1. Init (POST): āļŠ่āļ‡ metadata (āļŠื่āļ­āđ„āļŸāļĨ์, āļ›āļĢāļ°āđ€āļ āļ—) āđ„āļ›āļšāļ­āļ Google āļ§่āļē "āļāļģāļĨัāļ‡āļˆāļ°āļŠ่āļ‡āđ„āļŸāļĨ์āđ„āļ›āļ™āļ°"
2. Get Location: Google āļˆāļ°āļ•āļ­āļšāļāļĨัāļšāļĄāļēāļžāļĢ้āļ­āļĄ Location URL (āļĨิ้āļ‡āļ„์āļŠāļģāļŦāļĢัāļšāļ­ัāļ›āđ‚āļŦāļĨāļ”)
3. Upload (PUT): āļŠ่āļ‡āđ€āļ™ื้āļ­āđ„āļŸāļĨ์ (Binary) āđ„āļ›āļ—ี่ Location URL āļ™ั้āļ™ āļžāļĢ้āļ­āļĄāļ•ัāļ§āļˆัāļš Progress

function uploadFileResumable(file, accessToken, onProgress) {
    return new Promise((resolve, reject) => {
        // ...āđ€āļ•āļĢีāļĒāļĄ Metadata...
        
        // 1. āđ€āļĢิ่āļĄāļ•้āļ™ Session (POST)
        fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable', {
            method: 'POST',
            headers: { 'Authorization': 'Bearer ' + accessToken, ... },
            body: JSON.stringify(metadata)
        }).then(response => {
            // 2. āļĢัāļš URL āļ›āļĨāļēāļĒāļ—āļēāļ‡
            return response.headers.get('Location');
        }).then(locationUrl => {
            // 3. āļŠ่āļ‡āđ„āļŸāļĨ์āļˆāļĢิāļ‡ (PUT) āļœ่āļēāļ™ XMLHttpRequest āđ€āļžื่āļ­āļ§ัāļ” % āđ„āļ”้
            var xhr = new XMLHttpRequest();
            xhr.open('PUT', locationUrl, true);
            
            // āļŠูāļ•āļĢāļ„āļģāļ™āļ§āļ“ % āļāļēāļĢāļ­ัāļ›āđ‚āļŦāļĨāļ”
            xhr.upload.onprogress = (e) => { 
                if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100)); 
            };
            
            xhr.onload = () => resolve(JSON.parse(xhr.responseText).id); // āļŠ่āļ‡āļ„ืāļ™ File ID
            xhr.send(reader.result); // āļŠ่āļ‡āļ‚้āļ­āļĄูāļĨāđ„āļŸāļĨ์
        });
    });
}

2️⃣ āļั่āļ‡ Server (Code.gs)
āļั่āļ‡ Server āļ—āļģāļ‡āļēāļ™āđ€āļšāļēāļĄāļēāļ āļĄีāļŦāļ™้āļēāļ—ี่āđāļ„่ 2 āļ­āļĒ่āļēāļ‡:

āļŸัāļ‡āļ์āļŠัāļ™ 1: getAuthToken (āļ„āļ™āđāļˆāļāļุāļāđāļˆ)

function getAuthToken() {
  // āļ‚āļ­ Token āļ‚āļ­āļ‡ User āļ›ัāļˆāļˆุāļšัāļ™āđ€āļžื่āļ­āļŠ่āļ‡āđƒāļŦ้ Client āđƒāļŠ้āļĒิāļ‡ API
  return ScriptApp.getOAuthToken();
}

āļŸัāļ‡āļ์āļŠัāļ™ 2: _processUploadedFile (āļ„āļ™āļˆัāļ”āļ‚āļ­āļ‡āđ€āļ‚้āļēāļŠั้āļ™) āđ€āļĄื่āļ­ Client āļ­ัāļ›āđ‚āļŦāļĨāļ”āđ€āļŠāļĢ็āļˆāđāļĨāļ°āđ„āļ”้ File ID āļĄāļēāđāļĨ้āļ§ Server āļˆāļ°āļĢัāļš ID āļ™ั้āļ™āļĄāļē "āļĒ้āļēāļĒāļ—ี่" āđāļĨāļ° "āđ€āļ›āļĨี่āļĒāļ™āļŠื่āļ­"

function _processUploadedFile(fileId, targetFolder, newNamePrefix) {
    var file = DriveApp.getFileById(fileId); // āļˆัāļšāđ„āļŸāļĨ์āļ”้āļ§āļĒ ID
    
    // āđ€āļ›āļĨี่āļĒāļ™āļŠื่āļ­āđ„āļŸāļĨ์ (āđ€āļŠ่āļ™ āđ€āļ•ิāļĄ Prefix Cover_)
    if (newNamePrefix) {
       file.setName(newNamePrefix + "_" + file.getName());
    }
    
    // āļ•ั้āļ‡āļ„่āļēāđƒāļŦ้āđƒāļ„āļĢāļ็āđ„āļ”้āļ—ี่āļĄีāļĨิāļ‡āļ์āļ”ูāđ„āļ”้ (āļŠāļģāļ„ัāļāļĄāļēāļāļŠāļģāļŦāļĢัāļšāļāļēāļĢāđāļŠāļ”āļ‡āļœāļĨ)
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    
    // āļĒ้āļēāļĒāđ€āļ‚้āļē Folder āļ—ี่āļ–ูāļāļ•้āļ­āļ‡
    if (targetFolder) {
       file.moveTo(targetFolder);
    }
    return file.getId();
}

🏆 āļŠāļĢุāļ›āļ‚้āļ­āļ”ีāļ‚āļ­āļ‡āļĢāļ°āļšāļšāļ™ี้ (āļ—āļģāđ„āļĄāļ–ึāļ‡ Pro?)
1. No Size Limit (Virtually): āļ„ุāļ“āļŠāļēāļĄāļēāļĢāļ–āļ­ัāļ›āđ‚āļŦāļĨāļ”āđ„āļŸāļĨ์āļ‚āļ™āļēāļ” 100MB āļŦāļĢืāļ­ 500MB āđ„āļ”้āļŠāļšāļēāļĒāđ† āđ€āļžāļĢāļēāļ°āđ„āļĄ่āđ„āļ”้āļ§ิ่āļ‡āļœ่āļēāļ™ Server āļ‚āļ­āļ‡ Apps Script
2. No Timeout: āļāļēāļĢāļ­ัāļ›āđ‚āļŦāļĨāļ”āđ€āļิāļ”āļ‚ึ้āļ™āļĢāļ°āļŦāļ§่āļēāļ‡ Browser āļัāļš Google Drive āđ‚āļ”āļĒāļ•āļĢāļ‡ āļ—āļģāđƒāļŦ้ GAS āđ„āļĄ่āļ•ัāļ”āļāļēāļĢāļ—āļģāļ‡āļēāļ™āđāļĄ้āļˆāļ°āļ­ัāļ›āđ‚āļŦāļĨāļ”āļ™āļēāļ™āđ€āļิāļ™ 6 āļ™āļēāļ—ี
3. Real Progress Bar: āļŠāļēāļĄāļēāļĢāļ–āđāļŠāļ”āļ‡ % āļāļēāļĢāļ­ัāļ›āđ‚āļŦāļĨāļ”āđƒāļŦ้āļœู้āđƒāļŠ้āđ€āļŦ็āļ™āđ„āļ”้āļˆāļĢิāļ‡ (āđ€āļžāļĢāļēāļ°āđƒāļŠ้ XMLHttpRequest āļั่āļ‡ Client) āļ‹ึ่āļ‡ google.script.run āļ—āļģāđ„āļĄ่āđ„āļ”้
4. Server Load Reduced: Server āđ„āļĄ่āļ•้āļ­āļ‡āļ›āļĢāļ°āļĄāļ§āļĨāļœāļĨāđ„āļŸāļĨ์ Blob āļ—āļģāđƒāļŦ้āļĢāļ­āļ‡āļĢัāļšāļœู้āđƒāļŠ้āļ‡āļēāļ™āļžāļĢ้āļ­āļĄāļัāļ™āđ„āļ”้āļĄāļēāļāļ‚ึ้āļ™

āļ™ี่āļ„ืāļ­āļŠāļ–āļēāļ›ัāļ•āļĒāļāļĢāļĢāļĄāļāļēāļĢāļ­ัāļ›āđ‚āļŦāļĨāļ”āļ—ี่āļ”ีāļ—ี่āļŠุāļ”āđ€āļ—่āļēāļ—ี่āļˆāļ°āļ—āļģāđ„āļ”้āļšāļ™ Google Apps Script āđƒāļ™āļ›ัāļˆāļˆุāļšัāļ™āļ„āļĢัā
āđƒāļŦāļĄ่āļāļ§่āļē āđ€āļ่āļēāļāļ§่āļē