Drive JSON API for apps script



In Using Drive SDK I described why you might want to use the SDK JSON API rather than the built in Apps Script Drive Class. Before you can start with this you need to authorize your access with OAUTH2, as all API requests need an access token. This one off exercise is straightforward and described here.

This page describes how to use the API from apps script in some detail. Nowadays there is built in access to Advanced Drive Service (this is not the same as the regular DriveApp service). All you have to is enable it in Advanced Services and the cloud console and authorize it, but this library uses the JSON API directly. 

The API can be a little intimidating and complicated, so I've created a library with most of the functions you'll need to do basic operations directly with the API. A couple of concepts to start.

Results Object

This is a standard format I return from most operations. It looks like this

    {
      success: boolean,   // whether the operation succeeeded
      data: object,       // the Parsed data returned from the API
      code: number,       // the HTTP response code
      url: string,        // the constructed url that was used         
      extended: string,   // any additional debugging information I've inserted
      content:string      // the unparsed content returned from the API
    };

Fields

Left to its own devices the API returns an awful lot of stuff, most of which you don't need. It provides a method of asking for partial responses. The methods I have implemented that return data from the API all accept an options parameter that can be used to limit the returned data to only what's needed. I recommend you use it.

Folders

Drive seems to be organized around a flat system of linked IDS rather than the hierarchical structure we are used to with most file systems. Folder structures are emulated through linking to parents and children. You'll see that this library provides methods for working with folder paths, which get translated into drive structures behind the scenes.

Metadata versus data

Most of the documentation here refers to the meta data associated with a file rather than the data itself. Strangely I took a while to actually find out how to do this. I've implemented simple uploading and getting, which you'll find documented close to the end of this page.

Getting started

Once you have set up your oAuth2 environment, you are ready to go.  In the documentation below, I'll show examples of how to use, and also sometimes show the code in the library for information. The full code of the latest library will be shown at the end, so there may have been some changes since I initially wrote this.

Getting a handle

It's just as well to set up the accessToken right from the start when you take out the handle

var dapi = new cDriveJsonApi.DriveJsonApi().setAccessToken(accessToken);


Get a file's metadata by ID

Each file has a unique ID. It's the same as the one in the Drive UI. 
var project = dapi.getFileById (id);

However, this will return a huge amount of stuff, so if you know which fields you want, its better to say. This applies to all methods in this documentation that take an optFields argument. I wont repeat this for each one.
var project = dapi.getFileById (id, "id,title,createdDate,modifiedDate,version,exportLinks");

library code
 /**
  * given a fileID, return its info
  * @param {string} id
  * @param {string} optFields
  * @return {object} the response
  */
  self.getFileById = function (id,optFields) {
    return self.urlGet(self.apiBase() + "/" + id + self.joinParams(self.fields(optFields)));
  };

Get the root folder's metadata

Folders in Drive are not really different than files, except for MimeType.
    var root = dapi.getRootFolder();

library code
  /**
   * get the root folder
   * @param {string} optFields the fields to return
   * @return {object} standard results object
   */
  self.getRootFolder = function (optFields) {
    return self.getFileById('root', optFields);
  };

ENUMS

I provide various useful ENUMS that can be access via
    var enums = dapi.getEnums();
library code
  var ENUMS =  { 
    MIMES: {
      SOURCE:"application/vnd.google-apps.script+json",
      SCRIPT:"application/vnd.google-apps.script",
      FOLDER:"application/vnd.google-apps.folder",
      AUDIO:"application/vnd.google-apps.audio",
      DOCUMENT:"application/vnd.google-apps.document",
      DRAWING:"application/vnd.google-apps.drawing",
      FILE:"application/vnd.google-apps.file",
      FORM:"application/vnd.google-apps.form",
      PHOTO:"application/vnd.google-apps.photo",
      PRESENTATION:"application/vnd.google-apps.presentation",
      SITES:"application/vnd.google-apps.sites",
      FUSIONTABLE:"application/vnd.google-apps.fusiontable",
      SPREADSHEET:"application/vnd.google-apps.spreadsheet",
      UNKNOWN:"application/vnd.google-apps.unknown",
      VIDEO:"application/vnd.google-apps.video"
    }
  };

Get a collection of items that match a query

The API provides a comprehensive searching mechanism as described here. This query returns just the ids of all scripts in my drive that are owned by me

var result = dapi.query (["mimeType='" + dapi.getEnums().MIMES.SCRIPT + "'","'me' in owners"].join(" and "), "items(id)");

library code
/**
   * do an api query
   * @param {string} qString the query string
   * @param {string} optFields a list of fields to include in the response(if missing then all fields)
   * @return {object} a standard results object
   */
  self.query = function (qString,optFields) {
  
    // apend constraint to exclude delted files
    qString += ((qString ? " and " : "") + "trashed=false");
    
    // do the query
    return self.urlGet( this.apiBase() + 
      self.joinParams(["q=" + encodeURIComponent(qString), self.fields(optFields)]));
  };
  

Get a collection of child folders

Getting a collection of child folders is just a specialized form of query which includes the parentId in its url.

var result = dapi.getChildItems (parentId ,  "items(id)");

library code
  /**
   * get child folders
   * @param {string} parentId the id of the parent
   * @param {string} optFields the fields to return
   * @param {Array.string} optExtraQueries
   */
  self.getChildFolders = function (parentId,optFields,optExtraQueries) {
    return self.getChildItems(parentId , self.getEnums().MIMES.FOLDER , optFields, optExtraQueries) ;
  };

Get a collection of folders by name

Getting a folder by name is just an additional query filter on getting a list of child folders.


var result = dapi.getFoldersByName (parentId , "somefoldername");

library code
  /**
   * get folders by name
   * @param {string} parentId the parentId
   * @param {string} name 
   * @param {string} optFields the fields to return
   */
  self.getFoldersByName = function (parentId, name , optFields) {
    return self.getChildFolders (parentId, optFields , "title='" + name + "'");
  }; 

Getting or creating a folder id by path 

If you are planning to organize by folders, which most of us still do, this is probably the most useful method, since it takes a path like /abc/def/ghi and retrieves the id of 'ghi'. Optionally it will also take care of creating 'abc' and 'def' and 'ghi' if they don't already exist.

get folder id or null
var folderId = dapi.getFolderFromPath('/abc/def/ghi');

get folder id, and create folders if needed
var folderId = dapi.getFolderFromPath('/abc/def/ghi', true);


library code
/**
   * return a folder id from a path like /abc/def/ghi
   * @param {string} path the path
   * @param {boolean} optCreate if true, then create it if it doesnt exist
   * @return {object} {id:'xxxx'} or null
   */
  self.getFolderFromPath = function (path,optCreate)  {
    return (path || "/").split("/").reduce ( function(prev,current) {
      if (prev && current) {
        // this gets the folder with the name of the current fragment
        var fldrs = self.getFoldersByName(prev.id,current,"items(id)");
        
        // see if it existed
        var f = fldrs.success && fldrs.data.items.length ? fldrs.data.items[0] : null;
        
        // if not then create it.
        if (!f && optCreate) {
        
          // create it and return the id of created folder
          var r = self.createFolder(prev.id , current,"id");
          if(r.success && r.data) { 
            f = r.data;
          }
        }
        return f;
      }
      else { 
        return current ? null : prev; 
      }
    },self.getRootFolder("id").data); 
  };
  




 

Creating a folder

Creating a folder needs the parentId of the folder that will hold it.

var result = dapi.createFolder (id , "afolder");

library code
 /**
  * create a folder
  * @param {string} parentId the folder parent id
  * @param {string} name the filename
  * @param {string} optFields optional return fields
  * @return }object} a standard result object
  */
  self.createFolder = function (parentId, name,optFields) {
     return self.createItem(parentId , name , self.getEnums().MIMES.FOLDER, optFields);
  };

Creating a item

This is a more generalized form of creating an Item. createAFolder uses it

var result = dapi.createItem (id , "afile");

library code
 /**
  * create an item
  * @param {string} parentId the folder parent id
  * @param {string} name the filename
  * @param {string} mime the mimetype,
  * @param {string} optFields optional return fields
  * @return {object} a standard result object
  */
  self.createItem = function (parentId, name,mime, optFields) {
     if(!parentId || typeof parentId !== "string") {
       throw 'parentId invalid for create item';
     }
     return self.urlPost (self.apiBase() + self.joinParams(self.fields(optFields),true) , {
       title:name,
       parents:[{id:parentId}],
       mimeType:mime
     });
  }

Get files by name, or create one if it doesn't exist

This will find a file of the given name, or create it if it doesn't exist.

var result = dapi.getFilesByNameOrCreate (parentId, "afile");

library code
  /**
   * get files by name or create
   * @param {string} parentId the parentId
   * @param {string} name the name
   * @param {string} optMime the mime type
   * @param {string} optFields the fields to return
   */
  self.getFilesByNameOrCreate = function (parentId, name , optMime, optFields) {
    var result = self.getChildItems (parentId, optMime, optFields , "title='" + name + "'");
    if (result.success && result.data && !result.data.items.length) {
      // lets create it.
      var r = self.createItem(parentId , name , optMime, "id");
      
      // double check to make sure it got created
      result = self.getChildItems (parentId, optMime, optFields , "title='" + name + "'");
    }
    return result;
  };


Recursively get all files in a folder and its sub folders that match a particular query

This will return the data of all script files owned by me under a particular folder and its sub folders

var result = dapi.getRecursiveChildItems (parentId , dapi.getEnums().MIMES.SCRIPT , "items(id)", "'me' in owners")

library code
 /**
   * get child items for all folders and sub folders
   * @param {string} parentId the id of the parent
   * @param {ENUM.MIMES} optMime the mime type
   * @param {Array.string} optExtraQueries
   * @param {object} standard result 
   */
  self.getRecursiveChildItems = function (parentId,mime,optFields,optExtraQueries) {
      
      var r = recurse (parentId, []);
      
      // hack result a bit to return consolidated items
      r.result.data.items = r.items; 
      return r.result;
      
      // recursive get contents of all the directories + scripts
      function recurse(id, items) {
        // get any scripts here
        var result = self.getChildItems (id, mime,"items(id)",optExtraQueries);

        // accumulate script files
        if(result.success) {
          cUseful.arrayAppend(items, result.data.items); 

          // now recurse any folders in this folder
          result = self.getChildFolders (id,"items(id)");

          if (result.success) {
            result.data.items.forEach(function(d) {
              recurse (d.id , items);
            });
          }
        }

        return {items:items , result:result} ;
      }
  }


Get the data in a file

Up till now we've been getting metadata from files. Eventually you'll want to actually get (and write) some data. Here's how.

var result = dapi.getContentById (fileId);

library code
  /**
   * get content of a file given its id
   * @param {string} id the id
   * @return {object} a standard result object
   */
  self.getContentById = function (id) {
    return self.urlGet(self.apiBase() + "/" + id + "?alt=media");
  };



Update the data in a file

You should first create or find the file using one of the methods above, then update it using its id.

var result = dapi.getContentById (fileId);

library code
  /**
   * put content of a file given its id
   * @param {string} id the id
   * @param {string || object} content the content
   * @return {object} a standard result object
   */
  self.putContentById = function (id,content) {
    return self.urlPost (self.apiBase("upload") + "/" + id + "?uploadType=media",content,"PUT");
  };

You'll find the other 'behind the scenes code' in the full source below


The code


 For more on drive SDK see Using Drive SDK

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, All formats are available from O'ReillyAmazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.

Comments