18 December 2014

The hardest part of dealing with the Google APIs is getting OAuth to work correctly so that you can actually make the calls you intend to make.  With that done, the rest is pretty straightforward.  As a sample of something you might want to do, take writing a document to Google Drive.

Creating a document is really just a POST request to the Insert endpoint on the Files API.  This is really straightforward to do in C# with the Google.Apis.Drive.v2 Nuget package.  First install the package:

install-package Google.Apis.Drive.v2

This package has a long list of dependencies and, therefore, this statement at the Package Manager Console in Visual Studio installs a large number of packages.


<packages>
  <package id="Google.Apis" version="1.9.0" targetFramework="net45" />
  <package id="Google.Apis.Auth" version="1.9.0" targetFramework="net45" />
  <package id="Google.Apis.Core" version="1.9.0" targetFramework="net45" />
  <package id="Google.Apis.Drive.v2" version="1.9.0.1540" targetFramework="net45" />
  <package id="log4net" version="2.0.3" targetFramework="net45" />
  <package id="Microsoft.Bcl" version="1.1.9" targetFramework="net45" />
  <package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net45" />
  <package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net45" />
  <package id="Microsoft.Net.Http" version="2.2.22" targetFramework="net45" />
  <package id="Newtonsoft.Json" version="6.0.4" targetFramework="net45" />
  <package id="Zlib.Portable" version="1.10.0" targetFramework="net45" />
</packages>

With the Drive package installed we can now fill a stream with something to put into a text document and create a request to create a document.
var request = GetDriveService().Files.Insert(new File { Title = documentTitle }, stream, “text/plain”);

where GetDriveService() is a function that will return an instance of DriveService from the Google.Apis.Drive.v2 package.

Another line will submit the HTTP request to Google’s servers.
var response = await request.UploadAsync();

(or you can use request.Upload() if you are scared of using async and await)

So a quick way to create a text document in the root of your Drive is with this:


public async Task CreateDocument(string documentName, string documentContent)
{
    using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(documentContent)))
    {
        var request = GetDriveService().Files.Insert(new File { Title = documentName }, stream, "text/plain");
        var uploadResult = await request.UploadAsync();
        Console.WriteLine("Google Drive document created - bytes: {0}; status: {1}; exception: {2}", uploadResult.BytesSent, uploadResult.Status, uploadResult.Exception);
    }
}

It’s not much harder to create such a document in another folder in your drive if you know the Id of the folder in which you want to put it:


public async Task CreateDocument(string containingFolderId, string documentName, string documentContent)
{
    using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(documentContent)))
    {
        var request = GetDriveService().Files.Insert(new File { Title = documentName Parents = new[] { new ParentReference { Id = containingFolderId } } }, stream, "text/plain");
        var uploadResult = await request.UploadAsync();
        Console.WriteLine("Google Drive document created - bytes: {0}; status: {1}; exception: {2}", uploadResult.BytesSent, uploadResult.Status, uploadResult.Exception);
    }
}

It’s worth noting here that the Parents property for the File you are creating uses collection semantics (the type is IList<ParentReference> – I’ve use an array of ParentReference here, which implements IList<ParentReference>, as a quick way to satisfy this), meaning a document can be in more than one folder.  This is something that can be useful and is used regularly when sharing documents with others, but not something I’ll explore further in this post.  You can see, though, how easy it would be to add additional parent folder Ids to the array.

 

Queries

If you want to insert a document (or any time of file) into a directory without know the Id for that directory (perhaps you know only the name), you need to find the Id.  The query API is your friend here.  Executing a query against Drive is really just a GET request on the List endpoint of the Files API.  Keep in mind that a folder in Google Drive is just yet another type of file.  Getting a reference, including the Id, to a folder, is really straightforward to do in C# with the Google.Apis.Drive.v2 Nuget package.  We’ve already been through installation of the package, so with it installed, you can execute a query with:
var request = service.Files.List();

The request has a property called Q that is the string representing your query.  Specifying a query with the above reference to a request looks like:

request.Q = query;

where query is potentially a parameter to a method or some variable with a query for Google (more in a moment on how a query would look).  We can then execute this query with:

var files = await request.ExecuteAsync();
(or you can use request.Execute() if you are scared of using async and await)

The code I’m using in my applications to execute a query is this:

public async Task<File> ExecuteGoogleDriveQueryScalar(string query)
{
    var queryResults = (await ExecuteGoogleDriveQuery(query));
    return queryResults.Length > 0 ? queryResults[0] : null;
}

public async Task<File[]> ExecuteGoogleDriveQuery(string query)
{
    return await ExecuteGoogleDriveQuery(query, file => 0);
}

public async Task<File[]> ExecuteGoogleDriveQuery<TSortProperty>(string query, Func<File, TSortProperty> orderByFunc, bool orderByDescending = false)
{
    if (orderByFunc == null)
    {
        orderByFunc = file => default(TSortProperty);
    }
    var request = GetDriveService().Files.List();
    request.Q = query;
    var sortFunction = new Func<IList<File>, IOrderedEnumerable<File>>(collection => orderByDescending ? collection.OrderByDescending(orderByFunc) : collection.OrderBy(orderByFunc));
    var files = await request.ExecuteAsync();
    return sortFunction(files.Items).ToArray();
}

A reference for what to put in the string for querying can be found here. I’ll leave you to explore the majority of it to your interest, but give an example, in the theme of this post and for utility in using a query to find a folder by name.  Finding a folder by name could look something like this:

string.Format(“mimeType='application/vnd.google-apps.folder' and title='{0}'”, soughtfolderName);

You might want to limit the scope of the search to folders that are children of a particular parent folder:

string.Format(“mimeType='application/vnd.google-apps.folder' and '{0}' in parents and title='{1}'”, parentFolderId, soughtfolderName);

Something Unexpected

You have just seen how to create a file in a folder and how to find a folder.  With this combination, you can create a file in a folder using the name of the folder.  This isn’t the end of the story, though.

You can imagine my surprise, when I found that my code that was querying Drive for a subdirectory by name and using it is found and creating it otherwise and creating files inside these folders to find that I wasn’t seeing the new directories with new files.  This was code that had been working and creating files in the expected subdirectories.  In retrospect, it’s probably something I should have expected, but I was still caught by surprise.

If you remove a file from a folder on Google Drive either via the API or the browser user interface (or via some other mechanism like an authorized application or the Google Drive desktop client), the file goes into the trash.  To be more precise, the file is labeled as having been trashed.  It does not appear in the user interface anymore and is out of sight and out of mind.  It is still there, though, and still will be found by queries.  My code was looking for a subdirectory inside a parent directory and was finding it even though it wasn’t really there (OK, that’s a lie – it was there, it was just there and trashed and from the perspective of the Drive user interface, it looked like it wasn’t there).  There files I was inserting into this directory (and the directory itself that I thought I was creating) were not surfacing in the user interface.  This is because I was inserting files into a trashed subdirectory.  If you want you query  exclude trashed files, you need to tell it that.  That makes our query look like this:

string.Format(“mimeType='application/vnd.google-apps.folder' and '{0}' in parents and title='{1}' and trashed = false”, parentFolderId, soughtfolderName);

With all of this in place, we now have the following code to enable the easy creation of a document:

public async Task CreateDocumentInNamedFolder(string folderName, string documentName, string documentContent)
{
    await CreateDocument((await GetFolderByName(folderName)).Id, documentName, documentContent);
}

public async Task<File> GetFolderByName(string folderName)
{
    return await ExecuteGoogleDriveQueryScalar(string.Format("mimeType='application/vnd.google-apps.folder' and title='{0}' and trashed = false", folderName));
}



blog comments powered by Disqus