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..20122ef13ef11a0a6b5d3e18bd4921bac055627b |
--- /dev/null |
+++ b/chrome/browser/chromeos/drive/file_system/download_operation.cc |
@@ -0,0 +1,477 @@ |
+// 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); |
+} |
+ |
+// Prepares for downloading the file. Given the |gdata_entry|, refreshes the |
+// |metadata| and then 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 PrepareForDownloadFile( |
+ 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, refresh the metadata and allocate |
+ // the cache space. |
+ DownloadParams* params = new DownloadParams(context, download_url); |
+ base::PostTaskAndReplyWithResult( |
+ blocking_task_runner_, |
+ FROM_HERE, |
+ base::Bind(&PrepareForDownloadFile, |
+ 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::EnsureFileDownloadedAfterPrepareForDownloadFile, |
+ weak_ptr_factory_.GetWeakPtr(), |
+ base::Owned(params), |
+ callback)); |
+} |
+ |
+void DownloadOperation::EnsureFileDownloadedAfterPrepareForDownloadFile( |
+ 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 |