DriveApp.getRootFolder() is returning null to webapp - google-apps-script

As part of a larger Google App Script webapp, I want to create a rudimentary file system with files/folders in the user's Google Drive. I'm doing this through a element where each would be a different folder (prefixed with a '*') or file.
I have setup the webapp HTML to include the element, but within this element I call a script that will populate the via a call to google.script.run.withSuccessHandler. It appears that this code runs as I'd expect, but the result of DriveApp.getRootFolder() is null, thereby making me unable to access the file structure.
// In the HTML file.
...
<head>
<script>
...
// Populate options in the file/folder list based on the provided folder.
function setFiles(folder)
{
alert(folder);
return;
/* // Get the select item.
var e = document.getElementById("file-select");
// First list all the folders at the top.
//#TODO Adding an asterick on folders to identify them for now, maybe have a different method later?
var folderI = folder.getFolders();
var i = 0;
while(folderI.hasNext())
{
var fldr = folderI.next();
e.innerHTML += "<option id='f_'" + i + "'>*" + fldr.getName() + "</option>";
i++;
}
// Now list all the files in the current directory.
i = 0;
var fileI = folder.getFiles();
while(fileI.hasNext())
{
var fle = fileI.next();
e.inner.HTML += "<option id='f_'" + i + "'>*" + fle.getName() + "</option>";
i++
}
*/
....
</script>
</head>
<body>
...
<div id="select-files">
<select id="file-select" size="10">
<script>
// Populate the initial file/folder list.
google.script.run.withSuccessHandler(setFiles).getRootFolder();
</script>
</select>
</div>
...
// In code.gs
/**
* Returns the root folder for the user.
* #return The root folder of the user.
*/
function getRootFolder()
{
return DriveApp.getRootFolder();
}
This is the code as I'm testing it now, hence my commenting out most of setFiles(). alert() results in 'null', but I'd expect it to be an 'Object [Object]' type that I could iterate through.
Interestingly, when I've added Logger.log() lines in the code.gs file, no log output is produced (I can't figure out why, because if I change the return value of getRootFolder() to a string, that string is displayed in the alert, so I know the code is entering that function correctly.
I'm wondering if this is a misunderstanding such that Google Drive (or maybe, generally, Google App Script specific objects) cannot be passed to an HTML file, though I couldn't find any clear documentation that this is the case.

As Cooper said in the comments, the Folder type is not legal to send to the client. If you look at what a Folder contains, it is purely functions, which are not allowed to be sent over.
All that client-side you commented out in setFiles cannot function in the user's browser. Even if you were able to pass the Folder code into the client, what would folder.getFolders() mean to the user's browser? It would start looking for the rest of the code from DriveApp, which doesn't exist in the browser, and still fail.
I'm wondering if this is a misunderstanding such that Google Drive (or maybe, generally, Google App Script specific objects) cannot be passed to an HTML file
What you get passed to the HTML file is documented here. Pay special attention to how google.script.run works.
No, you cannot pass the entire environment of your server-side code to the client (e.g. pass all of DriveApp and its dependences over to the client).
What you can do on both sides is construct your own version of Folder which exports the strings on the server side and reconstructs them on the client side. Note that arrays of strings are OK, so I would put things like the child, parent folder names and IDs in arrays. Just to be safe, I use JSON stringify/parse to strip functions out. This example works without the JSON part, but on more complicated objects it can be nice to clean them up.
client-side code
// just to log that it works
google.script.run.withSuccessHandler(response => {
response = JSON.parse(response);
console.log({response})
}).getFolder();
Code.gs
// client-code calls this to get folder info
function getFolder(id) {
return JSON.stringify(new Folder_(id ? DriveApp.getFolderById(id) : DriveApp.getRootFolder()));
}
// constructor for a `folder` suitable to send to the client
function Folder_(folder) {
this.id = folder.getId();
this.name = folder.getName();
this.foldersIds = [];
this.foldersNames = [];
this.parentsIds = [];
this.parentsNames = [];
this._extractFolders(folder, "folders");
this._extractFolders(folder, "parents");
}
// one function for both "getFolders" and "getParents"
Folder_.prototype._extractFolders = function(folder, type) {
var folders = folder["get" + type.replace(/^./, function(str){return str.toUpperCase()})]();
while (folders.hasNext()) {
var folder = folders.next();
this[type + "Ids"].push(folder.getId());
this[type + "Names"].push(folder.getName());
}
};

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();
}

Getting a list of functions within your GAS project

I want to see if there is a way to obtain a list of all the functions I have in a Google Apps Script project. I've seen multiple threads on getting a list of all of your Google Apps Script projects but none as of yet for listing all of the functions in each project. Does anyone know if this is possible? I've looked through the Google Apps Script Reference Overview but I wasn't able to find anything that stood out to me (I, of course, could've missed it). If anyone has any suggestions, please let me know!.
The best example I can provide is:
I have a Google Spreadsheet file. Attached to that Google Spreadsheet is a GAS project (accessed through the Google Sheet menu "Tools -> Script Editor") that has a couple of different functions used to grab values from the sheet, do some calculations and post the results to a different sheet.
What I am trying to accomplish: Run some sort of function that can provide me a list of all of the functions I have in the GAS project (preferably as string values). Example would be:
["runMyCalculations","myOnEdit","sortClosedFiles","formatSheets"]
All of these are functions that can only be run if I open up the Script Editor and select it in the drop-down menu and click the "Run" button.
What I want to be able to do is create a dynamic list of all the functions I have so I can pass them into an "on open" triggered function that creates a custom menu in the sheet, listing out all of the functions I have. I want this so I can simply make changes to my sheet, go to the drop-down menu and run the function I need to run, rather than having to open up the Script Editor.
You can use the Apps Script API to get all the content out of an Apps Script file.
The following code has the option of passing in a file name to get. You must supply the Apps Script file ID. Passing in a gs file name is optional. Provided are 3 functions. The function that does all the work, a function to call that function with the parameters for testing, and a logging function. An OAuth library is not needed because the token is acquired from the ScriptApp service.
NOTE: You will need to enable the Apps Script API, and approve permission to your Drive in order for this code to work. Make sure to check the return from the UrlFetchApp.fetch() call the first time that you run this code for an error message. It may have a link that you need to use to enable the Apps Script API.
function getFuncNames(po) {
var allFiles,dataContentAsString,downloadUrl,fileContents,fileData,i,options,
theAccessTkn,thisFileName;
var ndxOfFunction=0,counter=0, ndxOfEnd=0, functionName="", allFncNames=[],
hasSpaces = 0;
var innerObj, thisFile, fileType = "", thisGS_Content,howManyFiles, allGsContent="";
/*
Get all script function names. If no gs file name is provided, the code
gets all the function names.
*/
/*
po.fileID - required - The Apps Script file ID
po.gsFileName - optional - the gs code file name to get - gets just one
file instead of all files
*/
//ll('po',po);
if (!po.fileID) {
return false;
}
theAccessTkn = ScriptApp.getOAuthToken();//Get an access token for OAuth
downloadUrl = "https://script.google.com/feeds/download/export?id=" +
po.fileID + "&format=json";//create url
options = {
"kind": "drive#file",
"id": po.fileID,
"downloadUrl": downloadUrl,
"headers": {
'Authorization': 'Bearer ' + theAccessTkn,
},
"contentType": "application/vnd.google-apps.script+json",
"method" : "GET"
};
fileData = UrlFetchApp.fetch(downloadUrl, options);//Get all the content from the Apps Script file
//ll('fileData',fileData)
dataContentAsString = fileData.getContentText();
fileContents = JSON.parse(dataContentAsString);//Parse string into object
allFiles = fileContents.files;//All the files in the Apps Script project
howManyFiles = allFiles.length;
for (i=0;i<howManyFiles;i++) {
thisFile = allFiles[i];//Get one inner element that represents one file
if (!thisFile) {continue;}
fileType = thisFile.type;
if (fileType !== "server_js") {continue;}//This is not a gs file - its HTML or json
thisFileName = thisFile.name;
//ll('typeof thisFileName',typeof thisFileName)
//ll('thisFileName',thisFileName)
//ll('equal',po.gsFileName !== thisFile.name)
if (po.gsFileName) {//Is there a setting for the file name to restrict the search to
if (po.gsFileName !== thisFile.name) {//The name to search for is not this file name
continue;
}
}
thisGS_Content = thisFile.source;//source is the key name for the file content
allGsContent = allGsContent + thisGS_Content;
}
//ll('allGsContent',allGsContent)
while (ndxOfFunction !== -1 || counter < 1000) {
ndxOfFunction = allGsContent.indexOf("function ");
//ll('ndxOfFunction',ndxOfFunction)
if (ndxOfFunction === -1) {break};
allGsContent = allGsContent.slice(ndxOfFunction+9);//Remove everything in front of 'function' first
ndxOfEnd = allGsContent.indexOf("(");
functionName = allGsContent.slice(0,ndxOfEnd);
allGsContent = allGsContent.slice(ndxOfEnd+2);//Remove the
hasSpaces = functionName.indexOf(" ");
if (hasSpaces !== -1) {continue;}
if (functionName.length < 150) {
allFncNames.push(functionName);
}//Any string over 150 long is probably not a function name
counter ++;
};
//ll('allFncNames',allFncNames)
return allFncNames;
};
function runOtherFnk() {
getFuncNames({fileID:"Your File ID here",gsFileName:"Code"});
}
function ll(a,b) {
//Logger.log(typeof a)
if (typeof b === 'object') {
b = JSON.stringify(b);
}
Logger.log(a + ":" + b)
}
The following code extracts file names from the this object:
function getAllFnks() {
var allFnks,fnkStr,k;
allFnks = [];
for (k in this) {
//Logger.log(k)
//Logger.log(typeof k)
//Logger.log(this[k])
//Logger.log(typeof this[k])
fnkStr = this[k];
if (fnkStr) {
fnkStr = fnkStr.toString();
//Logger.log(typeof fnkStr)
} else {
continue;
}
//Logger.log(fnkStr.toString().indexOf('function'))
if (fnkStr.indexOf('function') === 1) {
allFnks.push(k);
}
}
Logger.log(allFnks)
Logger.log('Number of functions: ' + allFnks.length)
}

Google Apps Scripts: Insert additional text into a text file

I'm trying to develop a method for adding Google Tasks to my running task list named todo.txt and is housed in Google Drive. I've found a way to grab the tasks. Of course, the data will nee to be manipulated as well (e.g., concatenate date formats to conform to todo.txt rules) but I'll deal with that next.
The question I have is how to insert them to the top of the todo.txt without overwriting the existing text contained within it. I have read in other circumstances, that I may need to read the current text out, add the new info and then write the whole thing back as an overwrite. Is this necessary? And, if so, how do I do it?
Below is the code I've written thus far, substituting logger.log for the destination file since I don't know how to do it. I;m not a programmer, so polease forgive any ignorance here. Thanks.
function listTasks(taskListId) {
var taskListId = '12345'; //this is the ID of my Google Tasks (the source data)
var name2="Text Journals"; //this is the folder in which my todo.text resides
var name="todo.txt"; //this is the name of my todo file (the destination)
var dir = DriveApp.getFoldersByName(name2).next()
var tasks = Tasks.Tasks.list(taskListId);
if (tasks.items) {
for (var i = 0; i < tasks.items.length; i++) {
var task = tasks.items[i];
Logger.log('Task with title "%s" and dute: "%s" and notes "%s" and status "%s" was found.',
task.title, task.due, task.notes, task.status);
}
} else {
Logger.log('No tasks found.');
}
}

How to delete/overwrite CSV file using google apps script?

My google apps script imports a csv file [test.csv] into a new tab, manipulates some values, then saves the manipulated tab as a csv file [test.csv]. When it saves the new version, it simply makes another copy [test(1).csv]. I wish instead to overwrite the previous file (or delete the old one then export/write the new version.) Help please?
I am using reference code from the Interacting With Docs List Tutorial
I know this is an old question, but much of the information in the accepted answer has been deprecated by Google since then. DocsList is gone, as are the clear() and append() methods on a file object.
I use the following function to create or overwrite a file:
// Creates or replaces an existing file
function updateFile (folder, filename, data) {
try {
// filename is unique, so we can get first element of iterator
var file = folder.getFilesByName(filename).next()
file.setContent(data)
} catch(e) {
folder.createFile(filename, data)
}
}
For reference, here's some code for doing the same for a folder. It assumes we're operating in the parent folder of the current sheet and want a folder
object for a new or existing folder there.
// Get folder under current folder. Create it if it does not exist
function getOrCreateFolder(csvFolderName) {
var thisFileId = SpreadsheetApp.getActive().getId();
var thisFile = DriveApp.getFileById(thisFileId);
var parentFolder = thisFile.getParents().next();
try {
// csvFolderName is unique, so we can get first element of iterator
var folder = parentFolder.getFoldersByName(csvFolderName).next();
// asking for the folder's name ensures we get an error if the
// folder doesn't exist. I've found I don't necessarily catch
// an error from getFoldersByName if csvFolderName doesn't exist.
fname = folder.getName()
} catch(e) {
var folder = parentFolder.createFolder(csvFolderName);
}
return folder
}
You could do DocsList.find(fileName) which gives you a list of files that have that name. If file names are unique, then you can just do var file = DocsList.find(fileName)[0].
If you are a Google Apps user, you can use file.clear() to remove all the contents of the old file, and then file.append() to insert all of the new contents.
Otherwise, you will have to file.setTrashed(true) and then DocsList.createFile() to make the new one.