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)));
  };

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");

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

"use strict";

function getLibraryInfo () {

  return { 
    info: {
      name:'cDriveJsonApi',
      version:'0.0.7',
      key:'MvIo2UPbHoLDhAVcRHrI-VSz3TLx7pV4j',
      description:'drive sdk json API for apps script',
      share:'https://script.google.com/d/1P0ZbhWVxXcYU8kJxtpdzm_tNuoBa34NLAubBUgEqsW7-pvEg5NVppTyx/edit?usp=sharing'
    },
    dependencies:[
      cUseful.getLibraryInfo(),
      cUrlResult.getLibraryInfo()
    ]
  }; 
}

/**
 * just a wrapper to simplify the drive dapi access
 */
function DriveJsonApi () {

  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"
    }
  };
  

  var self = this;
  /** 
   * if you are using ezyauth2 this is usually set by 
   * DriverJsonApi.setAccessToken(doGetPattern({} , constructConsentScreen, function (token) { return token; },'script'))
   */
  self.accessToken= null;
  var lookAhead_ = function(response,attempt) {
    var code = response.getResponseCode();
    return (code === 500 &amp;&amp; attempt < 3 ) || code === 403;
  };
  
  /**
   * set a lookahead for a get
   * @param {function} fun the lookahead function
   * @return {cDriveJsonApi} self
   */
  self.setLookAhead = function (fun) {
    lookAhead_ = fun;
    return self;
  };
  
  self.getEnums = function () {
    return ENUMS;
  }
  /**
   * set the access token we'll use for accessing drive dapi
   * @param {string} accessToken
   * @return self
   */
  self.setAccessToken = function (accessToken) {
    if (!accessToken) throw 'accesstoken is not defined';
    self.accessToken = accessToken;
    return self;
  };
  
  self.updateAllParams = function () {
    return "updateViewedDate=false&amp;maxResults=1000";
  };

  /**
   * finalize the uri parameters
   * @param {Array.string} params
   * @param {boolean} optOmitStandard whether to omit appending standard parms
   * @return {string} the paramstring
   */
  self.joinParams = function (params, optOmitStandard) {
    var p = "";
    if (params &amp;&amp; Array.isArray(params) &amp;&amp; params.length) {
      p= "?" + 
        params.filter(function(d) {
          return d;
        })
        .join("&amp;");
    }
    else if (params) {
      p= "?" + params;
    }
    else {
      p= "";
    }
    
    // add standard param
    return optOmitStandard ? p : (p + (p ? "&amp;" : "?" ) + self.updateAllParams());
  }
  /**
   * 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)]));
  };
  
  /**
   * sort out fields
   * @param {string} optFields optional fields
   * @return {string} the fields parameter or ""
   */
  self.fields = function (optFields) {
    return (optFields ? ("fields=" + encodeURIComponent(optFields)) : "");
  };

  
 /**
  * given a result from getFileById, return its content
  * @param {object} resultOb a result object
  * @return {object} the content
  */
  self.getFileContentByOb = function (resultOb,optFields) {
  
    // mime types contain "'" so get rid of them
    var k = self.getEnums().MIMES.SOURCE;

    // check we have all we need to get the script content
    var success = resultOb.success &amp;&amp; resultOb.data.exportLinks &amp;&amp; resultOb.data.exportLinks[k];
    
    if (success) {
      // go and get the script content
      return self.urlGet (resultOb.data.exportLinks[k],self.joinParams(self.fields(optFields)));
    }
    else {
      // modify the result ob with failure
      resultOb.success = success;
      resultOb.extended = "couldnt find feed link exportlinks for " + k;
      return resultOb;
    }
  };

  
 /**
  * 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)));
  };
  
 /**
  * function the api base url
  * @param {string} optMod opertion that might chnage the normal base
  * @return {string} the api base url
  */
  self.apiBase = function (optMod) {
    op = optMod ? optMod + "/" : "";
    return "https://www.googleapis.com/" + op + "drive/v2/files";
  };

  /**
   * 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);
  };
  
  /**
   * 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,optExtraQueries) {
      
      // need at least items(id)

      fields="items(id)";
      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, fields,optExtraQueries);
        
        // accumulate script files
        if(result.success) {
          cUseful.arrayAppend(items, result.data.items); 

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

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

        return {items:items , result:result} ;
      }
  }
  /**
   * get child items
   * @param {string} parentId the id of the parent
   * @param {ENUM.MIMES} optMime the mime type
   * @param {string} optFields the fields to return
   * @param {Array.string} optExtraQueries
   */
  self.getChildItems = function (parentId,mime,optFields,optExtraQueries) {

    // add the folder filter
    var q= mime ? ["mimeType='" + mime + "'"] : [];
    
    // dont include anything deleted
    q.push("trashed=false");
    
    //plus any extra queries
    if(optExtraQueries) {
      var e = Array.isArray(optExtraQueries) ? optExtraQueries : [optExtraQueries];
      Array.prototype.push.apply (q,e);
    } 
   
    // do a query
    return self.urlGet (self.apiBase() + "/" + parentId + "/children" + 
      self.joinParams(["q="+encodeURIComponent(q.join(" and ")),self.fields(optFields)]));
  };
  
  /**
   * get files by name
   * @param {string} parentId the parentId
   * @param {string} name the name
   * @param {string} optMime the mime type
   * @param {string} optFields the fields to return
   */
  self.getFilesByName = function (parentId, name , optMime, optFields) {
    return self.getChildItems (parentId, optMime , optFields , "title='" + name + "'");
  }; 
 
  /**
   * 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");
  };

  /**
   * 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");
  };
  /**
   * 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 &amp;&amp; result.data &amp;&amp; !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;
  }; 
  
  /**
   * 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) ;
  };
  
 /**
  * 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);
  };
  
 /**
  * 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 a files parents
   * @param {string} id the files id
   * @param {string} optFields optional return fields
   * @return {object} a result object
   */
  self.getParents = function (id,optFields) {
    return self.urlGet (self.apiBase() + "/" + id + "/parents" + self.joinParams(self.fields(optFields)));
  }
  /**
   * 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 + "'");
  }; 
  
  /**
   * 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 &amp;&amp; current) {
        // this gets the folder with the name of the current fragment
        var fldrs = self.getFoldersByName(prev.id,current,"items(id)");
        if(!fldrs.success || true) {
          Logger.log(JSON.stringify(fldrs));
        }
        // see if it existed
        var f = fldrs.success &amp;&amp; fldrs.data.items.length ? fldrs.data.items[0] : null;
        
        // if not then create it.
        if (!f &amp;&amp; optCreate) {
        
          // create it and return the id of created folder
          var r = self.createFolder(prev.id , current,"id");
          if(r.success &amp;&amp; r.data) { 
            f = r.data;
          }
        }
        return f;
      }
      else { 
        return current ? null : prev; 
      }
    },self.getRootFolder("id").data); 
  };
  
  
 /**
  * execute a get
  * @param {string} url the url
  * @return {HTTPResponse}
  */
  self.urlGet = function (url) {
    return cUrlResult.urlGet(url, self.accessToken , undefined, lookAhead_);
  };

 /**
  * execute a post
  * @param {string} url the url
  * @param {object} payload the payload
  * @param {string} optMethod the method
  * @return {HTTPResponse}
  */
  self.urlPost = function (url,payload,optMethod) {
    return cUrlResult.urlPost(url, payload, optMethod, self.accessToken,undefined,lookAhead_);
  };

  /**
   * this is a standard result object to simply error checking etc.
   * @param {HTTPResponse} response the response from UrlFetchApp
   / @param {string} optUrl the url if given
   * @return {object} the result object
   */
  self.makeResults = function (response,optUrl) {
    return cUrlResult.makeResults(response, optUrl);
  };
}

For more on drive SDK see Using Drive SDK