summaryrefslogtreecommitdiffstats
path: root/chrome
diff options
context:
space:
mode:
authorzelidrag@chromium.org <zelidrag@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-02-25 18:04:47 +0000
committerzelidrag@chromium.org <zelidrag@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-02-25 18:04:47 +0000
commitd0baa03ba5e91ff80241a7f8ac478be7120a4332 (patch)
tree9cea352a7c0d106c101ed04f6a493a936c299ea7 /chrome
parent66619f448a567ab5f27c8e572af30eb54c67ba8a (diff)
downloadchromium_src-d0baa03ba5e91ff80241a7f8ac478be7120a4332.zip
chromium_src-d0baa03ba5e91ff80241a7f8ac478be7120a4332.tar.gz
chromium_src-d0baa03ba5e91ff80241a7f8ac478be7120a4332.tar.bz2
gdata API changes for content fetching. Added GDataFileSystem which represents FS abstraction layer between File API and gdata. It autoupdates its content metadata on file/dir lookup requests.
BUG=chromium-os:26006 TEST=GDataFileSystemTest.* Review URL: https://chromiumcodereview.appspot.com/9437002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@123665 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r--chrome/DEPS1
-rw-r--r--chrome/browser/chromeos/gdata/gdata.cc79
-rw-r--r--chrome/browser/chromeos/gdata/gdata.h13
-rw-r--r--chrome/browser/chromeos/gdata/gdata_file_system.cc461
-rw-r--r--chrome/browser/chromeos/gdata/gdata_file_system.h244
-rw-r--r--chrome/browser/chromeos/gdata/gdata_file_system_proxy.cc291
-rw-r--r--chrome/browser/chromeos/gdata/gdata_file_system_proxy.h39
-rw-r--r--chrome/browser/chromeos/gdata/gdata_file_system_unittest.cc263
-rw-r--r--chrome/browser/chromeos/gdata/gdata_parser.cc17
-rw-r--r--chrome/browser/chromeos/gdata/gdata_parser.h40
-rw-r--r--chrome/chrome_browser.gypi4
-rw-r--r--chrome/chrome_tests.gypi1
-rw-r--r--chrome/test/data/chromeos/gdata/root_feed.json490
-rw-r--r--chrome/test/data/chromeos/gdata/subdir_feed.json236
14 files changed, 2141 insertions, 38 deletions
diff --git a/chrome/DEPS b/chrome/DEPS
index eb0cc95..27889df 100644
--- a/chrome/DEPS
+++ b/chrome/DEPS
@@ -32,6 +32,7 @@ include_rules = [
"+third_party/WebKit/Source/WebKit/chromium",
"+webkit/appcache",
"+webkit/blob",
+ "+webkit/chromeos/fileapi",
"+webkit/database",
"+webkit/fileapi",
"+webkit/quota",
diff --git a/chrome/browser/chromeos/gdata/gdata.cc b/chrome/browser/chromeos/gdata/gdata.cc
index db20fef..2294be7 100644
--- a/chrome/browser/chromeos/gdata/gdata.cc
+++ b/chrome/browser/chromeos/gdata/gdata.cc
@@ -54,15 +54,16 @@ const char kGDataVersionHeader[] = "GData-Version: 3.0";
// etag matching header.
const char kIfMatchHeaderFormat[] = "If-Match: %s";
-// URL requesting documents list.
+// URL requesting documents list that belong to the authenticated user only
+// (handled with '-/mine' part).
const char kGetDocumentListURL[] =
- "https://docs.google.com/feeds/default/private/full?"
- "v=3&alt=json&showfolders=true&max-results=%d";
+ "https://docs.google.com/feeds/default/private/full/-/mine";
#ifndef NDEBUG
-// Use super small 'page' size while debugging to ensure we hit feed reload
-// almost always.
-const int kMaxDocumentsPerFeed = 10;
+// Use smaller 'page' size while debugging to ensure we hit feed reload
+// almost always. Be careful not to use something too small on account that
+// have many items because server side 503 error might kick in.
+const int kMaxDocumentsPerFeed = 1000;
#else
const int kMaxDocumentsPerFeed = 1000;
#endif
@@ -136,6 +137,21 @@ std::string GetResponseHeadersAsString(const content::URLFetcher* url_fetcher) {
return headers;
}
+// Adds additional parameters for API version, output content type and to show
+// folders in the feed are added to document feed URLs.
+GURL AddFeedUrlParams(const GURL& url) {
+ GURL result = chrome_browser_net::AppendQueryParameter(url, "v", "3");
+ result = chrome_browser_net::AppendQueryParameter(result, "alt", "json");
+ result = chrome_browser_net::AppendQueryParameter(result,
+ "showfolders",
+ "true");
+ result = chrome_browser_net::AppendQueryParameter(
+ result,
+ "max-results",
+ base::StringPrintf("%d", kMaxDocumentsPerFeed));
+ return result;
+}
+
} // namespace
//=============================== UploadFileInfo ===============================
@@ -365,7 +381,9 @@ class GetDataOperation : public UrlFetchOperation<GetDataCallback> {
LOG(ERROR) << "Error while parsing entry response: "
<< error_message
<< ", code: "
- << error_code;
+ << error_code
+ << ", data:\n"
+ << data;
return NULL;
}
return root_value.release();
@@ -406,9 +424,9 @@ void GetDocumentsOperation::SetUrl(const GURL& url) {
GURL GetDocumentsOperation::GetURL() const {
if (!override_url_.is_empty())
- return override_url_;
+ return AddFeedUrlParams(override_url_);
- return GURL(base::StringPrintf(kGetDocumentListURL, kMaxDocumentsPerFeed));
+ return AddFeedUrlParams(GURL(kGetDocumentListURL));
}
//============================ DownloadFileOperation ===========================
@@ -788,45 +806,55 @@ void DocumentsService::Initialize(Profile* profile) {
GDataService::Initialize(profile);
}
-void DocumentsService::GetDocuments(GetDataCallback callback) {
+void DocumentsService::GetDocuments(const GURL& url,
+ const GetDataCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(kGDataAPICallThread));
if (!IsFullyAuthenticated()) {
// Fetch OAuth2 authetication token from the refresh token first.
StartAuthentication(base::Bind(&DocumentsService::GetDocumentsOnAuthRefresh,
base::Unretained(this),
+ url,
callback));
return;
}
get_documents_started_ = true;
- (new GetDocumentsOperation(profile_, oauth2_auth_token()))->Start(
- base::Bind(&DocumentsService::OnGetDocumentsCompleted,
- base::Unretained(this),
- callback));
+ // operation is self-destructing, we don't need to clean it here.
+ GetDocumentsOperation* operation = new GetDocumentsOperation(profile_,
+ oauth2_auth_token());
+ if (!url.is_empty())
+ operation->SetUrl(url);
+
+ operation->Start(base::Bind(&DocumentsService::OnGetDocumentsCompleted,
+ base::Unretained(this),
+ url,
+ callback));
}
-void DocumentsService::GetDocumentsOnAuthRefresh(GetDataCallback callback,
- GDataErrorCode error,
- const std::string& token) {
+void DocumentsService::GetDocumentsOnAuthRefresh(const GURL& url,
+ const GetDataCallback& callback,
+ GDataErrorCode error,
+ const std::string& token) {
if (error != HTTP_SUCCESS) {
if (!callback.is_null())
callback.Run(error, NULL);
return;
}
DCHECK(IsPartiallyAuthenticated());
- GetDocuments(callback);
+ GetDocuments(url, callback);
}
-void DocumentsService::OnGetDocumentsCompleted(GetDataCallback callback,
- GDataErrorCode error,
- base::Value* value) {
+void DocumentsService::OnGetDocumentsCompleted(const GURL& url,
+ const GetDataCallback& callback,
+ GDataErrorCode error,
+ base::Value* value) {
switch (error) {
case HTTP_UNAUTHORIZED:
DCHECK(!value);
auth_token_.clear();
// User authentication might have expired - rerun the request to force
// auth token refresh.
- GetDocuments(callback);
+ GetDocuments(url, callback);
return;
default:
break;
@@ -985,7 +1013,7 @@ void DocumentsService::InitiateUpload(const UploadFileInfo& upload_file_info,
// Start GetDocuments if it hasn't even started.
if (!get_documents_started_) {
- GetDocuments(base::Bind(&DocumentsService::UpdateFilelist,
+ GetDocuments(GURL(), base::Bind(&DocumentsService::UpdateFilelist,
base::Unretained(this)));
}
@@ -1133,10 +1161,9 @@ void DocumentsService::OnOAuth2RefreshTokenChanged() {
if (!IsPartiallyAuthenticated())
return;
- // TODO(zelidrag): Remove this becasue we probably don't want to fetch this
- // before it is really needed.
if (!feed_value_.get() && !get_documents_started_) {
- GetDocuments(base::Bind(&DocumentsService::UpdateFilelist,
+ GetDocuments(GURL(),
+ base::Bind(&DocumentsService::UpdateFilelist,
base::Unretained(this)));
}
}
diff --git a/chrome/browser/chromeos/gdata/gdata.h b/chrome/browser/chromeos/gdata/gdata.h
index 4ef5325..de77dcf 100644
--- a/chrome/browser/chromeos/gdata/gdata.h
+++ b/chrome/browser/chromeos/gdata/gdata.h
@@ -182,8 +182,10 @@ class DocumentsService : public GDataService {
// GDataService override.
virtual void Initialize(Profile* profile) OVERRIDE;
- // Gets the document list. Upon completion, invokes |callback| with results.
- void GetDocuments(GetDataCallback callback);
+ // Gets the document feed from |feed_url|. If this URL is empty, the call
+ // will fetch the default ('root') document feed. Upon completion,
+ // invokes |callback| with results.
+ void GetDocuments(const GURL& feed_url, const GetDataCallback& callback);
// Delete a document identified by its 'self' |url| and |etag|.
// Upon completion, invokes |callback| with results.
@@ -253,12 +255,15 @@ class DocumentsService : public GDataService {
// duplication.
// Callback when re-authenticating user during document list fetching.
- void GetDocumentsOnAuthRefresh(GetDataCallback callback,
+ void GetDocumentsOnAuthRefresh(const GURL& feed_url,
+ const GetDataCallback& callback,
GDataErrorCode error,
const std::string& auth_token);
// Pass-through callback for re-authentication during document list fetching.
- void OnGetDocumentsCompleted(GetDataCallback callback,
+ // If the call is successful, parsed document feed will be returned as |root|.
+ void OnGetDocumentsCompleted(const GURL& feed_url,
+ const GetDataCallback& callback,
GDataErrorCode error,
base::Value* root);
diff --git a/chrome/browser/chromeos/gdata/gdata_file_system.cc b/chrome/browser/chromeos/gdata/gdata_file_system.cc
new file mode 100644
index 0000000..e82c171
--- /dev/null
+++ b/chrome/browser/chromeos/gdata/gdata_file_system.cc
@@ -0,0 +1,461 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/gdata/gdata_file_system.h"
+
+#include "base/bind.h"
+#include "base/json/json_writer.h"
+#include "base/utf_string_conversions.h"
+#include "base/platform_file.h"
+#include "base/stringprintf.h"
+#include "base/string_util.h"
+#include "base/values.h"
+#include "chrome/browser/chromeos/gdata/gdata.h"
+#include "chrome/browser/chromeos/gdata/gdata_parser.h"
+#include "content/public/browser/browser_thread.h"
+#include "net/base/escape.h"
+#include "webkit/fileapi/file_system_file_util_proxy.h"
+#include "webkit/fileapi/file_system_types.h"
+#include "webkit/fileapi/file_system_util.h"
+
+using content::BrowserThread;
+
+namespace {
+
+// Content refresh time.
+const int kRefreshTimeInSec = 5*60;
+
+const char kGDataRootDirectory[] = "gdata";
+const char kFeedField[] = "feed";
+
+// Converts gdata error code into file platform error code.
+base::PlatformFileError GDataToPlatformError(gdata::GDataErrorCode status) {
+ switch (status) {
+ case gdata::HTTP_SUCCESS:
+ case gdata::HTTP_CREATED:
+ return base::PLATFORM_FILE_OK;
+ case gdata::HTTP_UNAUTHORIZED:
+ case gdata::HTTP_FORBIDDEN:
+ return base::PLATFORM_FILE_ERROR_ACCESS_DENIED;
+ case gdata::HTTP_NOT_FOUND:
+ return base::PLATFORM_FILE_ERROR_NOT_FOUND;
+ case gdata::GDATA_PARSE_ERROR:
+ case gdata::GDATA_FILE_ERROR:
+ return base::PLATFORM_FILE_ERROR_ABORT;
+ default:
+ return base::PLATFORM_FILE_ERROR_FAILED;
+ }
+}
+
+// Escapes file names since hosted documents from gdata can actually have
+// forward slashes in their titles.
+std::string EscapeFileName(const std::string& input) {
+ std::string tmp;
+ std::string output;
+ if (ReplaceChars(input, "%", std::string("%25"), &tmp) &&
+ ReplaceChars(tmp, "/", std::string("%2F"), &output)) {
+ return output;
+ }
+
+ return input;
+}
+
+} // namespace
+
+namespace gdata {
+
+// Delegate used to find a directory element for file system updates.
+class UpdateDirectoryDelegate : public FindFileDelegate {
+ public:
+ UpdateDirectoryDelegate() : directory_(NULL) {
+ }
+
+ GDataDirectory* directory() { return directory_; }
+
+ private:
+ // GDataFileSystem::FindFileDelegate overrides.
+ virtual void OnFileFound(gdata::GDataFile*) OVERRIDE {
+ directory_ = NULL;
+ }
+
+ // GDataFileSystem::FindFileDelegate overrides.
+ virtual void OnDirectoryFound(const FilePath&,
+ GDataDirectory* dir) OVERRIDE {
+ if (!dir->file_info().is_directory)
+ return;
+
+ directory_ = dir;
+ }
+
+ virtual FindFileTraversalCommand OnEnterDirectory(const FilePath&,
+ GDataDirectory*) OVERRIDE {
+ // Keep traversing while doing read only lookups.
+ return FIND_FILE_CONTINUES;
+ }
+
+ virtual void OnError(base::PlatformFileError) OVERRIDE {
+ directory_ = NULL;
+ }
+
+ GDataDirectory* directory_;
+};
+
+FindFileDelegate::~FindFileDelegate() {
+}
+
+// GDataFileBase class.
+
+GDataFileBase::GDataFileBase() {
+}
+
+GDataFileBase::~GDataFileBase() {
+}
+
+GDataFile* GDataFileBase::AsGDataFile() {
+ return NULL;
+}
+
+GDataDirectory* GDataFileBase::AsGDataDirectory() {
+ return NULL;
+}
+
+
+GDataFileBase* GDataFileBase::FromDocumentEntry(DocumentEntry* doc) {
+ if (doc->is_folder())
+ return GDataDirectory::FromDocumentEntry(doc);
+ else if (doc->is_hosted_document() || doc->is_file())
+ return GDataFile::FromDocumentEntry(doc);
+
+ return NULL;
+}
+
+GDataFileBase* GDataFile::FromDocumentEntry(DocumentEntry* doc) {
+ DCHECK(doc->is_hosted_document() || doc->is_file());
+ GDataFile* file = new GDataFile();
+ // Check if this entry is a true file, or...
+ if (doc->is_file()) {
+ file->original_file_name_ = UTF16ToUTF8(doc->filename());
+ file->file_name_ =
+ EscapeFileName(file->original_file_name_);
+ file->file_info_.size = doc->file_size();
+ file->file_md5_ = doc->file_md5();
+ } else {
+ // ... a hosted document.
+ file->original_file_name_ = UTF16ToUTF8(doc->title());
+ // Attach .g<something> extension to hosted documents so we can special
+ // case their handling in UI.
+ // TODO(zelidrag): Figure out better way how to pass entry info like kind
+ // to UI through the File API stack.
+ file->file_name_ = EscapeFileName(
+ base::StringPrintf("%s.g%s",
+ file->original_file_name_.c_str(),
+ doc->GetEntryKindText().c_str()));
+ // We don't know the size of hosted docs and it does not matter since
+ // is has no effect on the quota.
+ file->file_info_.size = 0;
+ }
+ file->kind_ = doc->kind();
+ const Link* self_link = doc->GetLinkByType(Link::SELF);
+ if (self_link)
+ file->self_url_ = self_link->href();
+ file->content_url_ = doc->content_url();
+ file->content_mime_type_ = doc->content_mime_type();
+ file->etag_ = doc->etag();
+ file->resource_id_ = doc->resource_id();
+ file->id_ = doc->id();
+ file->file_info_.last_modified = doc->updated_time();
+ file->file_info_.last_accessed = doc->updated_time();
+ file->file_info_.creation_time = doc->published_time();
+ return file;
+}
+
+// GDataFile class implementation.
+
+GDataFile::GDataFile() : kind_(gdata::DocumentEntry::UNKNOWN) {
+}
+
+GDataFile::~GDataFile() {
+}
+
+GDataFile* GDataFile::AsGDataFile() {
+ return this;
+}
+
+// GDataDirectory class implementation.
+
+GDataDirectory::GDataDirectory() {
+ file_info_.is_directory = true;
+}
+
+GDataDirectory::~GDataDirectory() {
+ RemoveChildren();
+}
+
+GDataDirectory* GDataDirectory::AsGDataDirectory() {
+ return this;
+}
+
+// static
+GDataFileBase* GDataDirectory::FromDocumentEntry(DocumentEntry* doc) {
+ DCHECK(doc->is_folder());
+ GDataDirectory* dir = new GDataDirectory();
+ dir->file_name_ = UTF16ToUTF8(doc->title());
+ dir->file_info_.last_modified = doc->updated_time();
+ dir->file_info_.last_accessed = doc->updated_time();
+ dir->file_info_.creation_time = doc->published_time();
+ // Extract feed link.
+ dir->start_feed_url_ = doc->content_url();
+ return dir;
+}
+
+void GDataDirectory::RemoveChildren() {
+ STLDeleteValues(&children_);
+ children_.clear();
+}
+
+bool GDataDirectory::NeedsRefresh(GURL* feed_url) {
+ if ((base::Time::Now() - refresh_time_).InSeconds() < kRefreshTimeInSec)
+ return false;
+
+ *feed_url = start_feed_url_;
+ return true;
+}
+
+void GDataDirectory::AddFile(GDataFileBase* file) {
+ // Do file name de-duplication - find files with the same name and
+ // append a name modifier to the name.
+ int max_modifier = 1;
+ FilePath full_file_name(file->file_name());
+ std::string extension = full_file_name.Extension();
+ std::string file_name = full_file_name.RemoveExtension().value();
+ while (children_.find(full_file_name.value()) != children_.end()) {
+ if (!extension.empty()) {
+ full_file_name = FilePath(base::StringPrintf("%s (%d)%s",
+ file_name.c_str(),
+ ++max_modifier,
+ extension.c_str()));
+ } else {
+ full_file_name = FilePath(base::StringPrintf("%s (%d)",
+ file_name.c_str(),
+ ++max_modifier));
+ }
+ }
+ if (full_file_name.value() != file->file_name())
+ file->set_file_name(full_file_name.value());
+
+ children_.insert(std::make_pair(file->file_name(), file));
+}
+
+// GDataFileSystem::FindFileParams struct implementation.
+
+GDataFileSystem::FindFileParams::FindFileParams(
+ const FilePath& in_file_path,
+ bool in_require_content,
+ const FilePath& in_directory_path,
+ const GURL& in_feed_url,
+ bool in_initial_feed,
+ scoped_refptr<FindFileDelegate> in_delegate)
+ : file_path(in_file_path),
+ require_content(in_require_content),
+ directory_path(in_directory_path),
+ feed_url(in_feed_url),
+ initial_feed(in_initial_feed),
+ delegate(in_delegate) {
+}
+
+GDataFileSystem::FindFileParams::~FindFileParams() {
+}
+
+// GDataFileSystem class implementatsion.
+
+GDataFileSystem::GDataFileSystem() {
+ root_.reset(new GDataDirectory());
+ root_->set_file_name(kGDataRootDirectory);
+}
+
+GDataFileSystem::~GDataFileSystem() {
+}
+
+void GDataFileSystem::FindFileByPath(
+ const FilePath& file_path, scoped_refptr<FindFileDelegate> delegate) {
+ base::AutoLock lock(lock_);
+ UnsafeFindFileByPath(file_path, delegate);
+}
+
+void GDataFileSystem::StartDirectoryRefresh(
+ const FindFileParams& params) {
+ // Kick off document feed fetching here if we don't have complete data
+ // to finish this call.
+ BrowserThread::PostTask(
+ BrowserThread::UI, FROM_HERE,
+ base::Bind(
+ &GDataFileSystem::RefreshFeedOnUIThread,
+ this,
+ params.feed_url,
+ base::Bind(&GDataFileSystem::OnGetDocuments,
+ this,
+ params)));
+}
+
+void GDataFileSystem::UnsafeFindFileByPath(
+ const FilePath& file_path, scoped_refptr<FindFileDelegate> delegate) {
+ lock_.AssertAcquired();
+
+ std::vector<FilePath::StringType> components;
+ file_path.GetComponents(&components);
+
+ GDataDirectory* current_dir = root_.get();
+ FilePath directory_path;
+ for (size_t i = 0; i < components.size() && current_dir; i++) {
+ directory_path = directory_path.Append(current_dir->file_name());
+
+ // Last element must match, if not last then it must be a directory.
+ if (i == components.size() - 1) {
+ if (current_dir->file_name() == components[i])
+ delegate->OnDirectoryFound(directory_path, current_dir);
+ else
+ delegate->OnError(base::PLATFORM_FILE_ERROR_NOT_FOUND);
+
+ return;
+ }
+
+ if (delegate->OnEnterDirectory(directory_path, current_dir) ==
+ FindFileDelegate::FIND_FILE_TERMINATES) {
+ return;
+ }
+
+ // Not the last part of the path, search for the next segment.
+ GDataFileCollection::const_iterator file_iter =
+ current_dir->children().find(components[i + 1]);
+ if (file_iter == current_dir->children().end()) {
+ delegate->OnError(base::PLATFORM_FILE_ERROR_NOT_FOUND);
+ return;
+ }
+
+ // Found file, must be the last segment.
+ if (file_iter->second->file_info().is_directory) {
+ // Found directory, continue traversal.
+ current_dir = file_iter->second->AsGDataDirectory();
+ } else {
+ if ((i + 1) == (components.size() - 1))
+ delegate->OnFileFound(file_iter->second->AsGDataFile());
+ else
+ delegate->OnError(base::PLATFORM_FILE_ERROR_NOT_FOUND);
+
+ return;
+ }
+ }
+ delegate->OnError(base::PLATFORM_FILE_ERROR_NOT_FOUND);
+}
+
+void GDataFileSystem::RefreshFeedOnUIThread(const GURL& feed_url,
+ const GetDataCallback& callback) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ DocumentsService::GetInstance()->GetDocuments(feed_url, callback);
+}
+
+void GDataFileSystem::OnGetDocuments(
+ const FindFileParams& params,
+ GDataErrorCode status,
+ base::Value* data) {
+ base::PlatformFileError error = GDataToPlatformError(status);
+
+ if (error == base::PLATFORM_FILE_OK &&
+ (!data || data->GetType() != Value::TYPE_DICTIONARY)) {
+ LOG(WARNING) << "No feed content!";
+ error = base::PLATFORM_FILE_ERROR_FAILED;
+ }
+
+ if (error != base::PLATFORM_FILE_OK) {
+ params.delegate->OnError(error);
+ return;
+ }
+
+ GURL next_feed_url;
+ error = UpdateDirectoryWithDocumentFeed(
+ params.directory_path, params.feed_url, data, params.initial_feed,
+ &next_feed_url);
+ if (error != base::PLATFORM_FILE_OK) {
+ params.delegate->OnError(error);
+ return;
+ }
+
+ // Fetch the rest of the content if the feed is not completed.
+ if (!next_feed_url.is_empty()) {
+ StartDirectoryRefresh(FindFileParams(params.file_path,
+ params.require_content,
+ params.directory_path,
+ next_feed_url,
+ false, /* initial_feed */
+ params.delegate));
+ return;
+ }
+
+ // Continue file content search operation.
+ FindFileByPath(params.file_path,
+ params.delegate);
+}
+
+
+base::PlatformFileError GDataFileSystem::UpdateDirectoryWithDocumentFeed(
+ const FilePath& directory_path, const GURL& feed_url,
+ base::Value* data, bool is_initial_feed, GURL* next_feed) {
+ base::DictionaryValue* feed_dict = NULL;
+ scoped_ptr<DocumentFeed> feed;
+ if (!static_cast<base::DictionaryValue*>(data)->GetDictionary(
+ kFeedField, &feed_dict)) {
+ return base::PLATFORM_FILE_ERROR_FAILED;
+ }
+
+ // Parse the document feed.
+ feed.reset(DocumentFeed::CreateFrom(feed_dict));
+ if (!feed.get())
+ return base::PLATFORM_FILE_ERROR_FAILED;
+
+ // We need to lock here as well (despite FindFileByPath lock) since directory
+ // instance below is a 'live' object.
+ base::AutoLock lock(lock_);
+
+ // Find directory element within the cached file system snapshot.
+ scoped_refptr<UpdateDirectoryDelegate> update_delegate(
+ new UpdateDirectoryDelegate());
+ UnsafeFindFileByPath(directory_path,
+ update_delegate);
+
+ GDataDirectory* dir = update_delegate->directory();
+ if (!dir)
+ return base::PLATFORM_FILE_ERROR_FAILED;
+
+ dir->set_start_feed_url(feed_url);
+ dir->set_refresh_time(base::Time::Now());
+ if (feed->GetNextFeedURL(next_feed))
+ dir->set_next_feed_url(*next_feed);
+
+ // Remove all child elements if we are refreshing the entire content.
+ if (is_initial_feed)
+ dir->RemoveChildren();
+
+ for (ScopedVector<DocumentEntry>::const_iterator iter =
+ feed->entries().begin();
+ iter != feed->entries().end(); ++iter) {
+ DocumentEntry* doc = *iter;
+
+ // For now, skip elements of the root directory feed that have parent.
+ // TODO(zelidrag): In theory, we could reconstruct the entire FS snapshot
+ // of the root file feed only instead of fetching one dir/collection at the
+ // time.
+ if (dir == root_.get()) {
+ const Link* parent_link = doc->GetLinkByType(Link::PARENT);
+ if (parent_link)
+ continue;
+ }
+
+ GDataFileBase* file = GDataFileBase::FromDocumentEntry(doc);
+ if (file)
+ dir->AddFile(file);
+ }
+ return base::PLATFORM_FILE_OK;
+}
+
+} // namespace gdata
diff --git a/chrome/browser/chromeos/gdata/gdata_file_system.h b/chrome/browser/chromeos/gdata/gdata_file_system.h
new file mode 100644
index 0000000..8d2a403
--- /dev/null
+++ b/chrome/browser/chromeos/gdata/gdata_file_system.h
@@ -0,0 +1,244 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_GDATA_GDATA_FILE_SYSTEM_H_
+#define CHROME_BROWSER_CHROMEOS_GDATA_GDATA_FILE_SYSTEM_H_
+
+#include <map>
+
+#include "base/memory/scoped_ptr.h"
+#include "base/platform_file.h"
+#include "base/synchronization/lock.h"
+#include "chrome/browser/chromeos/gdata/gdata.h"
+#include "chrome/browser/chromeos/gdata/gdata_parser.h"
+
+namespace gdata {
+
+class GDataDirectory;
+class GDataFile;
+
+// Base class for representing files and directories in gdata virtual file
+// system.
+class GDataFileBase {
+ public:
+ GDataFileBase();
+ virtual ~GDataFileBase();
+ // Converts gdata::DocumentEntry into chromeos::GDataFileBase.
+ static GDataFileBase* FromDocumentEntry(gdata::DocumentEntry* doc);
+ virtual GDataFile* AsGDataFile();
+ virtual GDataDirectory* AsGDataDirectory();
+ const base::PlatformFileInfo& file_info() const { return file_info_; }
+ const FilePath::StringType& file_name() const { return file_name_; }
+ const FilePath::StringType& original_file_name() const {
+ return original_file_name_;
+ }
+ void set_file_name(const FilePath::StringType& name) { file_name_ = name; }
+ const GURL& self_url() const { return self_url_; }
+
+ protected:
+ base::PlatformFileInfo file_info_;
+ FilePath::StringType file_name_;
+ FilePath::StringType original_file_name_;
+ // Files with the same original name will be uniquely identified with this
+ // field so we can represent them with unique URLs/paths in File API layer.
+ // For example, two files in the same directory with the same name "Foo"
+ // will show up in the virtual directory as "Foo" and "Foo (2)".
+ GURL self_url_;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(GDataFileBase);
+};
+
+typedef std::map<FilePath::StringType, GDataFileBase*> GDataFileCollection;
+
+// Represents "file" in in a GData virtual file system. On gdata feed side,
+// this could be either a regular file or a server side document.
+class GDataFile : public GDataFileBase {
+ public:
+ GDataFile();
+ virtual ~GDataFile();
+ virtual GDataFile* AsGDataFile() OVERRIDE;
+
+ static GDataFileBase* FromDocumentEntry(gdata::DocumentEntry* doc);
+
+ gdata::DocumentEntry::EntryKind kind() const { return kind_; }
+ const GURL& content_url() const { return content_url_; }
+ const std::string& content_mime_type() const { return content_mime_type_; }
+ const std::string& etag() const { return etag_; }
+ const std::string& resource() const { return resource_id_; }
+ const std::string& id() const { return id_; }
+ const std::string& file_md5() const { return file_md5_; }
+
+ private:
+ // Content URL for files.
+ gdata::DocumentEntry::EntryKind kind_;
+ GURL content_url_;
+ std::string content_mime_type_;
+ std::string etag_;
+ std::string resource_id_;
+ std::string id_;
+ std::string file_md5_;
+
+ DISALLOW_COPY_AND_ASSIGN(GDataFile);
+};
+
+// Represents "directory" in a GData virtual file system. Maps to gdata
+// collection element.
+class GDataDirectory : public GDataFileBase {
+ public:
+ GDataDirectory();
+ virtual ~GDataDirectory();
+ virtual GDataDirectory* AsGDataDirectory() OVERRIDE;
+
+ static GDataFileBase* FromDocumentEntry(gdata::DocumentEntry* doc);
+
+ // Adds child file to the directory and takes over the ownership of |file|
+ // object. The method will also do name deduplication to ensure that the
+ // exposed presentation path does not have naming conflicts. Two files with
+ // the same name "Foo" will be renames to "Foo (1)" and "Foo (2)".
+ void AddFile(GDataFileBase* file);
+
+ // Checks if directory content needs to be retrieved again. If it does,
+ // the function will return URL for next feed in |next_feed_url|.
+ bool NeedsRefresh(GURL* next_feed_url);
+
+ // Removes children elements.
+ void RemoveChildren();
+
+ // Last refresh time.
+ const base::Time& refresh_time() const { return refresh_time_; }
+ void set_refresh_time(const base::Time& time) { refresh_time_ = time; }
+ // Url for this feed.
+ const GURL& start_feed_url() const { return start_feed_url_; }
+ void set_start_feed_url(const GURL& url) { start_feed_url_ = url; }
+ // Continuing feed's url.
+ const GURL& next_feed_url() const { return next_feed_url_; }
+ void set_next_feed_url(const GURL& url) { next_feed_url_ = url; }
+ // Collection of children GDataFileBase items.
+ const GDataFileCollection& children() const { return children_; }
+
+ private:
+ base::Time refresh_time_;
+ // Url for this feed.
+ GURL start_feed_url_;
+ // Continuing feed's url.
+ GURL next_feed_url_;
+ // Collection of children GDataFileBase items.
+ GDataFileCollection children_;
+
+ DISALLOW_COPY_AND_ASSIGN(GDataDirectory);
+};
+
+// Delegate class used to deal with results of virtual directory request
+// to FindFileByPath() method. This class is refcounted since we pass it
+// around and access it from different threads.
+class FindFileDelegate : public base::RefCountedThreadSafe<FindFileDelegate> {
+ public:
+ virtual ~FindFileDelegate();
+
+ enum FindFileTraversalCommand {
+ FIND_FILE_CONTINUES,
+ FIND_FILE_TERMINATES,
+ };
+
+ // Called when |file| search is completed within the file system.
+ virtual void OnFileFound(GDataFile* file) = 0;
+
+ // Called when |directory| is found at |directory_path}| within the file
+ // system.
+ virtual void OnDirectoryFound(const FilePath& directory_path,
+ GDataDirectory* directory) = 0;
+
+ // Called while traversing the virtual file system when |directory|
+ // under |directory_path| is encountered. If this function returns
+ // FIND_FILE_TERMINATES the current find operation will be terminated.
+ virtual FindFileTraversalCommand OnEnterDirectory(
+ const FilePath& directory_path, GDataDirectory* directory) = 0;
+
+ // Called when an error occurs while fetching feed content from the server.
+ virtual void OnError(base::PlatformFileError error) = 0;
+};
+
+// GData file system abstraction layer. This class is refcounted since we
+// access it from different threads and aggregate into number of other objects.
+class GDataFileSystem : public base::RefCountedThreadSafe<GDataFileSystem> {
+ public:
+ struct FindFileParams {
+ FindFileParams(const FilePath& in_file_path,
+ bool in_require_content,
+ const FilePath& in_directory_path,
+ const GURL& in_feed_url,
+ bool in_initial_feed,
+ scoped_refptr<FindFileDelegate> in_delegate);
+ ~FindFileParams();
+
+ const FilePath file_path;
+ const bool require_content;
+ const FilePath directory_path;
+ const GURL feed_url;
+ const bool initial_feed;
+ const scoped_refptr<FindFileDelegate> delegate;
+ };
+
+ GDataFileSystem();
+ virtual ~GDataFileSystem();
+
+ // Finds file info by using virtual |file_path|. If |require_content| is set,
+ // the found directory will be pre-populated before passed back to the
+ // |delegate|. If |allow_refresh| is not set, directories' content
+ // won't be performed.
+ void FindFileByPath(const FilePath& file_path,
+ scoped_refptr<FindFileDelegate> delegate);
+
+
+ // Initiates directory feed fetching operation and continues previously
+ // initiated FindFileByPath() attempt upon its completion. Safe to be called
+ // from any thread. Internally, it will route content refresh request to
+ // RefreshFeedOnUIThread() method which will initiated content fetching from
+ // UI thread as required by gdata library (UrlFetcher).
+ void StartDirectoryRefresh(const FindFileParams& params);
+
+ private:
+ friend class GDataFileSystemTest;
+
+ // Unsafe (unlocked) version of the function above.
+ void UnsafeFindFileByPath(const FilePath& file_path,
+ scoped_refptr<FindFileDelegate> delegate);
+
+ // Initiates document feed fetching from UI thread.
+ void RefreshFeedOnUIThread(const GURL& feed_url,
+ const gdata::GetDataCallback& callback);
+
+ // Converts document feed from gdata service into DirectoryInfo. On failure,
+ // returns NULL and fills in |error| with an appropriate value.
+ GDataDirectory* ParseGDataFeed(gdata::GDataErrorCode status,
+ base::Value* data,
+ base::PlatformFileError *error);
+
+ // Callback for handling feed content fetching while searching for file info.
+ // This callback is invoked after async feed fetch operation that was
+ // invoked by StartDirectoryRefresh() completes. This callback will update
+ // the content of the refreshed directory object and continue initially
+ // started FindFileByPath() request.
+ void OnGetDocuments(const FindFileParams& params,
+ gdata::GDataErrorCode status,
+ base::Value* data);
+
+ // Updates content of the directory identified with |directory_path|. If the
+ // feed was not complete, it will return URL for the remaining portion in
+ // |next_feed|. On success, returns PLATFORM_FILE_OK.
+ base::PlatformFileError UpdateDirectoryWithDocumentFeed(
+ const FilePath& directory_path,
+ const GURL& feed_url,
+ base::Value* data,
+ bool is_initial_feed,
+ GURL* next_feed);
+
+ scoped_ptr<GDataDirectory> root_;
+ base::Lock lock_;
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_GDATA_GDATA_FILE_SYSTEM_H_
diff --git a/chrome/browser/chromeos/gdata/gdata_file_system_proxy.cc b/chrome/browser/chromeos/gdata/gdata_file_system_proxy.cc
new file mode 100644
index 0000000..d89bcd9
--- /dev/null
+++ b/chrome/browser/chromeos/gdata/gdata_file_system_proxy.cc
@@ -0,0 +1,291 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/gdata/gdata_file_system_proxy.h"
+
+#include "base/bind.h"
+#include "base/platform_file.h"
+#include "base/values.h"
+#include "content/public/browser/browser_thread.h"
+#include "webkit/fileapi/file_system_file_util_proxy.h"
+#include "webkit/fileapi/file_system_types.h"
+#include "webkit/fileapi/file_system_util.h"
+
+using content::BrowserThread;
+using fileapi::FileSystemOperationInterface;
+
+namespace {
+
+const char kGDataRootDirectory[] = "gdata";
+const char kFeedField[] = "feed";
+
+} // namespace
+
+namespace gdata {
+
+base::FileUtilProxy::Entry GDataFileToFileUtilProxyEntry(
+ const GDataFileBase& file) {
+ base::FileUtilProxy::Entry entry;
+ entry.is_directory = file.file_info().is_directory;
+
+ // TODO(zelidrag): Add file name modification logic to enforce uniquness of
+ // file paths across this file system.
+ entry.name = file.file_name();
+
+ entry.size = file.file_info().size;
+ entry.last_modified_time = file.file_info().last_modified;
+ return entry;
+}
+
+// Base class for proxy reply delegates. Keeps the track of the calling
+// thread to ensure its specializations will provide reply on it.
+class FindFileDelegateReplyBase : public FindFileDelegate {
+ public:
+ FindFileDelegateReplyBase(
+ scoped_refptr<GDataFileSystem> file_system,
+ const FilePath& search_file_path,
+ bool require_content)
+ : file_system_(file_system),
+ search_file_path_(search_file_path),
+ require_content_(require_content) {
+ BrowserThread::ID thread_id;
+ CHECK(BrowserThread::GetCurrentThreadIdentifier(&thread_id));
+ replay_message_proxy_ =
+ BrowserThread::GetMessageLoopProxyForThread(thread_id);
+ }
+
+ virtual ~FindFileDelegateReplyBase() {}
+
+ // chromeos::FindFileDelegate overrides.
+ virtual FindFileTraversalCommand OnEnterDirectory(
+ const FilePath& current_directory_path,
+ GDataDirectory* current_dir) {
+ return CheckAndRefreshContent(current_directory_path, current_dir) ?
+ FIND_FILE_CONTINUES : FIND_FILE_TERMINATES;
+ }
+
+ protected:
+
+ // Checks if the content of the |directory| under |directory_path| needs to be
+ // refreshed. Returns true if directory content is fresh, otherwise it kicks
+ // off content request request. After feed content content is received and
+ // processed in GDataFileSystem::OnGetDocuments(), that function will also
+ // restart the initiated FindFileByPath() request.
+ bool CheckAndRefreshContent(const FilePath& directory_path,
+ GDataDirectory* directory) {
+ GURL feed_url;
+ if (directory->NeedsRefresh(&feed_url)) {
+ // If content is stale/non-existing, first fetch the content of the
+ // directory in order to traverse it further.
+ file_system_->StartDirectoryRefresh(
+ GDataFileSystem::FindFileParams(
+ search_file_path_,
+ require_content_,
+ directory_path,
+ feed_url,
+ true, /* is_initial_feed */
+ this));
+ return false;
+ }
+ return true;
+ }
+
+ protected:
+ scoped_refptr<GDataFileSystem> file_system_;
+ // Search file path.
+ FilePath search_file_path_;
+ // True if the final directory content is required.
+ bool require_content_;
+ scoped_refptr<base::MessageLoopProxy> replay_message_proxy_;
+};
+
+// GetFileInfoDelegate is used to handle results of proxy's content search
+// during GetFileInfo call and route reply to the calling message loop.
+class GetFileInfoDelegate : public FindFileDelegateReplyBase {
+ public:
+ GetFileInfoDelegate(
+ scoped_refptr<GDataFileSystem> file_system,
+ const FilePath& search_file_path,
+ const FileSystemOperationInterface::GetMetadataCallback&
+ callback)
+ : FindFileDelegateReplyBase(file_system,
+ search_file_path,
+ false /* require_content */),
+ callback_(callback) {
+ }
+
+ // GDataFileSystemProxy::FindFileDelegate overrides.
+ virtual void OnFileFound(GDataFile* file) OVERRIDE {
+ DCHECK(file);
+ Reply(base::PLATFORM_FILE_OK, file->file_info(), search_file_path_);
+ }
+
+ virtual void OnDirectoryFound(const FilePath& directory_path,
+ GDataDirectory* dir) OVERRIDE {
+ DCHECK(dir);
+ Reply(base::PLATFORM_FILE_OK, dir->file_info(), search_file_path_);
+ }
+
+ virtual void OnError(base::PlatformFileError error) OVERRIDE {
+ Reply(error, base::PlatformFileInfo(), FilePath());
+ }
+
+ private:
+
+ // Relays reply back to the callback on calling thread.
+ void Reply(base::PlatformFileError result,
+ const base::PlatformFileInfo& file_info,
+ const FilePath& platform_path) {
+ if (!callback_.is_null()) {
+ replay_message_proxy_->PostTask(FROM_HERE,
+ Bind(&GetFileInfoDelegate::ReplyOnCallingThread,
+ this,
+ result,
+ file_info,
+ platform_path));
+ }
+ }
+
+ // Responds to callback.
+ void ReplyOnCallingThread(
+ base::PlatformFileError result,
+ const base::PlatformFileInfo& file_info,
+ const FilePath& platform_path) {
+ if (!callback_.is_null())
+ callback_.Run(result, file_info, platform_path);
+ }
+
+
+ FileSystemOperationInterface::GetMetadataCallback callback_;
+};
+
+
+// ReadDirectoryDelegate is used to handle results of proxy's content search
+// during ReadDirectories call and route reply to the calling message loop.
+class ReadDirectoryDelegate : public FindFileDelegateReplyBase {
+ public:
+ ReadDirectoryDelegate(
+ scoped_refptr<GDataFileSystem> file_system,
+ const FilePath& search_file_path,
+ const FileSystemOperationInterface::ReadDirectoryCallback&
+ callback)
+ : FindFileDelegateReplyBase(file_system,
+ search_file_path,
+ true /* require_content */),
+ callback_(callback) {
+ }
+
+ // GDataFileSystemProxy::FindFileDelegate overrides.
+ virtual void OnFileFound(GDataFile* file) OVERRIDE {
+ // We are not looking for a file here at all.
+ Reply(base::PLATFORM_FILE_ERROR_NOT_FOUND,
+ std::vector<base::FileUtilProxy::Entry>(), false);
+ }
+
+ virtual void OnDirectoryFound(const FilePath& directory_path,
+ GDataDirectory* directory) OVERRIDE {
+ DCHECK(directory);
+ // Since we are reading directory content here, we need to make sure
+ // that the directory found actually contains fresh content.
+ if (!CheckAndRefreshContent(directory_path, directory))
+ return;
+
+ std::vector<base::FileUtilProxy::Entry> results;
+ if (!directory || !directory->file_info().is_directory) {
+ Reply(base::PLATFORM_FILE_ERROR_NOT_FOUND, results, false);
+ return;
+ }
+
+ // Convert gdata files to something File API stack can understand.
+ for (GDataFileCollection::const_iterator iter =
+ directory->children().begin();
+ iter != directory->children().end(); ++iter) {
+ results.push_back(GDataFileToFileUtilProxyEntry(*(iter->second)));
+ }
+
+ GURL unused;
+ Reply(base::PLATFORM_FILE_OK, results, directory->NeedsRefresh(&unused));
+ }
+
+ virtual void OnError(base::PlatformFileError error) OVERRIDE {
+ Reply(error, std::vector<base::FileUtilProxy::Entry>(), false);
+ }
+
+ private:
+
+ // Relays reply back to the callback on calling thread.
+ void Reply(base::PlatformFileError result,
+ const std::vector<base::FileUtilProxy::Entry>& file_list,
+ bool has_more) {
+ if (!callback_.is_null()) {
+ replay_message_proxy_->PostTask(FROM_HERE,
+ Bind(&ReadDirectoryDelegate::ReplyOnCallingThread,
+ this,
+ result,
+ file_list,
+ has_more));
+ }
+ }
+
+ // Responds to callback.
+ void ReplyOnCallingThread(
+ base::PlatformFileError result,
+ const std::vector<base::FileUtilProxy::Entry>& file_list,
+ bool has_more) {
+ if (!callback_.is_null())
+ callback_.Run(result, file_list, has_more);
+ }
+
+ FileSystemOperationInterface::ReadDirectoryCallback callback_;
+};
+
+
+GDataFileSystemProxy::GDataFileSystemProxy()
+ : file_system_(new GDataFileSystem()) {
+}
+
+GDataFileSystemProxy::~GDataFileSystemProxy() {
+}
+
+void GDataFileSystemProxy::GetFileInfo(const GURL& file_url,
+ const FileSystemOperationInterface::GetMetadataCallback& callback) {
+ // what platform you're on.
+ FilePath file_path;
+ if (!ValidateUrl(file_url, &file_path)) {
+ scoped_refptr<GetFileInfoDelegate> delegate(
+ new GetFileInfoDelegate(NULL, FilePath(), callback));
+ delegate->OnError(base::PLATFORM_FILE_ERROR_NOT_FOUND);
+ return;
+ }
+
+ file_system_->FindFileByPath(
+ file_path, new GetFileInfoDelegate(file_system_, file_path, callback));
+}
+
+
+void GDataFileSystemProxy::ReadDirectory(const GURL& file_url,
+ const FileSystemOperationInterface::ReadDirectoryCallback& callback) {
+ FilePath file_path;
+ if (!ValidateUrl(file_url, &file_path)) {
+ scoped_refptr<ReadDirectoryDelegate> delegate(
+ new ReadDirectoryDelegate(NULL, FilePath(), callback));
+ delegate->OnError(base::PLATFORM_FILE_ERROR_NOT_FOUND);
+ return;
+ }
+
+ file_system_->FindFileByPath(
+ file_path, new ReadDirectoryDelegate(file_system_, file_path, callback));
+}
+
+bool GDataFileSystemProxy::ValidateUrl(const GURL& url, FilePath* file_path) {
+ // what platform you're on.
+ fileapi::FileSystemType type = fileapi::kFileSystemTypeUnknown;
+ if (!fileapi::CrackFileSystemURL(url, NULL, &type, file_path) ||
+ type != fileapi::kFileSystemTypeExternal) {
+ return false;
+ }
+ return true;
+}
+
+} // namespace gdata
diff --git a/chrome/browser/chromeos/gdata/gdata_file_system_proxy.h b/chrome/browser/chromeos/gdata/gdata_file_system_proxy.h
new file mode 100644
index 0000000..d01d0bb
--- /dev/null
+++ b/chrome/browser/chromeos/gdata/gdata_file_system_proxy.h
@@ -0,0 +1,39 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_GDATA_GDATA_FILE_SYSTEM_PROXY_H_
+#define CHROME_BROWSER_CHROMEOS_GDATA_GDATA_FILE_SYSTEM_PROXY_H_
+
+#include "chrome/browser/chromeos/gdata/gdata_file_system.h"
+#include "webkit/chromeos/fileapi/remote_file_system_proxy.h"
+
+namespace gdata {
+
+// The interface class for remote file system proxy.
+class GDataFileSystemProxy : public fileapi::RemoteFileSystemProxyInterface {
+ public:
+ GDataFileSystemProxy();
+ virtual ~GDataFileSystemProxy();
+
+ // fileapi::RemoteFileSystemProxyInterface overrides.
+ virtual void GetFileInfo(const GURL& path,
+ const fileapi::FileSystemOperationInterface::GetMetadataCallback&
+ callback) OVERRIDE;
+ virtual void ReadDirectory(const GURL& path,
+ const fileapi::FileSystemOperationInterface::ReadDirectoryCallback&
+ callback) OVERRIDE;
+ // TODO(zelidrag): More methods to follow as we implement other parts of FSO.
+
+ private:
+ // Checks if a given |url| belongs to this file system. If it does,
+ // the call will return true and fill in |file_path| with a file path of
+ // a corresponding element within this file system.
+ static bool ValidateUrl(const GURL& url, FilePath* file_path);
+
+ scoped_refptr<GDataFileSystem> file_system_;
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_GDATA_GDATA_FILE_SYSTEM_PROXY_H_
diff --git a/chrome/browser/chromeos/gdata/gdata_file_system_unittest.cc b/chrome/browser/chromeos/gdata/gdata_file_system_unittest.cc
new file mode 100644
index 0000000..9642994
--- /dev/null
+++ b/chrome/browser/chromeos/gdata/gdata_file_system_unittest.cc
@@ -0,0 +1,263 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/file_path.h"
+#include "base/file_util.h"
+#include "base/json/json_value_serializer.h"
+#include "base/path_service.h"
+#include "base/string16.h"
+#include "base/time.h"
+#include "base/utf_string_conversions.h"
+#include "base/values.h"
+#include "chrome/browser/chromeos/gdata/gdata_file_system.h"
+#include "chrome/browser/chromeos/gdata/gdata_parser.h"
+#include "chrome/common/chrome_paths.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::AnyNumber;
+using ::testing::Return;
+using ::testing::ReturnNull;
+using ::testing::_;
+
+using base::Value;
+using base::DictionaryValue;
+using base::ListValue;
+
+namespace gdata {
+
+class GDataFileSystemTest : public testing::Test {
+ protected:
+ virtual void SetUp() {
+ file_system_ = new GDataFileSystem();
+ }
+
+ // Loads test json file as root ("/gdata") element.
+ void LoadRootFeedDocument(const std::string& filename) {
+ LoadSubdirFeedDocument(FilePath("gdata"), filename);
+ }
+
+ // Loads test json file as subdirectory content of |directory_path|.
+ void LoadSubdirFeedDocument(const FilePath& directory_path,
+ const std::string& filename) {
+ std::string error;
+ scoped_ptr<Value> document(LoadJSONFile(filename));
+ ASSERT_TRUE(document.get());
+ ASSERT_TRUE(document->GetType() == Value::TYPE_DICTIONARY);
+ GURL unused;
+ ASSERT_TRUE(UpdateContent(directory_path, document.get()));
+ }
+
+ // Updates the content of directory under |directory_path| with parsed feed
+ // |value|.
+ bool UpdateContent(const FilePath& directory_path,
+ Value* value) {
+ GURL unused;
+ return file_system_->UpdateDirectoryWithDocumentFeed(
+ directory_path,
+ GURL(), // feed_url
+ value,
+ true, // is_initial_feed
+ &unused) == base::PLATFORM_FILE_OK;
+ }
+
+ void UpdateSubdirContent(const FilePath& search_file_path,
+ const FilePath& directory_path,
+ scoped_refptr<FindFileDelegate> delegate,
+ Value* data) {
+ GDataFileSystem::FindFileParams params(
+ search_file_path,
+ false,
+ directory_path,
+ GURL(),
+ true,
+ delegate);
+ file_system_->OnGetDocuments(params,
+ HTTP_SUCCESS,
+ data);
+ }
+
+ static Value* LoadJSONFile(const std::string& filename) {
+ FilePath path;
+ std::string error;
+ PathService::Get(chrome::DIR_TEST_DATA, &path);
+ path = path.AppendASCII("chromeos")
+ .AppendASCII("gdata")
+ .AppendASCII(filename.c_str());
+ EXPECT_TRUE(file_util::PathExists(path)) <<
+ "Couldn't find " << path.value();
+
+ JSONFileValueSerializer serializer(path);
+ Value* value = serializer.Deserialize(NULL, &error);
+ EXPECT_TRUE(value) <<
+ "Parse error " << path.value() << ": " << error;
+ return value;
+ }
+
+ scoped_refptr<GDataFileSystem> file_system_;
+};
+
+
+// Delegate used to find a directory element for file system updates.
+class MockFindFileDelegate : public gdata::FindFileDelegate {
+ public:
+ MockFindFileDelegate() {
+ }
+
+ virtual ~MockFindFileDelegate() {
+ }
+
+ // gdata::FindFileDelegate overrides.
+ MOCK_METHOD1(OnFileFound, void(GDataFile*));
+ MOCK_METHOD2(OnDirectoryFound, void(const FilePath&, GDataDirectory* dir));
+ MOCK_METHOD2(OnEnterDirectory, FindFileTraversalCommand(
+ const FilePath&, GDataDirectory* dir));
+ MOCK_METHOD1(OnError, void(base::PlatformFileError));
+};
+
+TEST_F(GDataFileSystemTest, SearchRootDirectory) {
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate =
+ new MockFindFileDelegate();
+
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnDirectoryFound(FilePath("gdata"), _))
+ .Times(1);
+
+ file_system_->FindFileByPath(FilePath("gdata"),
+ mock_find_file_delegate);
+}
+
+TEST_F(GDataFileSystemTest, SearchExistingFile) {
+ LoadRootFeedDocument("root_feed.json");
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate =
+ new MockFindFileDelegate();
+
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnEnterDirectory(FilePath("gdata"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_CONTINUES));
+ EXPECT_CALL(*mock_find_file_delegate.get(), OnFileFound(_))
+ .Times(1);
+
+ file_system_->FindFileByPath(FilePath("gdata/File 1.txt"),
+ mock_find_file_delegate);
+}
+
+TEST_F(GDataFileSystemTest, SearchExistingDocument) {
+ LoadRootFeedDocument("root_feed.json");
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate =
+ new MockFindFileDelegate();
+
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnEnterDirectory(FilePath("gdata"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_CONTINUES));
+ EXPECT_CALL(*mock_find_file_delegate.get(), OnFileFound(_))
+ .Times(1);
+
+ file_system_->FindFileByPath(FilePath("gdata/Document 1.gdocument"),
+ mock_find_file_delegate);
+}
+
+TEST_F(GDataFileSystemTest, SearchDuplicateNames) {
+ LoadRootFeedDocument("root_feed.json");
+
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate =
+ new MockFindFileDelegate();
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnEnterDirectory(FilePath("gdata"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_CONTINUES));
+ EXPECT_CALL(*mock_find_file_delegate.get(), OnFileFound(_))
+ .Times(1);
+ file_system_->FindFileByPath(FilePath("gdata/Duplicate Name.txt"),
+ mock_find_file_delegate);
+
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate2 =
+ new MockFindFileDelegate();
+ EXPECT_CALL(*mock_find_file_delegate2.get(),
+ OnEnterDirectory(FilePath("gdata"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_CONTINUES));
+ EXPECT_CALL(*mock_find_file_delegate2.get(), OnFileFound(_))
+ .Times(1);
+ file_system_->FindFileByPath(FilePath("gdata/Duplicate Name (2).txt"),
+ mock_find_file_delegate2);
+}
+
+TEST_F(GDataFileSystemTest, SearchExistingDirectory) {
+ LoadRootFeedDocument("root_feed.json");
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate =
+ new MockFindFileDelegate();
+
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnEnterDirectory(FilePath("gdata"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_CONTINUES));
+ EXPECT_CALL(*mock_find_file_delegate.get(), OnDirectoryFound(_, _))
+ .Times(1);
+
+ file_system_->FindFileByPath(FilePath("gdata/Directory 1"),
+ mock_find_file_delegate);
+}
+
+
+TEST_F(GDataFileSystemTest, SearchNonExistingFile) {
+ LoadRootFeedDocument("root_feed.json");
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate =
+ new MockFindFileDelegate();
+
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnEnterDirectory(FilePath("gdata"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_CONTINUES));
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnError(base::PLATFORM_FILE_ERROR_NOT_FOUND))
+ .Times(1);
+
+ file_system_->FindFileByPath(FilePath("gdata/nonexisting.file"),
+ mock_find_file_delegate);
+}
+
+TEST_F(GDataFileSystemTest, StopFileSearch) {
+ LoadRootFeedDocument("root_feed.json");
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate =
+ new MockFindFileDelegate();
+
+ // Stop on first directory entry.
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnEnterDirectory(FilePath("gdata"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_TERMINATES));
+
+ file_system_->FindFileByPath(FilePath("gdata/Directory 1"),
+ mock_find_file_delegate);
+}
+
+TEST_F(GDataFileSystemTest, SearchInSubdir) {
+ LoadRootFeedDocument("root_feed.json");
+ LoadSubdirFeedDocument(FilePath("gdata/Directory 1"), "subdir_feed.json");
+
+ scoped_refptr<MockFindFileDelegate> mock_find_file_delegate =
+ new MockFindFileDelegate();
+
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnEnterDirectory(FilePath("gdata"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_CONTINUES));
+
+ EXPECT_CALL(*mock_find_file_delegate.get(),
+ OnEnterDirectory(FilePath("gdata/Directory 1"), _))
+ .Times(1)
+ .WillOnce(Return(FindFileDelegate::FIND_FILE_CONTINUES));
+
+ EXPECT_CALL(*mock_find_file_delegate.get(), OnFileFound(_))
+ .Times(1);
+
+ file_system_->FindFileByPath(
+ FilePath("gdata/Directory 1/SubDirectory File 1.txt"),
+ mock_find_file_delegate);
+}
+
+} // namespace gdata
diff --git a/chrome/browser/chromeos/gdata/gdata_parser.cc b/chrome/browser/chromeos/gdata/gdata_parser.cc
index 6996a70..a70c9d0 100644
--- a/chrome/browser/chromeos/gdata/gdata_parser.cc
+++ b/chrome/browser/chromeos/gdata/gdata_parser.cc
@@ -43,6 +43,9 @@ const EntryKindMap kEntryKindMap[] = {
{ DocumentEntry::DOCUMENT, "document"},
{ DocumentEntry::SPREADSHEET, "spreadsheet" },
{ DocumentEntry::PRESENTATION, "presentation" },
+ { DocumentEntry::DRAWING, "drawing"},
+ { DocumentEntry::TABLE, "table"},
+ { DocumentEntry::SITE, "site"},
{ DocumentEntry::FOLDER, "folder"},
{ DocumentEntry::FILE, "file"},
{ DocumentEntry::PDF, "pdf"},
@@ -343,6 +346,20 @@ void DocumentEntry::RegisterJSONConverter(
kSuggestedFileNameField, &DocumentEntry::suggested_filename_);
}
+std::string DocumentEntry::GetEntryKindText() const {
+ return std::string(GetEntryKindDescription(kind_));
+}
+
+// static
+const char* DocumentEntry::GetEntryKindDescription(
+ DocumentEntry::EntryKind kind) {
+ for (size_t i = 0; i < arraysize(kEntryKindMap); i++) {
+ if (kEntryKindMap[i].kind == kind)
+ return kEntryKindMap[i].entry;
+ }
+ return "";
+}
+
// static
DocumentEntry::EntryKind DocumentEntry::GetEntryKindFromTerm(
const std::string& term) {
diff --git a/chrome/browser/chromeos/gdata/gdata_parser.h b/chrome/browser/chromeos/gdata/gdata_parser.h
index 2d9c38a..3ad0c1e 100644
--- a/chrome/browser/chromeos/gdata/gdata_parser.h
+++ b/chrome/browser/chromeos/gdata/gdata_parser.h
@@ -274,14 +274,21 @@ class GDataEntry {
class DocumentEntry : public GDataEntry {
public:
enum EntryKind {
- UNKNOWN,
- ITEM,
- DOCUMENT,
- SPREADSHEET,
- PRESENTATION,
- FOLDER,
- FILE,
- PDF,
+ UNKNOWN = 0x000000,
+ // Special entries.
+ ITEM = 0x001001,
+ SITE = 0x001002,
+ // Hosted documents.
+ DOCUMENT = 0x002001,
+ SPREADSHEET = 0x002002,
+ PRESENTATION = 0x002003,
+ DRAWING = 0x002004,
+ TABLE = 0x002005,
+ // Folders, collections.
+ FOLDER = 0x004001,
+ // Regular files.
+ FILE = 0x008001,
+ PDF = 0x008002,
};
virtual ~DocumentEntry();
@@ -335,6 +342,21 @@ class DocumentEntry : public GDataEntry {
// Document feed file size (exists only for kinds FILE and PDF).
int64 file_size() const { return file_size_; }
+ // Text version of document entry kind. Returns an empty string for
+ // unknown entry kind.
+ std::string GetEntryKindText() const;
+
+ // True if document entry is remotely hosted.
+ bool is_hosted_document() const { return (kind_ & 0x002000) != 0; }
+ // True if document entry is a folder (collection).
+ bool is_folder() const { return (kind_ & 0x004000) != 0; }
+ // True if document entry is regular file.
+ bool is_file() const { return (kind_ & 0x008000) != 0; }
+ // True if document entry can't be mapped to the file system.
+ bool is_special() const {
+ return !is_file() && !is_folder() && !is_hosted_document();
+ }
+
private:
friend class base::internal::RepeatedMessageConverter<DocumentEntry>;
friend class DocumentFeed;
@@ -346,6 +368,8 @@ class DocumentEntry : public GDataEntry {
// Converts categories.term into EntryKind enum.
static EntryKind GetEntryKindFromTerm(const std::string& term);
+ // Converts |kind| into its text identifier equivalent.
+ static const char* GetEntryKindDescription(EntryKind kind);
std::string resource_id_;
std::string id_;
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index 890501e..1bb3154 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -510,6 +510,10 @@
'browser/chromeos/frame/panel_controller.h',
'browser/chromeos/gdata/gdata.cc',
'browser/chromeos/gdata/gdata.h',
+ 'browser/chromeos/gdata/gdata_file_system.cc',
+ 'browser/chromeos/gdata/gdata_file_system.h',
+ 'browser/chromeos/gdata/gdata_file_system_proxy.cc',
+ 'browser/chromeos/gdata/gdata_file_system_proxy.h',
'browser/chromeos/gdata/gdata_parser.cc',
'browser/chromeos/gdata/gdata_parser.h',
'browser/chromeos/gdata/gdata_uploader.cc',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index ea2ed75..f58444d 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -1344,6 +1344,7 @@
'browser/chromeos/dbus/proxy_resolution_service_provider_unittest.cc',
'browser/chromeos/extensions/file_browser_notifications_unittest.cc',
'browser/chromeos/external_metrics_unittest.cc',
+ 'browser/chromeos/gdata/gdata_file_system_unittest.cc',
'browser/chromeos/gdata/gdata_parser_unittest.cc',
'browser/chromeos/gview_request_interceptor_unittest.cc',
'browser/chromeos/imageburner/burn_manager_unittest.cc',
diff --git a/chrome/test/data/chromeos/gdata/root_feed.json b/chrome/test/data/chromeos/gdata/root_feed.json
new file mode 100644
index 0000000..f74bd9e
--- /dev/null
+++ b/chrome/test/data/chromeos/gdata/root_feed.json
@@ -0,0 +1,490 @@
+{
+ "encoding": "UTF-8",
+ "feed": {
+ "author": [ {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ } ],
+ "category": [ {
+ "label": "item",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#item"
+ } ],
+ "entry": [ {
+ "app$edited": {
+ "$t": "2011-12-14T00:41:08.287Z",
+ "xmlns$app": "http://www.w3.org/2007/app"
+ },
+ "author": [ {
+ "email": {
+ "$t": "entry_tester@testing.com"
+ },
+ "name": {
+ "$t": "entry_tester"
+ }
+ } ],
+ "category": [ {
+ "label": "viewed",
+ "scheme": "http://schemas.google.com/g/2005/labels",
+ "term": "http://schemas.google.com/g/2005/labels#viewed"
+ }, {
+ "label": "folder",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#folder"
+ } ],
+ "content": {
+ "src": "https://1_folder_content_url",
+ "type": "application/atom+xml;type=feed"
+ },
+ "docs$writersCanInvite": {
+ "value": "true"
+ },
+ "gd$etag": "\"HhMOFgcNHSt7ImBr\"",
+ "gd$feedLink": [ {
+ "href": "https://1_folder_feed_linkurl",
+ "rel": "http://schemas.google.com/acl/2007#accessControlList"
+ } ],
+ "gd$lastModifiedBy": {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ },
+ "gd$lastViewed": {
+ "$t": "2011-11-02T04:37:38.469Z"
+ },
+ "gd$quotaBytesUsed": {
+ "$t": "0"
+ },
+ "gd$resourceId": {
+ "$t": "folder:1_folder_resouce_id"
+ },
+ "id": {
+ "$t": "https://1_folder_id"
+ },
+ "link": [ {
+ "href": "https://1_folder_alternate_link",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://1_folder_resumable_create_media_link",
+ "rel": "http://schemas.google.com/g/2005#resumable-create-media",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://1_self_link",
+ "rel": "self",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://1_edit_link",
+ "rel": "edit",
+ "type": "application/atom+xml"
+ } ],
+ "published": {
+ "$t": "2010-11-07T05:03:54.719Z"
+ },
+ "title": {
+ "$t": "Directory 1"
+ },
+ "updated": {
+ "$t": "2011-04-01T18:34:08.234Z"
+ }
+ }, {
+ "app$edited": {
+ "$t": "2011-12-14T00:40:57.162Z",
+ "xmlns$app": "http://www.w3.org/2007/app"
+ },
+ "author": [ {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ } ],
+ "category": [ {
+ "label": "audio/mpeg",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#file"
+ } ],
+ "content": {
+ "src": "https://file_content_url/",
+ "type": "audio/mpeg"
+ },
+ "docs$filename": {
+ "$t": "File 1.txt"
+ },
+ "docs$md5Checksum": {
+ "$t": "3b4382ebefec6e743578c76bbd0575ce"
+ },
+ "docs$size": {
+ "$t": "892721"
+ },
+ "docs$suggestedFilename": {
+ "$t": "File 1.txt"
+ },
+ "docs$writersCanInvite": {
+ "value": "true"
+ },
+ "gd$etag": "\"HhMOFgxXHit7ImBr\"",
+ "gd$feedLink": [ {
+ "href": "https://file_feed_link_url",
+ "rel": "http://schemas.google.com/docs/2007/revisions"
+ } ],
+ "gd$lastModifiedBy": {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ },
+ "gd$quotaBytesUsed": {
+ "$t": "892721"
+ },
+ "gd$resourceId": {
+ "$t": "file:2_file_resouce_id"
+ },
+ "id": {
+ "$t": "2_file_id"
+ },
+ "link": [ {
+ "href": "https://file_link_alternate",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://file_link_resumable_edit_media",
+ "rel": "http://schemas.google.com/g/2005#resumable-edit-media",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_link_self",
+ "rel": "self",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_link_edit",
+ "rel": "edit",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_edit_media",
+ "rel": "edit-media",
+ "type": "audio/mpeg"
+ } ],
+ "published": {
+ "$t": "2011-12-14T00:40:47.330Z"
+ },
+ "title": {
+ "$t": "File 1.txt"
+ },
+ "updated": {
+ "$t": "2011-12-14T00:40:47.330Z"
+ }
+ }, {
+ "app$edited": {
+ "$t": "2011-12-14T00:40:57.162Z",
+ "xmlns$app": "http://www.w3.org/2007/app"
+ },
+ "author": [ {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ } ],
+ "category": [ {
+ "label": "audio/mpeg",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#file"
+ } ],
+ "content": {
+ "src": "https://file_content_url/",
+ "type": "audio/mpeg"
+ },
+ "docs$filename": {
+ "$t": "Duplicate Name.txt"
+ },
+ "docs$md5Checksum": {
+ "$t": "3b4382ebefec6e743578c76bbd0575ce"
+ },
+ "docs$size": {
+ "$t": "892721"
+ },
+ "docs$suggestedFilename": {
+ "$t": "Duplicate Name.txt"
+ },
+ "docs$writersCanInvite": {
+ "value": "true"
+ },
+ "gd$etag": "\"HhMOFgxXHit7ImBr\"",
+ "gd$feedLink": [ {
+ "href": "https://file_feed_link_url",
+ "rel": "http://schemas.google.com/docs/2007/revisions"
+ } ],
+ "gd$lastModifiedBy": {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ },
+ "gd$quotaBytesUsed": {
+ "$t": "892721"
+ },
+ "gd$resourceId": {
+ "$t": "file:2_file_resouce_id"
+ },
+ "id": {
+ "$t": "2_file_id"
+ },
+ "link": [ {
+ "href": "https://file_link_alternate",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://file_link_resumable_edit_media",
+ "rel": "http://schemas.google.com/g/2005#resumable-edit-media",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_link_self",
+ "rel": "self",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_link_edit",
+ "rel": "edit",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_edit_media",
+ "rel": "edit-media",
+ "type": "audio/mpeg"
+ } ],
+ "published": {
+ "$t": "2011-12-14T00:40:47.330Z"
+ },
+ "title": {
+ "$t": "Duplicate Name.txt"
+ },
+ "updated": {
+ "$t": "2011-12-14T00:40:47.330Z"
+ }
+ }, {
+ "app$edited": {
+ "$t": "2011-12-14T00:40:57.162Z",
+ "xmlns$app": "http://www.w3.org/2007/app"
+ },
+ "author": [ {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ } ],
+ "category": [ {
+ "label": "audio/mpeg",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#file"
+ } ],
+ "content": {
+ "src": "https://file_content_url/",
+ "type": "audio/mpeg"
+ },
+ "docs$filename": {
+ "$t": "Duplicate Name.txt"
+ },
+ "docs$md5Checksum": {
+ "$t": "3b4382ebefec6e743578c76bbd0575ce"
+ },
+ "docs$size": {
+ "$t": "892721"
+ },
+ "docs$suggestedFilename": {
+ "$t": "Duplicate Name.txt"
+ },
+ "docs$writersCanInvite": {
+ "value": "true"
+ },
+ "gd$etag": "\"HhMOFgxXHit7ImBr\"",
+ "gd$feedLink": [ {
+ "href": "https://file_feed_link_url",
+ "rel": "http://schemas.google.com/docs/2007/revisions"
+ } ],
+ "gd$lastModifiedBy": {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ },
+ "gd$quotaBytesUsed": {
+ "$t": "892721"
+ },
+ "gd$resourceId": {
+ "$t": "file:2_file_resouce_id"
+ },
+ "id": {
+ "$t": "2_file_id"
+ },
+ "link": [ {
+ "href": "https://file_link_alternate",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://file_link_resumable_edit_media",
+ "rel": "http://schemas.google.com/g/2005#resumable-edit-media",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_link_self",
+ "rel": "self",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_link_edit",
+ "rel": "edit",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_edit_media",
+ "rel": "edit-media",
+ "type": "audio/mpeg"
+ } ],
+ "published": {
+ "$t": "2011-12-14T00:40:47.330Z"
+ },
+ "title": {
+ "$t": "Duplicate Name.txt"
+ },
+ "updated": {
+ "$t": "2011-12-14T00:40:47.330Z"
+ }
+ }, {
+ "app$edited": {
+ "$t": "2011-12-13T18:49:19.627Z",
+ "xmlns$app": "http://www.w3.org/2007/app"
+ },
+ "author": [ {
+ "email": {
+ "$t": "doc_author@testing.com"
+ },
+ "name": {
+ "$t": "doc_author"
+ }
+ } ],
+ "category": [ {
+ "label": "document",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#document"
+ }, {
+ "label": "viewed",
+ "scheme": "http://schemas.google.com/g/2005/labels",
+ "term": "http://schemas.google.com/g/2005/labels#viewed"
+ } ],
+ "content": {
+ "src": "https://3_document_content",
+ "type": "text/html"
+ },
+ "docs$writersCanInvite": {
+ "value": "true"
+ },
+ "gd$etag": "\"HhNaTRdUASt7ImBr\"",
+ "gd$feedLink": [ {
+ "href": "https://3_document_feed_link",
+ "rel": "http://schemas.google.com/docs/2007/revisions"
+ } ],
+ "gd$lastModifiedBy": {
+ "email": {
+ "$t": "doc_author@testing.com"
+ },
+ "name": {
+ "$t": "doc_author"
+ }
+ },
+ "gd$lastViewed": {
+ "$t": "2011-12-13T02:12:18.527Z"
+ },
+ "gd$quotaBytesUsed": {
+ "$t": "0"
+ },
+ "gd$resourceId": {
+ "$t": "document:3_document_resource_id"
+ },
+ "id": {
+ "$t": "3_document_id"
+ },
+ "link": [ {
+ "href": "https://3_document_alternate_link",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://3_document_thumbnail_link",
+ "rel": "http://schemas.google.com/docs/2007/thumbnail",
+ "type": "image/jpeg"
+ }, {
+ "href": "https://3_document_self_link",
+ "rel": "self",
+ "type": "application/atom+xml"
+ } ],
+ "published": {
+ "$t": "2011-12-12T23:28:46.686Z"
+ },
+ "title": {
+ "$t": "Document 1"
+ },
+ "updated": {
+ "$t": "2011-12-12T23:28:52.783Z"
+ }
+ }],
+ "gd$etag": "W/\"DkEEQH8-eSt7ImA9WhRQGE8.\"",
+ "id": {
+ "$t": "https://docs.google.com/feeds/default/private/full"
+ },
+ "link": [ {
+ "href": "http://alternate_link",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://resumable_create_media_link",
+ "rel": "http://schemas.google.com/g/2005#resumable-create-media",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://docs.google.com/feeds/default/private/full?v=3",
+ "rel": "http://schemas.google.com/g/2005#feed",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://docs.google.com/feeds/default/private/full?v=3",
+ "rel": "http://schemas.google.com/g/2005#post",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://docs.google.com/feeds/default/private/full/batch?v=3",
+ "rel": "http://schemas.google.com/g/2005#batch",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://self_link",
+ "rel": "self",
+ "type": "application/atom+xml"
+ } ],
+ "openSearch$itemsPerPage": {
+ "$t": "1000"
+ },
+ "openSearch$startIndex": {
+ "$t": "1"
+ },
+ "title": {
+ "$t": "Feed title"
+ },
+ "updated": {
+ "$t": "2011-12-14T01:03:21.151Z"
+ },
+ "xmlns": "http://www.w3.org/2005/Atom",
+ "xmlns$batch": "http://schemas.google.com/gdata/batch",
+ "xmlns$docs": "http://schemas.google.com/docs/2007",
+ "xmlns$gd": "http://schemas.google.com/g/2005",
+ "xmlns$openSearch": "http://a9.com/-/spec/opensearch/1.1/"
+ },
+ "version": "1.0"
+}
+
diff --git a/chrome/test/data/chromeos/gdata/subdir_feed.json b/chrome/test/data/chromeos/gdata/subdir_feed.json
new file mode 100644
index 0000000..7d38e9e
--- /dev/null
+++ b/chrome/test/data/chromeos/gdata/subdir_feed.json
@@ -0,0 +1,236 @@
+{
+ "encoding": "UTF-8",
+ "feed": {
+ "author": [ {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ } ],
+ "category": [ {
+ "label": "item",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#item"
+ } ],
+ "entry": [ {
+ "app$edited": {
+ "$t": "2011-12-14T00:41:08.287Z",
+ "xmlns$app": "http://www.w3.org/2007/app"
+ },
+ "author": [ {
+ "email": {
+ "$t": "entry_tester@testing.com"
+ },
+ "name": {
+ "$t": "entry_tester"
+ }
+ } ],
+ "category": [ {
+ "label": "viewed",
+ "scheme": "http://schemas.google.com/g/2005/labels",
+ "term": "http://schemas.google.com/g/2005/labels#viewed"
+ }, {
+ "label": "folder",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#folder"
+ } ],
+ "content": {
+ "src": "https://1_folder_content_url",
+ "type": "application/atom+xml;type=feed"
+ },
+ "docs$writersCanInvite": {
+ "value": "true"
+ },
+ "gd$etag": "\"HhMOFgcNHSt7ImBr\"",
+ "gd$feedLink": [ {
+ "href": "https://1_folder_feed_linkurl",
+ "rel": "http://schemas.google.com/acl/2007#accessControlList"
+ } ],
+ "gd$lastModifiedBy": {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ },
+ "gd$lastViewed": {
+ "$t": "2011-11-02T04:37:38.469Z"
+ },
+ "gd$quotaBytesUsed": {
+ "$t": "0"
+ },
+ "gd$resourceId": {
+ "$t": "folder:1_folder_resouce_id"
+ },
+ "id": {
+ "$t": "https://1_folder_id"
+ },
+ "link": [ {
+ "href": "https://1_folder_alternate_link",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://1_folder_resumable_create_media_link",
+ "rel": "http://schemas.google.com/g/2005#resumable-create-media",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://1_self_link",
+ "rel": "self",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://1_edit_link",
+ "rel": "edit",
+ "type": "application/atom+xml"
+ } ],
+ "published": {
+ "$t": "2010-11-07T05:03:54.719Z"
+ },
+ "title": {
+ "$t": "Sub Directory Folder"
+ },
+ "updated": {
+ "$t": "2011-04-01T18:34:08.234Z"
+ }
+ }, {
+ "app$edited": {
+ "$t": "2011-12-14T00:40:57.162Z",
+ "xmlns$app": "http://www.w3.org/2007/app"
+ },
+ "author": [ {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ } ],
+ "category": [ {
+ "label": "audio/mpeg",
+ "scheme": "http://schemas.google.com/g/2005#kind",
+ "term": "http://schemas.google.com/docs/2007#file"
+ } ],
+ "content": {
+ "src": "https://file_content_url/",
+ "type": "audio/mpeg"
+ },
+ "docs$filename": {
+ "$t": "SubDirectory File 1.txt"
+ },
+ "docs$md5Checksum": {
+ "$t": "3b4382ebefec6e743578c76bbd0575ce"
+ },
+ "docs$size": {
+ "$t": "892721"
+ },
+ "docs$suggestedFilename": {
+ "$t": "SubDirectory File 1.txt"
+ },
+ "docs$writersCanInvite": {
+ "value": "true"
+ },
+ "gd$etag": "\"HhMOFgxXHit7ImBr\"",
+ "gd$feedLink": [ {
+ "href": "https://file_feed_link_url",
+ "rel": "http://schemas.google.com/docs/2007/revisions"
+ } ],
+ "gd$lastModifiedBy": {
+ "email": {
+ "$t": "tester@testing.com"
+ },
+ "name": {
+ "$t": "tester"
+ }
+ },
+ "gd$quotaBytesUsed": {
+ "$t": "892721"
+ },
+ "gd$resourceId": {
+ "$t": "file:2_file_resouce_id"
+ },
+ "id": {
+ "$t": "2_file_id"
+ },
+ "link": [ {
+ "href": "https://file_link_alternate",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://file_link_resumable_edit_media",
+ "rel": "http://schemas.google.com/g/2005#resumable-edit-media",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_link_self",
+ "rel": "self",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_link_edit",
+ "rel": "edit",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://file_edit_media",
+ "rel": "edit-media",
+ "type": "audio/mpeg"
+ } ],
+ "published": {
+ "$t": "2011-12-14T00:40:47.330Z"
+ },
+ "title": {
+ "$t": "SubDirectory File 1.txt"
+ },
+ "updated": {
+ "$t": "2011-12-14T00:40:47.330Z"
+ }
+ }],
+ "gd$etag": "W/\"DkEEQH8-eSt7ImA9WhRQGE8.\"",
+ "id": {
+ "$t": "https://docs.google.com/feeds/default/private/full"
+ },
+ "link": [ {
+ "href": "http://alternate_link",
+ "rel": "alternate",
+ "type": "text/html"
+ }, {
+ "href": "https://resumable_create_media_link",
+ "rel": "http://schemas.google.com/g/2005#resumable-create-media",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://docs.google.com/feeds/default/private/full?v=3",
+ "rel": "http://schemas.google.com/g/2005#feed",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://docs.google.com/feeds/default/private/full?v=3",
+ "rel": "http://schemas.google.com/g/2005#post",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://docs.google.com/feeds/default/private/full/batch?v=3",
+ "rel": "http://schemas.google.com/g/2005#batch",
+ "type": "application/atom+xml"
+ }, {
+ "href": "https://self_link",
+ "rel": "self",
+ "type": "application/atom+xml"
+ } ],
+ "openSearch$itemsPerPage": {
+ "$t": "1000"
+ },
+ "openSearch$startIndex": {
+ "$t": "1"
+ },
+ "title": {
+ "$t": "Feed title"
+ },
+ "updated": {
+ "$t": "2011-12-14T01:03:21.151Z"
+ },
+ "xmlns": "http://www.w3.org/2005/Atom",
+ "xmlns$batch": "http://schemas.google.com/gdata/batch",
+ "xmlns$docs": "http://schemas.google.com/docs/2007",
+ "xmlns$gd": "http://schemas.google.com/g/2005",
+ "xmlns$openSearch": "http://a9.com/-/spec/opensearch/1.1/"
+ },
+ "version": "1.0"
+}
+