Search code examples
google-apps-scriptfile-uploadweb-applications

Uploading binary files broken since yesterday, workaround?


Update: I created an issue: https://issuetracker.google.com/issues/150675170

My web app worked well for a long time, but now suddenly the uploaded pdf files become corrupted. Below is a small reduced example that can be used to reproduce the issue.

Looking at the uploaded file content, it looks like the file content is treated as text, and several characters are replaced with EF BF BD, which is the UTF-8 byte sequence for 'REPLACEMENT CHARACTER' (U+FFFD).

For example, first bytes of the original PDF file:

25 50 44 46 2D 31 2E 34 0A 25 E2 E3 CF D3 0A 31 39 | %PDF-1.4\n%âãÏÓ\n19

It got uploaded as:

25 50 44 46 2D 31 2E 34 0A 25 EF BF BD EF BF BD EF BF BD EF BF BD 0A 31 39 | %PDF-1.4\n%����\n19

I'm not sure where to report it, I only hope that a Google employee will see it and fix it.

In the meanwhile, perhaps somebody familiar with Google Apps Script has an idea for a workaround.

Below is the small reduced example - deploy, upload a binary file, go to Drive, find it under the "test" folder, download it, observe that it's broken.

HTML template, file name test_form.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>

<form id="test-form">
    <input type="file" id="test-file" name="test-file">
    <button id="submit-button" type="submit">Upload</button>
</form>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
    (function () {
        'use strict';

        $('#test-form').submit(function (e) {
            e.preventDefault(); // prevent form from submitting

            google.script.run
                .withFailureHandler(fileUploadedFailure)
                .withSuccessHandler(fileUploaded)
                .uploadFilesFrame(this);
        });

        function fileUploaded(status) {
            alert(status);
        }

        function fileUploadedFailure(error) {
            alert('Failed: ' + error.message);
        }
    })();
</script>
</body>
</html>

Server code, file name Code.gs:

var rootFolderName = "test";

function doGet(e) {
  var template = HtmlService.createTemplateFromFile('test_form');
  return template.evaluate().setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function uploadFilesFrame(form) {
  try {
    var fileBlob = form['test-file'];

    var rootFolder = DriveApp.getFoldersByName(rootFolderName);
    if (rootFolder.hasNext()) {
      rootFolder = rootFolder.next();
    } else {
      rootFolder = DriveApp.createFolder(rootFolderName);
    }

    var file = rootFolder.createFile(fileBlob);

    return JSON.stringify({"status": 'ok', "msg": file.getId()});
  } catch (error) {
    return JSON.stringify({"status": 'error', "data": error.stack});
  }
}


Solution

  • Update (Nov 2021): The issue has been resolved: https://issuetracker.google.com/issues/155109626


    One workaround: base64-encode it on the client side, then base64-decode it on the server. Then the contents is not screwed up. Here's an example:

    HTML template, file name test_form.html:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
    
    <form id="test-form">
        <input type="file" id="test-file" name="test-file">
        <input type="hidden" id="test-file2" name="test-file2">
        <input type="hidden" id="test-file-name" name="test-file-name">
        <button id="submit-button" type="submit">Upload</button>
    </form>
    
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script>
        (function () {
            'use strict';
    
            $('#test-form').submit(function (e) {
                var thisForm = this;
                e.preventDefault(); // prevent form from submitting
    
                var reader = new FileReader();
                reader.onload = function (event) {
                    var result = event.target.result;
                    var base64 = result.substr(result.indexOf(',') + 1);
                    $('#test-file2').val(base64);
                    
                    var filename = $('#test-file').val().split('\\').pop();
                    $('#test-file-name').val(filename);
                    
                    $('#test-file').prop('disabled', true);
                    
                    google.script.run
                        .withFailureHandler(fileUploadedFailure)
                        .withSuccessHandler(fileUploaded)
                        .uploadFilesFrame(thisForm);
                };
                reader.onerror = function (event) {
                    alert("ERROR: " + event.target.error.code);
                };
                reader.readAsDataURL(document.getElementById('test-file').files[0]);
            });
    
            function fileUploaded(status) {
                alert(status);
            }
    
            function fileUploadedFailure(error) {
                alert('Failed: ' + error.message);
            }
        })();
    </script>
    </body>
    </html>
    

    Server code, file name Code.gs:

    var rootFolderName = "test";
    
    function doGet(e) {
      var template = HtmlService.createTemplateFromFile('test_form');
      return template.evaluate().setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
    }
    
    function uploadFilesFrame(form) {
      try {
        var fileBlob = Utilities.newBlob(Utilities.base64Decode(form['test-file2']), 'application/octet-stream', form['test-file-name']);
    
        var rootFolder = DriveApp.getFoldersByName(rootFolderName);
        if (rootFolder.hasNext()) {
          rootFolder = rootFolder.next();
        } else {
          rootFolder = DriveApp.createFolder(rootFolderName);
        }
    
        var file = rootFolder.createFile(fileBlob);
    
        return JSON.stringify({"status": 'ok', "msg": file.getId()});
      } catch (error) {
        return JSON.stringify({"status": 'error', "data": error.stack});
      }
    }