diff options
author | zelidrag@chromium.org <zelidrag@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-02-25 18:04:47 +0000 |
---|---|---|
committer | zelidrag@chromium.org <zelidrag@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-02-25 18:04:47 +0000 |
commit | d0baa03ba5e91ff80241a7f8ac478be7120a4332 (patch) | |
tree | 9cea352a7c0d106c101ed04f6a493a936c299ea7 /chrome | |
parent | 66619f448a567ab5f27c8e572af30eb54c67ba8a (diff) | |
download | chromium_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/DEPS | 1 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata.cc | 79 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata.h | 13 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata_file_system.cc | 461 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata_file_system.h | 244 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata_file_system_proxy.cc | 291 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata_file_system_proxy.h | 39 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata_file_system_unittest.cc | 263 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata_parser.cc | 17 | ||||
-rw-r--r-- | chrome/browser/chromeos/gdata/gdata_parser.h | 40 | ||||
-rw-r--r-- | chrome/chrome_browser.gypi | 4 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 | ||||
-rw-r--r-- | chrome/test/data/chromeos/gdata/root_feed.json | 490 | ||||
-rw-r--r-- | chrome/test/data/chromeos/gdata/subdir_feed.json | 236 |
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" +} + |