This is part of the writeup on Using Google Cloud Storage with Apps Script. By now you should have completed all the steps in Setting up or creating a console project, Enabling APIs and OAuth2 and Using the service account to enable access to cloud storage and now have a project and credentials you can use to create your storage buckets. You can also read more about the background of how all this works at GcsStore overview – Google Cloud Storage and Apps Script and see some examples at GcsStore examples
CORS
If you are accessing Google Store objects (perhaps created with Apps Script) from client apps, then you are likely to run into Cross Origin Resource sharing problems. For more info on what this is see Cross Origin Resource sharing (CORS)
One way of getting round this is to create as proxy as described in Apps Script as a proxy, but this introduces a double hop for your data and another dependency. Google Cloud storage supports CORS, so you can use the GcsStore library to easily set that up from Apps Script.
Some examples
One of the places you’ll find this handy is if you use codepen.io or some other tools like that to test your client code. If you try to access data stored on google cloud storage, you’ll get the usual cross-domain origin error reported. We’ll use codepen as an example.
Write some data
This could be any kind of data, but for the test we’ll just write an object and pick it up from CodePen. I’m assuming you’re already familiar with GcsStore and have taklen care of setting up authentication as described in Setting up or creating a console project, Enabling APIs and OAuth2
Getting started
Let’s, first of all, initialize GcsStore on the bucket we’re sharing, using this code, changing the bucket name to yours.
function cors() { // test cors getting.setting var bucket = 'xliberation-store'; var packageName = bucket; // this consumes up the Oauth2 authentication that was set up once off var goa = cGoa.make (packageName,PropertiesService.getScriptProperties()); // you can get the project id & accessToken from goa var accessToken = goa.getToken(); // create a new store manager var gcs = new cGcsStore.GcsStore() .setAccessToken (accessToken) .setBucket(bucket);
Now we’ll write some data, to a particular folder, set it for public sharing, and get its public url
/// set up an object for public reading gcs.setFolderKey("corstest") .put("testkey",{hello:"there"}) .patchPredefinedAcl("testkey","publicRead"); // show its public link Logger.log(gcs.getPublicLink ("testkey"));
That will create this object in the cloud store
and log its public link
https://storage.googleapis.com/xliberation-store/corstest/testkey
Disabling CORS
Disabling cors is easy – just call patchCors() with no arguments
// to disable all cors sharing and show gcs.patchCors(); Logger.log(gcs.getCors());
You’ll get this logged
[]
Allow s.codepen.io to access this bucket for GET operations
// set one origin using defaults gcs.patchCors ("https://s.codepen.io"); Logger.log(gcs.getCors());
Logs
[{method=[GET], origin=[https://s.codepen.io], responseHeader=[Content-Type, Origin], maxAgeSeconds=1800}]
Client side Cors
Here’s how to use CORS with plain JavaScript. The example is in the codepen below
And reproduced here.
testCors(); function testCors() { doCors("https://storage.googleapis.com/xliberation-store/corstest/testkey") .then(function(response) { console.log(JSON.parse(response.responseText)); }) .catch(function(err) { console.log(err); }); } /** *do a cors request *@param {string} url the url *@param {string} method the method (default GET) *@param {*} payload the optional payload *@return {object} the response */ function doCors(url, method, payload) { return new Promise(function(resolve, reject) { return corsRequest(resolve, reject, url, method, payload); }); } /** *do a cors request *thanks to http://enable-cors.org/ for info on cors/html5 *@param {function} callback the load callback *@param {function} errorCallback the error callback *@param {string} url the url *@param {string} method the method (default GET) *@param {*} payload the optional payload *@return {object} the response */ function corsRequest(callback, errorCallback, url, method, payload) { // get the appropriate xhr var xhr = getXhr(); if (!xhr) throw 'cant do cors with this browser'; // now we can go xhr.open(method || "GET", url, true); // set up callbacks xhr.onload = function(evt) { // meed to catch this since it doesnt actually catch http errors if (evt.target.status < 200 || evt.target.status >= 300) { errorCallback(evt.target.responseText); } else { callback(evt.target); } } xhr.onerror = function(response) { errorCallback("request error- check that " + url + "exists and has CORS enabled"); } // execute return xhr.send(payload); /** * get the correct xhr object for the browser being used * @return {XDomainRequest|XMLHttpReQuest} the xhr */ function getXhr() { // likely to be this, unless its IE var xhr = new XMLHttpRequest(); return isDefined(xhr.withCredentials) ? xhr : (isDefined(XDomainRequest) ? new XDomainRequest() : undefined); } function isDefined(ob) { return typeof ob !== typeof undefined; } }
As you can see, I can now access my data from another domain without needing to use a proxy.
Object { hello: "there" }
Setting multiple origins and other options
GcsStore provides various defaults to simplify the setting of cors attributes. However you can get more complicated by providing some or all of the cors resource described here.
Some examples
// set multiple origins using defaults gcs.patchCors (["https://s.codepen.com","*.mcpher.com"]); Logger.log(gcs.getCors()); // specify full thing gcs.patchCors([{method:["GET"], origin:["http://xliberation.com"]}]); Logger.log(gcs.getCors()); // set even more things gcs.patchCors ([{ method:["GET","POST"], origin:["https://s.codepen.io"], responseHeader:["Content-Type","Origin"], maxAgeSeconds:3600 }]); Logger.log(gcs.getCors()); }
MaxAgeSeconds
This is how long the CORS preflight conversation is valid for. This means that you can disable CORS and find you can still access it for while. If you are debugging, it’s worth setting this to a lower number to clear previous settings still alive by virtue of the maxAgeSeconds property they were initiated with.