Chromium Code Reviews| Index: chrome/browser/chromeos/drive/file_system/download_operation.cc |
| diff --git a/chrome/browser/chromeos/drive/file_system/download_operation.cc b/chrome/browser/chromeos/drive/file_system/download_operation.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..7764bda0808cc93a220079a2af3861eabd9e45b2 |
| --- /dev/null |
| +++ b/chrome/browser/chromeos/drive/file_system/download_operation.cc |
| @@ -0,0 +1,475 @@ |
| +// Copyright 2013 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/drive/file_system/download_operation.h" |
| + |
| +#include "base/file_util.h" |
| +#include "base/files/file_path.h" |
| +#include "base/logging.h" |
| +#include "base/task_runner_util.h" |
| +#include "chrome/browser/chromeos/drive/drive.pb.h" |
| +#include "chrome/browser/chromeos/drive/file_cache.h" |
| +#include "chrome/browser/chromeos/drive/file_errors.h" |
| +#include "chrome/browser/chromeos/drive/file_system/operation_observer.h" |
| +#include "chrome/browser/chromeos/drive/file_system_util.h" |
| +#include "chrome/browser/chromeos/drive/job_scheduler.h" |
| +#include "chrome/browser/chromeos/drive/resource_entry_conversion.h" |
| +#include "chrome/browser/chromeos/drive/resource_metadata.h" |
| +#include "chrome/browser/google_apis/gdata_errorcode.h" |
| +#include "content/public/browser/browser_thread.h" |
| + |
| +using content::BrowserThread; |
| + |
| +namespace drive { |
| +namespace file_system { |
| +namespace { |
| + |
| +// If the resource is a hosted document, creates a JSON file representing the |
| +// resource locally, and returns FILE_ERROR_OK with |cache_file_path| storing |
| +// the path to the JSON file. |
| +// If the resource is a regular file and its local cache is available, |
| +// returns FILE_ERROR_OK with |cache_file_path| storing the path to the |
| +// cache file. |
| +// If the resource is a regular file but its local cache is NOT available, |
| +// returns FILE_ERROR_OK, but |cache_file_path| is kept empty. |
| +// Otherwise returns error code. |
| +FileError CheckPreConditionForEnsureFileDownloaded( |
| + internal::ResourceMetadata* metadata, |
| + internal::FileCache* cache, |
| + const base::FilePath& file_path, |
| + base::FilePath* cache_file_path, |
| + ResourceEntry* entry) { |
| + DCHECK(metadata); |
| + DCHECK(cache); |
| + DCHECK(cache_file_path); |
| + DCHECK(entry); |
| + |
| + FileError error = metadata->GetResourceEntryByPath(file_path, entry); |
| + if (error != FILE_ERROR_OK) |
| + return error; |
| + |
| + if (entry->file_info().is_directory()) |
| + return FILE_ERROR_NOT_A_FILE; |
| + |
| + // The file's entry should have its file specific info. |
| + DCHECK(entry->has_file_specific_info()); |
| + |
| + // For a hosted document, we create a special JSON file to represent the |
| + // document instead of fetching the document content in one of the exported |
| + // formats. The JSON file contains the edit URL and resource ID of the |
| + // document. |
| + if (entry->file_specific_info().is_hosted_document()) { |
| + base::FilePath gdoc_file_path; |
| + if (!file_util::CreateTemporaryFileInDir( |
| + cache->GetCacheDirectoryPath( |
| + internal::FileCache::CACHE_TYPE_TMP_DOCUMENTS), |
| + &gdoc_file_path) || |
| + !util::CreateGDocFile(gdoc_file_path, |
| + GURL(entry->file_specific_info().alternate_url()), |
| + entry->resource_id())) |
| + return FILE_ERROR_FAILED; |
| + |
| + *cache_file_path = gdoc_file_path; |
| + return FILE_ERROR_OK; |
| + } |
| + |
| + // Look up if there exists the cache file. |
| + FileError cache_error = cache->GetFile( |
| + entry->resource_id(), |
| + entry->file_specific_info().file_md5(), |
| + cache_file_path); |
| + DCHECK((cache_error == FILE_ERROR_OK && !cache_file_path->empty()) || |
| + (cache_error == FILE_ERROR_NOT_FOUND && cache_file_path->empty())); |
| + |
| + return error; |
| +} |
| + |
| +// Creates a file with unique name in |dir| and stores the path to |temp_file|. |
| +// Additionally, sets the permission of the file to allow read access from |
| +// others and group member users (i.e, "-rw-r--r--"). |
| +// We need this wrapper because Drive cache files may be read from other |
| +// processes (e.g., cros_disks for mounting zip files). |
| +bool CreateTemporaryReadableFileInDir(const base::FilePath& dir, |
| + base::FilePath* temp_file) { |
| + if (!file_util::CreateTemporaryFileInDir(dir, temp_file)) |
| + return false; |
| + return file_util::SetPosixFilePermissions( |
| + *temp_file, |
| + file_util::FILE_PERMISSION_READ_BY_USER | |
| + file_util::FILE_PERMISSION_WRITE_BY_USER | |
| + file_util::FILE_PERMISSION_READ_BY_GROUP | |
| + file_util::FILE_PERMISSION_READ_BY_OTHERS); |
| +} |
| + |
| +// Allocates the enough space in the cache. If succeeded, returns FILE_ERROR_OK |
| +// with |entry| storing the ResourceEntry of the resource, |drive_file_path| |
| +// with storing the path of the entry, and |temp_download_file| storing the |
| +// path to the file in the cache. |
| +FileError AllocateCacheSpace( |
|
hashimoto
2013/05/24 09:48:38
nit: What this function does is more than just all
hidehiko
2013/05/26 15:39:36
Done.
|
| + internal::ResourceMetadata* metadata, |
| + internal::FileCache* cache, |
| + scoped_ptr<google_apis::ResourceEntry> gdata_entry, |
| + ResourceEntry* entry, |
| + base::FilePath* drive_file_path, |
| + base::FilePath* temp_download_file) { |
| + DCHECK(metadata); |
| + DCHECK(cache); |
| + DCHECK(gdata_entry); |
| + DCHECK(entry); |
| + DCHECK(drive_file_path); |
| + DCHECK(temp_download_file); |
| + |
| + FileError error = metadata->RefreshEntry( |
| + ConvertToResourceEntry(*gdata_entry), drive_file_path, entry); |
| + if (error != FILE_ERROR_OK) |
| + return error; |
| + |
| + // Ensure enough space in the cache. |
| + if (!cache->FreeDiskSpaceIfNeededFor(entry->file_info().size())) |
| + return FILE_ERROR_NO_SPACE; |
| + |
| + // Create the temporary file which will store the donwloaded content. |
| + return CreateTemporaryReadableFileInDir( |
| + cache->GetCacheDirectoryPath( |
| + internal::FileCache::CACHE_TYPE_TMP_DOWNLOADS), |
| + temp_download_file) ? |
| + FILE_ERROR_OK : FILE_ERROR_FAILED; |
| +} |
| + |
| +// Stores the downloaded file at |downloaded_file_path| into |cache|. |
| +// If succeeded, returns FILE_ERROR_OK with |cache_file_path| storing the |
| +// path to the cache file. |
| +// If failed, returns an error code with deleting |downloaded_file_path|. |
| +FileError UpdateLocalStateForDownloadFile( |
| + internal::FileCache* cache, |
| + const std::string& resource_id, |
| + const std::string& md5, |
| + google_apis::GDataErrorCode gdata_error, |
| + const base::FilePath& downloaded_file_path, |
| + base::FilePath* cache_file_path) { |
| + DCHECK(cache); |
| + |
| + // If user cancels download of a pinned-but-not-fetched file, mark file as |
| + // unpinned so that we do not sync the file again. |
| + if (gdata_error == google_apis::GDATA_CANCELLED) { |
| + FileCacheEntry cache_entry; |
| + if (cache->GetCacheEntry(resource_id, md5, &cache_entry) && |
| + cache_entry.is_pinned()) { |
| + // TODO(hshi): http://crbug.com/127138 notify when file properties change. |
| + // This allows file manager to clear the "Available offline" checkbox. |
| + cache->Unpin(resource_id, md5); |
| + } |
| + } |
| + |
| + FileError error = util::GDataToFileError(gdata_error); |
| + if (error != FILE_ERROR_OK) { |
| + file_util::Delete(downloaded_file_path, false /* recursive */); |
| + return error; |
| + } |
| + |
| + // Here the download is completed successfully, so store it into the cache. |
| + error = cache->Store(resource_id, md5, downloaded_file_path, |
| + internal::FileCache::FILE_OPERATION_MOVE); |
| + if (error != FILE_ERROR_OK) { |
| + file_util::Delete(downloaded_file_path, false /* recursive */); |
| + return error; |
| + } |
| + |
| + return cache->GetFile(resource_id, md5, cache_file_path); |
| +} |
| + |
| +} // namespace |
| + |
| +class DownloadOperation::DownloadCallback { |
| + public: |
| + DownloadCallback( |
| + const GetFileContentInitializedCallback initialized_callback, |
| + const google_apis::GetContentCallback get_content_callback, |
| + const GetFileCallback completion_callback) |
| + : initialized_callback_(initialized_callback), |
| + get_content_callback_(get_content_callback), |
| + completion_callback_(completion_callback) { |
| + DCHECK(!completion_callback_.is_null()); |
| + } |
| + |
| + void OnCacheFileFound(const ResourceEntry& entry, |
| + const base::FilePath& cache_file_path) const { |
| + if (initialized_callback_.is_null()) |
| + return; |
| + |
| + initialized_callback_.Run( |
| + FILE_ERROR_OK, make_scoped_ptr(new ResourceEntry(entry)), |
| + cache_file_path, base::Closure()); |
| + } |
| + |
| + void OnStartDownloading(const ResourceEntry& entry, |
| + const base::Closure& cancel_download_closure) const { |
| + if (initialized_callback_.is_null()) { |
| + return; |
| + } |
| + |
| + initialized_callback_.Run( |
| + FILE_ERROR_OK, make_scoped_ptr(new ResourceEntry(entry)), |
| + base::FilePath(), cancel_download_closure); |
| + } |
| + |
| + void OnError(FileError error) const { |
| + completion_callback_.Run( |
| + error, base::FilePath(), scoped_ptr<ResourceEntry>()); |
| + } |
| + |
| + void OnComplete(const base::FilePath& cache_file_path, |
| + scoped_ptr<ResourceEntry> entry) const { |
| + completion_callback_.Run(FILE_ERROR_OK, cache_file_path, entry.Pass()); |
| + } |
| + |
| + const google_apis::GetContentCallback& get_content_callback() const { |
| + return get_content_callback_; |
| + } |
| + |
| + private: |
| + const GetFileContentInitializedCallback initialized_callback_; |
| + const google_apis::GetContentCallback get_content_callback_; |
| + const GetFileCallback completion_callback_; |
| + |
| + // This class is copiable. |
| +}; |
| + |
| +struct DownloadOperation::DownloadParams { |
| + DownloadParams(const DriveClientContext& context, |
| + const GURL& download_url) |
| + : context(context), |
| + download_url(download_url), |
| + entry(new ResourceEntry) { |
| + } |
| + |
| + DriveClientContext context; |
| + GURL download_url; |
| + scoped_ptr<ResourceEntry> entry; |
| + base::FilePath drive_file_path; |
| + base::FilePath temp_download_file_path; |
| +}; |
| + |
| +DownloadOperation::DownloadOperation( |
| + base::SequencedTaskRunner* blocking_task_runner, |
| + OperationObserver* observer, |
| + JobScheduler* scheduler, |
| + internal::ResourceMetadata* metadata, |
| + internal::FileCache* cache) |
| + : blocking_task_runner_(blocking_task_runner), |
| + observer_(observer), |
| + scheduler_(scheduler), |
| + metadata_(metadata), |
| + cache_(cache), |
| + weak_ptr_factory_(this) { |
| +} |
| + |
| +DownloadOperation::~DownloadOperation() { |
| +} |
| + |
| +void DownloadOperation::EnsureFileDownloaded( |
| + const base::FilePath& file_path, |
| + DriveClientContext context, |
| + const GetFileContentInitializedCallback& initialized_callback, |
| + const google_apis::GetContentCallback& get_content_callback, |
| + const GetFileCallback& completion_callback) { |
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + DCHECK(!completion_callback.is_null()); |
| + |
| + DownloadCallback callback( |
| + initialized_callback, get_content_callback, completion_callback); |
| + |
| + ResourceEntry* entry = new ResourceEntry; |
| + base::FilePath* cache_file_path = new base::FilePath; |
| + base::PostTaskAndReplyWithResult( |
| + blocking_task_runner_, |
| + FROM_HERE, |
| + base::Bind(&CheckPreConditionForEnsureFileDownloaded, |
| + base::Unretained(metadata_), |
| + base::Unretained(cache_), |
| + file_path, |
| + cache_file_path, |
| + entry), |
| + base::Bind(&DownloadOperation::EnsureFileDownloadedAfterCheckPreCondition, |
| + weak_ptr_factory_.GetWeakPtr(), |
| + file_path, |
| + context, |
| + callback, |
| + base::Passed(make_scoped_ptr(entry)), |
| + base::Owned(cache_file_path))); |
| +} |
| + |
| +void DownloadOperation::EnsureFileDownloadedAfterCheckPreCondition( |
| + const base::FilePath& file_path, |
| + DriveClientContext context, |
| + const DownloadCallback& callback, |
| + scoped_ptr<ResourceEntry> entry, |
| + base::FilePath* cache_file_path, |
| + FileError error) { |
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + DCHECK(entry); |
| + DCHECK(cache_file_path); |
| + |
| + if (error != FILE_ERROR_OK) { |
| + // During precondition check, an error is found. |
| + callback.OnError(error); |
| + return; |
| + } |
| + |
| + if (!cache_file_path->empty()) { |
| + // The cache file is found. |
| + callback.OnCacheFileFound(*entry, *cache_file_path); |
| + callback.OnComplete(*cache_file_path, entry.Pass()); |
| + return; |
| + } |
| + |
| + // If cache file is not found, try to download the file from the server |
| + // instead. This logic is rather complicated but here's how this works: |
| + // |
| + // Retrieve fresh file metadata from server. We will extract file size and |
| + // download url from there. Note that the download url is transient. |
| + // |
| + // Check if we have enough space, based on the expected file size. |
| + // - if we don't have enough space, try to free up the disk space |
| + // - if we still don't have enough space, return "no space" error |
| + // - if we have enough space, start downloading the file from the server |
| + scheduler_->GetResourceEntry( |
| + entry->resource_id(), |
| + context, |
| + base::Bind(&DownloadOperation::EnsureFileDownloadedAfterGetResourceEntry, |
| + weak_ptr_factory_.GetWeakPtr(), |
| + context, |
| + callback)); |
| +} |
| + |
| +void DownloadOperation::EnsureFileDownloadedAfterGetResourceEntry( |
| + DriveClientContext context, |
| + const DownloadCallback& callback, |
| + google_apis::GDataErrorCode gdata_error, |
| + scoped_ptr<google_apis::ResourceEntry> resource_entry) { |
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + |
| + FileError error = util::GDataToFileError(gdata_error); |
| + if (error != FILE_ERROR_OK) { |
| + callback.OnError(error); |
| + return; |
| + } |
| + DCHECK(resource_entry); |
| + |
| + // The download URL is: |
| + // 1) src attribute of content element, on GData WAPI. |
| + // 2) the value of the key 'downloadUrl', on Drive API v2. |
| + // In both cases, we can use ResourceEntry::download_url(). |
| + const GURL& download_url = resource_entry->download_url(); |
| + |
| + // The download URL can be empty for non-downloadable files (such as files |
| + // shared from others with "prevent downloading by viewers" flag set.) |
| + if (download_url.is_empty()) { |
| + callback.OnError(FILE_ERROR_ACCESS_DENIED); |
| + return; |
| + } |
| + |
| + // Before starting to download actually, allocate the cache space. |
| + DownloadParams* params = new DownloadParams(context, download_url); |
| + base::PostTaskAndReplyWithResult( |
| + blocking_task_runner_, |
| + FROM_HERE, |
| + base::Bind(&AllocateCacheSpace, |
| + base::Unretained(metadata_), |
| + base::Unretained(cache_), |
| + base::Passed(&resource_entry), |
| + params->entry.get(), |
| + ¶ms->drive_file_path, |
| + ¶ms->temp_download_file_path), |
| + base::Bind( |
| + &DownloadOperation::EnsureFileDownloadedAfterAllocateCacheSpace, |
| + weak_ptr_factory_.GetWeakPtr(), |
| + base::Owned(params), |
| + callback)); |
| +} |
| + |
| +void DownloadOperation::EnsureFileDownloadedAfterAllocateCacheSpace( |
| + DownloadParams* params, |
| + const DownloadCallback& callback, |
| + FileError error) { |
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + DCHECK(params); |
| + |
| + if (error != FILE_ERROR_OK) { |
| + callback.OnError(error); |
| + return; |
| + } |
| + |
| + ResourceEntry* entry_ptr = params->entry.get(); |
| + JobID id = scheduler_->DownloadFile( |
| + params->drive_file_path, |
| + params->temp_download_file_path, |
| + params->download_url, |
| + params->context, |
| + base::Bind(&DownloadOperation::EnsureFileDownloadedAfterDownloadFile, |
| + weak_ptr_factory_.GetWeakPtr(), |
| + params->drive_file_path, |
| + base::Passed(¶ms->entry), |
| + callback), |
| + callback.get_content_callback()); |
| + |
| + // Notify via |initialized_callback| if necessary. |
| + callback.OnStartDownloading( |
| + *entry_ptr, |
| + base::Bind(&DownloadOperation::CancelJob, |
| + weak_ptr_factory_.GetWeakPtr(), id)); |
| +} |
| + |
| +void DownloadOperation::EnsureFileDownloadedAfterDownloadFile( |
| + const base::FilePath& drive_file_path, |
| + scoped_ptr<ResourceEntry> entry, |
| + const DownloadCallback& callback, |
| + google_apis::GDataErrorCode gdata_error, |
| + const base::FilePath& downloaded_file_path) { |
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + |
| + ResourceEntry* entry_ptr = entry.get(); |
| + base::FilePath* cache_file_path = new base::FilePath; |
| + base::PostTaskAndReplyWithResult( |
| + blocking_task_runner_, |
| + FROM_HERE, |
| + base::Bind(&UpdateLocalStateForDownloadFile, |
| + base::Unretained(cache_), |
| + entry_ptr->resource_id(), |
| + entry_ptr->file_specific_info().file_md5(), |
| + gdata_error, |
| + downloaded_file_path, |
| + cache_file_path), |
| + base::Bind(&DownloadOperation::EnsureFileDownloadedAfterUpdateLocalState, |
| + weak_ptr_factory_.GetWeakPtr(), |
| + drive_file_path, |
| + callback, |
| + base::Passed(&entry), |
| + base::Owned(cache_file_path))); |
| +} |
| + |
| +void DownloadOperation::EnsureFileDownloadedAfterUpdateLocalState( |
| + const base::FilePath& file_path, |
| + const DownloadCallback& callback, |
| + scoped_ptr<ResourceEntry> entry, |
| + base::FilePath* cache_file_path, |
| + FileError error) { |
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| + |
| + if (error != FILE_ERROR_OK) { |
| + callback.OnError(error); |
| + return; |
| + } |
| + |
| + // Storing to cache changes the "offline available" status, hence notify. |
| + observer_->OnDirectoryChangedByOperation(file_path.DirName()); |
| + callback.OnComplete(*cache_file_path, entry.Pass()); |
| +} |
| + |
| +void DownloadOperation::CancelJob(JobID job_id) { |
| + scheduler_->CancelJob(job_id); |
| +} |
| + |
| +} // namespace file_system |
| +} // namespace drive |