Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Working with Google Drive in WPF

0.00/5 (No votes)
6 Mar 2013 1  
Authenticating and uploading files to Google Drive.

Introduction

When designing my entry into the App Innovation contest (if you are interested, it is called Rev Warrior and can be found here) one of the features I wanted to have was the ability to store the data that the program logged into a user's Google Drive. The purpose was to make the data easily accessible for later analysis in Excel or some other program.

What I discovered was a lack of any documentation or working examples for doing this in .NET beyond a very simple sample provided by Google. Although their sample worked, it required explicit authorization each time it was run and it didn't really address how or why the sample worked. So I started digging into the problem and when I found a solution, I felt that I should share it here. The attached source code is a very simple WPF application that, when run, will open a window with a single button. Click the button and an open-file dialog will prompt you to select a file to upload. Select a file and the application will attempt to authenticate with Google Drive. If needed, it will open a browser so you can authorize the access and, provided you OK it, the program will upload the file you picked into your Google Drive.

Where my application differs from the other samples I found is that my application extends the Google sample code to store the authorization tokens and use them in subsequent accesses so the user is not prompted to authorize the application every time the program is run. The result is a pretty clean flow which will prompt the user the first time but then silently upload the chosen file from then on provided the application remains authorized. (Google users can revoke authorizations to their Google Services in their user control panel.)

The actual upload is very easy as Google provides all of the libraries for you to use. The complication was in the Authorization sequence and how to cache those tokens and use them on subsequent requests. Google's documentation reads like stereo instructions and there are not many examples out there that can be used for guidance. Google provides a full application, called Dr. Edit, as a .NET sample but it is an ASP MVC application and not real helpful for those of us working in WPF, WinForms, or something else. To get to a working WPF solution, I had to reverse engineer several Google sample projects. The upside is that the authorization steps are pretty generic and can be applied to just about all of Google's API services which includes Tasks, Calendar, Maps, etc so you can use this project as a basis for accessing many of Google's API Services.

Background 

Many Google Services require two types of authentication; the user must be authenticated (logged in) and the application must be explicitly authorized by the user. (For Google Drive Authorization information see the Google Drive Authorization API Docs.)

The authorization mechanisms are based on OAuth 2.0. Generally speaking, Google's OAuth mechanism requires the application requesting access to present a client ID (which identifies the application to Google) and a client secret to an authorization service endpoint along with the scope(s) the application wishes to access. (Note: The authorization scopes, included in the SCOPES array below, are different for each service. Refer to the Authorization section of the Google API documentation for the service(s) you wish to use.) The authorization service will check to see if the application is authorized to use the requested scopes and if so, it will return an access token to be passed with all of the service calls. All of this is done in JSON and is well defined in both Google's API documentation and other OAuth2.0 documents. The trick is to get the access token because that part of the process is not very well documented... at least not for .NET developers.

For installed applications, the two required keys are kept in the code (see the ClientCredentials class below) and not really considered to be a secret because the code could be easily decompiled or the JSON intercepted with a proxy tool like Fiddler. For this reason, when the application keys are generated, Google must be told it is for an 'installed application' so that it treats communications with a higher level of suspicion.

internal static class ClientCredentials
{
    /// <summary>
    /// The OAuth2.0 Client ID of your project.
    /// </summary>
    public static readonly string CLIENT_ID = "<< APPLICATION CLIENT ID GOES HERE >>";

    /// <summary>
    /// The OAuth2.0 Client secret of your project.
    /// </summary>
    public static readonly string CLIENT_SECRET = "<< APPLICATION SECRET GOES HERE >>";

    /// <summary>
    /// The OAuth2.0 scopes required by your project.
    /// </summary>
    public static readonly string[] SCOPES = new String[]
    {
        "https://www.googleapis.com/auth/drive.file",
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/userinfo.profile"
    };

    /// <summary>
    /// The Redirect URI of your project.
    /// </summary>
    public static readonly string REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
}

When an application which is registered with Google and attempts access for the first time (or otherwise does not have cached credentials), Google will check to see if the application identifier is authorized to access the user's requested service(s). If the application has not been authorized, Google will fail the call usually with a 401 Not Authorized exception. It is really up to the client to know if it has been authorized previously or not. Applications must be granted access to the specific services via the API console. More on that later...

The key to all of this is the Authorization Code. If the application has the code, it can assume that it has been previously authorized. The authorization code is like the number ticket you get when you are waiting in line at the Motor Vehicle Office.... it doesn't get you served, it only gets you the right to be served. The authorization code indicates that the user has authorized the application for service access and it is returned to the client via the REDIRECT_URI specified in the ClientCredentials class as seen above. That cryptic string seen there tells the authorization server to return the code in the title of the page sent back after the user authorizes the application.

When an application wants to access Google services, it should check to see if it has cached credentials (assuming you want to skip the authorization step after the first time). If it does not, it should pop-up a browser window requesting authorization for the Google services. The NativeClientApplication object knows how to generate the authorization URL so we don't need to figure it out ourselves.

The sample project has everything needed to do this for you. There is a Forms folder in the project which includes the required forms. They rely on the built-in browser so they should work just fine without any modification.

The page you see above in the browser control was generated by the Google authorization service. The permissions your app requires will be listed on the page and can be expanded by the user to get a better idea of what each permission entails. It goes without saying that your application should only request the minimum number of permissions as required to accomplish its task. And users should review these permissions before authorizing an application... be it your app or anyone elses.

Provided the user allows access, the authorization server will return the authorization code. It can be sent back either by the Google server calling a web service endpoint opened by your application or in the title of the page sent back to the browser. Because of the challenges of spooling up a web server, opening a connection through any possible firewall(s), etc... it is much easier for installed applications to simply scrape the authorization code from the title of the resulting web page. That is the technique used in the sample project. If successful, the title will be set to Success code=xxxxxxxxx where the xxxx's are replaced by a unique authorization code. 

As previously mentioned, the authorization code only gets you invited to the party. You can't do anything with that code as far as API access. The Authorization Code must be exchanged for a short-lived access code and a long-term refresh code. In the Google.Apis.Authentication.OAuth2 library is a class called NativeApplicationClient. This is the wrapper for the authorization server and it has a method called 'ProcessUserAuthorization'. This method takes the authorization code we retrieved after the user authorized the application's access and turns it into the access token and the refresh token. The access token is what we actually need for the task at hand and it is maintained in the NativeApplicationClient. It gets passed with all subsequent API calls. The nice thing about the NativeApplicationClient is that it knows how to verify the access token and how old the token is. If the token has expired, the client will use the refresh token to get a new access token. That takes the burden off of us to manage token lifetimes.

Cached Credentials

The workflow described previously addresses the situation where the application needs to get authorization to access the API Service(s). It is possible to always prompt for authorization and some of the guidance around these types of operations suggests that, as a good developer, we should do that. But the issue of user experience comes into play and we don't want to annoy them with the authorization dialog every time our program runs and we need to access their Google Drive. It is possible that the user may not keep a cached login in their browser and would not only be prompted to authorize the app, but could also be prompted for their login first thereby making them go through two screens to authorize the access each time. This would be a real turn-off for the average user so we need a better way. Once authorization is agreed to, we can safely assume the user knows that they have authorized our application to access their Google Drive so we can reasonably skip the authorization step. In practice, we do this with Cached Credentials.

In the sample project, there is a helper class called AuthorizationMgr that handles (among other things) the authorization caching. When we get our initial Authorization Code and turn it into an access token and a refresh token, we call the SetCachedRefreshToken method on the AuthorizationMgr class.

/// <summary>
/// Saves a refresh token to the specified storage name, 
/// and encrypts it using the specified key.
/// </summary>
public static void SetCachedRefreshToken(string storageName,
                                            string key,
                                            IAuthorizationState state)
{
    // Create the file content.
    string scopes = state.Scope.Aggregate("", (left, append) => left + " " + append);
    string content = scopes + "\r\n" + state.RefreshToken;

    // Encrypt it.
    byte[] salt = Encoding.Unicode.GetBytes(Assembly.GetEntryAssembly().FullName + key);
    byte[] encrypted = ProtectedData.Protect(
        Encoding.Unicode.GetBytes(content), salt, DataProtectionScope.CurrentUser);

    // Save the data to the auth file.
    string file = storageName + ".auth";
    AppData.WriteFile(file, encrypted);
}

This method will take a storage name and an encryption secret as well as the credentials we received back and store them in an ecrypted file in the application data folder. In my sample code, the name of the file and the secret are embedded in the source code... which we all know isn't safe. You should figure out a better way to do this... at the minimum the secret should not be in the source code.

This authorization information is sensitive because it contains the refresh token. This is what allows us to get a current access token without requiring the explicit authorization of the user. When the initial access is approved, the refresh token is generated and represents that access request and that explicit authorization. It can be used as a proxy for the user's authorization in the future. There is another method on the AuthorizationMgr class called GetCachedRefreshToken which will retrieve that token from the encrypted file. When the program attempts to get the authorization for an action on the Google Drive, the code attempts to load the cached refresh token using this method. If successful, the NativeApplicationClient class has a method called RefreshToken which takes the cached authorization information and requests a fresh access token from Google. If successful, the program has been silently authorized and API access will be granted.

Using the code  

The hard part is over. Getting the application authorized and getting a valid access token were the most difficult part. The included WPF project also shows how to get all the files from the user's drive to check and see if a file exists. The API calls to upload or update the file are also in the project.

Getting Started with Google API Access

The first thing you have to do to access any Google API is setup a project in the API console and generate the OAuth credentials. If you have a Google account you can login at the Google API Console. Once logged in, you must create a new project. Once you have done that, you will see that your project has been assigned an API key. This is the master key for your project. You may have up to seven applications granted access via your project. (And these may be different application types; installed, web, mobile, etc.)

Before your application can access Google Drive or most any other service of interest, you must create an OAuth2.0 Client ID. This can be found under 'API Access' on the left menu. If it isn't obvious, you click on the big blue button in the API Console. This will launch the process of generating an OAuth2 client credential. The process is different for different application types. Things may be a bit different if you aren't doing an installed application like this one.

The process starts by giving a name to the application.

The Product Name is what will show up in the authorization page when the authorization request is displayed to the user. You can optionally specify a URL where a product logo resides which will be displayed on the page as well. In the image earlier in the article, you can see the page generated for my sample application to see what the final looks like.

When you click next, you need to give the Google servers information about your app. In our case, we are going to specify it is an installed application and that we will be running it on 'other'.

Once we create the Client ID, we will be returned to the main console and we should see our new OAuth credentials:

In the screen shot you can see where the client ID and client secret are found. These values go into the ClientCredentials class in the sample code. These two values identify your application as the app you have just setup in your API console. You should do your best to keep these values secret knowing that it isn't ideal they are embedded in the code.

Now that your application is registered with Google and you have the Client ID and Client Secret in the code, you have to tell Google which services your application is going to access. If you don't do this step, you will get 401 Unauthorized errors when you try to access specific APIs. Granting access is pretty easy and is done under the 'Services' option on the left side of the console. The service access you turn on and off here is project wide (not application specific) and determines which the requested permissions list look like. For access to Google Drive, I have enabled the Drive Service and the Drive SDK.

There are certain restrictions and other terms related to API access. You should refer to Google's documents to make sure you understand those limitations.

Once you have completed these steps, your application should be able to access Google Drive so let's turn our attention to the code.

Using the sample code

Although this is a WPF app, the code would be the same for a WinForms app or something similar. The sample project simply creates a window with a single button labeled 'GO'. The event handler in the main page's code-behind simply calls the AuthorizeAndUpload method in the static MainClass.

/// <summary>
/// This is the worker method that executes when the user clicks the GO button.
/// It illustrates the workflow that would need to take place in an actual application.
/// </summary>
public static void AuthorizeAndUpload()
{

    // First, create a reference to the service you wish to use.
    // For this app, it will be the Drive service. But it could be Tasks, Calendar, etc.
    // The CreateAuthenticator method is passed to the service which will use that when it is time to authenticate
    // the calls going to the service.
    _service = new DriveService(CreateAuthenticator());

    // Open a dialog box for the user to pick a file.
    OpenFileDialog dialog = new OpenFileDialog();
    dialog.AddExtension = true;
    dialog.DefaultExt = ".txt";
    dialog.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*";
    dialog.Multiselect = false;
    dialog.ShowDialog();


    File body = new File();
    body.Title = System.IO.Path.GetFileName(dialog.FileName);
    body.Description = "A test document";
    body.MimeType = "text/plain";

    System.IO.Stream fileStream = dialog.OpenFile();
    byte[] byteArray = new byte[fileStream.Length];
    fileStream.Read(byteArray, 0, (int)fileStream.Length);

    System.IO.MemoryStream stream = new System.IO.MemoryStream(byteArray);

    // Get a listing of the existing files...
    List<File> fileList = Utilities.RetrieveAllFiles(_service);
    foreach (File item in fileList)
    {
        if (item.Title == body.Title)
        {
            // File exists in the drive already!
            MessageBoxResult result = System.Windows.MessageBox.Show("The file you picked already exists " + 
              "in your Google Drive. Do you wish to overwrite it?", 
              "Confirmation", MessageBoxButton.YesNoCancel);
            if (result == MessageBoxResult.Yes)
            {

                // Yes... overwrite the file
                Utilities.UpdateFile(_service, item.Id, item.Title, 
                          item.Description, item.MimeType, dialog.FileName, true);
            }

            else if (result == MessageBoxResult.No)
            {

                // No code here
                Utilities.InsertFile(_service, System.IO.Path.GetFileName(dialog.FileName), 
                  "An uploaded document", "", "text/plain", dialog.FileName);
            }

            else
            {

                // Cancel code here
                return;
            }
            break;
        }
    }
    System.Windows.MessageBox.Show("Upload Complete");
}

We start out by creating a new DriveService object. This is a class provided by the Google libraries and is meant to encapsulate the Drive API. If you were working with a different service, like Tasks for example, this would be a different object, like TaskService. The parameter to the class constructor is an object that implements the IAuthenticator interface. In this case, we are using the results of the CreateAuthenticator function to pass in an Authenticator object. CreateAuthenticator looks like this:

private static IAuthenticator CreateAuthenticator()
{
    var provider = new NativeApplicationClient(GoogleAuthenticationServer.Description);
    provider.ClientIdentifier = ClientCredentials.CLIENT_ID;
    provider.ClientSecret = ClientCredentials.CLIENT_SECRET;
    return new OAuth2Authenticator<NativeApplicationClient>(provider, GetAuthorization);
} 

You can see that CreateAuthenticator first creates a NativeApplicationClient. As mentioned before, this wraps a lot of the authentication mechanisms up for us. You can see that the next two lines set the ClientID and Secret from our static ClientCredentials class. The last thing we do is use the NativeApplicationClient to create a typed OAuth2Authenticator (which implements the IAuthenticator interface) which we return back.

What you may initially overlook is the reference to GetAuthorization in the OAuth2Authenticator constructor. That parameter requires an object which implements the IAuthorizationState interface. That interface contains the current authorization information and is used by the NativeApplicationClient to provide the access token on subsequent API calls.  

/// <summary>
/// Method to get the authorization from the user to access their Google Drive from the application
/// </summary>
/// <param name="client"></param>
/// <returns></returns>
private static IAuthorizationState GetAuthorization(NativeApplicationClient client)
{
    // You should use a more secure way of storing the key here as
    // .NET applications can be disassembled using a reflection tool.
    const string STORAGE = "gdrive_uploader";
    const string KEY = "z},drdzf11x9;87";
    string scope = DriveService.Scopes.Drive.GetStringValue();

    // Check if there is a cached refresh token available.
    IAuthorizationState state = AuthorizationMgr.GetCachedRefreshToken(STORAGE, KEY);
    if (state != null)
    {
        try
        {
            client.RefreshToken(state);
            return state; // Yes - we are done.
        }
        catch (DotNetOpenAuth.Messaging.ProtocolException ex)
        {
            Debug.WriteLine("Using existing refresh token failed: " + ex.Message);
        }
    }

    // If we get here, there is no stored token. Retrieve the authorization from the user.
    state = AuthorizationMgr.RequestNativeAuthorization(client, scope);
    AuthorizationMgr.SetCachedRefreshToken(STORAGE, KEY, state);
    return state;
}

GetAuthorization will first attempt to pull cached credentials by using the AuthorizationMgr helper class. If no cached credentials are available, the method will use the AuthorizationMgr.RequestNativeAuthorization function to get a user authorization code then exchange it for a valid access and refresh token which are saved via the SetCachedRefreshToken method on the AuthorizationMgr helper class. This is the part in the process where the web browser would be opened and authorization requested if we don't have cached credentials.

While the authorization mechanism is setup and defined at the beginning, it isn't actually executed until our first API access which happens later. Back in the AuthorizeAndUpload method we have created our Drive Service and passed in all of these objects and methods to handle the authentication. Next, we prompt the user to pick a file to upload. This is done with a standard file-open dialog box. Once we have the file, we have to turn it into a Google File object. The next few lines setup that file and copy the contents via a memory stream.

File body = new File();
body.Title = System.IO.Path.GetFileName(dialog.FileName);
body.Description = "A test document";
body.MimeType = "text/plain";

System.IO.Stream fileStream = dialog.OpenFile();
byte[] byteArray = new byte[fileStream.Length];
fileStream.Read(byteArray, 0, (int)fileStream.Length);

System.IO.MemoryStream stream = new System.IO.MemoryStream(byteArray); 

The next chunk of code calls an API to retrieve all of the files in the user's Google Drive. In my sample project, I'm calling the synchronous version of this API and doing it on the UI thread... both bad programming practice but this is an example... not production. Obviously, you would want to call this in an async manner and properly marshal the results over to the UI.

// Get a listing of the existing files...
List<File> fileList = Utilities.RetrieveAllFiles(_service); 

This call will return a list of Google File objects and it will be all files in the user's drive. Including files in subfolders. Each file has a parent ID property which can be used to recreate the hierarchy of the drive. My sample is simply looking to see if the file being uploaded already exists in the drive. There is actually a flaw in this logic because the sample only uploads into the root of the drive. If the target file existed in a subfolder, it would get reported to the user as already existing and asking the user to overwrite when, in actuality, it would not be an overwrite because they were at different levels. A production application would need to take this into consideration.

foreach (File item in fileList)
{
    if (item.Title == body.Title)
    {
        // File exists in the drive already!
        MessageBoxResult result = System.Windows.MessageBox.Show("The file you picked already exists " + 
          "in your Google Drive. Do you wish to overwrite it?", 
          "Confirmation", MessageBoxButton.YesNoCancel);
        if (result == MessageBoxResult.Yes)
        {
            // Yes... overwrite the file
            Utilities.UpdateFile(_service, item.Id, item.Title, 
              item.Description, item.MimeType, dialog.FileName, true);
        }

        else if (result == MessageBoxResult.No)
        {
            // No code here
            Utilities.InsertFile(_service, System.IO.Path.GetFileName(dialog.FileName), 
              "An uploaded document", "", "text/plain", dialog.FileName);
        }

        else
        {
            // Cancel code here
            return;
        }
        break;
    }
}

A simple foreach loop steps through the list looking to see if the file names match. If they do, we need to know that because the Drive API is a little different here. If a file does not exist, you should Insert it into the Drive. You can see the code to call that function above. If the file exists, you need to decide if you are going to update the current file or upload a side-by-side version. If you are going to overwrite the existing file with the new data, you must call the UpdateFile method passing in the new information. It is interesting here that you can 'update' the file with just new metadata without changing the actual contents. If you were renaming a file or changing the description, this is what you would do. Of course, if you are just updating the contents, you would use all of the existing metadata.

Alternately, if a file exists you can still use the Insert method to upload the file again. The difference is that Google will create an additional version of the file. So if foo.txt existed, the Drive service would upload foo.txt and save it as foo(1).txt in the drive. Subsequent uploads would simply increase the number appended to the file name. For a production application, it is important to understand the ramifications of this behavior. You wouldn't want to create 99 copies of the user's file!

In the sample code, the result of the user's decision to the message box determines how we will upload the file they have selected. The upload or insert file methods handle the process of getting the file up to the Drive.

/// <summary>
/// Inserts a new file into the Google Drive.
/// </summary>
/// <param name="service">Drive API service instance.</param>
/// <param name="title">Title (name) of the file to insert, including the extension.</param>
/// <param name="description">Description of the file to insert.</param>
/// <param name="parentId">Parent folder's ID. (Empty string to put the file in the drive root)</param>
/// <param name="mimeType">MIME type of the file to insert.</param>
/// <param name="filename">Filename (including path) of the file to upload relative to the local machine.</param>
/// <returns>Inserted file metadata, null is returned if an API error occurred.</returns>
/// <remarks>The filename passed to this function should be the entire path and filename (with extension) for the source file on the
/// local machine. The API will put the file in the Google Drive using the "Title" property.</remarks>
public static Google.Apis.Drive.v2.Data.File InsertFile(DriveService service, 
       String title, String description, String parentId, String mimeType, String filename)
{
    // 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;

    // Set the parent folder.
    if (!String.IsNullOrEmpty(parentId))
    {
        body.Parents = new List<ParentReference>() { new ParentReference() { Id = parentId } };
    }

    // Load the File's content and put it into a memory stream
    byte[] byteArray = System.IO.File.ReadAllBytes(filename);
    MemoryStream stream = new MemoryStream(byteArray);

    try
    {
        // When we add a file, we create an Insert request then call the Upload method on the request.
        // (If we were updating an existing file, we would use the Update function)
        FilesResource.InsertMediaUpload request = service.Files.Insert(body, stream, mimeType);
        request.Upload();

        // Set the file object to the response of the upload
        Google.Apis.Drive.v2.Data.File file = request.ResponseBody;

        // Uncomment the following line to print the File ID.
        // Console.WriteLine("File ID: " + file.Id);

        // return the file object so the caller has a reference to it.
        return file;
    }
    catch (Exception e)
    {
        // May want to log this or do something with it other than just dumping to the console.
        Console.WriteLine("An error occurred: " + e.Message);
        return null;
    }
}

The point of interest in this function is FileResource.InsertMediaUpload request object. This object packages up the information needed to insert a new file in the drive. Similarly, the UpdateFile method uses the UpdateMediaUpload object to help it. Both are instantiated with concrete objects from the DriveService.Files object. Again, if you were working with another API like Tasks, these would be InsertTaskUpload methods and TaskService.Tasks objects. The Upload method executes the actual upload. Because the request comes from the DriveService and because the drive service was setup with the Authentication methods, there is a bunch of stuff going on behind the scenes of that Upload method. Suffice to say it is all in Google's libraries and we can simply trust that it will all work. Our CreateAuthenticator and GetAuthorization methods in the MainClass may finally get called the first time we execute an upload (or a file listing) command.

When successful, the method returns a Google File object that is a local proxy for the remote object. That object contains key bits of information such as the file's ID number and it's parent. These could be used in follow-up operations to manipulate the file without having to go the long way around an find it through the file listing method.

Although you would have to check for the existence of the file and handle situations where it no longer existed as it did when you saved the information, you could persist the file information locally as a means to get back to it quickly the next time the application was run. The file ID should not change unless the file is deleted and re-create by the user or some other application.

Once the upload method completes, that is it. The only thing left is to tell the user that the file is uploaded. Now obviously, as a sample of how to do this, a lot of important things have been left out for the purposes of clarity. A production application should make a lot of this async and should notify the user of the progress as things are going on. Error handling should also be added as something as simple as the user not granting access can really throw a monkey wrench into the whole thing. The app should detect that and fail gracefully. And finally, something other than a big button labeled "GO" should probably be used... 

Conclusion

Once I grasped the steps for the authorization and figured out how to store and reuse the cached credentials, the Drive API became very easy to use. Indeed, most any Google API is pretty easy to consume in .NET provided you get the authorization part straightened out. Installed applications pose a challenge and I hope this article and the sample project save you a lot of time and headaches when it comes to adding Google API calls into your application. Good luck!

History

  • Nov 2, 2012 - Initial revision.
  • Dec 10, 2012 - Fixed error in code where file wasn't uploaded if it didn't exist. Cleaned up unused Google DLLs in the Google API project folder.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here