Wednesday, 21 August 2013

Using Google Drive as an Entry Point for Google Script

Scenario
We have a structured set of folders in Google Drive mapping to the organisation’s business functions. Think of the office filing cabinet.
There is a folder for each client. For control purposes and overall convenience, the clients are assigned to regional groups (folders again).
There are business operations that should be controlled to apply some records management auditability, and for which the user may require a guided path.
The documents on G Drive are essentially the business records (requests for action, review reports, closure etc) and form the usual source of information for staff working with the client. So if a staff member is working with a client, it is reasonable that they would be reviewing the records.

Scripting

There are several business actions that can be broadly visualised as a form entry, validation, response sequence. For example: Setting up a new client will require setting up a folder within the region folder; naming the folder; adding a structure of subfolders.
While we could maintain a database of clients and drive operations from that, the key data is actually in the base documents so we have chosen to script the business operations as functions of Drive itself. Our functions are create or open actions taken at the various folder levels in the structure and activated by a right click.

The Apps Script is built in 3 parts
  1. Integration with G-Drive which handles the initial authorisation; token handling; and calling either the open or create functions. This is based on an introduction that +Arun Nagarajan  put together and code samples from his team. A good startpoint is https://www.youtube.com/watch?v=R71oo-5NmPE. I have included my commented source below.
  2. Open and Create. Each of these produce a menu of operation that the user can chose from. These scripts receive the state parameter from G-Drive interface which allows the folder or files selected to be identified.
  3. Detail action scripts. These separate out the individual functions and simplify maintenance.



Source Code


/*  Integration of Google Drive with the Actions of CCSLT Document Manager
*The interconnection with Drive is based on Arun's DRIVE SDK + GAS Presentation and code
*Refresh token logic has been added to give continuous use capability
*Handles the installation for the user (execute script with no params and authorise through OAuth2 conversation
*/
// Globals used in api constructs
var AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth';
var TOKEN_URL = 'https://accounts.google.com/o/oauth2/token';
var REDIRECT_URL= ScriptApp.getService().getUrl();  // url of *this* script exec
var TOKENPROPERTYNAME = 'GOOGLE_OAUTH_TOKEN'; // access token valid until time expire or revoke by user
var REFRESHPROPERTYNAME = 'GOOGLE_OAUTH_REFRESH'; //oauth2 refresh token valid until revoked by user
var EXPIRYPROPERTYNAME = 'GOOGLE_OAUTH_EXPIRY' ; // expiry of oauth2 token time to do refresh!
// OAUTH2 data from API project - needs to be replaced if new project is created or project generates new secret/id
var CLIENT_ID = 'ghxcvhxcvhjdfshgjdsfahg';
var CLIENT_SECRET = 'fdsvvdfsh,gvsadhgfsdkj';
//
var DRIVE_API_URL = 'https://www.googleapis.com/drive/v2';
// maintain scope in step with changing application requirements
var SCOPE = 'https://www.googleapis.com/auth/drive.install https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/drive.metadata.readonly';
/* Main entry point
* functions depend on code and state parameters
* if state present ... app is installed
* createAction called from Google Drive when user selects DOCMAN app from type choice at CREATE menu state=create
* fileAction called from Google Drive when user selects DOCMAN app from type choice at file selection menu state otherwise
* if state not present then it is an installation and authorisation call
*/

function doGet(e) {
 var HTMLToOutput;
 // business operations of Drive API send state parameter, authentication returns code, initialisation has no parameters
 if(e.parameters.state){
   var state = JSON.parse(e.parameters.state);
   if(state.action === 'create'){
     // called as a result of  selection from CREATE menu of Google Drive user interface actually creates a UIApp
     return createAction(state);

   }
   else {
     // called as a result of selection from right click on file menu of Google Drive user interface
    // HTMLToOutput=fileAction(state);
    // return HtmlService.createHtmlOutput(HTMLToOutput)
    return fileAction(state);
   }
 }
 else if(e.parameters.code){//if we get "code" as a parameter in, then this is a callback from the install authorisation dance
   getAndStoreAccessToken(e.parameters.code);  // installer
   HTMLToOutput = '<html><h1>App is installed, you can close this window now or navigate to your <a href="https://drive.google.com">Google Drive</a>.</h1></html>';
 }
 else {//we are starting from scratch or resetting (result of running the /exec of this script)
   HTMLToOutput = "<html><h1>Install this App into your Google Drive!</h1><a href='"+getURLForAuthorization()+"'>click here to start</a></html>";
 }
 return HtmlService.createHtmlOutput(HTMLToOutput);
}
/*
* first step of OAUTH2 dance to get an authorisation code
*/
function getURLForAuthorization(){
 return AUTHORIZE_URL + '?' +
   'redirect_uri='+REDIRECT_URL +
     '&response_type=code' +
       '&client_id='+CLIENT_ID +
         '&approval_prompt=force'+
           '&scope=' + encodeURIComponent(SCOPE) +
             '&access_type=offline';
 
 
 
}
/*
* second step of  OAUTH2 dance to exchange authorisation code for access key and refresh key
*/
function getAndStoreAccessToken(code){
 var payload = "client_id=" + CLIENT_ID
 payload = payload + "&redirect_uri="+encodeURIComponent(REDIRECT_URL)
 payload = payload + "&client_secret="+CLIENT_SECRET
 payload = payload + "&code="+encodeURIComponent(code)
 payload = payload + "&scope=&grant_type=authorization_code"

 var parameters = {
   'method' : 'post',
   'contentType' : 'application/x-www-form-urlencoded',
   'payload' : payload
 };
 
 var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText();
  var tokenResponse = JSON.parse(response);
 // store the access token for later retrieval
 UserProperties.setProperty(TOKENPROPERTYNAME, tokenResponse.access_token);
 // store the refresh token for use when access token expires
 UserProperties.setProperty(REFRESHPROPERTYNAME, tokenResponse.refresh_token);
 // store the expiry time to determine when access token expires (expiry is returned as seconds to go - converted to UTC time in msecs)
 UserProperties.setProperty(EXPIRYPROPERTYNAME,tokenResponse.expires_in * 1000 +new Date().getTime());
}
/*
* Handles the token refresh function of OAUTH2 using saved refresh token
*/
function refreshAccessToken(){
 var payload = 'client_id=' +CLIENT_ID+
   '&client_secret='+CLIENT_SECRET+
     '&refresh_token='+UserProperties.getProperty(REFRESHPROPERTYNAME)+
       '&grant_type=refresh_token'
     
     var parameters = {
       'method' : 'post',
       'contentType' : 'application/x-www-form-urlencoded',
       'payload' : payload
     };
 
 var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText();

 var tokenResponse = JSON.parse(response);
 // store the token for later retrival - note refresh token does not expire
 UserProperties.setProperty(TOKENPROPERTYNAME, tokenResponse.access_token);
 UserProperties.setProperty(EXPIRYPROPERTYNAME,tokenResponse.expires_in * 1000 +new Date().getTime());
 return tokenResponse.access_token
}
/*
* Construct fetch options
*/

function getUrlFetchOptions() {
 return {'contentType' : 'application/json',
         'headers' : {'Authorization' : 'Bearer ' + isTokenValid,
                      'Accept' : 'application/json'}};
}
/*
* CHECK IF STORED token is valid, if not use refresh token to get new one
*/
function isTokenValid() {
 var now = new Date().getTime();
 var storedToken = UserProperties.getProperty(TOKENPROPERTYNAME);
 var storedRefresh = UserProperties.getProperty(REFRESHPROPERTYNAME);
 var expiry = UserProperties.getProperty(EXPIRYPROPERTYNAME);
 // if expired then refresh storedtoken
 if (expiry<= now){
   storedToken = refreshAccessToken();
 }
 
 return storedToken;

}

No comments: