nkink
nkink

Reputation: 118

Can I modify HtmlService Output using Google Apps Script?

I made an web app which copies folder and files of google drive. My goal is to show the name of the file and folder which is being copied on the web page('messages' id). Is it possible to change the web page from the google apps script?

When I open the web app, the GAS shows an output page using a form.html, and when I push the copy button, it will start start() function and begin copying the folders. After the successful process, the successHandler will call onSuccess() and it will change the innerHTML to Success.

I'd like to change the innerHTML during the copy process according to the folders' and files' name, but I don't know how to change it from the GAS function start() or copyFolder().

Thank you.

    function doGet(){
      return HtmlService.createHtmlOutputFromFile('form');
    }
    
    function start() {
    
      var sourceFolderId = "FOLDERID";
      var targetFolder = "TARGET_FOLDER_NAME";
      var source = DriveApp.getFolderById(sourceFolderId);
      var target = DriveApp.createFolder(targetFolder);

      copyFolder(source, target);

    }
    
    function copyFolder(source, target) {
    
      var folders = source.getFolders();
      var files   = source.getFiles();
    
      while(files.hasNext()) {
        var file = files.next();
        file.makeCopy(file.getName(), target);
      }
    
      while(folders.hasNext()) {
        var subFolder = folders.next();
        var folderName = subFolder.getName();
        var targetFolder = target.createFolder(folderName);
        copyFolder(subFolder, targetFolder);
      }
    
    }
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script>
      function callFolderDownload() {
        document.getElementById('messages').innerHTML = 'Copying...';
        google.script.run
          .withSuccessHandler(onSuccess)
          .withFailureHandler(onFailure)
          .start();
      }
      
    function onSuccess() {
      document.getElementById('messages').innerHTML = 'Success';
    }
    
    function onFailure(error) 
    {
      document.getElementById('messages').innerHTML = error.message;
    }
    
    </script>
  </head>
  <body>
    <div id="messages">Click the button to copy the folder.
    </div>
    <div>
      <button type="button" onclick='callFolderDownload();' id="download">Copy</button>
    </div>
  </body>
</html>

Upvotes: 0

Views: 562

Answers (1)

iansedano
iansedano

Reputation: 6481

You would need to break up your execution into various parts

You can't "stream" updates from the server process of copying a folder.

At the expense of making your execution slower, you can break it up into various phases, and at the end of each phase, update the client side with status. Remember that for every different stage you are having to:

- Make a call to Apps Script from client.
- Apps Script makes a separate call to Drive.
- Drive responds to Apps Script.
- Apps Script responds to client.

To get a message for each file copied, this needs to happen for each file. Depending on how many files you need to copy, this may not work. In that case, you would probably have to do it in batches. That is, maybe when you get the list of files back from the server, you can send 10 to the server to copy per request.

enter image description here

The key is that there has to be a separate call and response from server for each "real time update". These calls and responses need to be asynchronously programmed, that is, each stage needs to wait for previous stages to complete. This can result in the "pyramid of doom" which can be quite ugly and hard to understand:

google.script.run
  .withSuccessHandler((targetId) => {
    google.script.run
      .withSuccessHandler((list) => {
        newMessage("Starting to copy...");
        list.forEach((item) => {
          google.script.run
            .withSuccessHandler(() => {
              newMessage(item.name + " has been copied");
            })
            .copyItem(item.id, targetId);
        });
      })
      .getFolderItems();
  })
  .createTargetFolder(targetName);

This can be made better with promises or the async await syntax, but in the interest of not making this answer too long, I have omitted it.

Below is an example of it working:

HTML

<!DOCTYPE html>
<html>

<head>
    <base target="_top">
    <script>
    
    // This is a utility function to append a new message to the html
    function newMessage(message) {
      let messageElement = document.getElementById("messages")
      let sourceDiv = document.createElement("div")
      sourceDiv.innerText = message
      messageElement.append(sourceDiv)
    }

    function copyFolder() {

      newMessage("Preparing to copy...")

      let targetName = document.getElementById("newFolderName").value
      console.log(targetName)

      google.script.run
      .withSuccessHandler(targetId => {
          google.script.run
          .withSuccessHandler(list => {
            newMessage("Starting to copy...")
            list.forEach(item => {
              google.script.run
              .withSuccessHandler(() => {
                newMessage(item.name + " has been copied")
              })
              .copyItem(item.id, targetId)
            })
          })
          .getFolderItems()
        })
      .createTargetFolder(targetName)
    }

    </script>
</head>

<body>
    <div>
        <label for="newFolderName">Type in Name of new folder that contents will be copied to</label>
        <input id="newFolderName" type="text">
        <button type="button" onclick='copyFolder();' id="download">Copy</button>
    </div>
    <div id="messages">
    -
    </div>
</body>

</html>

Apps Script

class FileToCopy {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
}

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

function createTargetFolder(name){
  Logger.log(name)
  var target = DriveApp.createFolder(name);
  return target.getId()
}

function getFolderItems() {
  var sourceFolderId = "[SOURCE_FOLDER_ID]";
  var source = DriveApp.getFolderById(sourceFolderId);
  var files = source.getFiles();
  let output = []

  while (files.hasNext()) {
    var file = files.next();
    let id = file.getId();
    let name = file.getName();
    let newItem = new FileToCopy(id,name)
    output.push(newItem)
  }

  return output;
}

function copyItem(id, target) {
  let file = DriveApp.getFileById(id)
  let folder = DriveApp.getFolderById(target)
  file.makeCopy(folder);
}

Result

enter image description here

enter image description here

Reference

Upvotes: 1

Related Questions