Home | History | Annotate | Download | only in drive
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/drive/drive_api_util.h"
      6 
      7 #include <string>
      8 
      9 #include "base/command_line.h"
     10 #include "base/files/scoped_platform_file_closer.h"
     11 #include "base/logging.h"
     12 #include "base/md5.h"
     13 #include "base/platform_file.h"
     14 #include "base/strings/string16.h"
     15 #include "base/strings/string_util.h"
     16 #include "base/strings/stringprintf.h"
     17 #include "base/strings/utf_string_conversions.h"
     18 #include "base/values.h"
     19 #include "chrome/browser/drive/drive_switches.h"
     20 #include "content/public/browser/browser_thread.h"
     21 #include "google_apis/drive/drive_api_parser.h"
     22 #include "google_apis/drive/gdata_wapi_parser.h"
     23 #include "net/base/escape.h"
     24 #include "third_party/re2/re2/re2.h"
     25 #include "url/gurl.h"
     26 
     27 namespace drive {
     28 namespace util {
     29 namespace {
     30 
     31 // Google Apps MIME types:
     32 const char kGoogleDocumentMimeType[] = "application/vnd.google-apps.document";
     33 const char kGoogleDrawingMimeType[] = "application/vnd.google-apps.drawing";
     34 const char kGooglePresentationMimeType[] =
     35     "application/vnd.google-apps.presentation";
     36 const char kGoogleSpreadsheetMimeType[] =
     37     "application/vnd.google-apps.spreadsheet";
     38 const char kGoogleTableMimeType[] = "application/vnd.google-apps.table";
     39 const char kGoogleFormMimeType[] = "application/vnd.google-apps.form";
     40 const char kDriveFolderMimeType[] = "application/vnd.google-apps.folder";
     41 
     42 ScopedVector<std::string> CopyScopedVectorString(
     43     const ScopedVector<std::string>& source) {
     44   ScopedVector<std::string> result;
     45   result.reserve(source.size());
     46   for (size_t i = 0; i < source.size(); ++i)
     47     result.push_back(new std::string(*source[i]));
     48 
     49   return result.Pass();
     50 }
     51 
     52 // Converts AppIcon (of GData WAPI) to DriveAppIcon.
     53 scoped_ptr<google_apis::DriveAppIcon>
     54 ConvertAppIconToDriveAppIcon(const google_apis::AppIcon& app_icon) {
     55   scoped_ptr<google_apis::DriveAppIcon> resource(
     56       new google_apis::DriveAppIcon);
     57   switch (app_icon.category()) {
     58     case google_apis::AppIcon::ICON_UNKNOWN:
     59       resource->set_category(google_apis::DriveAppIcon::UNKNOWN);
     60       break;
     61     case google_apis::AppIcon::ICON_DOCUMENT:
     62       resource->set_category(google_apis::DriveAppIcon::DOCUMENT);
     63       break;
     64     case google_apis::AppIcon::ICON_APPLICATION:
     65       resource->set_category(google_apis::DriveAppIcon::APPLICATION);
     66       break;
     67     case google_apis::AppIcon::ICON_SHARED_DOCUMENT:
     68       resource->set_category(google_apis::DriveAppIcon::SHARED_DOCUMENT);
     69       break;
     70     default:
     71       NOTREACHED();
     72   }
     73 
     74   resource->set_icon_side_length(app_icon.icon_side_length());
     75   resource->set_icon_url(app_icon.GetIconURL());
     76   return resource.Pass();
     77 }
     78 
     79 // Converts InstalledApp to AppResource.
     80 scoped_ptr<google_apis::AppResource>
     81 ConvertInstalledAppToAppResource(
     82     const google_apis::InstalledApp& installed_app) {
     83   scoped_ptr<google_apis::AppResource> resource(new google_apis::AppResource);
     84   resource->set_application_id(installed_app.app_id());
     85   resource->set_name(installed_app.app_name());
     86   resource->set_object_type(installed_app.object_type());
     87   resource->set_supports_create(installed_app.supports_create());
     88   resource->set_product_url(installed_app.GetProductUrl());
     89 
     90   {
     91     ScopedVector<std::string> primary_mimetypes(
     92         CopyScopedVectorString(installed_app.primary_mimetypes()));
     93     resource->set_primary_mimetypes(primary_mimetypes.Pass());
     94   }
     95   {
     96     ScopedVector<std::string> secondary_mimetypes(
     97         CopyScopedVectorString(installed_app.secondary_mimetypes()));
     98     resource->set_secondary_mimetypes(secondary_mimetypes.Pass());
     99   }
    100   {
    101     ScopedVector<std::string> primary_file_extensions(
    102         CopyScopedVectorString(installed_app.primary_extensions()));
    103     resource->set_primary_file_extensions(primary_file_extensions.Pass());
    104   }
    105   {
    106     ScopedVector<std::string> secondary_file_extensions(
    107         CopyScopedVectorString(installed_app.secondary_extensions()));
    108     resource->set_secondary_file_extensions(secondary_file_extensions.Pass());
    109   }
    110 
    111   {
    112     const ScopedVector<google_apis::AppIcon>& app_icons =
    113         installed_app.app_icons();
    114     ScopedVector<google_apis::DriveAppIcon> icons;
    115     icons.reserve(app_icons.size());
    116     for (size_t i = 0; i < app_icons.size(); ++i) {
    117       icons.push_back(ConvertAppIconToDriveAppIcon(*app_icons[i]).release());
    118     }
    119     resource->set_icons(icons.Pass());
    120   }
    121 
    122   // supports_import, installed and authorized are not supported in
    123   // InstalledApp.
    124 
    125   return resource.Pass();
    126 }
    127 
    128 // Returns the argument string.
    129 std::string Identity(const std::string& resource_id) { return resource_id; }
    130 
    131 }  // namespace
    132 
    133 
    134 bool IsDriveV2ApiEnabled() {
    135   const CommandLine* command_line = CommandLine::ForCurrentProcess();
    136 
    137   // Enable Drive API v2 by default.
    138   if (!command_line->HasSwitch(switches::kEnableDriveV2Api))
    139     return true;
    140 
    141   std::string value =
    142       command_line->GetSwitchValueASCII(switches::kEnableDriveV2Api);
    143   StringToLowerASCII(&value);
    144   // The value must be "" or "true" for true, or "false" for false.
    145   DCHECK(value.empty() || value == "true" || value == "false");
    146   return value != "false";
    147 }
    148 
    149 std::string EscapeQueryStringValue(const std::string& str) {
    150   std::string result;
    151   result.reserve(str.size());
    152   for (size_t i = 0; i < str.size(); ++i) {
    153     if (str[i] == '\\' || str[i] == '\'') {
    154       result.push_back('\\');
    155     }
    156     result.push_back(str[i]);
    157   }
    158   return result;
    159 }
    160 
    161 std::string TranslateQuery(const std::string& original_query) {
    162   // In order to handle non-ascii white spaces correctly, convert to UTF16.
    163   base::string16 query = UTF8ToUTF16(original_query);
    164   const base::string16 kDelimiter(
    165       base::kWhitespaceUTF16 + base::string16(1, static_cast<char16>('"')));
    166 
    167   std::string result;
    168   for (size_t index = query.find_first_not_of(base::kWhitespaceUTF16);
    169        index != base::string16::npos;
    170        index = query.find_first_not_of(base::kWhitespaceUTF16, index)) {
    171     bool is_exclusion = (query[index] == '-');
    172     if (is_exclusion)
    173       ++index;
    174     if (index == query.length()) {
    175       // Here, the token is '-' and it should be ignored.
    176       continue;
    177     }
    178 
    179     size_t begin_token = index;
    180     base::string16 token;
    181     if (query[begin_token] == '"') {
    182       // Quoted query.
    183       ++begin_token;
    184       size_t end_token = query.find('"', begin_token);
    185       if (end_token == base::string16::npos) {
    186         // This is kind of syntax error, since quoted string isn't finished.
    187         // However, the query is built by user manually, so here we treat
    188         // whole remaining string as a token as a fallback, by appending
    189         // a missing double-quote character.
    190         end_token = query.length();
    191         query.push_back('"');
    192       }
    193 
    194       token = query.substr(begin_token, end_token - begin_token);
    195       index = end_token + 1;  // Consume last '"', too.
    196     } else {
    197       size_t end_token = query.find_first_of(kDelimiter, begin_token);
    198       if (end_token == base::string16::npos) {
    199         end_token = query.length();
    200       }
    201 
    202       token = query.substr(begin_token, end_token - begin_token);
    203       index = end_token;
    204     }
    205 
    206     if (token.empty()) {
    207       // Just ignore an empty token.
    208       continue;
    209     }
    210 
    211     if (!result.empty()) {
    212       // If there are two or more tokens, need to connect with "and".
    213       result.append(" and ");
    214     }
    215 
    216     // The meaning of "fullText" should include title, description and content.
    217     base::StringAppendF(
    218         &result,
    219         "%sfullText contains \'%s\'",
    220         is_exclusion ? "not " : "",
    221         EscapeQueryStringValue(UTF16ToUTF8(token)).c_str());
    222   }
    223 
    224   return result;
    225 }
    226 
    227 std::string ExtractResourceIdFromUrl(const GURL& url) {
    228   return net::UnescapeURLComponent(url.ExtractFileName(),
    229                                    net::UnescapeRule::URL_SPECIAL_CHARS);
    230 }
    231 
    232 std::string CanonicalizeResourceId(const std::string& resource_id) {
    233   // If resource ID is in the old WAPI format starting with a prefix like
    234   // "document:", strip it and return the remaining part.
    235   std::string stripped_resource_id;
    236   if (RE2::FullMatch(resource_id, "^[a-z-]+(?::|%3A)([\\w-]+)$",
    237                      &stripped_resource_id))
    238     return stripped_resource_id;
    239   return resource_id;
    240 }
    241 
    242 ResourceIdCanonicalizer GetIdentityResourceIdCanonicalizer() {
    243   return base::Bind(&Identity);
    244 }
    245 
    246 const char kDocsListScope[] = "https://docs.google.com/feeds/";
    247 const char kDriveAppsScope[] = "https://www.googleapis.com/auth/drive.apps";
    248 
    249 void ParseShareUrlAndRun(const google_apis::GetShareUrlCallback& callback,
    250                          google_apis::GDataErrorCode error,
    251                          scoped_ptr<base::Value> value) {
    252   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    253 
    254   if (!value) {
    255     callback.Run(error, GURL());
    256     return;
    257   }
    258 
    259   // Parsing ResourceEntry is cheap enough to do on UI thread.
    260   scoped_ptr<google_apis::ResourceEntry> entry =
    261       google_apis::ResourceEntry::ExtractAndParse(*value);
    262   if (!entry) {
    263     callback.Run(google_apis::GDATA_PARSE_ERROR, GURL());
    264     return;
    265   }
    266 
    267   const google_apis::Link* share_link =
    268       entry->GetLinkByType(google_apis::Link::LINK_SHARE);
    269   callback.Run(error, share_link ? share_link->href() : GURL());
    270 }
    271 
    272 scoped_ptr<google_apis::AboutResource>
    273 ConvertAccountMetadataToAboutResource(
    274     const google_apis::AccountMetadata& account_metadata,
    275     const std::string& root_resource_id) {
    276   scoped_ptr<google_apis::AboutResource> resource(
    277       new google_apis::AboutResource);
    278   resource->set_largest_change_id(account_metadata.largest_changestamp());
    279   resource->set_quota_bytes_total(account_metadata.quota_bytes_total());
    280   resource->set_quota_bytes_used(account_metadata.quota_bytes_used());
    281   resource->set_root_folder_id(root_resource_id);
    282   return resource.Pass();
    283 }
    284 
    285 scoped_ptr<google_apis::AppList>
    286 ConvertAccountMetadataToAppList(
    287     const google_apis::AccountMetadata& account_metadata) {
    288   scoped_ptr<google_apis::AppList> resource(new google_apis::AppList);
    289 
    290   const ScopedVector<google_apis::InstalledApp>& installed_apps =
    291       account_metadata.installed_apps();
    292   ScopedVector<google_apis::AppResource> app_resources;
    293   app_resources.reserve(installed_apps.size());
    294   for (size_t i = 0; i < installed_apps.size(); ++i) {
    295     app_resources.push_back(
    296         ConvertInstalledAppToAppResource(*installed_apps[i]).release());
    297   }
    298   resource->set_items(app_resources.Pass());
    299 
    300   // etag is not supported in AccountMetadata.
    301 
    302   return resource.Pass();
    303 }
    304 
    305 
    306 scoped_ptr<google_apis::FileResource> ConvertResourceEntryToFileResource(
    307     const google_apis::ResourceEntry& entry) {
    308   scoped_ptr<google_apis::FileResource> file(new google_apis::FileResource);
    309 
    310   file->set_file_id(entry.resource_id());
    311   file->set_title(entry.title());
    312   file->set_created_date(entry.published_time());
    313 
    314   if (std::find(entry.labels().begin(), entry.labels().end(),
    315                 "shared-with-me") != entry.labels().end()) {
    316     // Set current time to mark the file is shared_with_me, since ResourceEntry
    317     // doesn't have |shared_with_me_date| equivalent.
    318     file->set_shared_with_me_date(base::Time::Now());
    319   }
    320 
    321   file->set_shared(std::find(entry.labels().begin(), entry.labels().end(),
    322                              "shared") != entry.labels().end());
    323 
    324   file->set_download_url(entry.download_url());
    325   if (entry.is_folder())
    326     file->set_mime_type(kDriveFolderMimeType);
    327   else
    328     file->set_mime_type(entry.content_mime_type());
    329 
    330   file->set_md5_checksum(entry.file_md5());
    331   file->set_file_size(entry.file_size());
    332 
    333   file->mutable_labels()->set_trashed(entry.deleted());
    334   file->set_etag(entry.etag());
    335 
    336   google_apis::ImageMediaMetadata* image_media_metadata =
    337     file->mutable_image_media_metadata();
    338   image_media_metadata->set_width(entry.image_width());
    339   image_media_metadata->set_height(entry.image_height());
    340   image_media_metadata->set_rotation(entry.image_rotation());
    341 
    342   ScopedVector<google_apis::ParentReference> parents;
    343   for (size_t i = 0; i < entry.links().size(); ++i) {
    344     using google_apis::Link;
    345     const Link& link = *entry.links()[i];
    346     switch (link.type()) {
    347       case Link::LINK_PARENT: {
    348         scoped_ptr<google_apis::ParentReference> parent(
    349             new google_apis::ParentReference);
    350         parent->set_parent_link(link.href());
    351 
    352         std::string file_id =
    353             drive::util::ExtractResourceIdFromUrl(link.href());
    354         parent->set_file_id(file_id);
    355         parent->set_is_root(file_id == kWapiRootDirectoryResourceId);
    356         parents.push_back(parent.release());
    357         break;
    358       }
    359       case Link::LINK_EDIT:
    360         file->set_self_link(link.href());
    361         break;
    362       case Link::LINK_THUMBNAIL:
    363         file->set_thumbnail_link(link.href());
    364         break;
    365       case Link::LINK_ALTERNATE:
    366         file->set_alternate_link(link.href());
    367         break;
    368       case Link::LINK_EMBED:
    369         file->set_embed_link(link.href());
    370         break;
    371       default:
    372         break;
    373     }
    374   }
    375   file->set_parents(parents.Pass());
    376 
    377   file->set_modified_date(entry.updated_time());
    378   file->set_last_viewed_by_me_date(entry.last_viewed_time());
    379 
    380   return file.Pass();
    381 }
    382 
    383 google_apis::DriveEntryKind GetKind(
    384     const google_apis::FileResource& file_resource) {
    385   if (file_resource.IsDirectory())
    386     return google_apis::ENTRY_KIND_FOLDER;
    387 
    388   const std::string& mime_type = file_resource.mime_type();
    389   if (mime_type == kGoogleDocumentMimeType)
    390     return google_apis::ENTRY_KIND_DOCUMENT;
    391   if (mime_type == kGoogleSpreadsheetMimeType)
    392     return google_apis::ENTRY_KIND_SPREADSHEET;
    393   if (mime_type == kGooglePresentationMimeType)
    394     return google_apis::ENTRY_KIND_PRESENTATION;
    395   if (mime_type == kGoogleDrawingMimeType)
    396     return google_apis::ENTRY_KIND_DRAWING;
    397   if (mime_type == kGoogleTableMimeType)
    398     return google_apis::ENTRY_KIND_TABLE;
    399   if (mime_type == kGoogleFormMimeType)
    400     return google_apis::ENTRY_KIND_FORM;
    401   if (mime_type == "application/pdf")
    402     return google_apis::ENTRY_KIND_PDF;
    403   return google_apis::ENTRY_KIND_FILE;
    404 }
    405 
    406 scoped_ptr<google_apis::ResourceEntry>
    407 ConvertFileResourceToResourceEntry(
    408     const google_apis::FileResource& file_resource) {
    409   scoped_ptr<google_apis::ResourceEntry> entry(new google_apis::ResourceEntry);
    410 
    411   // ResourceEntry
    412   entry->set_resource_id(file_resource.file_id());
    413   entry->set_id(file_resource.file_id());
    414   entry->set_kind(GetKind(file_resource));
    415   entry->set_title(file_resource.title());
    416   entry->set_published_time(file_resource.created_date());
    417 
    418   std::vector<std::string> labels;
    419   if (!file_resource.shared_with_me_date().is_null())
    420     labels.push_back("shared-with-me");
    421   if (file_resource.shared())
    422     labels.push_back("shared");
    423   entry->set_labels(labels);
    424 
    425   // This should be the url to download the file_resource.
    426   {
    427     google_apis::Content content;
    428     content.set_url(file_resource.download_url());
    429     content.set_mime_type(file_resource.mime_type());
    430     entry->set_content(content);
    431   }
    432   // TODO(kochi): entry->resource_links_
    433 
    434   // For file entries
    435   entry->set_filename(file_resource.title());
    436   entry->set_suggested_filename(file_resource.title());
    437   entry->set_file_md5(file_resource.md5_checksum());
    438   entry->set_file_size(file_resource.file_size());
    439 
    440   // If file is removed completely, that information is only available in
    441   // ChangeResource, and is reflected in |removed_|. If file is trashed, the
    442   // file entry still exists but with its "trashed" label true.
    443   entry->set_deleted(file_resource.labels().is_trashed());
    444 
    445   // ImageMediaMetadata
    446   entry->set_image_width(file_resource.image_media_metadata().width());
    447   entry->set_image_height(file_resource.image_media_metadata().height());
    448   entry->set_image_rotation(file_resource.image_media_metadata().rotation());
    449 
    450   // CommonMetadata
    451   entry->set_etag(file_resource.etag());
    452   // entry->authors_
    453   // entry->links_.
    454   ScopedVector<google_apis::Link> links;
    455   for (size_t i = 0; i < file_resource.parents().size(); ++i) {
    456     google_apis::Link* link = new google_apis::Link;
    457     link->set_type(google_apis::Link::LINK_PARENT);
    458     link->set_href(file_resource.parents()[i]->parent_link());
    459     links.push_back(link);
    460   }
    461   if (!file_resource.self_link().is_empty()) {
    462     google_apis::Link* link = new google_apis::Link;
    463     link->set_type(google_apis::Link::LINK_EDIT);
    464     link->set_href(file_resource.self_link());
    465     links.push_back(link);
    466   }
    467   if (!file_resource.thumbnail_link().is_empty()) {
    468     google_apis::Link* link = new google_apis::Link;
    469     link->set_type(google_apis::Link::LINK_THUMBNAIL);
    470     link->set_href(file_resource.thumbnail_link());
    471     links.push_back(link);
    472   }
    473   if (!file_resource.alternate_link().is_empty()) {
    474     google_apis::Link* link = new google_apis::Link;
    475     link->set_type(google_apis::Link::LINK_ALTERNATE);
    476     link->set_href(file_resource.alternate_link());
    477     links.push_back(link);
    478   }
    479   if (!file_resource.embed_link().is_empty()) {
    480     google_apis::Link* link = new google_apis::Link;
    481     link->set_type(google_apis::Link::LINK_EMBED);
    482     link->set_href(file_resource.embed_link());
    483     links.push_back(link);
    484   }
    485   entry->set_links(links.Pass());
    486 
    487   // entry->categories_
    488   entry->set_updated_time(file_resource.modified_date());
    489   entry->set_last_viewed_time(file_resource.last_viewed_by_me_date());
    490 
    491   entry->FillRemainingFields();
    492   return entry.Pass();
    493 }
    494 
    495 scoped_ptr<google_apis::ResourceEntry>
    496 ConvertChangeResourceToResourceEntry(
    497     const google_apis::ChangeResource& change_resource) {
    498   scoped_ptr<google_apis::ResourceEntry> entry;
    499   if (change_resource.file())
    500     entry = ConvertFileResourceToResourceEntry(*change_resource.file()).Pass();
    501   else
    502     entry.reset(new google_apis::ResourceEntry);
    503 
    504   entry->set_resource_id(change_resource.file_id());
    505   // If |is_deleted()| returns true, the file is removed from Drive.
    506   entry->set_removed(change_resource.is_deleted());
    507   entry->set_changestamp(change_resource.change_id());
    508 
    509   return entry.Pass();
    510 }
    511 
    512 scoped_ptr<google_apis::ResourceList>
    513 ConvertFileListToResourceList(const google_apis::FileList& file_list) {
    514   scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
    515 
    516   const ScopedVector<google_apis::FileResource>& items = file_list.items();
    517   ScopedVector<google_apis::ResourceEntry> entries;
    518   for (size_t i = 0; i < items.size(); ++i)
    519     entries.push_back(ConvertFileResourceToResourceEntry(*items[i]).release());
    520   feed->set_entries(entries.Pass());
    521 
    522   ScopedVector<google_apis::Link> links;
    523   if (!file_list.next_link().is_empty()) {
    524     google_apis::Link* link = new google_apis::Link;
    525     link->set_type(google_apis::Link::LINK_NEXT);
    526     link->set_href(file_list.next_link());
    527     links.push_back(link);
    528   }
    529   feed->set_links(links.Pass());
    530 
    531   return feed.Pass();
    532 }
    533 
    534 scoped_ptr<google_apis::ResourceList>
    535 ConvertChangeListToResourceList(const google_apis::ChangeList& change_list) {
    536   scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
    537 
    538   const ScopedVector<google_apis::ChangeResource>& items = change_list.items();
    539   ScopedVector<google_apis::ResourceEntry> entries;
    540   for (size_t i = 0; i < items.size(); ++i) {
    541     entries.push_back(
    542         ConvertChangeResourceToResourceEntry(*items[i]).release());
    543   }
    544   feed->set_entries(entries.Pass());
    545 
    546   feed->set_largest_changestamp(change_list.largest_change_id());
    547 
    548   ScopedVector<google_apis::Link> links;
    549   if (!change_list.next_link().is_empty()) {
    550     google_apis::Link* link = new google_apis::Link;
    551     link->set_type(google_apis::Link::LINK_NEXT);
    552     link->set_href(change_list.next_link());
    553     links.push_back(link);
    554   }
    555   feed->set_links(links.Pass());
    556 
    557   return feed.Pass();
    558 }
    559 
    560 std::string GetMd5Digest(const base::FilePath& file_path) {
    561   const int kBufferSize = 512 * 1024;  // 512kB.
    562 
    563   base::PlatformFile file = base::CreatePlatformFile(
    564       file_path, base::PLATFORM_FILE_OPEN | base::PLATFORM_FILE_READ,
    565       NULL, NULL);
    566   if (file == base::kInvalidPlatformFileValue)
    567     return std::string();
    568   base::ScopedPlatformFileCloser file_closer(&file);
    569 
    570   base::MD5Context context;
    571   base::MD5Init(&context);
    572 
    573   int64 offset = 0;
    574   scoped_ptr<char[]> buffer(new char[kBufferSize]);
    575   while (true) {
    576     // Avoid using ReadPlatformFileCurPosNoBestEffort for now.
    577     // http://crbug.com/145873
    578     int result = base::ReadPlatformFileNoBestEffort(
    579         file, offset, buffer.get(), kBufferSize);
    580 
    581     if (result < 0) {
    582       // Found an error.
    583       return std::string();
    584     }
    585 
    586     if (result == 0) {
    587       // End of file.
    588       break;
    589     }
    590 
    591     offset += result;
    592     base::MD5Update(&context, base::StringPiece(buffer.get(), result));
    593   }
    594 
    595   base::MD5Digest digest;
    596   base::MD5Final(&digest, &context);
    597   return MD5DigestToBase16(digest);
    598 }
    599 
    600 const char kWapiRootDirectoryResourceId[] = "folder:root";
    601 
    602 }  // namespace util
    603 }  // namespace drive
    604