Google Drive SDK

Example Drive App: DrEdit for .NET

DrEdit is a sample Google Drive app written in C# using the ASP.NET MVC 3 web framework. 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 using ASP.NET MVC 3 and performing some configuration. Follow the instructions in the repository's README to set up the application.

Features

DrEdit is a web application written in C# using the ASP.NET MVC 3 web framework. The app uses these two types of endpoints:

  • User interface at /

    The user interface is rendered including HTML, JavaScript, and CSS. Requests to this endpoint are handled by the DriveController class.

  • AJAX services 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. Requests to these endpoints are handled by the svcController, aboutController and userController class respectively.

DrEdit implements two Google Drive use cases:

  1. Create a new file from the Google Drive UI
  2. Open a file from the Google Drive UI

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 APIs Client Library for .NET 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. In DrEdit for .NET, these values are stored in Models\ClientCredentials.cs.

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 APIs 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. The IAuthenticator instance (i.e. the user's credentials), along with the authorized DriveService are stored in the session for later use.

IAuthenticator authenticator = Utils.GetCredentials(code, state);
// Store the authenticator and the authorized service in session
Session["authenticator"] = authenticator;
Session["service"] = Utils.BuildService(authenticator);

The Utils class is responsible for exchanging the authorization code for an access token:

/// <summary>
/// Retrieve credentials using the provided authorization code.
///
/// This function exchanges the authorization code for an access token and
/// queries the User Info API to retrieve the user's e-mail address. If a
/// refresh token has been retrieved along with an access token, it is
/// stored in the application database using the user's e-mail address as
/// key. If no refresh token has been retrieved, the function checks in
/// the application database for one and returns it if found or throws a
/// NoRefreshTokenException with the authorization URL to redirect
/// the user to.
/// </summary>
/// <param name="authorizationCode">Authorization code to use to retrieve
/// an access token.</param>
/// <param name="state">State to set to the authorization URL in case
/// of error.</param>
/// <returns>OAuth 2.0 credentials instance containing an access and
/// refresh token.</returns>
/// <exception cref="CodeExchangeException">
/// An error occurred while exchanging the authorization code.
/// </exception>
/// <exception cref="NoRefreshTokenException">
/// No refresh token could be retrieved from the available sources.
/// </exception>
public static IAuthenticator GetCredentials(String authorizationCode, String state)
{
    String emailAddress = "";
    try
    {
        IAuthorizationState credentials = ExchangeCode(authorizationCode);
        Userinfo userInfo = GetUserInfo(credentials);
        String userId = userInfo.Id;
        emailAddress = userInfo.Email;
        if (!String.IsNullOrEmpty(credentials.RefreshToken))
        {
            StoreCredentials(userId, credentials);
            return GetAuthenticatorFromState(credentials);
        }
        else
        {
            credentials = GetStoredCredentials(userId);
            if (credentials != null &&
                !String.IsNullOrEmpty(credentials.RefreshToken))
            {
                return GetAuthenticatorFromState(credentials);
            }
        }
    }
    catch (CodeExchangeException e)
    {
        Console.WriteLine("An error occurred during code exchange.");
        // Drive apps should try to retrieve the user and credentials for
        // the current session.
        // If none is available, redirect the user to the authorization URL.
        e.AuthorizationUrl = GetAuthorizationUrl(emailAddress, state);
        throw e;
    }
    catch (NoUserIdException)
    {
        Console.WriteLine("No user ID could be retrieved.");
    }
    // No refresh token has been retrieved.
    String authorizationUrl = GetAuthorizationUrl(emailAddress, state);
    throw new NoRefreshTokenException(authorizationUrl);
}

/// <summary>
/// Exchange an authorization code for OAuth 2.0 credentials.
/// </summary>
/// <param name="authorizationCode">Authorization code to exchange
//// for OAuth 2.0 credentials.</param>
/// <returns>OAuth 2.0 credentials.</returns>
/// <exception cref="CodeExchangeException">An error occurred.</exception>
static IAuthorizationState ExchangeCode(String authorizationCode)
{
    var provider = new NativeApplicationClient(
        GoogleAuthenticationServer.Description,
        ClientCredentials.CLIENT_ID,
        ClientCredentials.CLIENT_SECRET);
    IAuthorizationState state = new AuthorizationState();
    state.Callback = new Uri(ClientCredentials.REDIRECT_URI);
    try
    {
        state = provider.ProcessUserAuthorization(authorizationCode, state);
        return state;
    }
    catch (ProtocolException)
    {
        throw new CodeExchangeException(null);
    }
}

/// <summary>
/// Send a request to the User Info API to retrieve the user's information.
/// </summary>
/// <param name="credentials">OAuth 2.0 credentials to authorize
//// the request.</param>
/// <returns>User's information.</returns>
/// <exception cref="NoUserIdException">An error occurred.</exception>
static Userinfo GetUserInfo(IAuthenticator credentials)
{
    Oauth2Service userInfoService = new Oauth2Service(credentials);
    Userinfo userInfo = null;
    try
    {
        userInfo = userInfoService.Userinfo.Get().Fetch();
    }
    catch (GoogleApiRequestException e)
    {
        Console.WriteLine("An error occurred: " + e.Message);
    }

    if (userInfo != null && !String.IsNullOrEmpty(userInfo.Id))
    {
        return userInfo;
    }
    else
    {
        throw new NoUserIdException();
    }
}

/// <summary>
/// Retrieve an IAuthenticator instance using the provided state.
/// </summary>
/// <param name="credentials">OAuth 2.0 credentials to use.</param>
/// <returns>Authenticator using the provided OAuth 2.0
/// credentials</returns>
public static IAuthenticator GetAuthenticatorFromState(
    IAuthorizationState credentials)
{
    var provider = new StoredStateClient(
        GoogleAuthenticationServer.Description, ClientCredentials.CLIENT_ID,
        ClientCredentials.CLIENT_SECRET, credentials);
    var auth = new OAuth2Authenticator<StoredStateClient>(
        provider,StoredStateClient.GetState);
    auth.LoadAccessToken();
    return auth;
}

Loading credentials from the session

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

public JsonResult svc(string file_id)
{
    ...

    IAuthenticator authenticator =
        Session["authenticator"] as IAuthenticator;
    DriveService service = Session["service"] as DriveService;

    if (authenticator == null || service == null)
    {
        // redirect user to authentication
    }

    ...
}

Storing and retrieving credentials from the database

Google Drive applications only receive a long-lived refresh token during the very first exchange of an authorization code. This refresh token is used to request new access tokens after a short-lived access token expires, and it must be stored in the application's database to be retrieved every time the user returns to the app.

DrEdit relies on the Entity Framework to map model classes into database entities stored by SQL Server Compact:

public class StoredCredentials
{
    [Key]
    public string UserId { get; set; }

    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
}

public class StoredCredentialsDBContext : DbContext
{
    public DbSet<StoredCredentials> StoredCredentialSet { get; set; }
}

The Utils class defines two methods to respectively store and retrieve credentials from the database:

/// <summary>
/// Store OAuth 2.0 credentials in the application's database.
/// </summary>
/// <param name="userId">User's ID.</param>
/// <param name="credentials">The OAuth 2.0 credentials to store.</param>
static void StoreCredentials(String userId, IAuthorizationState credentials)
{
    StoredCredentialsDBContext db = new StoredCredentialsDBContext();
    StoredCredentials sc =
        db.StoredCredentialSet.FirstOrDefault(x => x.UserId == userId);
    if (sc != null)
    {
        sc.AccessToken = credentials.AccessToken;
        sc.RefreshToken = credentials.RefreshToken;
    }
    else
    {
        db.StoredCredentialSet.Add(new StoredCredentials {
            UserId = userId,
            AccessToken = credentials.AccessToken,
            RefreshToken = credentials.RefreshToken });
    }
    db.SaveChanges();
}

/// <summary>
/// Retrieved stored credentials for the provided user ID.
/// </summary>
/// <param name="userId">User's ID.</param>
/// <returns>Stored GoogleAccessProtectedResource if found,
/// null otherwise.</returns>
static IAuthorizationState GetStoredCredentials(String userId)
{
    StoredCredentialsDBContext db = new StoredCredentialsDBContext();
    StoredCredentials sc =
        db.StoredCredentialSet.FirstOrDefault(x => x.UserId == userId);
    if (sc != null)
    {
        return new AuthorizationState() {
            AccessToken = sc.AccessToken,
            RefreshToken = sc.RefreshToken };
    }
    return null;
}

Failing to authorize

There are a number of points at which the authorization process can fail while retrieving credentials. DrEdit throws two different exceptions according to the type of error:

  • CodeExchangeException is thrown when a code exchange has failed.
  • NoRefreshTokenException is thrown when no refresh token has been found.

In both cases, the user is redirected to the OAuth 2.0 authorization endpoint so that they can reauthorize and return to DrEdit.

public ActionResult Index(string state, string code)
{
    try
    {
        IAuthenticator authenticator = Utils.GetCredentials(code, state);
        // Store the authenticator and the authorized service in session
        Session["authenticator"] = authenticator;
        Session["service"] = Utils.BuildService(authenticator);
    }
    catch (CodeExchangeException)
    {
        if (Session["service"] == null || Session["authenticator"] == null)
        {
            Response.Redirect(Utils.GetAuthorizationUrl("", state));
        }
    }
    catch (NoRefreshTokenException e)
    {
        Response.Redirect(e.AuthorizationUrl);
    }

    ...
}

Making authorized API requests

Once the access token has been retrieved, it is used to authorize the DriveService that will be used for making API requests.

/// <summary>
/// Build a Drive service object.
/// </summary>
/// <param name="credentials">OAuth 2.0 credentials.</param>
/// <returns>Drive service object.</returns>
internal static Google.Apis.Drive.v2.DriveService BuildService(
    IAuthenticator credentials)
{
    return new Google.Apis.Drive.v2.DriveService(credentials);
}

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, an editor 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.

public ActionResult Index(string state, string code)
{
    ...

    DriveState driveState = new DriveState();

    if (!string.IsNullOrEmpty(state))
    {
        JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
        driveState = jsonSerializer.Deserialize<DriveState>(state);
    }

    if (driveState.action == "open")
    {
        return OpenWith(driveState);
    }
    else
    {
        return CreateNew(driveState);
    }
}

public class DriveState
{
    public string action { get; set; }
    public string parentId { get; set; }
    public string[] ids { get; set; }
}

Rendering the user interface

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

The DriveController renders the same view for both the Open with and the Create New scenarios, the only difference being the value of the ViewBag.FileIds property, which contains the file id(s) that have been sent from the Drive user interface when opening existing files.

private ActionResult OpenWith(DriveState state)
{
    ViewBag.FileIds = state.ids;
    return View();
}

private ActionResult CreateNew(DriveState state)
{
    ViewBag.FileIds = new string[] {""};
    return View();
}

Requests to the Files 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. Both metadata and file content are used to instantiate a DriveFile object that is then serialized into JSON and returned to the user.

//
// GET: /svc

public JsonResult svc(string file_id)
{
    if (string.IsNullOrWhiteSpace(file_id))
    {
        return Json(null, JsonRequestBehavior.AllowGet);
    }

    IAuthenticator authenticator =
        Session["authenticator"] as IAuthenticator;
    DriveService service = Session["service"] as DriveService;

    if (authenticator == null || service == null)
    {
        // redirect user to authentication
    }

    Google.Apis.Drive.v2.Data.File file =
        service.Files.Get(file_id).Fetch();
    string data = Utils.DownloadFile(authenticator, file.DownloadUrl);
    DriveFile df = new DriveFile(file, data);
    return Json(df, JsonRequestBehavior.AllowGet);
}

The file content is retrieved by the Utils.DownloadFile helper method:

/// <summary>
/// Download a file and return a string with its content.
/// </summary>
/// <param name="auth">Authenticator responsible for creating
/// web requests.</param>
/// <param name="downloadUrl">Url to be used to download the
/// resource.</param>
public static string DownloadFile(IAuthenticator auth, String downloadUrl)
{
    string result = "";
    try
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(downloadUrl));
        auth.ApplyAuthenticationToRequest(request);

        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        System.IO.Stream stream = response.GetResponseStream();
        StreamReader reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
    catch (Exception e)
    {
        Console.WriteLine("An error occurred: " + e.Message);
    }
    return result;
}

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.

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.

//
// POST: /svc

[HttpPost, ActionName("svc")]
public JsonResult svcPost(
    string title, string description, string mimeType, string content)
{
    IAuthenticator authenticator = Session["authenticator"] as IAuthenticator;
    DriveService service = Session["service"] as DriveService;

    if (authenticator == null || service == null)
    {
        // redirect user to authentication
    }

    Google.Apis.Drive.v2.Data.File file = Utils.InsertResource(
        service, authenticator, title, description, mimeType, content);
    return Json(file.Id);
}

The Utils.InsertResource helper method is responsible for inserting the metadata and the file content.

/// <summary>
/// Create a new file and return it.
/// </summary>
public static Google.Apis.Drive.v2.Data.File InsertResource(
    Google.Apis.Drive.v2.DriveService service, IAuthenticator auth,
    String title, String description, String mimeType, String content)
{
    // File's metadata.
    Google.Apis.Drive.v2.Data.File body = new Google.Apis.Drive.v2.Data.File();
    body.Title = title;
    body.Description = description;
    body.MimeType = mimeType;

    byte[] byteArray = Encoding.ASCII.GetBytes(content);
    MemoryStream stream = new MemoryStream(byteArray);

    Google.Apis.Drive.v2.FilesResource.InsertMediaUpload request = service.Files.Insert(body, stream, mimeType);
    request.Upload();

    return request.ResponseBody;
}

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 resource_id parameter.

//
// PUT: /svc

[HttpPut, ActionName("svc")]
public JsonResult svcPut(string title, string description, string mimeType,
    string content, string resource_id, bool newRevision)
{
    IAuthenticator authenticator = Session["authenticator"] as IAuthenticator;
    DriveService service = Session["service"] as DriveService;

    if (authenticator == null || service == null)
    {
        // redirect user to authentication
    }

    Google.Apis.Drive.v2.Data.File file;
    if (string.IsNullOrWhiteSpace(resource_id))
    {
        file = Utils.InsertResource(
            service, authenticator, title, description, mimeType, content);
    }
    else
    {
        file = Utils.UpdateResource(
            service, authenticator, resource_id, title,
            description, mimeType, content, newRevision);
    }
    return Json(file.Id);
}

The Utils.UpdateResource helper method is responsible for updating the metadata and the file content.

/// <summary>
/// Update both metadata and content of a file and return the updated file.
/// </summary>
public static Google.Apis.Drive.v2.Data.File UpdateResource(
    Google.Apis.Drive.v2.DriveService service, IAuthenticator auth,
    String fileId, String newTitle, String newDescription,
    String newMimeType, String content, bool newRevision)
{
    // First retrieve the file from the API.
    Google.Apis.Drive.v2.Data.File body = service.Files.Get(fileId).Fetch();

    body.Title = newTitle;
    body.Description = newDescription;
    body.MimeType = newMimeType;

    byte[] byteArray = Encoding.ASCII.GetBytes(content);
    MemoryStream stream = new MemoryStream(byteArray);

    Google.Apis.Drive.v2.FilesResource.UpdateMediaUpload request = service.Files.Update(body, fileId, stream, newMimeType);
    request.Upload();

    return request.ResponseBody;
}

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.

//
// GET: /about

public JsonResult about()
{
    DriveService service = Session["service"] as DriveService;

    if (service == null)
    {
        // redirect user to authentication
    }

    Google.Apis.Drive.v2.Data.About about = service.About.Get().Fetch();

    JsonResult result = Json(new { quotaBytesTotal = about.QuotaBytesTotal, quotaBytesUsed = about.QuotaBytesUsed });
    result.JsonRequestBehavior = JsonRequestBehavior.AllowGet;

    return result;
}

User service

The /user endpoint allows one HTTP method.

  • GET: Fetches information about the user

This service returns information about the user that was cached during authorization.

//
// GET: /user

public JsonResult user()
{
    IAuthenticator authenticator = Session["authenticator"] as IAuthenticator;

    if (authenticator == null)
    {
        // redirect user to authentication
    }

    Userinfo userInfo = Utils.GetUserInfo(authenticator);

    JsonResult result = Json(new { email = userInfo.Email, link = userInfo.Link, picture = userInfo.Picture });
    result.JsonRequestBehavior = JsonRequestBehavior.AllowGet;

    return result;
}

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.