Synchronize Google drive with remote web server - google-drive-api

as per the subject I would need to synchronize the contents of some Google drive folders with the same ones located in a shared remote server(web site) . Normally on the server itself, for internal things, I use a cron. But in this case I don't know what the correct solution is. I took a look online but, despite having an idea, I am not sure whether to build specific bees or if there is a more functional way. Thanks to those who have the patience to answer.

What about creating a Google Apps Script Web app deployment, and using it as a REST API?
These are the steps to follow:
Create a new Google Apps Script project.
Create a Web App that serves the content as a JSON. In my example I will list the folders of Google Drive, but you can use it as you wish. In this example I am implementing a simple API key system for preventing unwanted access.
Code.gs
const getDriveFolders = () => {
const listFolders = []
const folderIterator = DriveApp.getFolders()
while (folderIterator.hasNext()) {
listFolders.push(folderIterator.next().getName())
}
return listFolders
}
const errMsgRes = ContentService
.createTextOutput()
.append(JSON.stringify({ "msg": "INVALID API KEY" }))
.setMimeType(MimeType.JSON)
const succMsgRes = ContentService
.createTextOutput()
.setContent(JSON.stringify(getDriveFolders(), null, 2))
.setMimeType(MimeType.JSON)
const doGet = (e) => {
return e.parameter.api_key != "123123"
? errMsgRes
: succMsgRes
}
Click Deploy > Select type > Wep app. In the Who has acces field I put it Anyone. Grab the URL for later.
In your web site, you only need to fetch the data.
index.html
<div id="drive-content" ></div>
<script>
const GASURL = "<DEPLOYMENT_URL>";
const container = document.getElementById("drive-content");
fetch(GASURL + "?api_key=123123")
.then((d) => d.json())
.then((d) => {
console.log(d);
container.innerHTML += d.join("<br />");
});
</script>
This way, every time you enter your web site, the content will be updated.
Documentation
ContentService
DriveApp
doGet Request parameters

Related

Google Apps Scripts - Optimal way to replace text

I have tons of strings that I need to replace in a Google document. My performance to run my script is taking a huge hit and takes forever to run now. I am looking at how to optimize.
body.replaceText("oldvalue1",newValue1)
body.replaceText("oldvalue2",newValue2)
body.replaceText("oldvalue3",newValue3)
..
..
..
Is there a more optimal way to replace text in a Google Doc using google scripts?
As #Kos refers in the comments, the best approximation is using Google Docs API batchUpdates as Advanced Service inside Google Apps Script.
I leave you an example on how Docs API works as an Advanced Service. For the example I suppose you have an array of objects that contains the oldValue and newValue:
function batchUpdating(DocID = DocID) {
const replaceRules = [
{
toReplace: "oldValue1",
newValue: "newValue1"
},
...
]
const requestBuild = replaceRules.map(rule => {
var replaceAllTextRequest = Docs.newReplaceAllTextRequest()
replaceAllTextRequest.replaceText = rule.newValue
replaceAllTextRequest.containsText = Docs.newSubstringMatchCriteria()
replaceAllTextRequest.containsText.text = rule.toReplace
replaceAllTextRequest.containsText.matchCase = false
Logger.log(replaceAllTextRequest)
var request = Docs.newRequest()
request.replaceAllText = replaceAllTextRequest
return request
})
var batchUpdateRequest = Docs.newBatchUpdateDocumentRequest()
batchUpdateRequest.requests = requestBuild
var result = Docs.Documents.batchUpdate(batchUpdateRequest, DocID)
Logger.log(result)
}
Google Apps Script helps us handle the authorization flow, and gives us hints on how to construct our request. For example, Docs.newReplaceAllTextRequest(), helps us build the request for that service, giving us hints that it contains replaceText and containText. In any case, we could also supply the object directly to the request:
const requestBuild = replaceRules.map(rule => {
return {
replaceAllText:
{
replaceText: rule.newValue,
containsText: { text: rule.oldValue, matchCase: false }
}
}
})
To take in account
Each request is validated before being applied. If any request is not valid, then the entire request will fail and nothing will be applied.
If your script project uses a default GCP project created on or after April 8, 2019, the API is enabled automatically after you enable the advanced service and save the script project. If you have not done so already, you may also be asked to agree to the Google Cloud Platform and Google APIs Terms of Service as well.
Documentation:
ReplaceAllTextRequest
SubstringMatchCriteria
Array.forEach(obj => { body.replaceText(obj.old,obj.new)})
#kos way is probably a better way to go.

Google Web Apps - Get user email but run scripts as owner

I've recently gravitated to google web apps and I have a bit of a dilemma. I'm trying to build an app that is open to very specific users and the data they are viewing filters based on their access group.
In a google sheet I'm listing the user emails and their respective access groups. Column A - email, Column B - access group
The issue
When the user accesses the web app I'm using this to grab their email:
var email = Session.getActiveUser().getEmail();
And then I run this code to get their access group:
function validate(email){
var sheet = SpreadsheetApp.openById(ID).getSheetByName(ssUserList);
try{
var group = getRowsData(sheet).find(e => e.userEmail === email).securityGroup;
return group;
} catch(e){
return "Not Authorized";
}
}
Because the user doesn't have access to my google sheet, they get an error when the function runs. And I can't deploy the web app to run as me because I need the user's email. I understand this very well.
What I've read:
Tons of other posts and articles about access tokens and credentials and urlFetchApps ... I don't understand any of it and to be honest I don't know which one makes more sense for my situation.
What I've tried:
I can't use the 1st usable option I've found which is to access web app 1 (which runs as user), then call web app 2 using the user email as a parameter because if they share that link from web app 2 then anyone could see the data and I'm working with really sensitive data.
I realize I could just put these parameters in a separate sheet and give them view only access and the scripts will run fine, but I'm extra and I want to do it right.
In reality I'm going to have a few other functions that will need to run as me. If you were in my shoes, where would you start? Or can someone explain it in layman's terms? Should I be looking into something like this? Any help is appreciated!
Summary
One of the possibilities, as suggested here, is to create a separate web application to handle access to SpreadSheets.
The client (the main web app) would make a request through UrlFetchApp to the middleware (web app in charge of consulting the SpreadSheet), the middleware would make the needed queries and would return them to the client. Finally, depending on the response obtained, one content or another would be rendered.
Minimal Example
Configuring the Project
First, we create two GAS projects, one we call Main and the other Middleware. The main point is that the Middleware would run as USER_DEPLOYING and the client as USER_ACCESSING. This allows access to the sheet without requiring additional permissions.
The appscripts.json file would look like this on the client. :
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/userinfo.email"
],
"webapp": {
"executeAs": "USER_ACCESSING",
"access": "ANYONE"
}
And like this on the middleware:
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets"
],
"webapp": {
"executeAs": "USER_DEPLOYING",
"access": "ANYONE_ANONYMOUS"
}
If you have any questions about editing or viewing appscript.json, check the Manifest and Scopes documentation.
Attention: "access": "ANYONE" and "access": "ANYONE_ANONYMOUS" are only being used for testing purposes. This is dangerous, and should be reviewed for the specific needs of your project.
Code Sample
As for the client, we only need to ask for the email of the user who is accessing through Session.getActiveUser().getEmail() and then send it to the middleware to obtain the response. Depending on the response obtained, we will render one content or another (I assume there are two roles present: USER and ADMIN)
Client
const doGet = () => {
var data = {email: Session.getActiveUser().getEmail()}
var options = {
'method': 'POST',
'contentType': 'application/json',
'payload': JSON.stringify(data)
}
var fetch = UrlFetchApp.fetch(URL_MIDDLEWARE, options)
var userAccess = JSON.parse(fetch).accessLevel
return HtmlService.createHtmlOutput(
userAccess === "ADMIN"
? `<h1>${data.email} - ADMIN USER</h1>`
: userAccess === "USER"
? `<h1>${data.email} - COMMON USER</h1>`
: "<h1>Unauthorized</h1>" )
}
For the middleware we need to obtain that email and compare it with our sheet to check the access level of the user. Then we return the result.
Middleware
const doPost = (request) => {
// destructuring the request
const { parameter, postData: { contents, type } = {} } = request;
const userEmail = JSON.parse(contents).email;
let userAccess = SpreadsheetApp.openById(SPREADSHEET_ID).getRange('A1:B2').getValues()
// This can be replaced by a filter function
let userAccessLevel;
for (let user of userAccess) { if (userEmail == user[0]) userAccessLevel = user[1] }
return ContentService.createTextOutput(Utilities.jsonStringify({
user: userEmail,
accessLevel: userAccessLevel
}))
};
Finally, you access the Main Web App to check that everything is working.
Remember that this is a test implementation, and should not be used in production. If you need more information on these topics, you can visit the following links:
Load data asynchronously, Best Practices
Request Parameters doGet() doPost()
ContentService Class

How do I make Appscript Web App less brittle for use commercially

I've been playing around with the appscript web app thingy and thought I could use it for commercial purposes among a group of say 15 users. However, I get this sporadically:
In the incognito window on the top it works fine. It's logged in as the owner of the app which is me but not really me because I have another account for stuff like this. On the top, is my normal chrome which is logged in as me and as the owner of the account. This is how I set it up
And this is the sharing thing on the my projects
Finally, it draws from this google sheet which for now only I have access to, so hopefully that means no one else can access the data with permission which hopefully means anyone can open it but no one can get access to my precious data necessary to actually use the sheet unless they have access. Of course, in case I'm wrong I have to obscure the url. Anyway, how do I prevent this "Sorry, unable to open the file at this time" error which happens randomly.
It looks like what you are experiencing is an ongoing issue which many users have reported on Google Issue Tracker here and it stems from multiple accounts being logged in.
I suggest you star ★ the issue above and eventually add a comment saying that you are affected by it.
I use Firebase hosting, which has a free tier and is a Google product, for my website.
Go to your Firebase console when logged into a Google account:
https://console.firebase.google.com/?pli=1
I have minimal content in the Firebase index.html file, and then I load more content after the home page loads by using an AJAX POST call to Apps Script and return the content from Apps Script using the Content Service. I think that Firebase hosting is better than a Google Site. A Firebase hosting site requires some set up and some learning, so it's not as easy as Embedding an Apps Script Web App into a Google Site, but for the long term I think it's a better solution.
function doPost(e) {
try{
var html;
var fileName = "MyWebsite";
html = HtmlService.createTemplateFromFile(fileName).evaluate().getContent();
//Get the html file as a string
if (!html) {
html = '<div>There was an error</div>';
}
return ContentService.createTextOutput(html).setMimeType(ContentService.MimeType.TEXT);
}catch(e) {
MailApp.sendEmail(Session.getEffectiveUser().getEmail(), "Load Error", e.message +
"\n\n" + e.stack);
html = '<div>There was an error</div>';
return ContentService.createTextOutput(html).setMimeType(ContentService.MimeType.TEXT);
}
}
In your index.html file you need to call the Web App:
window.getWebSiteContent = function() {
//console.log('getMainContent')
var payload,url;
url = "Put the Apps Script published Web App URL here";
//console.log('url: ' + url)
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
//console.log('onreadystatechange')
if (this.readyState == 4 && this.status == 200) {
//console.log('this.responseText ' + this.responseText);
addTheMainContentToTheWebApp(this.responseText);//Run a function that adds the content
} else if (this.readyState == 4 && this.status !== 200) {
//console.log('Error',this.status)
}
};
xhttp.open("POST",url, true);
xhttp.setRequestHeader('Content-Type', 'text/plain');
payload = {data:{"keyName":"Value"}};
payload = JSON.stringify(payload);
//console.log('payload: ' + payload)
xhttp.send(payload);
}
document.addEventListener('DOMContentLoaded', function() {//Runs when the site is loaded
getWebSiteContent();
});

Calling a Google Apps Script web app with access token

I need to execute a GAS service on behalf of a user that is logged to my system. So I have her/his access token. I would like somehow to transfer the token to the web app and without having to authorize again the user to use it for some activities. Can this be accomplished? Thank you.
EDIT: I think I didn't explain right what I try to accomplish. Here is the work flow I try to achieve:
We authorize a user visiting our website using OAuth2 and Google;
We get hold of her/his access token that Google returns;
There is a Google Apps Script web app that is executed as the user running the web app;
We want to call this app (3) by providing the access token (2) so Google not to ask again for authorization;
Actually, we want to call this app (3) not by redirecting the user to it but by calling it as a web service.
Thanks
Martin's answer worked for me in the end, but when I was making a prototype there was a major hurdle.
I needed to add the following scope manually, as the "automatic scope detection system" of google apps script did not ask for it: "https://www.googleapis.com/auth/drive.readonly". This resulted in UrlFetchApp.fetch always giving 401 with additional information I did not understand. Logging this additional information would show html, including the following string
Sorry, unable to open the file at this time.</p><p> Please check the address and try again.
I still don't really understand why "https://www.googleapis.com/auth/drive.readonly" would be necessary. It may have to do with the fact that we can use the /dev url, but who may use the /dev url is managed is checked using the drive permissions of the script file.
That said, the following setup then works for me (it also works with doGet etc, but I chose doPost). I chose to list the minimally needed scopes explicitly in the manifest file, but you can also make sure the calling script will ask for permissions to access drive in different ways. We have two google apps script projects, Caller and WebApp.
In the manifest file of Caller, i.e. appsscript.json
{
...
"oauthScopes":
[
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/script.external_request"]
}
In Code.gs of Caller
function controlCallSimpleService(){
var webAppUrl ='https://script.google.com/a/DOMAIN/macros/s/id123123123/exec';
// var webAppUrl =
// 'https://script.google.com/a/DOMAIN/macros/s/id1212121212/dev'
var token = ScriptApp.getOAuthToken();
var options = {
'method' : 'post'
, 'headers': {'Authorization': 'Bearer '+ token}
, muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(webAppUrl, options);
Logger.log(response.getContentText());
}
In Code.gs of WebApp (the web app being called)
function doPost(event){
return ContentService.createTextOutput("Hello World");
}
The hard answer is NO you can't use the built-in services of Apps Script with a service token. But if you already have the token for a user generated by a service account, access to the users data is pretty similar to any other language. All calls would be to the REST interface of the service your token is scoped for.
Take this small script for example. It will build a list of all the user's folders and return them as JSON:
function doGet(e){
var token = e.parameter.token;
var folderArray = [];
var pageToken = "";
var query = encodeURIComponent("mimeType = 'application/vnd.google-apps.folder'");
var params = {method:"GET",
contentType:'application/json',
headers:{Authorization:"Bearer "+token},
muteHttpExceptions:true
};
var url = "https://www.googleapis.com/drive/v2/files?q="+query;
do{
var results = UrlFetchApp.fetch(url,params);
if(results.getResponseCode() != 200){
Logger.log(results);
break;
}
var folders = JSON.parse(results.getContentText());
url = "https://www.googleapis.com/drive/v2/files?q="+query;
for(var i in folders.items){
folderArray.push({"name":folders.items[i].title, "id":folders.items[i].id})
}
pageToken = folders.nextPageToken;
url += "&pageToken="+encodeURIComponent(pageToken);
}while(pageToken != undefined)
var folderObj = {};
folderObj["folders"] = folderArray;
return ContentService.createTextOutput(JSON.stringify(folderObj)).setMimeType(ContentService.MimeType.JSON);
}
You do miss out on a lot of the convenience that makes Apps Script so powerful, mainly the built in services, but all functionality is available through the Google REST APIs.
I found a way! Just include the following header in the request:
Authorization: Bearer <user's_access_token>

Use Google Script's Web App as Webhook to receive Push Notification directly

My Goal: Changes in Google Drive => Push Notification to https://script.google.com/a/macros/my-domain/... => App is pushed to take action.
I don't want to setup an middle Webhook agent for receiving notification. Instead, let the Web App (by Google Script) to receive it and be pushed directly.
Since the relevant function is quite undocumented (just here: https://developers.google.com/drive/web/push) , below is the code I tried but failure.
1. Is above idea feasible??
2. My code doPost(R) seems cannot receive notification (R parameter) properly. Anyway, no response after I change the Google Drive. Any problem? (I have tried to log the input parameter R so as to see its real structure and decide if the parameter Obj for OAuth is the same as normal Drive App, but error occur before log)
function SetWatchByOnce(){
var Channel = {
'address': 'https://script.google.com/a/macros/my-domain/.../exec',
'type': 'web_hook',
'id': 'my-UUID'
};
var Result = Drive.Changes.watch(Channel);
...
}
function doPost(R) {
var SysEmail = "My Email";
MailApp.sendEmail(SysEmail, 'Testing ', 'Successfully to received Push Notification');
var Response = JSON.parse(R.parameters);
if (Response.kind == "drive#add") {
var FileId = Response.fileId;
MyFile = DriveApp.getFolderById(FileId);
...
}
}
function doGet(e) {
var HTMLToOutput;
var SysEmail = "My Email";
if (e.parameters.kind) {
//I think this part is not needed, since Push Notification by Drive is via Post, not Get. I should use onPost() to receive it. Right?
} else if (e.parameters.code) {
getAndStoreAccessToken(e.parameters.code);
HTMLToOutput = '<html><h1>App is successfully installed.</h1></html>';
} else { //we are starting from scratch or resetting
HTMLToOutput = "<html><h1>Install this App now...!</h1><a href='" + getURLForAuthorization() + "'>click here to start</a></html>";
}
return HtmlService.createHtmlOutput(HTMLToOutput);
}
....
Cloud Functions HTTP trigger(s) might also be an option ...
(which not yet existed at time of this question). this just requires setting the trigger URL as the notification URL, in the Google Drive settings - and adding some NodeJS code for the trigger; whatever it shall do. one can eg. send emails and/or FCM push notifications alike that. that trigger could also be triggered from App Script, with UrlFetchApp and there is the App Script API. one can have several triggers, which are performing different tasks (App Script is only one possibilty).
Cicada,
We have done similar functions to receive webhooks/API calls many times. Notes:
to get R, you need: var Response = R.parameters and then you can do Response.kind, Response.id, etc.
Logger will not work with doGet() and doPost(). I set it up a write to spreadsheet -- before any serious code. That way I know if it is getting triggered.