Hermes/IBRAHIM_MKUSA.md

6.2 KiB

Interface to Google Drive in Racket

Fred Martin

April 22, 2017

Overview

This set of code provides an interface to searching through one's Google Drive account. Its most important feature is that it provides a folder-delimited search.

The essential model of files in Google Drive is that they are in one big “pile.” So you can't directly find a file in a given folder.

This code recursively collects all folders found within a given folder, and then construct a search query that includes a list of all the subfolders (flattened into a single list).

This then allows you to perform a folder-delimited search.

Authorship note: All of the code described here was written by myself.

Libraries Used

The code uses four libraries:

(require net/url)
(require (planet ryanc/webapi:1:=1/oauth2))
(require json)
(require net/uri-codec)
  • The net/url library provides the ability to make REST-style https queries to the Google Drive API.
  • Ryan Culpepper's webapi library is used to provide the oauth2 interface required for authentication.
  • The json library is used to parse the replies from the Google Drive API.
  • The net/uri-codec library is used to format parameters provided in API calls into an ASCII encoding used by Google Drive.

Key Code Excerpts

Here is a discussion of the most essential procedures, including a description of how they embody ideas from UMass Lowell's COMP.3010 Organization of Programming languages course.

Five examples are shown and they are individually numbered.

1. Initialization using a Global Object

The following code creates a global object, drive-client that is used in each of the subsequent API calls:

(define drive-client
  (oauth2-client
   #:id "548798434144-6s8abp8aiqh99bthfptv1cc4qotlllj6.apps.googleusercontent.com"
   #:secret "<email me for secret if you want to use my API>"))

While using global objects is not a central theme in the course, it's necessary to show this code to understand the later examples.

2. Selectors and Predicates using Procedural Abstraction

A set of procedures was created to operate on the core drive-file object. Drive-files may be either actual file objects or folder objects. In Racket, they are represented as a hash table.

folder? accepts a drive-file, inspects its mimeType, and returns #t or #f:

(define (folder? drive-file)
  (string=? (hash-ref drive-file 'mimeType "nope") "application/vnd.google-apps.folder"))

Another object produced by the Google Drive API is a list of drive-file objects ("drive#fileList"). When converted by the JSON library, this list appears as hash map.

get-files retrieves a list of the files themselves, and get-id retrieves the unique ID associated with a drive#fileList object:

(define (get-files obj)
  (hash-ref obj 'files))

(define (get-id obj)
  (hash-ref obj 'id))

3. Using Recursion to Accumulate Results

The low-level routine for interacting with Google Drive is named list-children. This accepts an ID of a folder object, and optionally, a token for which page of results to produce.

A lot of the work here has to do with pagination. Because it's a web interface, one can only obtain a page of results at a time. So it's necessary to step through each page. When a page is returned, it includes a token for getting the next page. The list-children just gets one page:

(define (list-children folder-id . next-page-token)
  (read-json
   (get-pure-port
    (string->url (string-append "https://www.googleapis.com/drive/v3/files?"
                                "q='" folder-id "'+in+parents"
                                "&key=" (send drive-client get-id)
                                (if (= 1 (length next-page-token))
                                    (string-append "&pageToken=" (car next-page-token))
                                    "")
;                                "&pageSize=5"
                                ))
    token)))

The interesting routine is list-all-children. This routine is directly invoked by the user. It optionally accepts a page token; when it's used at top level this parameter will be null.

The routine uses let* to retrieve one page of results (using the above list-children procedure) and also possibly obtain a token for the next page.

If there is a need to get more pages, the routine uses append to pre-pend the current results with a recursive call to get the next page (and possibly more pages).

Ultimately, when there are no more pages to be had, the routine terminates and returns the current page.

This then generates a recursive process from the recursive definition.

(define (list-all-children folder-id . next-page-token)
  (let* ((this-page (if (= 0 (length next-page-token))
                      (list-children folder-id)
                      (list-children folder-id (car next-page-token))))
         (page-token (hash-ref this-page 'nextPageToken #f)))
    (if page-token
        (append (get-files this-page)
              (list-all-children folder-id page-token))
        (get-files this-page))))

4. Filtering a List of File Objects for Only Those of Folder Type

The list-all-children procedure creates a list of all objects contained within a given folder. These objects include the files themselves and other folders.

The filter abstraction is then used with the folder? predicate to make a list of subfolders contained in a given folder:

(define (list-folders folder-id)
  (filter folder? (list-all-children folder-id)))

5. Recursive Descent on a Folder Hierarchy

These procedures are used together in list-all-folders, which accepts a folder ID and recursively obtains the folders at the current level and then recursively calls itself to descend completely into the folder hierarchy.

map and flatten are used to accomplish the recursive descent:

(define (list-all-folders folder-id)
  (let ((this-level (list-folders folder-id)))
    (begin
      (display (length this-level)) (display "... ")
      (append this-level
              (flatten (map list-all-folders (map get-id this-level)))))))