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.
Related
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.
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());
}
};
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
I am trying to attach an already created spreadsheet and email it. I have found out that, if I try
opts.fileIds.forEach(function(fileId) {
console.log('fileId ' + fileId);
var file = DriveApp.getFileById(fileId);
var blob = file.getAs(file.getMimeType());
console.log('blob length' + blob.getDataAsString().length);
console.log('file retrieved size ' + file.getSize());
console.log('file mime type ' + file.getMimeType());
attachmentList.push(blob);
});
I do not get a blob object, file.getAs returns a null however, file.getBlob works fine but turns it into a pdf which is not what I want. Is there any way to attach this a spreadsheet?
The attachments option for MailApp requires an array of BlobSource objects. If you take a look at the link you can see that Google Files can just be attached without additional work. This includes Spreadsheets, Documents, PDFs, and more.
Try just passing back a reference to the file!
opts.fileIds.forEach(function(fileId) {
console.log('fileId ' + fileId);
var file = DriveApp.getFileById(fileId);
//logging
attachmentList.push(file);
});
Edit: The reason for this behaviour by MailApp, I think is because MIMEType and getAs() ContentType are different. Take a look at what the Google Scripts site says about https://developers.google.com/apps-script/reference/base/blob#getAs(String):
For most blobs, 'application/pdf' is the only valid option.
Can someone confirm this?
I am trying to connect to google maps api and get lant/long of place, but no matter what I'm trying to get, I receive ZERO_RESULTS every time. For example if I type
http://maps.googleapis.com/maps/api/geocode/json?address=Moscow+Tverskaya+18 into browser, it gives me correct result, but if I'm trying to send the exact same string via WWW class from unity I get zero results.
IEnumerator GetGoogleCoords() {
var url = "http://maps.googleapis.com/maps/api/geocode/json?";
var qs = "";
// qs += "address=" + savedAddress;
qs += "address=Moscow +Tverskaya+18";
var req = new WWW(url + "?" + qs);
Debug.Log(url + qs);
yield return req;
Debug.Log(req.text);
}
I tried every request and in every order
You have an extra "?" in your url as #Engerlost said.
This post is about how to prevent doing similar mistakes again.
Best programming practice would be to build the full url not in WWW constructor but in a separate line.
var url = "http://maps.googleapis.com/maps/api/geocode/json?";
var qs = "address=Moscow +Tverskaya+18";
var fullUrl = url + "?" + qs
var request = new WWW(fullUrl);
That is, one line should contain only one job. Which makes it much easier to manage the code. In your case, it gets much easier to debug and see there is an error building the full url. Now you are able to easily add a Debug.Log if you suspicious about final url which goes into WWW as parameter.
Debug.Log("Request url: " + fullUrl);
And you would easily see the resulting url contains two "?" characters.