Google Drive SDK

Example Drive App: DrEdit for Ruby

DrEdit is a sample Google Drive app written in Ruby. It is a text editor capable of editing files with the MIME types text/* and extensions such as .txt, .html etc. that are stored in a user's Google Drive. This article describes the complete application to help you in your integrations.

Setting up this sample application

Setting up DrEdit requires you to perform some configuration. Follow the instructions in the repository's README to set up the application.

Architecture

DrEdit is a web application written in Ruby using the Sinatra web framework. With two endpoints:

  • User interface at /

    The user interface is rendered including HTML, JavaScript, and CSS. It is built with AngularJS, Bootstrap, and ACE

  • Ajax service to communicate with Drive at /svc, /about, and /user

    Drive API requests are made in response to GET, PUT, and POST requests and return JSON data.

DrEdit implements several Google Drive use cases:

  1. Create New file from the Google Drive UI
  2. Open with file from the Google Drive UI
  3. Open files with the Picker

The flow for both use cases is similar. A user is redirected to DrEdit after selecting DrEdit from the Create menu or context menu of a file with a registered MIME type.

This redirect takes place with two important parts of data as query parameters:

  1. An authorization code, capable of being exchanged for an access token to access the user's data.
  2. A state object describing the user's action, i.e. which file(s) and which action (open, create) to perform.

DrEdit responds by performing the required action on the passed files using the authorization credentials. The authorization credentials are stored in a session for future use by Ajax requests from the user interface.

Authorization

In order for DrEdit to be able to make calls to the Drive API, an authorized API client must be created. The first requirement, using the Google API Ruby Client is to get the credentials of the user to authorize the client.

These credentials can be loaded from:

  1. A code passed from the Google Drive UI
  2. The user's session

The choice of credential source is determined by the action that DrEdit is performing. In cases where the user has been redirected from the Drive user interface, an authorization code is always provided and DrEdit always uses it. In cases where the user is making Ajax calls from the user interface, the credentials are loaded from a session.

Setting up the client ID, client secret, and other OAuth 2.0 parameters

Google Drive applications need these three values for authorization:

  1. Client ID
  2. Client secret
  3. List of valid redirect URIs

The client ID and client secret for an application are created when an application is registered in the Google APIs Console and the OAuth 2.0 client IDs are generated. These can be viewed in API Access tab of a project.

DrEdit stores these settings in the file client_secrets.json. The DrEdit source download includes a file named client_secrets.json.example. This example file must be copied to client_secrets.json and the contents modified to reflect an application's own client ID, client secret, and list of valid redirect URIs. Developers normally do not need to change the auth_uri or token_uri parameters.

{
  "web": {
    "client_id": "abc123456789.apps.googleusercontent.com",
    "client_secret": "def987654321",
    "redirect_uris": ["https://my-dredit-instance.herokuapp.com"],
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://accounts.google.com/o/oauth2/token"
  }
}

Authorizing a code passed from the Google Drive UI

After a user chooses DrEdit to open or create a file, the user's browser is redirected to DrEdit's registered redirect URI (set in the Google Developers Console), with an OAuth 2.0 authorization code attached as the code query parameter in the URI. The given authorization code is scoped specifically to open the single file (Open With) or to create any file (Create New), and must be exchanged for an access token with the OAuth 2.0 web server flow.

Once this token is obtained, it is used to fetch the user profile information from the Userinfo service.

# Preload API definitions
client = Google::APIClient.new
set :drive, client.discovered_api('drive', 'v2')
set :oauth2, client.discovered_api('oauth2', 'v2')

##
# Exchanges the authorization code to fetch an access
# and refresh token. Stores the retrieved tokens in the session.
def authorize_code(code)
  api_client.authorization.code = code
  api_client.authorization.fetch_access_token!
  # put the tokens in the session
  session[:access_token] = api_client.authorization.access_token
  session[:refresh_token] = api_client.authorization.refresh_token
  session[:expires_in] = api_client.authorization.expires_in
  session[:issued_at] = api_client.authorization.issued_at
end

##
# Main entry point for the app. Ensures the user is authorized
# & inits the editor for either edit of the opened files or creating
# a new file.
get '/' do
  # handle possible callback from OAuth2 consent page.
  if params[:code]
    authorize_code(params[:code])
    redirect '/'
  elsif params[:error] # User denied the oauth grant
    halt 403
  end

  # If not authorized, redirect to OAuth2 consent page.
  redirect api_client.authorization.authorization_uri.to_s unless authorized?

  # render the homepage here

Loading credentials from the session

Once the user interface has loaded, it communicates with DrEdit over the /svc endpoint. These requests use credentials that were earlier stored against the session.

before do
  # Make sure access token is up to date for each request
  api_client.authorization.update_token!(session)

  # if existing access token is expired and refresh token is set,
  # ask for a new access token.
  if api_client.authorization.refresh_token &&
    api_client.authorization.expired?
    api_client.authorization.fetch_access_token!
  end
end

Failing to authorize

There are a number of points at which the authorization process can fail. In all these cases, the user is redirected to the OAuth 2.0 authorization endpoint so that they can reauthorize and return to DrEdit.

Handling HTTP requests

This section describes how DrEdit handles HTTP requests for operations such as creating and opening new files and rendering the user interface.

Requests to the user interface

The user interface is provided as a single HTML template which is used for both opening (Open with) and creating new files. Once loaded, the user interface loads any data required.

The text editing component uses the Ace Editor, a text-editing component with syntax highlighting and configurable key bindings. It is used in DrEdit as a form field with advanced editing features.

Users may arrive at the / endpoint from two separate sources:

  1. Drive User interface, along with Drive State
  2. By directly accessing the URL, with no Drive State

Loading the Drive state

DrEdit contains a helper class for parsing and loading the Drive state. The state parameter is a JSON string containing two important properties:

  1. The action to be performed, create or open in the action field
  2. The file IDs (if any) to perform the action on in the ids field

DrEdit uses this state to determine how it should behave. If file ids are provided they are loaded immediately after the user interface is loaded.

Rendering the user interface

The user interface is rendered in response to GET requests. The Drive state is loaded and the template is rendered. These is no communication with the Drive API at this stage.

##
# Main entry point for the app. Ensures the user is authorized
# & inits the editor for either edit of the opened files or creating
# a new file.
get '/' do
  # handle possible callback from OAuth2 consent page.
  if params[:code]
    authorize_code(params[:code])
    redirect '/'
  elsif params[:error] # User denied the oauth grant
    halt 403
  end

  # If not authorized, redirect to OAuth2 consent page.
  redirect api_client.authorization.authorization_uri.to_s unless authorized?

  if params[:state]
    state = MultiJson.decode(params[:state] || '{}')
    if state['parentId']
      redirect to("/#/create/#{state['parentId']}")
    else
      doc_id = state['ids'] ? state['ids'].first : ''
      redirect to("/#/edit/#{doc_id}")
    end
  end

  # render index.html
  return File.read(File.join(settings.public_folder, 'public/index.html'))
end

Requests to the API service

The /svc endpoint allows three HTTP methods.

  • GET: Fetches file metadata and content from Google Drive.
  • POST: Creates new files in Google Drive.
  • PUT: Updates existing files in Google Drive.

Data is returned as JSON with the application/json content-type and when data is required (in POST and PUT requests) it is provided from the user interface as JSON with the application/json content-type.

Fetching file metadata and content from Google Drive

The file is retrieved in two steps. First, the metadata is fetched using the files.get method, passing the file_id that was sent in the initial redirect. Second, the file content is fetched by making a simple authorized GET request to the file's downloadUrl.

###
# Gets file metadata and contents.
#
get '/svc' do
  result = api_client.execute!(
    :api_method => settings.drive.files.get,
    :parameters => { :fileId => params['file_id'] })
  file = result.data.to_hash
  result = api_client.execute(:uri => result.data.downloadUrl)
  file['content'] = result.body
  json file
end

Saving files

When the user clicks Save from the DrEdit user interface, an AJAX request is made from the user interface to the server. This request is either:

  • POST: indicating that a new file is to be created.
  • PUT: indicating that an existing file is to be updated, or

These methods match standard REST semantics, and are convenient because both can be represented as a single URL which handles different HTTP methods.

The data is sent as JSON containing the following fields:

  • title: Title of the file.
  • description: Description of the file.
  • content: Content of the file, i.e., the content from the text editor.
  • file_id: File ID of the file, or an empty string if this is a new file.

Both PUT and POST methods return a JSON response that contains the file ID of the saved or created file. The file ID is stored in the user interface, and is sent along with future requests.

Saving new files

The POST method is used to save newly created files. A files.insert call is made to create the new file and set the metadata.

##
# Creates a new file.
post '/svc' do
  _, file, content = prepare_data(request.body)
  result = api_client.execute!(
    :api_method => settings.drive.files.insert,
    :body_object => file,
    :media => content,
    :parameters => {
      'uploadType' => 'multipart',
      'alt' => 'json'})
  json result.data.id
end

Updating files

The process for updating a file is similar to that of creating one. File updates are made using an HTTP PUT instead of an HTTP POST.

Unlike when creating files, the files.update method of the API is used, and this requires the file ID to be passed as the id parameter.

Additionally, a newRevision parameter is passed as a boolean, to indicate whether the update should be marked as a new revision in the Drive user interface.

##
# Updates existing file metadata and contents.
put '/svc' do
  resource_id, file, content = prepare_data(request.body)
  result = api_client.execute(
    :api_method => settings.drive.files.update,
    :body_object => file,
    :media => content,
    :parameters => {
      'fileId' => resource_id,
      'newRevision' => params['newRevision'] || 'false',
      'uploadType' => 'multipart',
      'alt' => 'json' }
  )
  json result.data.id
end

Responding with the file ID

Both creating new files and updating existing files returns a JSON response containing the file ID of the file that has been created or modified. The user interface can use this file ID to ensure that future saves are to the same file, and that a new file is not created each time.

Personalizing the user interface

Additional services are provided to customize the user interface for the authorized user. This has the benefit of providing a familiar and personalized experience for the user.

About service

The /about endpoint allows one HTTP method.

  • GET: Fetches information about the authorized user and their account.

This service returns information about the currently authorized user and settings for their account. This service fetches and returns the JSON resource representation described in about.get.

###
# Gets about.
#
get '/about' do
  result = api_client.execute!(:api_method => settings.drive.about.get)
  json result.data
end

User service

The /user endpoint allows one HTTP method.

  • GET: Fetches information about the user

This service returns information about the authenticated user using the UserInfo API.

###
# Gets the current user profile.
#
get '/user' do
  result = api_client.execute!(:api_method => settings.oauth2.userinfo.get)
  json result.data
end

Additional resources

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.