1

need some help :)

my code is supposed to delete the contents of a folder, and then, and only then, take the input from a file selector and have those new files uploaded to the folder that was emptied previously.

the problem:

It looks like the sendFileToDrive is executed somehow on the server side at the same time that the folder gets emptied also on the server side so some files remain in the google drive and some others get deleted by the time the whole file set is uploaded to google drive.

The solution (UPDATED):

As per @TheAddonDepot and also @NaziA's suggestion: I moved my upload code to the function executed with withSuccessHandler so only once the folder is cleared on the server-side, my upload code takes care of the uploading without having any folder clearing happening asynchronously (in paralell) at the same time.

here's my code (updated and working :)):

html

<!DOCTYPE html>
<html>
  <head>
    <!--<base target="_parent"> -->
    <!--Let browser know website is optimized for mobile-->
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <!--Import Google Icon Font-->
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <!-- Compiled and minified CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
  </head>
  <body class = "container">
    <form class = "col s12">
      
      <!-- File Upload Form Component STARTS-->
      <div class = "row">
        <div class="file-field input-field">
          <div class="btn">
            <span>Browse</span>
            <input type="file" accept=".csv" id="fileSelector" multiple> <!-- input for file and attribute to allow multiple upload -->
          </div>  
          <div class="file-path-wrapper">
            <input class="file-path validate" type="text" placeholder="Upload one or more csv files">
          </div> 
          <a class="waves-effect waves-light btn-small" id="upload">Upload Files</a>
        </div>
      </div>
      <!-- File Upload Form Component ENDS-->
      
      <!-- show files uploaded STARTS -->
      <div id="output" style="text-align:center;"></div>
      <!-- show files uploaded ENDS -->

    </form>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/js/materialize.min.js"></script>
    <?!= include("uploadFiles_js"); ?>
  </body>
</html>


my js

//crear a event listerner for upload button to execute a function when clicked
  var uploadButton = document.getElementById('upload');
  uploadButton.addEventListener("click", handleFiles, false);
  
  //function triggered when the file form component is changed
  function handleFiles(){
    try{
      //remove the files in the target folder
      google.script.run.withSuccessHandler(filesRemoved)
                       .withFailureHandler(failOnServerSide)
                       .clearTargetFolder();
    }catch(e){
      alert("error in handleFiles" + e.toString());
    }
  }

  //this function receives the number of files removed 
  //on destination folde prior the upload and attempts the upload
  //once the removal of files is completed
  function filesRemoved (numberOfFilesRemoved){
    try{
      filesRemovedOnDestinationFolder = numberOfFilesRemoved;
      //grab the files from file iterator in to a files list variable
      var fileList = document.getElementById('fileSelector').files;
      //update variable to show files to upload
      filesToUpload = fileList.length;
      //for every file in the filelist...
      for (let i = 0; i < fileList.length; i++){
        //grab the individual file
        var file = fileList[i];
        //if the file has a file name
        if (file.name != ""){
          //save file to Drive
          sendFileToDrive(file);      
        }
      }
    }catch(e){
      alert("error in filesRemoved" + e.toString());
    }
  }

  //function to read and execute for each file to save/upload to google drive
  function sendFileToDrive(file) {
    try{
      //create a reader
      var reader = new FileReader();
      //when upload event occurs...
      reader.onload = function (e) {
        //create an fileObj to upload to gdrive
        //withe 3 object components to create file in gdrive from blob
        //var blob = Utilities.newBlob(fileObj.fileContent, fileObj.fileType, fileObj.fileName);
        const fileObj = {
          fileName: file.name,
          fileType: file.type,
          fileContent: e.target.result //this apparently the same as reader.result
        }
        //send the fileObj to saveFile server side function
        google.script.run.withSuccessHandler(uploadResults)
                         .withFailureHandler(failOnServerSide)
                         .saveFile(fileObj);
      };
      // this reader method seems to work to read the csv files 
      //which is key to get the file content
      reader.readAsBinaryString(file);

    }catch(e){
      alert("error in sendFileToDrive"+ e.toString());
    }
  }

my gs code

function saveFile(fileObj){
  try{
    //Logger.log('fileObj.fileContent: '+ fileObj.fileContent);
    //make a blob out of the file object so a file can be created with it in google drive
    var blob = Utilities.newBlob(fileObj.fileContent, fileObj.fileType, fileObj.fileName);
    //get the folder
    var folder = DriveApp.getFolderById(targetFolderID);
    //Let's create the file, got from the form, within the folder.
    var file = folder.createFile(blob);
    SpreadsheetApp.flush();
    //Let's return the file's url
    return file.getUrl();
    //return "this is the file name received at saveFile in GS: "+ fileName;
  }catch(e){
    Logger.log('e: ' + e.toString());
    return "error in gs saveFile: "+ e.toString();
  }
}

//function that clears the contents of the target folder
//target folder id is defined as a global variable outside function
function clearTargetFolder(){
  //files removed (return variable)
  var filesTrashed = 0;
  //get folder
  var folder = DriveApp.getFolderById(targetFolderID); //targetFolderID global variable defined at top
  //get files iterator from folder
  var filesInFolder = folder.getFiles();
  //for each file in folder...
  while(filesInFolder.hasNext()){
    //get file
    var file = filesInFolder.next();
    //set it to trash
    file.setTrashed(true);
    //update filesTrashed variable
    filesTrashed = filesTrashed + 1;
  }
  //return totals files trashed
  return filesTrashed;
}
Francisco Cortes
  • 1,121
  • 10
  • 19
  • 1
    `google.script.run` is asynchronous, so while it kicks off the process of clearing your target folder, it doesn't wait for that process to complete before moving on to the next line of code in your `try` block. That's why the `withSuccessHandler` function is provided, it allows you to assign a function that will be invoked when the server-side function `clearTargetFolder()` is complete. So you need to leverage the `withSuccessHandler` callback more effectively. – TheAddonDepot Jul 12 '21 at 14:20
  • Thank you, makes sense.. I though of executing the file upload code snippet as it is or thorugh a function from the filesRemoved function so tha that gets executed after the folder clearing is done, the problem now that I have is that I have no access to files (the file selector object) to execute the file upload from the filesRemoved function. any suggestions how to access the file selector files from within the files removed? – Francisco Cortes Jul 12 '21 at 15:21
  • 1
    Are you familiar with Javascript Promises and Async/Await? You can wrap your google.script.run call in a Promise and use Async/Await to achieve synchronous code execution. – TheAddonDepot Jul 12 '21 at 15:38
  • Thank you TAD, will give it a try with those... I did check on that before my posting but the one thing I'm not familiar with is on how to do apply those structures with google.script.run.. that's why I really liked your option to do it as part of the successeventhandler... but that has turned out to be somehow more complicated than expected. – Francisco Cortes Jul 12 '21 at 16:16
  • as it happens sometimes.. something small was missing.. in my case I was missing the '.files' portion at the end of this statement. var fileList = document.getElementById('fileSelector').files;```, that's why I wasn't able to get files from the form component. Thank you @TheAddonDepot for the solution, and patience! – – Francisco Cortes Jul 12 '21 at 20:30

1 Answers1

1

Since Apps Script code was missing, I tried to find a reference and found a similar code by Tanaike that can simplify your issue and adjusted it to do what you would like to achieve which are:

  • Delete the contents of the target folder
  • Print the number of deleted files
  • Save your files in the same folder

index.html:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <input name="file" id="files" type="file" multiple>
    <input type='button' value='Upload' onclick='handleFiles()'>

    <script>
      function handleFiles() {
        google.script.run.withSuccessHandler(getFiles).clearTargetFolder();
      }

      function getFiles(deletedFiles) {
        console.log("deleted files: " + deletedFiles);
        const f = document.getElementById('files');
        // iterate each file
        [...f.files].forEach((file, i) => {
          const fr = new FileReader();
          fr.onload = (e) => {
            const data = e.target.result.split(",");
            const obj = {fileName: f.files[i].name, mimeType: data[0].match(/:(\w.+);/)[1], data: data[1]};
            google.script.run.saveFile(obj);
          }
          fr.readAsDataURL(file);
        });
      }
    </script>
  </body>
</html>

Code.gs

var targetFolderID = 'folderID';

function doGet() {
  return HtmlService.createHtmlOutputFromFile('index');
}

function clearTargetFolder(){
  var filesTrashed = 0;
  var folder = DriveApp.getFolderById(targetFolderID); 
  var filesInFolder = folder.getFiles();
  while(filesInFolder.hasNext()){
    var file = filesInFolder.next();
    file.setTrashed(true);
    filesTrashed = filesTrashed + 1;
  }
  return filesTrashed;
}

function saveFile(obj) {
  var blob = Utilities.newBlob(Utilities.base64Decode(obj.data), obj.mimeType, obj.fileName);
  DriveApp.getFolderById(targetFolderID).createFile(blob);
}

Resource:

NightEye
  • 10,634
  • 2
  • 5
  • 24
  • 1
    Hi and thank you, yes.. I tried this first.. but somehow I can't access the ```files``` variable (the file selector form component). it's just empty somehow.. weirdly, I can access that object no problem from the ```handleFiles```.. simple stuff like that is so frustrating. – Francisco Cortes Jul 12 '21 at 15:37
  • @FranciscoCortes, I see. If it's not an issue to you, can you at least provide the necessary code so I can replicate it on my side? Just make sure to hide private/important details. – NightEye Jul 12 '21 at 15:38
  • @FranciscoCortes, if it would be impossible, see this [answer](https://stackoverflow.com/a/21518470/14606045) which was similar to what TheAddonDepot pointed out. – NightEye Jul 12 '21 at 15:43
  • 1
    I updated my question with all the code, execpt the the html which has only the file selector. Thank you for pointing that answer out, I'm looking into that.. the one thing that it's combersome is that I'm not sure how to apply that when I'm using google.script.run.. but I'm check it out nevertheless. thank you :) – Francisco Cortes Jul 12 '21 at 16:18
  • @FranciscoCortes, I have provided a a different code using some parts of your code above by updating my answer. Does the same thing you want to achieve and do it more efficiently.. Kindly check. – NightEye Jul 12 '21 at 17:15
  • 1
    as it happens sometimes.. something small was missing.. in my case I was missing the '.files' portion at the end of this statement. ```var fileList = document.getElementById('fileSelector').files;```, that's why I wasn't able to get files from the form component. Thank you @NaziA for the solution, code and patience! – Francisco Cortes Jul 12 '21 at 20:28
  • @FranciscoCortes, that's why it returned null, sorry if I forgot to mention it. But if the above code does work for you, I do recommend it. Also, if we answered your question, please click the accept button. By doing so, other people in the community, who may have the same concern as you, will know that theirs can be resolved. If the accept button is unavailable to you, feel free to tell me. [how to accept answer](https://stackoverflow.com/help/accepted-answer) – NightEye Jul 12 '21 at 20:30
  • @FranciscoCortes, please do note that the answer already have `.files` before the `forEach` so you don't need to append the `.files` in the line you edited. If you want to add the `.files` there, remove the `.files` before `forEach` – NightEye Jul 12 '21 at 20:35
  • 1
    Oops.. yeah.. just notice that!.. sorry about that! – Francisco Cortes Jul 12 '21 at 20:37