Folder.searchFiles() method raises "Invalid argument: q" error - google-apps-script

Given the following Google Apps Script code snippet:
function myFunction() {
var files = DriveApp.getFolderById('xxx').searchFiles('123')
while (files.hasNext()) {
var file = files.next()
Logger.log(file.getName())
}
}
I'm getting:
Invalid argument: q (line x, file "Code")
from the line with while (files.hasNext()) statement.
The line doesn't contain any error I'm aware of. The funny thing is when I exchange .searchFiles('123') with .getFiles(), the error doesn't appear and the code executes.
Does it mean that there's a problem with the object that .searchFiles('123') returns (it should return FileIterator object)? Unfortunately, Javascript only checks the return data type at runtime so I cannot really see if it's correct until I run it.

Per the function documentation of searchFiles, the expected argument is a query string that conforms to the syntax described in the Google Drive API's "Search for Files " guide
Gets a collection of all files in the user's Drive that match the given search criteria. The search criteria are detailed in the Google Drive SDK documentation. Note that the params argument is a query string that may contain string values, so take care to escape quotation marks correctly (for example "title contains 'Gulliver\\'s Travels'" or 'title contains "Gulliver\'s Travels"').
Note that Drive v2 uses title for the filename, and Drive v3 uses name for the filename. Apps Script's advanced service client library Drive uses v2, so this may be the syntax used by the Drive Service (DriveApp) as well.
The reason the error message does not appear on the line with searchFiles is because the return value is a FileIterator, rather than the actual results. This (and most) iterators are lazily-evaluated, and thus your search query is not actually executed until you call files.hasNext().
To resolve this issue, you must indicate what should be searched and how, i.e. provide the Field and the Operator, not just the Value.
function foo() {
const queries = ["fullText contains '123'",
"mimeType='" + MimeType.GOOGLE_SHEETS + "'"];
const matches = queries.map(function (qs) {
return getMatchingFiles_(qs);
});
// Log the results in the Apps Script logger. (Use console.log for Stackdriver)
matches.forEach(function (queryResult, i) {
Logger.log("Results for query #" + i
+ ", \"" + queries[i] + "\" are:");
queryResult.forEach(function (file) {
Logger.log(file.getId() + ": '" + file.getName() + "'");
});
});
}
function getMatchingFiles_(query, folderId) {
// Default to DriveApp.searchFiles if no folder ID given.
folder = folderId ? DriveApp.getFolderById(folderId) : DriveApp;
const search = folder.searchFiles(query);
const results = [];
while (search.hasNext())
results.push(search.next());
return results;
}

Related

how to use nextPageToken

I have a script that archives old classrooms, until the end of 2021 it was working fine.
In the lasts months I got an error (the script works ok, but terminate with error) and today I was investigating it, the script runs only once per month.
The error is due to a supposed change in .nextPageToken function.
var parametri = {"courseStates": "ARCHIVED"};
var page = Classroom.Courses.list(parametri);
var listaClassi = page.courses;
var xyz = page.nextPageToken;
if (page.nextPageToken !== '') {
parametri.pageToken = page.nextPageToken;
page = Classroom.Courses.list(parametri);
listaClassi = listaClassi.concat(page.courses);
};
var xyz has been added to better understand what was happening.
So, in this case the list does not have pagination, is only one page. var xyz returns "undefined", and the "if" statement results "true", this makes that variable listaClassi got appended the same content a second time. That generate the error and the abnormal end of the script.
I found an issue reported here https://issuetracker.google.com/issues/225941023?pli=1 that may be related with my problem.
Now I could change .nextPageToken with .getNextPageToken but I found no docs on the second function and many issues reporting that is not working, can anyone help me?
When using the nextPageToken value obtained to the response make sure to enter it as a separate parameter with a slightly different name. You will obtain nextPageToken in the response, the pageToken parameter needs to be entered in the request. It does look like you are doing it right, the way you add the parameter is a bit odd, yet it should be functional.
To discard problems with the Classroom API (that we can certainly take a look at) try with this simple code example in a new Google Apps Script project, remember you will need to add an Advanced service, information about advanced services can be found in this documentation article https://developers.google.com/apps-script/guides/services/advanced. Use listFiles as the main method in your Apps Script project.
function listFiles() {
var totalClasses = 0;
nextPageToken = "";
console.log("Found the following classes:")
do {
var response = loadPage(nextPageToken);
var classes = response.courses;
for (let x in classes){
console.log("Class ID: " + classes[x].id + " named: '" + classes[x].name + "'.");
}
totalClasses += classes.length;
} while (nextPageToken = response.nextPageToken)
console.log("There are " + totalClasses + " classes.")
}
function loadPage(token = ""){
return Classroom.Courses.list({
fields: 'nextPageToken,courses(id,name)',
pageSize: 10,
pageToken: token
});
}
When we first make the API call with Apps Script we don't specify a pageToken, since it is the first run we don't have one. All calls to the List method may return a nextPageToken value if the returned page contains an incomplete response.
while (nextPageToken = response.nextPageToken)
In my code at the line above once response.nextPageToken is empty (not in the response) inside the condition block JavaScript will return false, breaking the loop and allowing the code to finish execution.
To have your incident reviewed by a Google Workspace technician you can also submit a form to open a ticket with the Google Workspace API Support team at https://support.google.com/a/contact/wsdev.

DriveApp.getRootFolder() is returning null to webapp

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

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

Find googleusercontent.com URL for an InlineImage

Whenever I upload an image (i.e. InlineImage) in a Google Doc, it uploads it to a CDN and references a googleusercontent.com URL. If I'm using Google Apps Script on a DocumentApp, I can get an instance of InlineImage. I know I can convert it to a base64 and then create a data URL for this image. However, instead of creating this gigantic URL, I'd rather just use the existing googleusercontent.com URL.
How do I find out the googleusercontent.com URL for an InlineImage?
Essentially you need to do the following:
Set a unique alt description on the InlineImage.
Get the HTML of the entire document.
Use a regex to find the <img tag using the unique alt description from step 1.
function getUrlOfInlineImage(inlineImage) {
var altDescription = inlineImage.getAltDescription(); // warning: assumes that the alt description is a uuid. If it's not unique, this function might return a different image's url. If it's not a UUID, it might contain illegal regex characters and crash.
if (!altDescription) {
inlineImage.setAltDescription(Utilities.getUuid());
// TODO: We currently crash because if we attempt to get the HTML right after calling setAltDescription(), it won't exist in the HTML. We must wait a bit of time before running it again. If there was something like DocumentApp.flush() (similar to how the Spreadsheet App has the same function), it might resolve this issue and we wouldn't need to crash.
throw "Image was missing an alt description. Run again."
}
var html = getGoogleDocumentAsHTML();
var regex = new RegExp('<img alt="' + altDescription + '" src="([^"]+)"');
var matches = regex.exec(html);
if (matches) {
return matches[1];
} else {
return null;
}
}
function getGoogleDocumentAsHTML() {
var id = DocumentApp.getActiveDocument().getId() ;
var forDriveScope = DriveApp.getStorageUsed(); //needed to get Drive Scope requested
var url = "https://docs.google.com/feeds/download/documents/export/Export?id="+id+"&exportFormat=html";
var param = {
method : "get",
headers : {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
muteHttpExceptions:true,
};
var html = UrlFetchApp.fetch(url,param).getContentText();
return html;
}
There is obviously room for improvement with the script (e.g. not making it crash), but this was good enough for my use-case.

Google Apps Script - get URL of File in Drive with File Name

I am attempting to create a form in Google Spreadsheets which will pull an image file from my Drive based on the name of the file and insert it into a cell. I've read that you can't currently do this directly through Google Scripts, so I'm using setFormula() adn the =IMAGE() function in the target cell to insert the image. However, I need the URL of the image in order to do this. I need to use the name of the file to get the URL, since the form concatenates a unique numerical ID into a string to use the standardized naming convention for these files. My issue is that, when I use getFilesByName, it returns a File Iteration, and I need a File in order to use getUrl(). Below is an snippet of my code which currently returns the error "Cannot find function getUrl in object FileIterator."
var poNumber = entryFormSheet.getRange(2, 2);
var proofHorizontal = drive.getFilesByName('PO ' + poNumber + ' Proof Horizontal.png').getUrl();
packingInstructionsSheet.getRange(7, 1).setFormula('IMAGE(' + proofHorizontal + ')');
If you know the file name exactly, You can use DriveApp to search the file and getUrl()
function getFile(name) {
var files = DriveApp.getFilesByName(name);
while (files.hasNext()) {
var file = files.next();
//Logs all the files with the given name
Logger.log('Name:'+file.getName()+'\nUrl'+ file.getUrl());
}
}
If you don't know the name exactly, You can use DriveApp.searchFiles() method.
You're close - once you have the FileIterator, you need to advance it to obtain a File, i.e. call FileIterator.next().
If multiple files can have the same name, the file you want may not be the first one. I recommend checking this in your script, just in case:
var searchName = "PO + .....";
var results = DriveApp.getFilesByName(searchName);
var result = "No matching files";
while (results.hasNext()) {
var file = results.next();
if (file.getMimeType() == MimeType. /* pick your image type here */ ) {
result = "=IMAGE( .... " + file.getUrl() + ")");
if (results.hasNext()) console.warn("Multiple files found for search '%s'", searchName);
break;
}
}
sheet.getRange( ... ).setFormula(result);
You can view the available MimeTypes in documentation