// 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/drive/drive_api_service.h" #include #include #include "base/bind.h" #include "base/strings/stringprintf.h" #include "base/task_runner_util.h" #include "base/values.h" #include "chrome/browser/drive/drive_api_util.h" #include "chrome/browser/google_apis/auth_service.h" #include "chrome/browser/google_apis/drive_api_parser.h" #include "chrome/browser/google_apis/drive_api_requests.h" #include "chrome/browser/google_apis/gdata_errorcode.h" #include "chrome/browser/google_apis/gdata_wapi_parser.h" #include "chrome/browser/google_apis/request_sender.h" #include "content/public/browser/browser_thread.h" using content::BrowserThread; using google_apis::AppList; using google_apis::AuthStatusCallback; using google_apis::AuthorizeAppCallback; using google_apis::CancelCallback; using google_apis::ChangeList; using google_apis::DownloadActionCallback; using google_apis::EntryActionCallback; using google_apis::FileList; using google_apis::FileResource; using google_apis::GDATA_OTHER_ERROR; using google_apis::GDATA_PARSE_ERROR; using google_apis::GDataErrorCode; using google_apis::GetAboutRequest; using google_apis::GetAboutResourceCallback; using google_apis::GetAppListCallback; using google_apis::GetApplistRequest; using google_apis::GetChangelistRequest; using google_apis::GetContentCallback; using google_apis::GetFileRequest; using google_apis::GetFilelistRequest; using google_apis::GetResourceEntryCallback; using google_apis::GetResourceListCallback; using google_apis::GetShareUrlCallback; using google_apis::HTTP_NOT_IMPLEMENTED; using google_apis::HTTP_SUCCESS; using google_apis::InitiateUploadCallback; using google_apis::ProgressCallback; using google_apis::RequestSender; using google_apis::ResourceEntry; using google_apis::ResourceList; using google_apis::UploadRangeCallback; using google_apis::UploadRangeResponse; using google_apis::drive::ContinueGetFileListRequest; using google_apis::drive::CopyResourceRequest; using google_apis::drive::CreateDirectoryRequest; using google_apis::drive::DeleteResourceRequest; using google_apis::drive::DownloadFileRequest; using google_apis::drive::GetUploadStatusRequest; using google_apis::drive::InitiateUploadExistingFileRequest; using google_apis::drive::InitiateUploadNewFileRequest; using google_apis::drive::InsertResourceRequest; using google_apis::drive::RenameResourceRequest; using google_apis::drive::ResumeUploadRequest; using google_apis::drive::TouchResourceRequest; using google_apis::drive::TrashResourceRequest; namespace drive { namespace { // OAuth2 scopes for Drive API. const char kDriveScope[] = "https://www.googleapis.com/auth/drive"; const char kDriveAppsReadonlyScope[] = "https://www.googleapis.com/auth/drive.apps.readonly"; // Expected max number of files resources in a http request. // Be careful not to use something too small because it might overload the // server. Be careful not to use something too large because it takes longer // time to fetch the result without UI response. const int kMaxNumFilesResourcePerRequest = 500; const int kMaxNumFilesResourcePerRequestForSearch = 50; scoped_ptr ParseChangeListJsonToResourceList( scoped_ptr value) { scoped_ptr change_list(ChangeList::CreateFrom(*value)); if (!change_list) { return scoped_ptr(); } return ResourceList::CreateFromChangeList(*change_list); } scoped_ptr ParseFileListJsonToResourceList( scoped_ptr value) { scoped_ptr file_list(FileList::CreateFrom(*value)); if (!file_list) { return scoped_ptr(); } return ResourceList::CreateFromFileList(*file_list); } // Parses JSON value representing either ChangeList or FileList into // ResourceList. scoped_ptr ParseResourceListOnBlockingPool( scoped_ptr value) { DCHECK(value); // Dispatch the parsing based on kind field. if (ChangeList::HasChangeListKind(*value)) { return ParseChangeListJsonToResourceList(value.Pass()); } if (FileList::HasFileListKind(*value)) { return ParseFileListJsonToResourceList(value.Pass()); } // The value type is unknown, so give up to parse and return an error. return scoped_ptr(); } // Callback invoked when the parsing of resource list is completed, // regardless whether it is succeeded or not. void DidParseResourceListOnBlockingPool( const GetResourceListCallback& callback, scoped_ptr resource_list) { GDataErrorCode error = resource_list ? HTTP_SUCCESS : GDATA_PARSE_ERROR; callback.Run(error, resource_list.Pass()); } // Sends a task to parse the JSON value into ResourceList on blocking pool, // with a callback which is called when the task is done. void ParseResourceListOnBlockingPoolAndRun( scoped_refptr blocking_task_runner, const GetResourceListCallback& callback, GDataErrorCode error, scoped_ptr value) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (error != HTTP_SUCCESS) { // An error occurs, so run callback immediately. callback.Run(error, scoped_ptr()); return; } PostTaskAndReplyWithResult( blocking_task_runner.get(), FROM_HERE, base::Bind(&ParseResourceListOnBlockingPool, base::Passed(&value)), base::Bind(&DidParseResourceListOnBlockingPool, callback)); } // Parses the FileResource value to ResourceEntry and runs |callback| on the // UI thread. void ParseResourceEntryAndRun( const GetResourceEntryCallback& callback, GDataErrorCode error, scoped_ptr value) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (!value) { callback.Run(error, scoped_ptr()); return; } // Converting to ResourceEntry is cheap enough to do on UI thread. scoped_ptr entry = ResourceEntry::CreateFromFileResource(*value); if (!entry) { callback.Run(GDATA_PARSE_ERROR, scoped_ptr()); return; } callback.Run(error, entry.Pass()); } // Parses the JSON value to AppList runs |callback| on the UI thread // once parsing is done. void ParseAppListAndRun(const google_apis::GetAppListCallback& callback, google_apis::GDataErrorCode error, scoped_ptr value) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (!value) { callback.Run(error, scoped_ptr()); return; } // Parsing AppList is cheap enough to do on UI thread. scoped_ptr app_list = google_apis::AppList::CreateFrom(*value); if (!app_list) { callback.Run(google_apis::GDATA_PARSE_ERROR, scoped_ptr()); return; } callback.Run(error, app_list.Pass()); } // Parses the FileResource value to ResourceEntry for upload range request, // and runs |callback| on the UI thread. void ParseResourceEntryForUploadRangeAndRun( const UploadRangeCallback& callback, const UploadRangeResponse& response, scoped_ptr value) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (!value) { callback.Run(response, scoped_ptr()); return; } // Converting to ResourceEntry is cheap enough to do on UI thread. scoped_ptr entry = ResourceEntry::CreateFromFileResource(*value); if (!entry) { callback.Run(UploadRangeResponse(GDATA_PARSE_ERROR, response.start_position_received, response.end_position_received), scoped_ptr()); return; } callback.Run(response, entry.Pass()); } void ExtractOpenUrlAndRun(const std::string& app_id, const AuthorizeAppCallback& callback, GDataErrorCode error, scoped_ptr value) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); if (!value) { callback.Run(error, GURL()); return; } const std::vector& open_with_links = value->open_with_links(); for (size_t i = 0; i < open_with_links.size(); ++i) { if (open_with_links[i].app_id == app_id) { callback.Run(HTTP_SUCCESS, open_with_links[i].open_url); return; } } // Not found. callback.Run(GDATA_OTHER_ERROR, GURL()); } // The resource ID for the root directory for Drive API is defined in the spec: // https://developers.google.com/drive/folder const char kDriveApiRootDirectoryResourceId[] = "root"; } // namespace DriveAPIService::DriveAPIService( OAuth2TokenService* oauth2_token_service, net::URLRequestContextGetter* url_request_context_getter, base::TaskRunner* blocking_task_runner, const GURL& base_url, const GURL& base_download_url, const std::string& custom_user_agent) : oauth2_token_service_(oauth2_token_service), url_request_context_getter_(url_request_context_getter), blocking_task_runner_(blocking_task_runner), url_generator_(base_url, base_download_url), custom_user_agent_(custom_user_agent) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); } DriveAPIService::~DriveAPIService() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (sender_.get()) sender_->auth_service()->RemoveObserver(this); } void DriveAPIService::Initialize() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); std::vector scopes; scopes.push_back(kDriveScope); scopes.push_back(kDriveAppsReadonlyScope); sender_.reset(new RequestSender( new google_apis::AuthService( oauth2_token_service_, url_request_context_getter_, scopes), url_request_context_getter_, blocking_task_runner_.get(), custom_user_agent_)); sender_->auth_service()->AddObserver(this); } void DriveAPIService::AddObserver(DriveServiceObserver* observer) { observers_.AddObserver(observer); } void DriveAPIService::RemoveObserver(DriveServiceObserver* observer) { observers_.RemoveObserver(observer); } bool DriveAPIService::CanSendRequest() const { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); return HasRefreshToken(); } std::string DriveAPIService::CanonicalizeResourceId( const std::string& resource_id) const { return drive::util::CanonicalizeResourceId(resource_id); } std::string DriveAPIService::GetRootResourceId() const { return kDriveApiRootDirectoryResourceId; } CancelCallback DriveAPIService::GetAllResourceList( const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); // The simplest way to fetch the all resources list looks files.list method, // but it seems impossible to know the returned list's changestamp. // Thus, instead, we use changes.list method with includeDeleted=false here. // The returned list should contain only resources currently existing. return sender_->StartRequestWithRetry( new GetChangelistRequest( sender_.get(), url_generator_, false, // include deleted 0, kMaxNumFilesResourcePerRequest, base::Bind(&ParseResourceListOnBlockingPoolAndRun, blocking_task_runner_, callback))); } CancelCallback DriveAPIService::GetResourceListInDirectory( const std::string& directory_resource_id, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!directory_resource_id.empty()); DCHECK(!callback.is_null()); // Because children.list method on Drive API v2 returns only the list of // children's references, but we need all file resource list. // So, here we use files.list method instead, with setting parents query. // After the migration from GData WAPI to Drive API v2, we should clean the // code up by moving the responsibility to include "parents" in the query // to client side. // We aren't interested in files in trash in this context, neither. return sender_->StartRequestWithRetry( new GetFilelistRequest( sender_.get(), url_generator_, base::StringPrintf( "'%s' in parents and trashed = false", drive::util::EscapeQueryStringValue( directory_resource_id).c_str()), kMaxNumFilesResourcePerRequest, base::Bind(&ParseResourceListOnBlockingPoolAndRun, blocking_task_runner_, callback))); } CancelCallback DriveAPIService::Search( const std::string& search_query, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!search_query.empty()); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new GetFilelistRequest( sender_.get(), url_generator_, drive::util::TranslateQuery(search_query), kMaxNumFilesResourcePerRequestForSearch, base::Bind(&ParseResourceListOnBlockingPoolAndRun, blocking_task_runner_, callback))); } CancelCallback DriveAPIService::SearchByTitle( const std::string& title, const std::string& directory_resource_id, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!title.empty()); DCHECK(!callback.is_null()); std::string query; base::StringAppendF(&query, "title = '%s'", drive::util::EscapeQueryStringValue(title).c_str()); if (!directory_resource_id.empty()) { base::StringAppendF( &query, " and '%s' in parents", drive::util::EscapeQueryStringValue(directory_resource_id).c_str()); } query += " and trashed = false"; return sender_->StartRequestWithRetry( new GetFilelistRequest( sender_.get(), url_generator_, query, kMaxNumFilesResourcePerRequest, base::Bind(&ParseResourceListOnBlockingPoolAndRun, blocking_task_runner_, callback))); } CancelCallback DriveAPIService::GetChangeList( int64 start_changestamp, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new GetChangelistRequest( sender_.get(), url_generator_, true, // include deleted start_changestamp, kMaxNumFilesResourcePerRequest, base::Bind(&ParseResourceListOnBlockingPoolAndRun, blocking_task_runner_, callback))); } CancelCallback DriveAPIService::ContinueGetResourceList( const GURL& override_url, const GetResourceListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new ContinueGetFileListRequest( sender_.get(), override_url, base::Bind(&ParseResourceListOnBlockingPoolAndRun, blocking_task_runner_, callback))); } CancelCallback DriveAPIService::GetResourceEntry( const std::string& resource_id, const GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry(new GetFileRequest( sender_.get(), url_generator_, resource_id, base::Bind(&ParseResourceEntryAndRun, callback))); } CancelCallback DriveAPIService::GetShareUrl( const std::string& resource_id, const GURL& embed_origin, const GetShareUrlCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); // TODO(mtomasz): Implement this, once it is supported by the Drive API. NOTIMPLEMENTED(); callback.Run(HTTP_NOT_IMPLEMENTED, GURL()); return CancelCallback(); } CancelCallback DriveAPIService::GetAboutResource( const GetAboutResourceCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new GetAboutRequest( sender_.get(), url_generator_, callback)); } CancelCallback DriveAPIService::GetAppList(const GetAppListCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry(new GetApplistRequest( sender_.get(), url_generator_, base::Bind(&ParseAppListAndRun, callback))); } CancelCallback DriveAPIService::DownloadFile( const base::FilePath& local_cache_path, const std::string& resource_id, const DownloadActionCallback& download_action_callback, const GetContentCallback& get_content_callback, const ProgressCallback& progress_callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!download_action_callback.is_null()); // get_content_callback may be null. return sender_->StartRequestWithRetry( new DownloadFileRequest(sender_.get(), url_generator_, resource_id, local_cache_path, download_action_callback, get_content_callback, progress_callback)); } CancelCallback DriveAPIService::DeleteResource( const std::string& resource_id, const std::string& etag, const EntryActionCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry(new TrashResourceRequest( sender_.get(), url_generator_, resource_id, callback)); } CancelCallback DriveAPIService::AddNewDirectory( const std::string& parent_resource_id, const std::string& directory_title, const GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new CreateDirectoryRequest( sender_.get(), url_generator_, parent_resource_id, directory_title, base::Bind(&ParseResourceEntryAndRun, callback))); } CancelCallback DriveAPIService::CopyResource( const std::string& resource_id, const std::string& parent_resource_id, const std::string& new_title, const GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new CopyResourceRequest( sender_.get(), url_generator_, resource_id, parent_resource_id, new_title, base::Bind(&ParseResourceEntryAndRun, callback))); } CancelCallback DriveAPIService::CopyHostedDocument( const std::string& resource_id, const std::string& new_title, const GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new CopyResourceRequest( sender_.get(), url_generator_, resource_id, std::string(), // parent_resource_id. new_title, base::Bind(&ParseResourceEntryAndRun, callback))); } CancelCallback DriveAPIService::RenameResource( const std::string& resource_id, const std::string& new_title, const EntryActionCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new RenameResourceRequest( sender_.get(), url_generator_, resource_id, new_title, callback)); } CancelCallback DriveAPIService::TouchResource( const std::string& resource_id, const base::Time& modified_date, const base::Time& last_viewed_by_me_date, const GetResourceEntryCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!modified_date.is_null()); DCHECK(!last_viewed_by_me_date.is_null()); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new TouchResourceRequest( sender_.get(), url_generator_, resource_id, modified_date, last_viewed_by_me_date, base::Bind(&ParseResourceEntryAndRun, callback))); } CancelCallback DriveAPIService::AddResourceToDirectory( const std::string& parent_resource_id, const std::string& resource_id, const EntryActionCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new InsertResourceRequest( sender_.get(), url_generator_, parent_resource_id, resource_id, callback)); } CancelCallback DriveAPIService::RemoveResourceFromDirectory( const std::string& parent_resource_id, const std::string& resource_id, const EntryActionCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new DeleteResourceRequest( sender_.get(), url_generator_, parent_resource_id, resource_id, callback)); } CancelCallback DriveAPIService::InitiateUploadNewFile( const std::string& content_type, int64 content_length, const std::string& parent_resource_id, const std::string& title, const InitiateUploadCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new InitiateUploadNewFileRequest( sender_.get(), url_generator_, content_type, content_length, parent_resource_id, title, callback)); } CancelCallback DriveAPIService::InitiateUploadExistingFile( const std::string& content_type, int64 content_length, const std::string& resource_id, const std::string& etag, const InitiateUploadCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new InitiateUploadExistingFileRequest( sender_.get(), url_generator_, content_type, content_length, resource_id, etag, callback)); } CancelCallback DriveAPIService::ResumeUpload( const GURL& upload_url, int64 start_position, int64 end_position, int64 content_length, const std::string& content_type, const base::FilePath& local_file_path, const UploadRangeCallback& callback, const ProgressCallback& progress_callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry( new ResumeUploadRequest( sender_.get(), upload_url, start_position, end_position, content_length, content_type, local_file_path, base::Bind(&ParseResourceEntryForUploadRangeAndRun, callback), progress_callback)); } CancelCallback DriveAPIService::GetUploadStatus( const GURL& upload_url, int64 content_length, const UploadRangeCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry(new GetUploadStatusRequest( sender_.get(), upload_url, content_length, base::Bind(&ParseResourceEntryForUploadRangeAndRun, callback))); } CancelCallback DriveAPIService::AuthorizeApp( const std::string& resource_id, const std::string& app_id, const AuthorizeAppCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); return sender_->StartRequestWithRetry(new GetFileRequest( sender_.get(), url_generator_, resource_id, base::Bind(&ExtractOpenUrlAndRun, app_id, callback))); } bool DriveAPIService::HasAccessToken() const { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); return sender_->auth_service()->HasAccessToken(); } void DriveAPIService::RequestAccessToken(const AuthStatusCallback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(!callback.is_null()); const std::string access_token = sender_->auth_service()->access_token(); if (!access_token.empty()) { callback.Run(google_apis::HTTP_NOT_MODIFIED, access_token); return; } // Retrieve the new auth token. sender_->auth_service()->StartAuthentication(callback); } bool DriveAPIService::HasRefreshToken() const { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); return sender_->auth_service()->HasRefreshToken(); } void DriveAPIService::ClearAccessToken() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); sender_->auth_service()->ClearAccessToken(); } void DriveAPIService::ClearRefreshToken() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); sender_->auth_service()->ClearRefreshToken(); } void DriveAPIService::OnOAuth2RefreshTokenChanged() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (CanSendRequest()) { FOR_EACH_OBSERVER( DriveServiceObserver, observers_, OnReadyToSendRequests()); } else if (!HasRefreshToken()) { FOR_EACH_OBSERVER( DriveServiceObserver, observers_, OnRefreshTokenInvalid()); } } } // namespace drive