blob: 6a1da6a001953c3d4c84afefb188c2d0d1eccfde [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/drive/drive_uploader.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "components/drive/service/drive_service_interface.h"
#include "google_apis/drive/drive_api_parser.h"
#include "services/device/public/mojom/wake_lock.mojom.h"
using google_apis::ApiErrorCode;
using google_apis::CancelCallbackOnce;
using google_apis::CANCELLED;
using google_apis::DRIVE_NO_SPACE;
using google_apis::FileResource;
using google_apis::HTTP_CONFLICT;
using google_apis::HTTP_CREATED;
using google_apis::HTTP_FORBIDDEN;
using google_apis::HTTP_NOT_FOUND;
using google_apis::HTTP_PRECONDITION;
using google_apis::HTTP_RESUME_INCOMPLETE;
using google_apis::HTTP_SUCCESS;
using google_apis::ProgressCallback;
using google_apis::UploadRangeResponse;
namespace drive {
namespace {
// Upload data is split to multiple HTTP request each conveying kUploadChunkSize
// bytes (except the request for uploading the last chunk of data).
// The value must be a multiple of 512KB according to the spec of GData WAPI and
// Drive API v2. It is set to a smaller value than 2^31 for working around
// server side error (crbug.com/264089).
const int64_t kUploadChunkSize = (1LL << 30); // 1GB
// Maximum file size to be uploaded by multipart requests. The file that is
// larger than the size is processed by resumable upload.
const int64_t kMaxMultipartUploadSize = (1LL << 20); // 1MB
// Drive upload protocol. This is used to back a histogram. Sync this with UMA
// enum "DriveUploadProtocol" and treat this as append-only.
enum DriveUploadProtocol {
UPLOAD_METHOD_RESUMABLE,
UPLOAD_METHOD_MULTIPART,
UPLOAD_METHOD_BATCH,
UPLOAD_METHOD_MAX_VALUE
};
void RecordDriveUploadProtocol(DriveUploadProtocol protocol) {
UMA_HISTOGRAM_ENUMERATION("Drive.UploadProtocol", protocol,
UPLOAD_METHOD_MAX_VALUE);
}
} // namespace
// Refcounted helper class to manage batch request. DriveUploader uses the class
// for keeping the BatchRequestConfigurator instance while it prepares upload
// file information asynchronously. DriveUploader discard the reference after
// getting file information and the instance will be destroyed after all
// preparations complete. At that time, the helper instance commits owned batch
// request at the destrutor.
class DriveUploader::RefCountedBatchRequest
: public base::RefCounted<RefCountedBatchRequest> {
public:
RefCountedBatchRequest(
std::unique_ptr<BatchRequestConfiguratorInterface> configurator)
: configurator_(std::move(configurator)) {}
// Gets pointer of BatchRequestConfiguratorInterface owned by the instance.
BatchRequestConfiguratorInterface* configurator() const {
return configurator_.get();
}
private:
friend class base::RefCounted<RefCountedBatchRequest>;
~RefCountedBatchRequest() { configurator_->Commit(); }
std::unique_ptr<BatchRequestConfiguratorInterface> configurator_;
};
// Structure containing current upload information of file, passed between
// DriveServiceInterface methods and callbacks.
struct DriveUploader::UploadFileInfo {
UploadFileInfo(const base::FilePath& local_path,
const std::string& content_type,
UploadCompletionCallback callback,
ProgressCallback progress_callback,
device::mojom::WakeLockProvider* wake_lock_provider)
: file_path(local_path),
content_type(content_type),
completion_callback(std::move(callback)),
progress_callback(progress_callback),
content_length(0),
next_start_position(-1),
cancelled(false) {
if (wake_lock_provider) {
wake_lock_provider->GetWakeLockWithoutContext(
device::mojom::WakeLockType::kPreventAppSuspension,
device::mojom::WakeLockReason::kOther, "Upload in progress",
wake_lock.BindNewPipeAndPassReceiver());
wake_lock->RequestWakeLock();
}
}
UploadFileInfo(const UploadFileInfo&) = delete;
UploadFileInfo& operator=(const UploadFileInfo&) = delete;
~UploadFileInfo() = default;
// Useful for printf debugging.
std::string DebugString() const {
return "file_path=[" + file_path.AsUTF8Unsafe() + "], content_type=[" +
content_type + "], content_length=[" +
base::NumberToString(content_length) + "]";
}
// Returns the callback to cancel the upload represented by this struct.
CancelCallbackOnce GetCancelCallback() {
return base::BindOnce(&UploadFileInfo::Cancel,
weak_ptr_factory_.GetWeakPtr());
}
// The local file path of the file to be uploaded.
const base::FilePath file_path;
// Content-Type of file.
const std::string content_type;
// Callback to be invoked once the upload has finished.
UploadCompletionCallback completion_callback;
// Callback to periodically notify the upload progress.
const ProgressCallback progress_callback;
// Location URL where file is to be uploaded to, returned from
// InitiateUpload. Used for the subsequent ResumeUpload requests.
GURL upload_location;
// Header content-Length.
int64_t content_length;
int64_t next_start_position;
// Blocks system suspend while upload is in progress.
mojo::Remote<device::mojom::WakeLock> wake_lock;
// Fields for implementing cancellation. |cancel_callback| is non-null if
// there is an in-flight HTTP request. In that case, |cancell_callback| will
// cancel the operation. |cancelled| is initially false and turns to true
// once Cancel() is called. DriveUploader will check this field before after
// an async task other than HTTP requests and cancels the subsequent requests
// if this is flagged to true.
CancelCallbackOnce cancel_callback;
bool cancelled;
private:
// Cancels the upload represented by this struct.
void Cancel() {
cancelled = true;
if (!cancel_callback.is_null())
std::move(cancel_callback).Run();
}
base::WeakPtrFactory<UploadFileInfo> weak_ptr_factory_{this};
};
DriveUploader::DriveUploader(
DriveServiceInterface* drive_service,
const scoped_refptr<base::TaskRunner>& blocking_task_runner,
mojo::PendingRemote<device::mojom::WakeLockProvider> wake_lock_provider)
: drive_service_(drive_service),
blocking_task_runner_(blocking_task_runner),
wake_lock_provider_(std::move(wake_lock_provider)) {}
DriveUploader::~DriveUploader() = default;
CancelCallbackOnce DriveUploader::UploadNewFile(
const std::string& parent_resource_id,
const base::FilePath& local_file_path,
const std::string& title,
const std::string& content_type,
const UploadNewFileOptions& options,
UploadCompletionCallback callback,
ProgressCallback progress_callback) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!parent_resource_id.empty());
DCHECK(!local_file_path.empty());
DCHECK(!title.empty());
DCHECK(!content_type.empty());
DCHECK(!callback.is_null());
return StartUploadFile(
std::make_unique<UploadFileInfo>(local_file_path, content_type,
std::move(callback), progress_callback,
GetWakeLockProvider()),
base::BindOnce(&DriveUploader::CallUploadServiceAPINewFile,
weak_ptr_factory_.GetWeakPtr(), parent_resource_id, title,
options, current_batch_request_));
}
void DriveUploader::StartBatchProcessing() {
DCHECK(!current_batch_request_);
current_batch_request_ =
new RefCountedBatchRequest(drive_service_->StartBatchRequest());
}
void DriveUploader::StopBatchProcessing() {
current_batch_request_ = nullptr;
}
CancelCallbackOnce DriveUploader::UploadExistingFile(
const std::string& resource_id,
const base::FilePath& local_file_path,
const std::string& content_type,
const UploadExistingFileOptions& options,
UploadCompletionCallback callback,
ProgressCallback progress_callback) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!resource_id.empty());
DCHECK(!local_file_path.empty());
DCHECK(!content_type.empty());
DCHECK(!callback.is_null());
return StartUploadFile(
std::make_unique<UploadFileInfo>(local_file_path, content_type,
std::move(callback), progress_callback,
GetWakeLockProvider()),
base::BindOnce(&DriveUploader::CallUploadServiceAPIExistingFile,
weak_ptr_factory_.GetWeakPtr(), resource_id, options,
current_batch_request_));
}
CancelCallbackOnce DriveUploader::ResumeUploadFile(
const GURL& upload_location,
const base::FilePath& local_file_path,
const std::string& content_type,
UploadCompletionCallback callback,
ProgressCallback progress_callback) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!local_file_path.empty());
DCHECK(!content_type.empty());
DCHECK(!callback.is_null());
auto upload_file_info = std::make_unique<UploadFileInfo>(
local_file_path, content_type, std::move(callback), progress_callback,
GetWakeLockProvider());
upload_file_info->upload_location = upload_location;
return StartUploadFile(std::move(upload_file_info),
base::BindOnce(&DriveUploader::StartGetUploadStatus,
weak_ptr_factory_.GetWeakPtr()));
}
CancelCallbackOnce DriveUploader::StartUploadFile(
std::unique_ptr<UploadFileInfo> upload_file_info,
StartInitiateUploadCallback start_initiate_upload_callback) {
DCHECK(thread_checker_.CalledOnValidThread());
DVLOG(1) << "Uploading file: " << upload_file_info->DebugString();
UploadFileInfo* info_ptr = upload_file_info.get();
blocking_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&base::GetFileSize, info_ptr->file_path,
&info_ptr->content_length),
base::BindOnce(&DriveUploader::StartUploadFileAfterGetFileSize,
weak_ptr_factory_.GetWeakPtr(),
std::move(upload_file_info),
std::move(start_initiate_upload_callback)));
return info_ptr->GetCancelCallback();
}
void DriveUploader::StartUploadFileAfterGetFileSize(
std::unique_ptr<UploadFileInfo> upload_file_info,
StartInitiateUploadCallback start_initiate_upload_callback,
bool get_file_size_result) {
DCHECK(thread_checker_.CalledOnValidThread());
if (!get_file_size_result) {
UploadFailed(std::move(upload_file_info), HTTP_NOT_FOUND);
return;
}
DCHECK_GE(upload_file_info->content_length, 0);
if (upload_file_info->cancelled) {
UploadFailed(std::move(upload_file_info), CANCELLED);
return;
}
std::move(start_initiate_upload_callback).Run(std::move(upload_file_info));
}
void DriveUploader::CallUploadServiceAPINewFile(
const std::string& parent_resource_id,
const std::string& title,
const UploadNewFileOptions& options,
const scoped_refptr<RefCountedBatchRequest>& batch_request,
std::unique_ptr<UploadFileInfo> upload_file_info) {
DCHECK(thread_checker_.CalledOnValidThread());
UploadFileInfo* const info_ptr = upload_file_info.get();
if (info_ptr->content_length <= kMaxMultipartUploadSize) {
DriveServiceBatchOperationsInterface* service;
// If this is a batched request, calls the API on the request instead.
if (batch_request) {
service = batch_request->configurator();
RecordDriveUploadProtocol(UPLOAD_METHOD_BATCH);
} else {
service = drive_service_;
RecordDriveUploadProtocol(UPLOAD_METHOD_MULTIPART);
}
info_ptr->cancel_callback = service->MultipartUploadNewFile(
info_ptr->content_type, info_ptr->content_length, parent_resource_id,
title, info_ptr->file_path, options,
base::BindOnce(&DriveUploader::OnMultipartUploadComplete,
weak_ptr_factory_.GetWeakPtr(),
std::move(upload_file_info)),
info_ptr->progress_callback);
} else {
RecordDriveUploadProtocol(UPLOAD_METHOD_RESUMABLE);
info_ptr->cancel_callback = drive_service_->InitiateUploadNewFile(
info_ptr->content_type, info_ptr->content_length, parent_resource_id,
title, options,
base::BindOnce(&DriveUploader::OnUploadLocationReceived,
weak_ptr_factory_.GetWeakPtr(),
std::move(upload_file_info)));
}
}
void DriveUploader::CallUploadServiceAPIExistingFile(
const std::string& resource_id,
const UploadExistingFileOptions& options,
const scoped_refptr<RefCountedBatchRequest>& batch_request,
std::unique_ptr<UploadFileInfo> upload_file_info) {
DCHECK(thread_checker_.CalledOnValidThread());
UploadFileInfo* const info_ptr = upload_file_info.get();
if (info_ptr->content_length <= kMaxMultipartUploadSize) {
DriveServiceBatchOperationsInterface* service;
// If this is a batched request, calls the API on the request instead.
if (batch_request) {
service = batch_request->configurator();
RecordDriveUploadProtocol(UPLOAD_METHOD_BATCH);
} else {
service = drive_service_;
RecordDriveUploadProtocol(UPLOAD_METHOD_MULTIPART);
}
info_ptr->cancel_callback = service->MultipartUploadExistingFile(
info_ptr->content_type, info_ptr->content_length, resource_id,
info_ptr->file_path, options,
base::BindOnce(&DriveUploader::OnMultipartUploadComplete,
weak_ptr_factory_.GetWeakPtr(),
std::move(upload_file_info)),
info_ptr->progress_callback);
} else {
RecordDriveUploadProtocol(UPLOAD_METHOD_RESUMABLE);
info_ptr->cancel_callback = drive_service_->InitiateUploadExistingFile(
info_ptr->content_type, info_ptr->content_length, resource_id, options,
base::BindOnce(&DriveUploader::OnUploadLocationReceived,
weak_ptr_factory_.GetWeakPtr(),
std::move(upload_file_info)));
}
}
void DriveUploader::OnUploadLocationReceived(
std::unique_ptr<UploadFileInfo> upload_file_info,
ApiErrorCode code,
const GURL& upload_location) {
DCHECK(thread_checker_.CalledOnValidThread());
DVLOG(1) << "Got upload location [" << upload_location.spec() << "] for ["
<< upload_file_info->file_path.value() << "]";
if (code != HTTP_SUCCESS) {
if (code == HTTP_PRECONDITION)
code = HTTP_CONFLICT; // ETag mismatch.
UploadFailed(std::move(upload_file_info), code);
return;
}
upload_file_info->upload_location = upload_location;
upload_file_info->next_start_position = 0;
UploadNextChunk(std::move(upload_file_info));
}
void DriveUploader::StartGetUploadStatus(
std::unique_ptr<UploadFileInfo> upload_file_info) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(upload_file_info);
UploadFileInfo* info_ptr = upload_file_info.get();
info_ptr->cancel_callback = drive_service_->GetUploadStatus(
info_ptr->upload_location, info_ptr->content_length,
base::BindOnce(&DriveUploader::OnUploadRangeResponseReceived,
weak_ptr_factory_.GetWeakPtr(),
std::move(upload_file_info)));
}
void DriveUploader::UploadNextChunk(
std::unique_ptr<UploadFileInfo> upload_file_info) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(upload_file_info);
DCHECK_GE(upload_file_info->next_start_position, 0);
DCHECK_LE(upload_file_info->next_start_position,
upload_file_info->content_length);
if (upload_file_info->cancelled) {
UploadFailed(std::move(upload_file_info), CANCELLED);
return;
}
// Limit the size of data uploaded per each request by kUploadChunkSize.
const int64_t end_position =
std::min(upload_file_info->content_length,
upload_file_info->next_start_position + kUploadChunkSize);
UploadFileInfo* info_ptr = upload_file_info.get();
info_ptr->cancel_callback = drive_service_->ResumeUpload(
info_ptr->upload_location, info_ptr->next_start_position, end_position,
info_ptr->content_length, info_ptr->content_type, info_ptr->file_path,
base::BindOnce(&DriveUploader::OnUploadRangeResponseReceived,
weak_ptr_factory_.GetWeakPtr(),
std::move(upload_file_info)),
base::BindRepeating(
&DriveUploader::OnUploadProgress, weak_ptr_factory_.GetWeakPtr(),
info_ptr->progress_callback, info_ptr->next_start_position,
info_ptr->content_length));
}
void DriveUploader::OnUploadRangeResponseReceived(
std::unique_ptr<UploadFileInfo> upload_file_info,
const UploadRangeResponse& response,
std::unique_ptr<FileResource> entry) {
DCHECK(thread_checker_.CalledOnValidThread());
if (response.code == HTTP_CREATED || response.code == HTTP_SUCCESS) {
// When uploading a new file, we expect HTTP_CREATED, and when uploading
// an existing file (to overwrite), we expect HTTP_SUCCESS.
// There is an exception: if we uploading an empty file, uploading a new
// file also returns HTTP_SUCCESS on Drive API v2. The correct way of the
// fix should be uploading the metadata only. However, to keep the
// compatibility with GData WAPI during the migration period, we just
// relax the condition here.
// TODO(hidehiko): Upload metadata only for empty files, after GData WAPI
// code is gone.
DVLOG(1) << "Successfully created uploaded file=["
<< upload_file_info->file_path.value() << "]";
// Done uploading.
std::move(upload_file_info->completion_callback)
.Run(HTTP_SUCCESS, GURL(), std::move(entry));
return;
}
// ETag mismatch.
if (response.code == HTTP_PRECONDITION) {
UploadFailed(std::move(upload_file_info), HTTP_CONFLICT);
return;
}
// If code is 308 (RESUME_INCOMPLETE) and |range_received| starts with 0
// (meaning that the data is uploaded from the beginning of the file),
// proceed to upload the next chunk.
if (response.code != HTTP_RESUME_INCOMPLETE ||
response.start_position_received != 0) {
DVLOG(1) << "UploadNextChunk http code=" << response.code
<< ", start_position_received=" << response.start_position_received
<< ", end_position_received=" << response.end_position_received;
UploadFailed(std::move(upload_file_info), response.code == HTTP_FORBIDDEN
? DRIVE_NO_SPACE
: response.code);
return;
}
DVLOG(1) << "Received range " << response.start_position_received << "-"
<< response.end_position_received << " for ["
<< upload_file_info->file_path.value() << "]";
upload_file_info->next_start_position = response.end_position_received;
UploadNextChunk(std::move(upload_file_info));
}
void DriveUploader::OnUploadProgress(ProgressCallback callback,
int64_t start_position,
int64_t total_size,
int64_t progress_of_chunk,
int64_t total_of_chunk) {
if (!callback.is_null())
callback.Run(start_position + progress_of_chunk, total_size);
}
void DriveUploader::UploadFailed(
std::unique_ptr<UploadFileInfo> upload_file_info,
ApiErrorCode error) {
DCHECK(thread_checker_.CalledOnValidThread());
DVLOG(1) << "Upload failed " << upload_file_info->DebugString();
if (upload_file_info->next_start_position < 0) {
// Discard the upload location because no request could succeed with it.
// Maybe it's obsolete.
upload_file_info->upload_location = GURL();
}
std::move(upload_file_info->completion_callback)
.Run(error, upload_file_info->upload_location,
std::unique_ptr<FileResource>());
}
void DriveUploader::OnMultipartUploadComplete(
std::unique_ptr<UploadFileInfo> upload_file_info,
google_apis::ApiErrorCode error,
std::unique_ptr<FileResource> entry) {
DCHECK(thread_checker_.CalledOnValidThread());
if (error == HTTP_CREATED || error == HTTP_SUCCESS) {
DVLOG(1) << "Successfully created uploaded file=["
<< upload_file_info->file_path.value() << "]";
// Done uploading.
std::move(upload_file_info->completion_callback)
.Run(HTTP_SUCCESS, upload_file_info->upload_location, std::move(entry));
} else {
DVLOG(1) << "Upload failed " << upload_file_info->DebugString();
if (error == HTTP_PRECONDITION)
error = HTTP_CONFLICT; // ETag mismatch.
std::move(upload_file_info->completion_callback)
.Run(error, upload_file_info->upload_location,
std::unique_ptr<FileResource>());
}
}
device::mojom::WakeLockProvider* DriveUploader::GetWakeLockProvider() {
return wake_lock_provider_ ? wake_lock_provider_.get() : nullptr;
}
} // namespace drive