If statement that does nothing if the first condition is not met - google-apps-script

I'm busy writing a script that includes an If statement and I'm trying to make the else part of the statement do nothing if the first condition isn't met. The script checks a google drive folder for a file name and if it is present the first condition should run, else if it isn't present it should do nothing.
I've tried using else{ return false;} , else{ } and none are working. I've also tried swapping the conditions around but that didn't work either
I keep getting an error saying "Exception: Cannot retrieve the next object: iterator has reached the end. (line 7, file "calls")" because the file is deleted once the data is retrieved from it.
The first condition works perfectly but if the csv file isn't present in the folder the above message is displayed. The idea is that I set a trigger that runs every minute to check the folder.
Below is what I have so far. Any help would be greatly appreciated.
function calls(){
var hotFolder = DriveApp.getFolderById('idHere');
var targetSheet = SpreadsheetApp.openById('idHere').getSheetByName('sheetNameHere');
var callsSheet = SpreadsheetApp.openById('idHere').getSheetByName('sheetNameHere');
var callsCsv = hotFolder.getFilesByName('fileName.csv.Here').next();
if(callsCsv) {
var csvData = Utilities.parseCsv(callsCsv.getBlob().getDataAsString());
targetSheet.getRange(1,1,csvData.length,csvData[0].length).setValues(csvData);
callsCsv.setTrashed(true);
var targetSheetData = targetSheet.getRange(2,1,targetSheet.getLastRow()-1,targetSheet.getLastColumn()).getValues();
var handleTime = targetSheetData.map(function(row){
if([row[9]] != 'ABANDON') {
return [row[6] + row[7] +15];
} else {
return [['']];
}
});
targetSheet.getRange(2,12,handleTime.length).setValues(handleTime);
var newData = targetSheet.getRange(2,1,targetSheet.getLastRow(),targetSheet.getLastColumn()).getValues();
callsSheet.getRange(callsSheet.getLastRow,1,newData.length,newData[0].length).setValues(newData);
targetSheet.clear();
} else {
}
}

The "Exception: Cannot retrieve the next object: iterator has reached the end. (line 7, file "calls")" error message you are receiving is due to the fact that the it's not possible to get the next item in the collection of files or folders - probably because of the deletion from the previous run.
What you can do in this situation is to use the hasNext() method. According to its documentation :
hasNext() > Determines whether calling next() will return an item.
Return
Boolean — true if next() will return an item; false if not
Therefore, you can change your code to this:
var callsCsv = hotFolder.getFilesByName('fileName.csv.Here');
if (callsCsv.hasNext()) {
var callsCsvFile = callsCsv.next();
// your code here - callsCsv becomes callsCsvFile
}
Reference
Apps Script Class FileIterator.

Related

Google apps script permissions issue

I've been trying to set up Google apps script with a spreadsheet getting values from Tag Manager and I've used this before so I know it is working.
This is the tutorial Im using - https://measureschool.com/google-sheets-tracking-google-tag-manager/
However, when I try to set this up now I am getting an error and it has always worked before. I have clicked also the permission to "allow" the app.
The error I get is this:
{"result":"error","error":{"name":"Exception"}}
This error is given simply if I create a new apps script and deploy it. When I click on the link to test it, it shows me this error and the sheet remains disfunctional.
I also tried just creating the most simplest app with just "myFunction" function inside as the default and that doesnt work either and gives this error:
Script function not found: doGet
This is so confusing. Such a simple problem. Always worked before. Never had problems like this before. It's bizarre. Would be grateful for any helps.
This is the code that gives me the "name: error" message if I put this in a app script it.
// Usage
// 1. Enter sheet name where data is to be written below
// 1. Enter sheet name and key where data is to be written below
var SHEET_NAME = "Sheet1";
var SHEET_KEY = "1jO5LaaIOfnAwkCCRpNPq0nee97ZjYh9D2YeJD_5OVys";
// 2. Run > setup
//
// 3. Publish > Deploy as web app
// - enter Project Version name and click 'Save New Version'
// - set security level and enable service (most likely execute as 'me' and access 'anyone, even anonymously)
//
// 4. Copy the 'Current web app URL' and post this in your form/script action
//
// 5. Insert column names on your destination sheet matching the parameter names of the data you are passing in (exactly matching case)
var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service
// If you don't want to expose either GET or POST methods you can comment out the appropriate function
function doGet(e){
return handleResponse(e);
}
function doPost(e){
return handleResponse(e);
}
function handleResponse(e) {
var lock = LockService.getPublicLock();
lock.waitLock(30000); // wait 30 seconds before conceding defeat.
try {
// next set where we write the data - you could write to multiple/alternate destinations
var doc = SpreadsheetApp.openById(SHEET_KEY);
var sheet = doc.getSheetByName(SHEET_NAME);
// we'll assume header is in row 1 but you can override with header_row in GET/POST data
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow()+1; // get next row
var row = [];
// loop through the header columns
for (i in headers){
if (headers[i] == "Timestamp"){ // special case if you include a 'Timestamp' column
row.push(new Date());
} else { // else use header name to get data
row.push(e.parameter[headers[i]]);
}
}
// more efficient to set values as [][] array than individually
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
// return json success results
return ContentService
.createTextOutput(JSON.stringify({"result":"success", "row": nextRow}))
.setMimeType(ContentService.MimeType.JSON);
} catch(e){
// if error return this
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "error": e}))
.setMimeType(ContentService.MimeType.JSON);
} finally { //release lock
lock.releaseLock();
}
}
I resolved this because I made a silly mistake in that my spreadsheet didnt contain the values timestamp and any params in the 1st line.

Issue running an Installed Trigger in Google App Script

Fairly new to app script so bare with me.
Wrote this massive script, then went to set it up on a times trigger and it just refuses to run. I've gone ahead an back tracked as much as I could, to get at least something to work, yet I can't even get a basic toast to appear on a minute interval.
This is the script I've built, which I'm running directly to enable the trigger:
function createTriggers() {
ScriptApp.newTrigger('testTime')
.timeBased()
.everyMinutes(1)
.create();
};
The function it's calling is super simple, I've used it a lot and change it a lot too:
var gSS = SpreadsheetApp.openById("this_1s/the.1d")
function testTime() {
var d = new Date()
var Start = d.getTime();
gSS.toast(Start, "Testing", 30)
};
So how it should work, and it does if I just call the 'testTime' function directly, is a little toast pop-up appears on the spreadsheet in question, and stays visible for 30s.
When I run the trigger function 'createTriggers', nothing happens..
Please help! All the code I wrote is for nothing if I can't get it to run on its own.. :(
***** EDIT - 08/04/20 - based on comments *****
It's possible this was an XY example, I tried to run a small segment of the original code which works when I run it directly, and its not working here either.. this snippit does not have any UI facing functions in it, so it shouldn't be the issue..
All i did was take the above trigger function and change the name to 'testClear', which calls to the following functions:
function testClear(){
sheetVars(1)
clearSheetData(sheetSPChange)
};
function sheetVars(numSprints) {
// returns the global vars for this script
try {
sheetNameSprints = "Name of Sprint Sheet"
sheetNameSPChange = "Name of Story Point Change Sheet"
sheetSprints = gSS.getSheetByName(sheetNameSprints)
sheetSPChange = gSS.getSheetByName(sheetNameSPChange)
arraySprints = iterateColumn(sheetSprints,"sprintIDSorted", 1, numSprints)
}
catch(err) {
Logger.log(err)
};
};
function iterateColumn(sheet, header, columnNum, numRows) {
// Create an array of first column values to iterate through
// numRows is an int, except for the string "all"
var gData = sheet.getDataRange();
var gVals = gData.getValues();
var gLastR = ""
var gArray = []
// check how many rows to iterate
if (numRows == "all") {
gLastR = gData.getLastRow();
}
else {
gLastR = numRows
};
// Iterate through each row of columnNum column
for (i = 1; i < gLastR; i++){
// iterate through
if(gVals[i][columnNum] !== "" && gVals[i][columnNum] !== header){
// push to array
gArray.push(gVals[i][columnNum]);
}
else if (gVals[i][columnNum] == "") {
break
};
};
return gArray
};
function clearSheetData(sheet) {
// Delete all rows with data in them in a sheet
try {
if (!sheet.getRange(sheet.getLastRow(),1).isBlank()){
sheet.getRange(2, 1, sheet.getLastRow()-1, sheet.getLastColumn()-1).clearContent()
Logger.log("Sheet cleared from old data.")
}
else {
sheet.deleteRows(2, sheet.getLastRow()-1)
Logger.log("Sheet rows deleted from old data.")
};
}
catch(err){
Logger.log(err)
emailLogs()
};
};
The 'emailLogs' function is a basic MailApp so i get notified of an issue with the script:
function emailLogs() {
// Email Nikita the loggs of the script on error
var email = "my work email#jobbie"
var subject = "Error in Sheet: " + gSS.getName()
var message = Logger.getLog()
MailApp.sendEmail(email, subject, message)
};
Thanks to a comment I've now discovered the executions page!! :D This was the error for the edited script.
Aug 4, 2020, 10:48:18 AM Error Exception: Cannot call
SpreadsheetApp.getUi() from this context.
at unknown function
To show a toast every certain "time" (every n iterations) add this to the for loop
if (!((i+1) % n)) spreadsheet.toast("Working...")
From the question
Aug 4, 2020, 10:48:18 AM Error Exception: Cannot call SpreadsheetApp.getUi() from this context. at unknown function
The above error means that your time-drive script is calling a method that can only be executed when a user has opened the spreadsheet in the web browser.
In other words, toast can't be used in a time-driven trigger. The solution is to use client-side code to show that message every minute. To do this you could use a sidebar and a recursive function that executes a setTimeout
References
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Timeouts_and_intervals
Based on all the comments, and new things I'd learned from that..:
I'd been calling to a global variable for my onOpen function:
var gUI = SpreadsheetApp.getUi();
even though I wasn't using it for the trigger, since it was global it tried to run and then failed.
I moved the actual definition of gUI into the onOpen function, and tried again and it worked.
Thank you all for the support!

Google App Script continuefolderiteration not continuing from last folder, but restarting ever time

I am trying to find a specific spreadsheet(Target) within each folder (A). These folders are within Folder (B) which are in turn within Folder (C). The current script i have retrieve folder (C), and search through each (B) for folder (A) then spreadsheet. However, because of large numbers of folder (B), i have placed a continuationtoken at the folder (A) level to track which folder (B) have been searched [or at least i believe that's what i am doing]. The issue I have is that the script always resume from the same first folder (B) instead of continuing from the last folder (B) that was searched.
Folder (C) = baseFolder
Folder (B) = studentFolder
Folder (A) = AssessmentFolder
Spreadsheet (Target) = any spreadsheet which includes the given searchkey
Following are snippets of the script that i am using.
if (continuationToken == null) {
// firt time execution, get all files from Drive
var allFolders = baseFolder.getFolders();
var Tokenalert = ui.alert('There is no token');
}
else {
// not the first time, pick up where we left off
var allFolders = DriveApp.continueFolderIterator(continuationToken);
}
while (allFolders.hasNext() && end.getTime() - start.getTime() <= maxTime) {
i++;
var studentFolder = allFolders.next();
var AssessmentFolder = studentFolder.getFoldersByName("02 Assessment");
if (AssessmentFolder.hasNext() == true){
Logger.log(studentFolder.getName());
progress.getRange(i,1).setValue(studentFolder.getName());
AssessmentFolder = AssessmentFolder.next();
if (AssessmentFolder.searchFiles(checkcode).hasNext() == true){
var filetochange = AssessmentFolder.searchFiles(checkcode);
var editfile = filetochange.next();
progress.getRange(i,2).setValue(editfile);
Logger.log(editfile);var fileid = editfile.getId();
var editss = SpreadsheetApp.openById(fileid);
Logger.log(editss.getName());
progress.getRange(i,3).setValue(fileid);
var editsheet = editss.getSheetByName(sheettoedit);
// remove protection from the sheet mentioned
// Protect the active sheet except B2:C5, then remove all other users from the list of editors.
var protection = editsheet.protect().setDescription('Test');
if (protectrange != 0) {
var unprotected = editsheet.getRange(protectrange);
}
if (protectrange2 != 0) {
var unprotected2 = editsheet.getRange(protectrange2);
}
else {
unprotected2 = unprotected;
}
if (protectrange3 != 0) {
var unprotected3 = editsheet.getRange(protectrange3);
}
else {
unprotected3 = unprotected2;
}
protection.setUnprotectedRanges([unprotected, unprotected2]);
// Ensure the current user is an editor before removing others. Otherwise, if the user's edit
// permission comes from a group, the script throws an exception upon removing the group.
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
progress.getRange(i,4).setValue("complete");
}
else {
progress.getRange(i,4).setValue("fail");
}
}
end = new Date()
}
// Save your place by setting the token in your user properties
if(allFolders.hasNext()){
var continuationToken = allFolders.getContinuationToken();
userProperties.setProperty('CONTINUATION_TOKEN', continuationToken);
progress.getRange(1,6).setValue(continuationToken);
} else {
i++;
progress.getRange(i,1).setValue("Completed")
// Delete the token
PropertiesService.getUserProperties().deleteProperty('CONTINUATION_TOKEN');
ss.deleteSheet("Progress");
var Completionalert = ui.alert('completed');
}
Pardon my messy code as I am new to coding.
I have checked and the continuation token is stored, and is retrieved. I have also ensured that the script does not end prematurely before the token is stored. The only issue i can think of is that either the way I enter the token again is wrong, or a wrong token is stored. But i am unsure of which. I have tried storing the token at B level and A level, but it doesn't make sense and doesn't work.
Also, I have read the following:
https://developers.google.com/apps-script/reference/drive/folder-iterator Howver it was not very helpful as it only shows how to get a token.
Google Apps Script: How to use ContinuationToken with recursive folder iterator but i do not understand the the term recursive.
It would be great if someone can explain how continuationtoken works. Does it just track the last folder and resume from there? or does it actually take the whole list of folder and gives the position?
To reiterate, the main issue is that the token is created and retrieved, but somehow the script is not resuming from the last folder checked.
Any offhand feedback on other portions of the script is welcomed^^ and thank you in advance!:)
There are script run-time limitations
regular user: 6 minutes
Business/Enterprise/Education: 30 mins
Take a brief look for detailed article on Quotas for Google Services
Take a look on Stackoverflow threat Google Apps Script: How to use ContinuationToken with recursive folder iterator, it has similarities to your question
Recursive function is the function which calls itself
Here is the little example of function which returns the base to the exponent power
function pow(a, b) {
if (b === 1) { return a }
else { return a * pow(a, b - 1) }
}
console.log(pow(3, 3)) // same as 3 * 3 * 3 = 27
P.S. This is just an example, use Math.pow() instead
Does it clarify your concerns?
After checking through my script, I realized that the error came from me declaring the variable allFolders twice, one more time before this chunk of script and the script is now working as it should after removing the first declaration of allFolders.
Therefore, there was nothing wrong with the script posted here.

The coordinates or dimensions of the range are invalid

No matter what i do, i get the error in the title. If i replace my script with the standard
function doGet(e) {
var params = JSON.stringify(e);
return HtmlService.createHtmlOutput(params);
}
from Googles very own example: https://developers.google.com/apps-script/guides/web#url_parameters i have to run it once (inside the script editing thingy) to not show the error anymore, but when i put my code back in place it stays at the output of that dummy function. Probably because i cannot run my own function without errors (see below).
Settings for the web-app deployment:
version: 1
execute as: me
access: anyone, even anonymous
I think it has something to do with having to run a function once (or atleast that's what it looks like to me), but i cannot run my function since it's relying on url parameters. This already fails at e.parameter == undefined obviously.
function doGet(e) {
Logger.log(JSON.stringify(e));
var result = 'Ok';
if(e.parameter == undefined) {
result = 'no param';
}
else {
var id = 'id is normally here obviously not now';
var sheet = SpreadsheetApp.openById(id).getActiveSheet();
var newRow = sheet.getLastRow()+1;
var rowData = [];
for (var param in e.parameter) {
var value = stripQuotes(e.parameter[param]);
Logger.log(param + ': ' + e.parameter[param]);
switch (param) {
case 'timeStampBegin': //Parameter
rowData[0] = value; //Value in column B
break;
case 'timeStampEnd':
rowData[1] = value;
break;
default:
result = 'unsupported parameter';
}
}
Logger.log(JSON.stringify(rowData));
var newRange = sheet.getRange(newRow, 1, 1, rowData.length);
newRange.setValues([rowData]);
}
return ContentService.createTextOutput(result);
}
function stripQuotes( value ) {
return value.replace(/^["']|['"]$/g, "");
}
I appreciate any suggestions why this could be happening or how i could run my function with a test input.
You do need to run your script once in the IDE after you add any new services (e.g. HTML service, spreadsheet service, etc.) to approve those changes from a security standpoint.
To test your code without making tons of new versions and deploying each one (i.e. steps 2-3), you can either run it in the web IDE, or navigate to "Publish" > "Deploy as web app..." then click on "Test web app for your latest code." Unlike the live version, that link uses whatever code is currently saved, not the last deployed version.
I don't know of this is intentional by Google, but this is kinda ridiculous. Apparently a function has to be run once in the editor to be able to used. But here it is anyway:
Make an empty function which generates the needed input for your functions that you need to run, and call each. If they call each other, you only need to call the "parent"-function.
Make a new version under File->Manage versions.
Deploy your app again. Select the new version in the process.
I'm not sure 2+3 are really necessary, but for me it still ran the old dummy code (print back json) without doing that.

Iterators built with different continuation tokens are producing the same results in Google Apps

I am programming a Google Apps script within a spreadsheet. My use case includes iterating over a large set of folders that are children of a given one. The problem is that the processing takes longer than the maximum that Google allows (6 minutes), so I had to program my script to be able to resume later. I am creating a trigger to resume the task, but that is not part of my problem (at least, not the more important one at this moment).
My code looks like this (reduced to the minimum to illustrate my problem):
function launchProcess() {
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty(SOURCE_PARENT_FOLDER_KEY, SOURCE_PARENT_FOLDER_ID);
scriptProperties.deleteProperty(CONTINUATION_TOKEN_KEY);
continueProcess();
}
function continueProcess() {
try {
var startTime = (new Date()).getTime();
var scriptProperties = PropertiesService.getScriptProperties();
var srcParentFolderId = scriptProperties.getProperty(SOURCE_PARENT_FOLDER_KEY);
var continuationToken = scriptProperties.getProperty(CONTINUATION_TOKEN_KEY);
var iterator = continuationToken == null ? DriveApp.getFolderById(srcParentFolderId).getFolders() : DriveApp.continueFolderIterator(continuationToken);
var timeLimitIsNear = false;
var currTime;
while (iterator.hasNext() && !timeLimitIsNear) {
var folder = iterator.next();
processFolder_(folder);
currTime = (new Date()).getTime();
timeLimitIsNear = (currTime - startTime >= MAX_RUNNING_TIME);
}
if (!iterator.hasNext()) {
scriptProperties.deleteProperty(CONTINUATION_TOKEN_KEY);
} else {
var contToken = iterator.getContinuationToken();
scriptProperties.setProperty(CONTINUATION_TOKEN_KEY, contToken);
}
} catch (e) {
//sends a mail with the error
}
}
When launchProcess is invoked, it only prepares the program for the other method, continueProcess, that iterates over the set of folders. The iterator is obtained by using the continuation token, when it is present (it will not be there in the first invocation). When the time limit is near, continueProcess obtains the continuation token, saves it in a property and waits for the next invocation.
The problem I have is that the iterator is always returning the same set of folders although it has been built from different tokens (I have printed them, so I know they are different).
Any idea about what am I doing wrong?
Thank you in advance.
It appears that your loop was not built correctly. (edit : actually, probably also another issue about how we break the while loop, see my thoughts about that in comments)
Note also that there is no special reason to use a try/catch in this context since I see no reason that the hasNext() method would return an error (but if you think so you can always add it)
here is an example that works, I added the trigger creation / delete lines to implement my test.
EDIT : code updated with logs and counter
var SOURCE_PARENT_FOLDER_ID = '0B3qSFd3iikE3MS0yMzU4YjQ4NC04NjQxLTQyYmEtYTExNC1lMWVhNTZiMjlhMmI'
var MAX_RUNNING_TIME = 5*35*6;
function launchProcessFolder() {
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty('SOURCE_PARENT_FOLDER_KEY', SOURCE_PARENT_FOLDER_ID);
scriptProperties.setProperty('counter', 0);
scriptProperties.deleteProperty('CONTINUATION_TOKEN_KEY');
ScriptApp.newTrigger('continueProcess').timeBased().everyMinutes(10).create();
continueProcessFolder();
}
function continueProcessFolder() {
var startTime = (new Date()).getTime();
var scriptProperties = PropertiesService.getScriptProperties();
var srcParentFolderId = scriptProperties.getProperty('SOURCE_PARENT_FOLDER_KEY');
var continuationToken = scriptProperties.getProperty('CONTINUATION_TOKEN_KEY');
var iterator = continuationToken == null ? DriveApp.getFolderById(srcParentFolderId).getFolders() : DriveApp.continueFolderIterator(continuationToken);
var timeLimitIsNear = false;
var currTime;
var counter = Number(scriptProperties.getProperty('counter'));
while (iterator.hasNext() && !timeLimitIsNear) {
var folder = iterator.next();
counter++;
Logger.log(counter+' - '+folder.getName());
currTime = (new Date()).getTime();
timeLimitIsNear = (currTime - startTime >= MAX_RUNNING_TIME);
if (!iterator.hasNext()) {
scriptProperties.deleteProperty('CONTINUATION_TOKEN_KEY');
ScriptApp.deleteTrigger(ScriptApp.getProjectTriggers()[0]);
Logger.log('******************no more folders**************');
break;
}
}
if(timeLimitIsNear){
var contToken = iterator.getContinuationToken();
scriptProperties.setProperty('CONTINUATION_TOKEN_KEY', contToken);
scriptProperties.setProperty('counter', counter);
Logger.log('write to scriptProperties');
}
}
EDIT 2 :
(see also last comment)
Here is a test with the script modified to get files in a folder. From my different tests it appears that the operation is very fast and that I needed to set a quite short timeout limit to make it happen before reaching the end of the list.
I added a couple of Logger.log() and a counter to see exactly what was happening and to know for sure what was interrupting the while loop.
With the current values I can see that it works as expected, the first (and second) break happens with time limitation and the logger confirms that the token is written. On a third run I can see that all files have been dumped.
var SOURCE_PARENT_FOLDER_ID = '0B3qSFd3iikE3MS0yMzU4YjQ4NC04NjQxLTQyYmEtYTExNC1lMWVhNTZiMjlhMmI'
var MAX_RUNNING_TIME = 5*35*6;
function launchProcess() {
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty('SOURCE_PARENT_FOLDER_KEY', SOURCE_PARENT_FOLDER_ID);
scriptProperties.setProperty('counter', 0);
scriptProperties.deleteProperty('CONTINUATION_TOKEN_KEY');
ScriptApp.newTrigger('continueProcess').timeBased().everyMinutes(10).create();
continueProcess();
}
function continueProcess() {
var startTime = (new Date()).getTime();
var scriptProperties = PropertiesService.getScriptProperties();
var srcParentFolderId = scriptProperties.getProperty('SOURCE_PARENT_FOLDER_KEY');
var continuationToken = scriptProperties.getProperty('CONTINUATION_TOKEN_KEY');
var iterator = continuationToken == null ? DriveApp.getFolderById(srcParentFolderId).getFiles() : DriveApp.continueFileIterator(continuationToken);
var timeLimitIsNear = false;
var currTime;
var counter = Number(scriptProperties.getProperty('counter'));
while (iterator.hasNext() && !timeLimitIsNear) {
var file = iterator.next();
counter++;
Logger.log(counter+' - '+file.getName());
currTime = (new Date()).getTime();
timeLimitIsNear = (currTime - startTime >= MAX_RUNNING_TIME);
if (!iterator.hasNext()) {
scriptProperties.deleteProperty('CONTINUATION_TOKEN_KEY');
ScriptApp.deleteTrigger(ScriptApp.getProjectTriggers()[0]);
Logger.log('******************no more files**************');
break;
}
}
if(timeLimitIsNear){
var contToken = iterator.getContinuationToken();
scriptProperties.setProperty('CONTINUATION_TOKEN_KEY', contToken);
scriptProperties.setProperty('counter', counter);
Logger.log('write to scriptProperties');
}
}
As of January 1, 2016 this is still a problem. The bug report lists a solution using the Advanced Drive API, which is documented here, under "Listing folders".
If you don't want to use Advanced services, an alternative solution would be to use the Folder Iterator to make an array of File Ids.
It appears to me that the Folder Iterator misbehaves only when created using DriveApp.continueFolderIterator(). When using this method, only 100 Folders are included in the returned Folder Iterator.
Using DriveApp.getFolders() and only getting Folder Ids, I am able to iterate through 694 folders in 2.734 seconds, according the Execution transcript.
function allFolderIds() {
var folders = DriveApp.getFolders(),
ids = [];
while (folders.hasNext()) {
var id = folders.next().getId();
ids.push(id);
}
Logger.log('Total folders: %s', ids.length);
return ids;
}
I used the returned array to work my way through all the folders, using a trigger. The Id array is too big to save in the cache, so I created a temp file and used the cache to save the temp file Id.
This is caused by a bug in GAS:
https://code.google.com/p/google-apps-script-issues/issues/detail?id=4116
It appears you're only storing a single continuation token. If you want to recursively iterate over a set of folders and allow the script to pause at any point (e.g. to avoid the timeout) and resume later, you'll need to store a bunch more continuation tokens (e.g. in an array of objects).
I've outlined a template that you can use here to get it working properly. This worked with thousands of nested files over the course of 30+ runs perfectly.