Saving an image to google drive through google app script without using the Drive API

1,181 views
Skip to first unread message

Lofton Gentry

unread,
Jun 13, 2023, 4:45:02 PM6/13/23
to Google Apps Script Community
(This might be irrelevant, but it is important for background purposes)
I have a form that is submitted by users that can contain an image. When this form is submitted, an email is sent out with all of the relevant information from the submitted form, including the image that was attached to the form. The image is attached to the email as an attachment. To get the image as an attachment to the email, when the form is submitted, the image is saved to a google drive folder, and its id is returned. The id is used in a separate function to basically find the image in google drive, and attach it to the email.

(My specific issue)
I am trying to save an image to a google drive folder without using the Drive API (as I'm not allowed to for company reasons). I've determined that the most effective methodology in my case is to use a server-side proxy function to avoid a CORS based error. (Someone please correct me if I'm wrong with how I'm describing this) When a user submits a form on my front end, the image file uploaded is sent to my app script back end, which is then sent to my google drive. The issue I'm encountering is, once again, a CORS-policy error, but this time my error is with my own web app restricting me from sending the data to the back end. I tried to rectify this by setting headers that allow specific information, but it is not working. I'm thinking this is because ```.setHeaders()``` does not work in App Script, or, even though it's "my" web app, it's still being hosted by google, so they'll still restrict it. I will list my front-end and back-end code below. I can provide any further details as necessary.

Front End Code:
```
<script>
  function saveAlert() {
    google.script.run.withSuccessHandler(saveNewAlert).getPayload()
  }
 
  //takes user input from a modal dialog form and sends it to a server-side function named saveAlerts() for processing and storage in a Google Sheet
  async function saveNewAlert(data){
    let out = []
   
    //Setting controls object to each of the html input elements in the form
    let controls = {
      alertSelect: document.getElementById('alertSelect'),
      alertLocation: document.getElementById('alertLocation'),
      alertCode: document.getElementById('alertCode'),
      servicesAffected: document.getElementById('servicesAffected'),
      alertAddress: document.getElementById('alertAddress'),
      alertSubject: document.getElementById('alertSubject'),
      alertBody: tinymce.activeEditor.getContent(),
      alertDate: document.getElementById('alertDate'),
      alertEndDate: document.getElementById('alertEndDate'),
      userEmail: document.getElementById('userEmail'),
      imageUpload: document.getElementById('imageUpload')
    }

    //If-check to make sure form is filled out properly
    if(validateInput(controls)){
      let modalSpinner = document.getElementById('modalSpinner')
      modalSpinner.hidden = false
      let alertDateFinal = controls.alertDate.value.replaceAll('-','/')
      let alertEndDateFinal = controls.alertEndDate.value.replaceAll('-','/')
      alertDate.value === '' ? new Date(alertDateFinal).toLocaleDateString() : new Date(alertDateFinal)
      alertEndDate.value === '' ? new Date(alertEndDateFinal).toLocaleDateString() : new Date(alertEndDateFinal)

      let accessToken = data.accessToken
      webAppUrl = data.webAppUrl
      let imageIds = []
      let formData = new FormData()

      for(let i = 0; i < imageUpload.files.length; i++){
        let formData = new FormData()
        formData.append('imageFile', imageUpload.files[i])

        await fetch(webAppUrl + '?action=uploadImage', {
          method: 'POST',
          mode: 'cors',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Authorization': `Bearer ${accessToken}`
          },
          body: formData
        })
        .then(res => res.text())
        .then(fileId => {
          imageIds.push(fileId)
        })
        .catch(error => {
          console.error('Error saving files', error)
        })
      }

      //Creating new record based on values in the controls object
      let record = {
        'Alert Type': controls.alertSelect.value,
        'Alert Location': controls.alertLocation.value,
        'Alert Code': controls.alertCode.value,
        'Services Affected': controls.servicesAffected.value,
        'Alert Address': controls.alertAddress.value,
        'Alert Subject': controls.alertSubject.value,
        'Alert Details': controls.alertBody,
        'Alert Date': alertDateFinal,
        'Alert End Date': alertEndDateFinal,
        'User Email': controls.userEmail.value,
        'Alert Active': true,
        // 'Images': imageIds
      }

      //Pushing the record to the server side function 'saveAlerts()', which adds the alert created from the form to the google sheet
      out.push(record)      
      google.script.run.withSuccessHandler(clearForm).saveAlerts(out)
     
    } else{
      window.alert('Please Check Input')
    }
  }
</script>
```

Back End Code:
```
function doPost(e){
  if(e.parameter.action === 'uploadImage'){
    let imageFile = e.parameter.imageFile
    let fileId = saveImageToDrive(imageFile)

    let headers = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'POST',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization'
    }

    return ContentService.createTextOutput(fileId).setMimeType(ContentService.MimeType.TEXT).setHeaders(headers)
  }
}
```

Relevant Function ```.getPayload()```
```
function getPayload() {
  const payload = {
    accessToken: ScriptApp.getOAuthToken(),
    webAppUrl: ScriptApp.getService().getUrl()
  }

  return payload
}
```
This function returns a token to allow access to google APIs (I believe I need it here, but am unsure), and the web app URL. I return the web app URL as well because when testing, the URL changes, so I just capture the current URL and use that instead of setting a pre-defined one. 

cbmserv...@gmail.com

unread,
Jun 13, 2023, 4:52:59 PM6/13/23
to google-apps-sc...@googlegroups.com

Can you provide the code for this function:

 

saveImageToDrive(imageFile)

 

Not sure how you are saving the file without using the Drive API..

This e-mail and any materials contained in or accompanying it may be confidential, subject to legal privilege, or otherwise protected from disclosure, and is intended solely for the intended recipient(s). If you have received this communication in error, please notify the sender immediately, delete all electronic or hard copies in your possession and do not disclose, copy, disseminate, distribute or use any information herein. Neither Ocean Network Express (North America) Inc. nor any of its related corporations or affiliates make any warranties in relation to this e-mail or any materials contained in or accompanying it. We reserve the right to monitor all email communications through our networks.

 

Our Privacy Notice has changed. Please visit https://www.one-line.com/standard-page/privacy-policy to view the updated notice.

 

--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/google-apps-script-community/442ea050-fdfd-4266-98c6-0c76c8f0ac58n%40googlegroups.com.

Lofton Gentry

unread,
Jun 14, 2023, 8:32:03 AM6/14/23
to Google Apps Script Community
Apolgies, I thought I had it, but this is the saveImageToFile function:
function saveImageToDrive(imageFile) {
  var fileName = imageFile.getName(); // Assuming imageFile is a File object
 
  var folder = DriveApp.getFolderById(imgFolderID);
  var newFile = folder.createFile(imageFile);

  return newFile.getId(); // Return the ID of the saved file
}

DimuDesigns

unread,
Jun 14, 2023, 8:48:49 AM6/14/23
to Google Apps Script Community
Doesn't the built-in and advanced services (DriveApp and Drive) both use the Drive API under the hood? 
Message has been deleted

Lofton Gentry

unread,
Jun 14, 2023, 9:48:08 AM6/14/23
to Google Apps Script Community
That is correct. I was trying to replicate a method that a co-worker used that allowed them to not have to enable the google cloud console Drive API, but the API endpoint may have been changed since he did that as the method is very old that he used. The specific method is this one:
function doUpload(data,fr) {
  var fileName = fr.fileName
  var fileSize = fr.fileSize
  var fileType = fr.fileType
  var folderId = data.id
  var auth = data.token
  var xhr = new XMLHttpRequest()
  xhr.setRequestHeader('Authorization', "Bearer " + auth)
  xhr.setRequestHeader('Content-Type', "application/json")
  xhr.setRequestHeader('X-Upload-Content-Type', fileType)
  xhr.setRequestHeader('X-Upload-Content-Length', fileSize)
  xhr.onload = () => {
    var location = xhr.getResponseHeader("location")
    var upr = new XMLHttpRequest()
    fr.progel.hidden = false
    upr.upload.onprogress = (e) => {
      var stat = (e.loaded/e.total) * 100
      fr.progel.firstElementChild.style.width = stat + '%'
    }
    upr.open("PUT", location)
    upr.send(fr.result)
    upr.onload = () => {
      fr.spinel.hidden = true
      fr.progel.hidden = true
    }
  }
  xhr.send(JSON.stringify({parents: [folderId], name: fileName}))
}

cwl...@gmail.com

unread,
Jun 14, 2023, 9:56:20 AM6/14/23
to Google Apps Script Community
That will definitely not work since the XML services have completely changed. That will have to be rewritten using urlfetch. 

Also, you may be able to hack together your form using the form API since it has a file upload type.  My thought is you can build the google form to match your html version, then inspect the html of the google form to get the form item ids you need. There is a video out there somewhere talking about this. I'll try to find it.

Lofton Gentry

unread,
Jun 14, 2023, 10:07:12 AM6/14/23
to Google Apps Script Community
That would make sense, if the XML service has changed, as I've been bashing my head against it as well as the fetch version I've originally used. I was told that the token I generate on the backend that is sent to the front end is properly scoped for Drive API, as on my script, under OAuth scopes, I can read, write, etc. to google drive., but I'm still encountering CORS issues. The main issue right now is just CORS policy blocking me from accessing Drive, and I'm not sure why.

Lofton Gentry

unread,
Jun 14, 2023, 12:00:34 PM6/14/23
to Google Apps Script Community
This is now for anyone whose looking for a solution, but since this is a web app made exclusively through google app script, I may have been overcomplicating it. Basically you can remove the fetch and replace the function with this (apologies that it's not formatted properly):

let files = imageUpload.files

for (var i = 0; i < files.length; i++) {
let file = files[i];
let reader = new FileReader();
reader.onload = (function(file) {
return function(e) {
let dataUrl = e.target.result;
google.script.run.withSuccessHandler(function() {
alert("Image uploaded successfully!");
}).uploadImage(file.name, dataUrl);
};
})(file);
reader.readAsDataURL(file);
}

And now the back end portion of the code:
function uploadImage(fileName, dataURL){
let folder = DriveApp.getFolderById(imgFolderID)
let blob = Utilities.newBlob(dataURL, 'image/jpeg', fileName)
let file = folder.createFile(blob)
return file.getUrl()
}

cwl...@gmail.com

unread,
Jun 14, 2023, 1:08:33 PM6/14/23
to Google Apps Script Community
If you are interested, here is the hack I wrote about. https://www.youtube.com/watch?v=Q9BHCv2x_Pg
Reply all
Reply to author
Forward
0 new messages