// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/drive/model/drive_upload_task.h"
#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/thread_pool.h"
#import "ios/chrome/browser/download/model/download_mimetype_util.h"
#import "ios/chrome/browser/drive/model/drive_file_uploader.h"
#import "ios/chrome/browser/drive/model/drive_metrics.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "net/base/apple/url_conversions.h"
#import "net/base/url_util.h"
#import "url/gurl.h"
namespace {
constexpr int64_t kBytesPerMegabyte = 1024 * 1024;
constexpr char kHistogramSuffixTaskNotStarted[] = ".NotStarted";
constexpr char kHistogramSuffixTaskInProgress[] = ".InProgress";
constexpr char kHistogramSuffixTaskCancelled[] = ".Cancelled";
constexpr char kHistogramSuffixTaskComplete[] = ".Complete";
constexpr char kHistogramSuffixTaskFailed[] = ".Failed";
// Returns the appropriate histogram suffix for upload task state `state`.
const char* HistogramSuffixForUploadTaskState(UploadTask::State state) {
switch (state) {
case UploadTask::State::kNotStarted:
return kHistogramSuffixTaskNotStarted;
case UploadTask::State::kInProgress:
return kHistogramSuffixTaskInProgress;
case UploadTask::State::kCancelled:
return kHistogramSuffixTaskCancelled;
case UploadTask::State::kComplete:
return kHistogramSuffixTaskComplete;
case UploadTask::State::kFailed:
return kHistogramSuffixTaskFailed;
}
}
// Converts `state` to `UploadTaskStateHistogram`.
UploadTaskStateHistogram UploadTaskStateToHistogram(UploadTask::State state) {
switch (state) {
case UploadTask::State::kNotStarted:
return UploadTaskStateHistogram::kNotStarted;
case UploadTask::State::kInProgress:
return UploadTaskStateHistogram::kInProgress;
case UploadTask::State::kCancelled:
return UploadTaskStateHistogram::kCancelled;
case UploadTask::State::kComplete:
return UploadTaskStateHistogram::kComplete;
case UploadTask::State::kFailed:
return UploadTaskStateHistogram::kFailed;
}
}
// Records whether `result.error == nil` for boolean histogram `histogram`.
// Returns `result`.
DriveFolderResult RecordDriveFolderResultSuccessful(
const char* histogram,
const DriveFolderResult& result) {
base::UmaHistogramBoolean(histogram, result.error == nil);
return result;
}
// If `result.error`, records `result.error.code` for histogram `histogram`.
// Returns `result`.
DriveFolderResult RecordDriveFolderResultErrorCode(
const char* histogram,
const DriveFolderResult& result) {
if (result.error) {
base::UmaHistogramSparse(histogram, result.error.code);
}
return result;
}
// Name of query parameter for the user ID to open a Drive response link.
constexpr const char kHashedUserIdQueryParameterName[] = "huid";
} // namespace
DriveUploadTask::DriveUploadTask(std::unique_ptr<DriveFileUploader> uploader)
: uploader_{std::move(uploader)} {}
DriveUploadTask::~DriveUploadTask() {
if (GetState() == State::kInProgress) {
Cancel();
}
// Record histograms for the task at destruction.
base::UmaHistogramEnumeration(kDriveUploadTaskFinalState,
UploadTaskStateToHistogram(GetState()));
if (!file_path_) {
return;
}
// Only record file details histograms if a file was given to upload.
const char* histogram_suffix = HistogramSuffixForUploadTaskState(GetState());
base::UmaHistogramCounts100(
std::string(kDriveUploadTaskNumberOfAttempts) + histogram_suffix,
number_of_attempts_);
const DownloadMimeTypeResult mime_type_result =
GetDownloadMimeTypeResultFromMimeType(file_mime_type_);
base::UmaHistogramEnumeration(
std::string(kDriveUploadTaskMimeType) + histogram_suffix,
mime_type_result);
if (file_total_bytes_ != -1) {
base::UmaHistogramMemoryMB(
std::string(kDriveUploadTaskFileSize) + histogram_suffix,
file_total_bytes_ / kBytesPerMegabyte);
}
}
#pragma mark - Public
id<SystemIdentity> DriveUploadTask::GetIdentity() const {
return uploader_->GetIdentity();
}
void DriveUploadTask::SetFileToUpload(const base::FilePath& path,
const base::FilePath& suggested_name,
const std::string& mime_type,
const int64_t total_bytes) {
file_path_ = path;
suggested_file_name_ = suggested_name;
file_mime_type_ = mime_type;
file_total_bytes_ = total_bytes;
}
void DriveUploadTask::SetDestinationFolderName(const std::string& folder_name) {
folder_name_ = folder_name;
}
#pragma mark - UploadTask
UploadTask::State DriveUploadTask::GetState() const {
return state_;
}
void DriveUploadTask::Start() {
if (state_ == State::kInProgress || state_ == State::kCancelled ||
state_ == State::kComplete) {
// If upload is in progress, cancelled or completed, do nothing.
return;
}
upload_progress_.reset();
upload_result_.reset();
SetState(State::kInProgress);
SearchFolderThenCreateFolderOrDirectlyUploadFile();
}
void DriveUploadTask::Cancel() {
if (uploader_->IsExecutingQuery()) {
uploader_->CancelCurrentQuery();
}
upload_progress_.reset();
upload_result_.reset();
SetState(State::kCancelled);
}
float DriveUploadTask::GetProgress() const {
if (!upload_progress_ ||
upload_progress_->total_bytes_expected_to_upload == 0) {
return 0;
}
return static_cast<float>(upload_progress_->total_bytes_uploaded) /
upload_progress_->total_bytes_expected_to_upload;
}
std::optional<GURL> DriveUploadTask::GetResponseLink(
bool add_user_identifier) const {
if (!upload_result_ || !upload_result_->file_link) {
return std::nullopt;
}
GURL result(base::SysNSStringToUTF8(upload_result_->file_link));
if (add_user_identifier) {
NSString* user_identifier = GetIdentity().hashedGaiaID;
result = net::AppendOrReplaceQueryParameter(
result, kHashedUserIdQueryParameterName,
base::SysNSStringToUTF8(user_identifier));
}
return result;
}
NSError* DriveUploadTask::GetError() const {
if (!upload_result_) {
return nil;
}
return upload_result_->error;
}
#pragma mark - Private
void DriveUploadTask::SearchFolderThenCreateFolderOrDirectlyUploadFile() {
number_of_attempts_++;
// Search a destination Drive folder using
// `SearchSaveToDriveFolder(folder_name, ...)`;
uploader_->SearchSaveToDriveFolder(
base::SysUTF8ToNSString(folder_name_),
base::BindOnce(&DriveUploadTask::CreateFolderOrDirectlyUploadFile,
weak_ptr_factory_.GetWeakPtr()));
}
void DriveUploadTask::CreateFolderOrDirectlyUploadFile(
const DriveFolderResult& folder_search_result) {
// Record folder search success histogram.
base::UmaHistogramBoolean(kDriveSearchFolderResultSuccessful,
!folder_search_result.error);
// If folder search failed, update state and result with the error object.
if (folder_search_result.error) {
base::UmaHistogramSparse(kDriveSearchFolderResultErrorCode,
folder_search_result.error.code);
upload_result_ =
DriveFileUploadResult({.error = folder_search_result.error});
SetState(State::kFailed);
return;
}
// If the first step returned an existing folder, upload file directly.
if (folder_search_result.folder_identifier) {
UploadFile(folder_search_result);
return;
}
// Otherwise, create a destination Drive folder using
// `CreateSaveToDriveFolder(folder_name, ...)`;
auto record_result_successful_callback = base::BindOnce(
RecordDriveFolderResultSuccessful, kDriveCreateFolderResultSuccessful);
auto record_result_error_code_callback = base::BindOnce(
RecordDriveFolderResultErrorCode, kDriveCreateFolderResultErrorCode);
auto upload_file_callback = base::BindOnce(&DriveUploadTask::UploadFile,
weak_ptr_factory_.GetWeakPtr());
uploader_->CreateSaveToDriveFolder(
base::SysUTF8ToNSString(folder_name_),
std::move(record_result_successful_callback)
.Then(std::move(record_result_error_code_callback))
.Then(std::move(upload_file_callback)));
}
void DriveUploadTask::UploadFile(const DriveFolderResult& folder_result) {
CHECK(file_path_);
const auto file_path = *file_path_;
// If `folder_result` contains an error, then a destination folder did not
// exist and could not be created, update state and result with the error
// object.
if (folder_result.error) {
upload_result_ = DriveFileUploadResult({.error = folder_result.error});
SetState(State::kFailed);
return;
}
// If a destination folder was created/found then upload the file at
// `file_url` using `UploadFile(file_url, ...)`.
uploader_->UploadFile(
base::apple::FilePathToNSURL(file_path),
base::apple::FilePathToNSString(suggested_file_name_),
base::SysUTF8ToNSString(file_mime_type_), folder_result.folder_identifier,
base::BindRepeating(&DriveUploadTask::OnDriveFileUploadProgress,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&DriveUploadTask::OnDriveFileUploadResult,
weak_ptr_factory_.GetWeakPtr()));
}
void DriveUploadTask::OnDriveFileUploadProgress(
const DriveFileUploadProgress& progress) {
upload_progress_ = progress;
OnUploadUpdated();
}
void DriveUploadTask::OnDriveFileUploadResult(
const DriveFileUploadResult& result) {
// Record file upload result histograms.
base::UmaHistogramBoolean(kDriveFileUploadResultSuccessful,
result.error == nil);
if (result.error) {
base::UmaHistogramSparse(kDriveFileUploadResultErrorCode,
result.error.code);
}
// Store result and update state.
upload_result_ = result;
SetState(result.error == nil ? State::kComplete : State::kFailed);
}
void DriveUploadTask::SetState(State state) {
state_ = state;
OnUploadUpdated();
}