Skip to content

Instantly share code, notes, and snippets.

@DimuDesigns
Last active January 15, 2024 14:25
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save DimuDesigns/7b49c9fe4de79f851fbeaa4cb18d0126 to your computer and use it in GitHub Desktop.
Save DimuDesigns/7b49c9fe4de79f851fbeaa4cb18d0126 to your computer and use it in GitHub Desktop.
Drive Push Notification solution using Cloud Functions

Drive Push Notifications Using Cloud Functions

Setting up push notifications (via watch) for a file on Google Drive, with an App Script Web App as a web-hook end-point is a painful experience for the uninitiated.

There are 2 major hurdles to overcome:

  • Site verification/Domain registration for your web-hook endpoint
  • Retrieving HTTP request headers when your endpoint receives a message

The first is problematic since web app urls cannot be verified using the Search Console. The current work-around is to deploy the published web app as an add-on which somehow verifies the web app's url (as per +Spencer Easton's discoveries).

EDIT: For stand-alone scripts, there is an option under the Publish menu to register a published web app with chrome webstore. Once registered the web app url can be verified from the search console.

Which leads us into the second issue. The majority of notification messages generated and sent by 'watched' files do not have a POST body; message state is primarily stored as custom HTTP headers (whose names are pre-fixed with 'x-goog-...").

This is a critical issue since we cannot currently retrieve HTTP headers from incoming POST requests made to a Web App URL. There are no properties on the event object passed to a doPost() invocation that would allow you to inspect them.

Some solutions work around this limitation by switching from a 'Files' based watch to a 'Changes' based watch as it returns a POST body for a few of its events. But now you're tracking changes globally across your Drive without the ability to pinpoint the file that may have triggered the event. Other solutions fall back on time-based triggers to create a custom push notification flow.

Each of the aforementioned techniques has their pros and cons but I think I found a better way (its not without it's own drawbacks). This method leverages Firebase cloud hosting and cloud functions. But you must Enable Billing on your google platform account for this method to work. You get 2 million free requests per month before incurring any charges after which you pay $0.0000004 per invocation. That's a good trade-off in my book. (Ideally, we should be able to make calls to google owned domains for free but that doesn't seem to be the case here.)

This approach uses a cloud-function as a middle man to capture the incoming HTTP request headers and rePOST them to the web app url in a POST body. Using this method we can actually watch for changes in a specific File as opposed to watching for global changes in the Drive.

Here's a general guide:

Using firebase hosting create a site and verify via Search Console. The verification process will require that you upload a special html file to the site's public folder. You'll also need to register your domain (see Push Notification link at page bottom for details).

Firebase Hosting and Deployment
https://firebase.google.com/docs/hosting/deploying

Search Console https://www.google.com/webmasters/tools/home?hl=en

Prep the site to use cloud-functions and set up routes (via rewrites) so that a path on the site will trigger the function:

Firebase Cloud Function integration https://firebase.google.com/docs/hosting/functions

Next step, write a cloud function that will parse incoming request headers and re-POST them in the message body sent to your target Web App. If you're clever about it you can make it more dynamic by sending the web app url as part of the notification (via the token parameter - see Push Notifications documentation linked below).


RESOURCES

GAS files

Cloud Function files

Search Console verification HTML file

Useful links and resources:

{
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites":[{
"source":"/get_watch_headers", "function":"getWatchHeaders"
}]
}
}
google-site-verification: google565dfa980a81f792.html
const functions = require('firebase-functions');
/**
* Webhook endpoint for Google Drive "watch" notifications.
* Acts as a proxy ferrying Drive "watch" events to GSuite
* Web App.
*/
exports.getWatchHeaders = functions.https.onRequest((req, res) => {
var watchHeaders = {};
for (var key in req.headers) {
if (key.startsWith('x-goog-')) {
watchHeaders[key.replace('x-goog-','')] = req.headers[key];
}
}
var request = require('request');
request(
{
"uri":req.headers['x-goog-channel-token'],
"method":"POST",
"json":true,
"body":watchHeaders
},
(error, response, body) => {
if (!error && response.statusCode === 200) {
console.log(body);
res.status(200).send(body);
} else {
console.error(error);
}
}
);
});
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"dependencies": {
"firebase-admin": "~5.2.1",
"firebase-functions": "^0.6.2",
"request":"^2.81.0"
},
"private": true
}
/**
* Starts a watch notification.
* @see https://developers.google.com/drive/v3/web/push
*
* @param {String} fileId - Id of file to 'watch'
* @param {Number} duration - Watch duration measured in seconds (max 24hrs).
*
* @returns {Object} A resource object.
*/
function startWatch(fileId, duration) {
var channel = Drive.newChannel(),
duration = (duration || 60),
resource,
response;
// Required parameters
channel.id = Utilities.getUuid();
channel.type = "web_hook";
channel.address = "https://<YOUR-FIREBASE-DOMAIN>.firebaseapp.com/get_watch_headers";
// Optional parameters
channel.token = ScriptApp.getService().getUrl(); // assumes that your web app is deployed
channel.expiration = Date.now() + (duration * 1000);
response = UrlFetchApp.fetch(
Utilities.formatString("https://www.googleapis.com/drive/v3/files/%s/watch", fileId),
{
"method":"post",
"contentType":"application/json",
"headers":{
"Authorization":"Bearer " + ScriptApp.getOAuthToken()
},
"payload":JSON.stringify(channel)
}
);
resource = JSON.parse(response);
return resource;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment