My objective for a project I’m working on was to find a secure and simple way to publish ephemeral content without actually sharing files or hosting it anywhere.

I’m not going to lie – this has been a super difficult problem to solve. Here’s the Apps Script library I came up with.

Background

The project needed to find a way of securely sharing content with a 3rd party browser based app that normally needs its input data from an uploaded file. So the normal work flow would either be to download a file from Drive, then upload it or host the drive file somewhere that could serve it up to the app.

I use Google Forms data to create a network visualization of the responses, then export the result to Drive as a GraphML file. NodeXL, which is an Excel Add-on that runs on the desktop, usually consumes these kind of files.

This extra step was to be able to get an immediate vizualisation using a hosted version of gephi-lite and avoid all the downloading and uploading faff associated with a desktop application.

Objectives

This Drive data could be private, so I wanted the solution to meet these objectives

  • Create a link to content that would work from the browser, a Node or other server based app, from Apps Script and most importantly, from web based app without hitting CORS problems
  • Limit the longevity and number of times the link could be used
  • Avoid uploading the data to any hosting service
  • Avoid the need for login/auth while keeping the data private to the person creating the link in the frst place.

Solution

Workflow summary

  • The user creates a link request by passing over an access token, some optional metadata, and the content that will need served.
  • The ContentServer writes the content to the user cache belonging to the requesting user. It will also compress the content, and if necessary, it can also spread the compressed data across multiple physical cache entries.
  • This returns a public link that eventually leads to the original content. The link has a limited lifetime, and a limited number of usages set by the original creator. It also has various fingerprint validation checks built in. You can use this link without the need for login or auth to access.
  • Both the public and private link are one off – it will never generate the same link. The webapp link is useless without the url parameters, and will return an error. The url parameters are checked against the cache contents to ensure that the authorized cache entry is being accessed
  • The link is to a webapp, managed by the library which accesses metadata in the script cache. It doesn’t return anything directly to the user, but instead calls itself again, this time with the private link it found in the script cache, impersonating the original user that created the content.
  • This allows it to access the user cache, decompress the data and use Apps Script’s ContentService to return the original data.

Examples

Links to library etc, at bottom of article.

Hello world

const helloWorld = () => {

const link = Exports.ContentServe.toCache({
content: 'hello world',
accessToken: ScriptApp.getOAuthToken()
})

// tnis link can be used only once and will expire after a short time
// you can use it from the browser, to feed a webapp, serverapp - whatever
console.log (link)

}

// produces a link that looks like this
// https://script.google.com/macros/s/AKfycbzC9uU9IHqxpm8N7pQOT14IB_-FgUMyid3y-QXy0aGPQjumohqG78GJuRs_BAxR1sQ/exec?key=miPNcYyUacp823DZE-GmaZ60f3c=&nonce=1724938957009
hello world

Sharing the contents of a file

const fileContent = () => {

const small ="1-0nNGD1zcDd2t17dCR1HmVMtaQIwO60h"
const content = DriveApp.getFileById(small).getBlob().getDataAsString()

const link = Exports.ContentServe.toCache({
content,
accessToken: ScriptApp.getOAuthToken()
})

// tnis link can be used only once and will expire after a short time
// you can use it from the browser, to feed a webapp, serverapp - whatever
console.log (link)

}
// returned link
//https://script.google.com/macros/s/AKfycbzC9uU9IHqxpm8N7pQOT14IB_-FgUMyid3y-QXy0aGPQjumohqG78GJuRs_BAxR1sQ/exec?key=-QF1UHbPnqPASZa_7454Ao3hugQ=&nonce=1724939407745
File content

Serve json content

const jsonContent = () => {

const json ="1NYaGGZZXDCnBkxBKz9P0HmTRkbJfMq2r"
const content = DriveApp.getFileById(json).getBlob().getDataAsString()

const link = Exports.ContentServe.toCache({
content,
accessToken: ScriptApp.getOAuthToken(),
serveAs: "JSON"
})

// tnis link can be used only once and will expire after a short time
// you can use it from the browser, to feed a webapp, serverapp - whatever
console.log (link)

}
//https://script.google.com/macros/s/AKfycbzC9uU9IHqxpm8N7pQOT14IB_-FgUMyid3y-QXy0aGPQjumohqG78GJuRs_BAxR1sQ/exec?key=z663vbjR9YIDQKq3BahC_0sIEJs=&nonce=1724939868732
serve json

Allow more than 1 access and/or change the link lifetime

const moreHits = () => {

const json ="1NYaGGZZXDCnBkxBKz9P0HmTRkbJfMq2r"
const content = DriveApp.getFileById(json).getBlob().getDataAsString()

const link = Exports.ContentServe.toCache({
content,
accessToken: ScriptApp.getOAuthToken(),
serveAs: "JSON",
maxHits: 10,
timeToLive: 5* 60
})

// tnis link can be used only once and will expire after a short time
// you can use it from the browser, to feed a webapp, serverapp - whatever
console.log (link)

}

Probing

You can use probe on the returned links if you want to check on whether the link is live. Just add &probe=yes to the returned link. You’ll get this served as JSON

{"method":"hit","good":true,"expires":1724940830301,"probeInfo":{}}
probe response

Probing an expired link will return this

{"good":false}
probing an expired link

probeInfo

You can include probeInfo with the content – this will be returned in response to a probe request.

const probeInfo = () => {

const link = Exports.ContentServe.toCache({
content: 'hello world',
accessToken: ScriptApp.getOAuthToken(),
probeInfo: {
name: "hello world",
foo: "bar"
}
})

// tnis link can be used only once and will expire after a short time
// you can use it from the browser, to feed a webapp, serverapp - whatever
console.log (link)

}
// response
//{"method":"hit","good":true,"expires":1724941161486,"probeInfo":{"name":"hello world","foo":"bar"}}
probeInfo request

Note that probe requests never return content data, and only access the public cache. Accessing a probe counts as an access, if your use case includes probing then retrieving content, set the maxHits to more than 1.

Add-on usage example

For this project I needed to feed a web hosted (Gephi) with GraphML content from a Drive file created by the Add-on. Gephi normally expects files to be interactively uploaded, but it also takes a parameter to pick up a feed from a remotely hosted file.

I didn’t want to temporarily host this file anywhere, so using this one shot Content serve method was a good solution, especially since it defeats the CORS problem you would normally get with Apps Script content service in this scenario.

Here’s the card (it’s an editor Add-on) invites the user to do a quick online visualization of the GraphML file they’ve created in previous steps.

Vizualize on gephi-lite

Clicking on the “viz lite” button will prepare content taken from the graphML file on Drive, create one off links and startup gephi lite using the public link as input. Gephi will be able to read the file content without needing any oauth complexity. Each time you click a button it creates a new one-off link, which expires shortly afterwards or after it has been used once.

Links

To use this library you’ll just need the BmContentServer library ID

12ZqFY1xFQjjuF43JsvQbOASfPwuiazMKTTlwNSGgxyjHLaJ1HC_hu2w5

The code on Github if you are interested.

Add an Exports script file in your project with this content.

const Exports = {
get ContentServe () {
return BmContentServer.Exports.ContentServe
}
}
Exports script

The test code mentioned in this article is here or on github.

Related

Here are a some other articles that could provide some background on some of the code used in this library