DriveApp.getFolders() not working as expected - google-apps-script

I have a Google Apps Script for Sheets (running as a script under Sheets) that is supposed to return all the folders in my Google drive, however, the following code is stuck in an endless loop and only shows 1 of 3 folders in the drive - over and over again.
//this doesn't work -only shows 1 folder and repeats indefinitely
function getMyFolders() {
var f;
while (DriveApp.getFolders().hasNext()) {
f = DriveApp.getFolders().next();
console.log("f: " +f.getName());
}
}
The code returns the name of only one of my folders (hasNext() is clearly not working or there is a bug in Google Apps Scripts - since the while condition is never false (I ran it for several minutes and it never stopped!)
Could this be a security issue? This is just one of the problems I've run into. The other is that the 3 folders are subfolders of the same parent folder. No sure why getFolders() is not returning just the parent (that would make more sense).

Get All My Files
function getAllMyFiles(folder=DriveApp.getRootFolder()) {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet0');
let files = folder.getFiles();
while(files.hasNext()) {
let file = files.next()
sh.appendRow([file.getName(),file.getId()]);
}
let sfldrs = folder.getFolders();
while(sfldrs.hasNext()) {
let sfldr = sfldrs.next();
getAllMyFiles(sfldr)
}
}

Try this as your function instead to return just the parent folders!
function getMyFolders() {
let folders = DriveApp.getFolders();
while (folders.hasNext()) {
console.log("f: " +folders.next().getName());
}
}
If you want child folders as well, you would need to use recursion. Are you familiar with the concept? Happy to update my answer with that information if it's helpful.
Explanation:
You were calling DriveApp.getFolders(); on every iteration, so hasNext() was always retrieving the same item (the first folder in the iterator). Hence, you had an infinite loop.

I rewrote Cooper's answer to get all folders underneath a given parent.
In my case the parent folder is called "Gill".
function getAllMyFolders(folder=DriveApp.getFoldersByName("Gill").next()) {
let sfldrs = folder.getFolders();
while(sfldrs.hasNext()) {
let sfldr = sfldrs.next();
console.log("folder name: " +sfldr.getName());
getAllMyFolders(sfldr)
}
}
Ok - I played around with this code and discovered recursion is completely unnecessary, so here's code that will return all folders at all levels, then you can test the level with getParents()
function getMyFolders() {
let folders = DriveApp.getFolders();
while (folders.hasNext()) {
let folder = folders.next();
Logger.log(folder.getName() + ", Parent Folder: " + folder.getParents().next().getName());
}
}
I had a hunch that using a recursive function was the wrong way to go (and it looks terrible plus adds unnecessary overhead) but was confused why, my first attempt in this question did not work. The answer was the way my code was written - apparently, you need to assign DriveApp.getFolders() to a variable only once. Simply putting it in more than once, seems to reset it. That is, checking for DriveApp.getFolders().hasNext() and followed by DriveApp.getFolders().next() will cause the endless loop! Lesson here: assign it to a variable and then check for hasNext as calling it changes the state of the iterator which is reset again if getFolders is called again.
That was my actual bug here.

Related

Google Apps Script trigger - run whenever a new file is added to a folder

I want to execute a google apps script whenever a new file is added to a specific folder.
Currently I'm using a run-every-x-minutes clock trigger, but I only need to run the script whenever I add a file to a folder. Is there a way to do this?
The same as this question - which is now almost 3 years old. The comment below the question states that:
There's not a trigger for that, if that's what you're hoping. How are
things getting into the folder, and do you have any control over that?
– Jesse Scherer Apr 8 '18 at 3:02
I wonder if this comment is still valid, and if it is, then if there's a workaround.
Issue:
Unfortunately, the comment you read is still true. Here is a list of all the available triggers and a trigger for a new file added to a folder is not one of them.
Workaround / Explanation:
I can offer you a workaround which is usually used by developers when they built their add-ons. You can take advantage of the PropertiesService class. The logic is quite simple.
You will store key-value pairs scoped to the script:
In your case, the key will be the folder id, and the value will be the number of files under this folder.
You will setup a time-driven trigger to execute mainFunction for example every one minute.
The script will count the current number of files within the selected folder. The function responsible for that is countFiles.
The checkProperty function is responsible for checking if the current number of files under this folder matches the old number of files. If there is a match, meaning no files were added, then checkProperty returns false, otherwise return true and update the property for the current folder ID, so when the script runs after 1 minute, it will compare with the fresh value.
If checkProperty returns true, then execute the desired code.
Code snippet:
Set up a time-driven trigger for mainFunction. Whatever code you put inside the brackets of the if(runCode) statement will be executed if the number of files under the folderID has changed.
function mainFunction(){
const folderID = 'folderID'; //provide here the ID of the folder
const newCounter = countFiles(folderID);
const runCode = checkProperty(folderID, newCounter);
if(runCode){
// here execute your main code
//
console.log("I am executed!");
//
}
}
And here are the helper functions which need to be in the same project (you can put them in the same script or different scripts but in the same "script editor").
function countFiles(folderID) {
const theFolder = DriveApp.getFolderById(folderID);
const files = theFolder.getFiles();
let count = 0;
while (files.hasNext()) {
let file = files.next();
count++;
};
return count;
}
function checkProperty(folderID, newC){
const scriptProperties = PropertiesService.getScriptProperties();
const oldCounter = scriptProperties.getProperty(folderID);
const newCounter = newC.toString();
if(oldCounter){
if(oldCounter==newCounter){
return false;
}
else{
scriptProperties.setProperty(folderID, newCounter);
return true;
}
}
else{
scriptProperties.setProperty(folderID, newCounter);
return true;
}
}

Return Collection of Google Drive Files Shared With Specific User

I'm trying to get a collection of files where user (let's use billyTheUser#gmail.com) is an editor.
I know this can be accomplished almost instantly on the front-end of google drive by doing a search for to:billyTheUser#gmail.com in the drive search bar.
I presume this is something that can be done in Google App Scripts, but maybe I'm wrong. I figured DriveApp.searchFiles would work, but I'm having trouble structuring the proper string syntax. I've looked at the Google SDK Documentation and am guessing I am doing something wrong with the usage of the in matched to the user string search? Below is the approaches I've taken, however if there's a different method to accomplishing the collection of files by user, I'd be happy to change my approach.
var files = DriveApp.searchFiles(
//I would expect this to work, but this doesn't return values
'writers in "billyTheUser#gmail.com"');
//Tried these just experimenting. None return values
'writers in "to:billyTheUser#gmail.com"');
'writers in "to:billyTheUser#gmail.com"');
'to:billyTheUser#gmail.com');
// this is just a test to confirm that some string searches successfully work
'modifiedDate > "2013-02-28" and title contains "untitled"');
Try flipping the operands within the in clause to read as:
var files = DriveApp.searchFiles('"billyTheUser#gmail.com" in writers');
Thanks #theAddonDepot! To illustrate specifically how the accepted answer is useful, I used it to assist in building a spreadsheet to help control files shared with various users. The source code for the full procedure is at the bottom of this post. It can be used directly within this this google sheet if you copy it.
The final result works rather nicely for listing out files by rows and properties in columns (i.e. last modified, security, descriptions... etc.).
The ultimate purpose is to be able to update large number of files without impacting other users. (use case scenario for sudden need to immediately revoke security... layoffs, acquisition, divorce, etc).
//code for looking up files by security
//Posted on stackoverlow here: https://stackoverflow.com/questions/62940196/return-collection-of-google-drive-files-shared-with-specific-user
//sample google File here: https://docs.google.com/spreadsheets/d/1jSl_ZxRVAIh9ULQLy-2e1FdnQpT6207JjFoDq60kj6Q/edit?usp=sharing
const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("FileList");
const clearRange = true;
//const clearRange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("ClearRange").getValue();
//if you have the named range setup.
function runReport() {
//var theEmail= SpreadsheetApp.getActiveSpreadsheet().getRangeByName("emailFromExcel").getValue();
//or
var theEmail = 'billyTheUser#gmail.com';
findFilesByUser(theEmail);
}
function findFilesByUser(theUserEmail) {
if(clearRange){
ss.getDataRange().offset(1,0).deleteCells(SpreadsheetApp.Dimension.ROWS)
}
var someFiles = DriveApp.searchFiles('"' + theUserEmail + '" in writers');
var aListOfFiles = []
while(someFiles.hasNext()){
var aFile = someFiles.next();
aListOfFiles.push([aFile.getId()
,aFile.getName()
,aFile.getDescription()
,aFile.getSharingAccess()
,aFile.getSharingPermission()
,listEmails(aFile.getEditors())
,listEmails(aFile.getViewers())
,aFile.getMimeType().replace('application/','').replace('vnd.google-apps.','')
,aFile.getDateCreated()
,aFile.getLastUpdated()
,aFile.getSize()
,aFile.getUrl()
,aFile.getDownloadUrl()
])
}
if(aListOfFiles.length==0){
aListOfFiles.push("no files for " + theUserEmail);
}
ss.getRange(ss.getDataRange().getLastRow()+1,1, aListOfFiles.length, aListOfFiles[0].length).setValues(aListOfFiles);
}
function listEmails(thePeople){
var aList = thePeople;
for (var i = 0; i < aList.length;i++){
aList[i] = aList[i].getEmail();
}
return aList.toString();
}

How do I retrieve and store files and folders structure in a google spreadsheet using apps script?

I know this question has been asked before, but I am trying to go a bit deeper in the problem. I have this apps script code I have taken online and I am using it in a google spreadsheet, to scan for a list of folders.
In short, I have a (shared) folder structure with most likely over 1000 sub-folders, which contain sub-subfolders, that looks similar to this:
MAIN FOLDER
FOLDER 1
FILE 1.1
SUBFOLDER 1.A
SUBFILE 1.A.2
SUBFILE 1.A.3
FOLDER 2
FILE 2.1
SUBFOLDER 2.A
SUBFILE 2.A.2
SUBFILE 2.A.3
FOLDER 3
FILE 3.1
SUBFOLDER 3.A
SUBFILE 3.A.2
SUBFILE 3.A.3
...
I am trying to fix 2 things:
1) My function obviously breaks when running because there are too many folders and files. I don't think it's due to the limit of rows in a spreadsheet. So, ideally, I would have to manually batch 200 folders in the main folder, run the function, and re-iterate. Any hints how to avoid that, and how to potentially, dump temporary results in a first spreadsheet and create a new spreadsheet if it's a caching problem?
2) I am not able to retrieve subfolders details and the details of the files it contains. Can anyone help adapt the code to go deeper in the folder hierarchy? Do I need some kind of loop function here?
With the code provided hereunder which I have used and tested a few times, I can only retrieve the folders 1,2,3 and files 1.1, 2.1, 3.1...but not subfolders or subfiles.
Fixing this function, would save me weeks of manual work. Any help is welcomed. Thanks in advance for your help.
function getAndListFilesInFolder() {
var arr,f,file,folderName,subFolders,id,mainFolder,name,own,sh,thisSubFolder,url;
sh = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
sh.getRange(1, 1, 100, 10).clear({contentsOnly: true})
id = "FOLDER ID HERE";
arr = [["FILE NAME", "URLs", "OWNER","FOLDER"]];
mainFolder = DriveApp.getFolderById(id);
subFolders = mainFolder.getFolders();
folderName = mainFolder.getName();
f = mainFolder.getFiles();
while (f.hasNext()) {
file = f.next();
name = file.getName()
url = file.getUrl()
own = file.getOwner().getName()
arr.push([name, url, own, folderName]);
};
while (subFolders.hasNext()) {
thisSubFolder = subFolders.next();
f = thisSubFolder.getFiles();
folderName = thisSubFolder.getName();
while (f.hasNext()) {
file = f.next();
name = file.getName()
url = file.getUrl()
own = file.getOwner().getName()
arr.push([name, url, own,folderName]);
};
};
sh.getRange(1,1, arr.length, arr[0].length).setValues(arr);
sh.getRange(2,1, arr.length, arr[0].length).sort(1);
//var second = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2")
//second.activate()
}
The reason you aren't seeing any of the subfolders is because you are not actually adding them to your array (arr) at any time.
What you really need here is is to write a recursive function, which receives a folder and returns itself plus it's file's details details in an array. When you encounter a folder, you pass that folder back to the same function.
Investigate recursion and recursive functions.
Here is a brief example you could expand, it'll need some tweaking since I'm writing this off the top of my head:
function getFilesFromFolder(folder){
var arr = [];
arr.push([folder.getName(),folder.getUrl(),folder.getOwner().getName()])
var subfolders = folder.getFolders();
var files = folder.getFiles():
while(files.hasNext()){
arr.push(/*file details*/);
}
//recurse into subfolders here.
while(subFolders.hasNext()){
arr.concat(getFilesFromFolder(subFolders.next()); //notice how we call the function from within itself.
}
return arr;
}

Google App Script Timed Out after 30 mins

I'm running below script but it errors out with a Error Message of "Timed Out" because the folder Id I'm giving have lots of lots folders under.
Can someone please help me out to optimize it or alternative approach.
function listFolderContents() {
var currentFolder = DriveApp.getFolderById("0B89Y-hAfWt_HVkhSbWprOVhPM00");
traverseFolder(currentFolder);
}
function traverseFolder(folder) {
DriveApp.getRootFolder().removeFolder(folder);
var subFolders = folder.getFolders();
while (subFolders.hasNext()) {
traverseFolder(subFolders.next());
}
}
It seems like you are doing a recursive delete of a folder. Try moving the removeFolder call to after the recursive calls. Otherwise, you delete the folder before you call getFolders on it, which is why I think it's timing out on you.
Like this:
function deleteFolder(folder) {
var subFolders = folder.getFolders();
while (subFolders.hasNext()) {
deleteFolder(subFolders.next());
}
DriveApp.getRootFolder().removeFolder(folder);
}
The termination condition for the resursive function is that there are no sub-folders, that is you are in a leaf folder.
See this related answer to a question about recursive deletion of a binary tree
I think that solution in this case could be using of getContinuationToken()
and example of iteration with continuationToken is available in another answer on stackoverflow.

Migrating from DocsList to DriveApp?

I've been using DocsList for a big project and it was working perfectly. Lately, bugs have been popping up and they mostly have roots with getting a folder or file. When I did research, I found that DriveApp had been updated. The problem is that DriveApp doesn't have search parameters like DocsList had.
For example, if I had a folder structure like this:
Root
-Main Folder 1
--Folder 1
--Folder 2
-Main Folder 2
--Folder 1
--Folder 2
To get folder "Folder 1" in "Main Folder 2," I could put in the search parameter like so: DocsList.getFolder('Main Folder 2/Folder 1')
With DriveApp, I just can't understand how to work with it. From what I understand, I have to do something like this for DriveApp:
var mainFolders = DriveApp.getFoldersByName('Main Folder 2');
while (mainFolders.hasNext()) {
var mainFolder = termFolders.next();
var subFolders = termFolder.getFoldersByName('Folder 1');
// Something like this...
}
So if I had a folder that is more "deep" I would have to expand this even further..?
I feel like instead of making things easier, they made it more complicated with all the FileIterators and FolderIterators. And just making it hard to "get" a file or folder in code terms.
So basically, the point of this thread is to find out how a person who is use to DocsList to navigate and edit Drive files/folders can migrate to DriveApp and achieve the same things.
Small/Discrete examples of different scenarios would be really helpful. I can take it from there. I'll edit this more, if you guys think I'm not being clear about what I need help on.
The discussions from wchiquito's comment are an interesting read, but following all the links is time-consuming.
Bottom line: There will not be DriveApp version of getFolderByPath(), so you will need to roll your own. In the Google+ group, Faustino proposed a work-around and Eric improved it. Here it is, with an added check to allow paths that start with "/".
function getFolderByPath(path) {
var parts = path.split("/");
if (parts[0] == '') parts.shift(); // Did path start at root, '/'?
var folder = DriveApp.getRootFolder();
for (var i = 0; i < parts.length; i++) {
var result = folder.getFoldersByName(parts[i]);
if (result.hasNext()) {
folder = result.next();
} else {
return null;
}
}
return folder;
}
With that, you can simply do myFolder = getFolderByPath('Main Folder 2/Folder 1');. You will end up with a DriveApp Folder instance.
The code below works at mine.
It is based on having the same Id
function convertFileFromDocsListToDriveApp(file)
{ // Because of difference between DocsList and DriveApp
return (file === null) ? null : DriveApp.getFileById(file.getId());
}
function convertFolderFromDocsListToDriveApp(folder)
{ // Because of difference between DocsList and DriveApp
return (folder === null) ? null : DriveApp.getFolderById(folder.getId());
}
I call this in a few 'strategic' positions in my code.
I didn't test conversion from DriveApp to DocsList, but I expect this to work as well.