// Copyright 2017 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/web/download/download_task_impl.h"
#import <WebKit/WebKit.h>
#import <limits>
#import "base/apple/foundation_util.h"
#import "base/files/file.h"
#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/bind_post_task.h"
#import "base/task/sequenced_task_runner.h"
#import "ios/web/download/download_result.h"
#import "ios/web/public/download/download_task_observer.h"
#import "ios/web/public/web_state.h"
#import "net/base/filename_util.h"
#import "net/base/net_errors.h"
namespace web {
namespace download {
namespace internal {
// Helper struct that store the error code and the opened file object (in case
// of success).
struct CreateFileResult {
int net_error_code = net::OK;
base::FilePath file_path;
explicit CreateFileResult(int error_code) : net_error_code(error_code) {
DCHECK_NE(net_error_code, net::OK);
}
explicit CreateFileResult(base::FilePath path) : file_path(std::move(path)) {
DCHECK(!file_path.empty());
}
CreateFileResult(CreateFileResult&& other) = default;
CreateFileResult& operator=(CreateFileResult&& other) = default;
~CreateFileResult() = default;
};
namespace {
CreateFileResult CreateFileForDownload(base::FilePath path) {
if (path.empty()) {
if (!base::CreateTemporaryFile(&path)) {
return CreateFileResult(
net::MapSystemError(logging::GetLastSystemErrorCode()));
}
DCHECK(!path.empty());
} else {
base::File::Error error;
if (!base::CreateDirectoryAndGetError(path.DirName(), &error)) {
return CreateFileResult(net::FileErrorToNetError(error));
}
}
// If `path` exists and is a directory, fail with an error as we don't
// want the download task to delete an existing directory.
if (base::DirectoryExists(path)) {
return CreateFileResult(net::ERR_ACCESS_DENIED);
}
// Try to delete any existing file at `path` (deleting a non-existent
// file is not an error for `base::DeleteFile(...)`). This is needed
// as some sub-classes of DownloadTaskImpl fail if the destination
// file already exists.
if (!base::DeleteFile(path)) {
return CreateFileResult(
net::MapSystemError(logging::GetLastSystemErrorCode()));
}
return CreateFileResult(std::move(path));
}
NSData* ReadDataFromFile(base::FilePath path, int64_t bytes) {
// base::ReadFile uses int for the count value, so it will fail if we
// try to read more than INT_MAX bytes. Given that this is already 2GB
// and we can't allocate that much memory, there is no point trying to
// read the data in 2GB chunks, instead just fail.
if (bytes < 0 || std::numeric_limits<int>::max() < bytes) {
return nil;
}
NSMutableData* data = [NSMutableData dataWithLength:bytes];
std::optional<uint64_t> bytes_read =
base::ReadFile(path, base::apple::NSMutableDataToSpan(data));
if (!bytes_read || *bytes_read != static_cast<uint64_t>(bytes)) {
return nil;
}
return [data copy];
}
} // anonymous namespace
} // namespace internal
} // namespace download
DownloadTaskImpl::DownloadTaskImpl(
WebState* web_state,
const GURL& original_url,
NSString* http_method,
const std::string& content_disposition,
int64_t total_bytes,
const std::string& mime_type,
NSString* identifier,
const scoped_refptr<base::SequencedTaskRunner>& task_runner)
: original_url_(original_url),
http_method_(http_method),
total_bytes_(total_bytes),
content_disposition_(content_disposition),
original_mime_type_(mime_type),
mime_type_(mime_type),
identifier_([identifier copy]),
web_state_(web_state),
task_runner_(task_runner) {
DCHECK(web_state_);
DCHECK(task_runner_);
base::RepeatingClosure closure = base::BindPostTaskToCurrentDefault(
base::BindRepeating(&DownloadTaskImpl::OnAppWillResignActive,
weak_factory_.GetWeakPtr()));
base::WeakPtr<DownloadTaskImpl> weak_Task = weak_factory_.GetWeakPtr();
observer_ = [NSNotificationCenter.defaultCenter
addObserverForName:UIApplicationWillResignActiveNotification
object:nil
queue:nil
usingBlock:^(NSNotification* _Nonnull) {
closure.Run();
}];
}
DownloadTaskImpl::~DownloadTaskImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
[NSNotificationCenter.defaultCenter removeObserver:observer_];
for (auto& observer : observers_)
observer.OnDownloadDestroyed(this);
// Delete the downloaded file if it was a temporary file or if the download
// failed (it is not an error to delete a non-existent file).
if (owns_file_ || state_ != State::kComplete) {
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(base::IgnoreResult(&base::DeleteFile), path_));
}
}
WebState* DownloadTaskImpl::GetWebState() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return web_state_;
}
DownloadTask::State DownloadTaskImpl::GetState() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return state_;
}
void DownloadTaskImpl::Start(const base::FilePath& path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_NE(state_, State::kInProgress);
state_ = State::kInProgress;
percent_complete_ = 0;
received_bytes_ = 0;
owns_file_ = path.empty();
using download::internal::CreateFileForDownload;
task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&CreateFileForDownload, path),
base::BindOnce(&DownloadTaskImpl::OnDownloadFileCreated,
weak_factory_.GetWeakPtr()));
}
void DownloadTaskImpl::Cancel() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
state_ = State::kCancelled;
CancelInternal();
OnDownloadUpdated();
}
NSString* DownloadTaskImpl::GetIdentifier() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return identifier_;
}
const GURL& DownloadTaskImpl::GetOriginalUrl() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return original_url_;
}
NSString* DownloadTaskImpl::GetHttpMethod() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return http_method_;
}
bool DownloadTaskImpl::IsDone() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
switch (state_) {
case State::kNotStarted:
case State::kInProgress:
return false;
case State::kCancelled:
case State::kComplete:
case State::kFailed:
case State::kFailedNotResumable:
return true;
}
}
int DownloadTaskImpl::GetErrorCode() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return download_result_.error_code();
}
int DownloadTaskImpl::GetHttpCode() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return http_code_;
}
int64_t DownloadTaskImpl::GetTotalBytes() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return total_bytes_;
}
int64_t DownloadTaskImpl::GetReceivedBytes() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return received_bytes_;
}
int DownloadTaskImpl::GetPercentComplete() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return percent_complete_;
}
std::string DownloadTaskImpl::GetContentDisposition() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return content_disposition_;
}
std::string DownloadTaskImpl::GetOriginalMimeType() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return original_mime_type_;
}
std::string DownloadTaskImpl::GetMimeType() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return mime_type_;
}
base::FilePath DownloadTaskImpl::GenerateFileName() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return net::GenerateFileName(original_url_, content_disposition_,
/*referrer_charset=*/std::string(),
/*suggested_name=*/GetSuggestedName(),
/*mime_type=*/std::string(),
/*default_name=*/"document");
}
bool DownloadTaskImpl::HasPerformedBackgroundDownload() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return has_performed_background_download_;
}
void DownloadTaskImpl::AddObserver(DownloadTaskObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observers_.AddObserver(observer);
}
void DownloadTaskImpl::RemoveObserver(DownloadTaskObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observers_.RemoveObserver(observer);
}
void DownloadTaskImpl::GetResponseData(
ResponseDataReadCallback callback) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(IsDone());
using download::internal::ReadDataFromFile;
task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&ReadDataFromFile, path_, received_bytes_),
std::move(callback));
}
const base::FilePath& DownloadTaskImpl::GetResponsePath() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(IsDone());
static const base::FilePath kEmptyPath;
return owns_file_ ? kEmptyPath : path_;
}
std::string DownloadTaskImpl::GetSuggestedName() const {
return std::string();
}
void DownloadTaskImpl::OnAppWillResignActive() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (GetState() == DownloadTask::State::kInProgress) {
has_performed_background_download_ = YES;
}
}
void DownloadTaskImpl::OnDownloadFileCreated(
download::internal::CreateFileResult result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (result.net_error_code == net::OK) {
path_ = std::move(result.file_path);
StartInternal(path_);
return;
}
OnDownloadFinished(DownloadResult(result.net_error_code));
}
void DownloadTaskImpl::OnDownloadFinished(DownloadResult download_result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
download_result_ = download_result;
if (download_result_.error_code()) {
state_ = download_result_.can_retry() ? State::kFailed
: State::kFailedNotResumable;
} else {
state_ = State::kComplete;
}
OnDownloadUpdated();
}
void DownloadTaskImpl::OnDownloadUpdated() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (auto& observer : observers_)
observer.OnDownloadUpdated(this);
}
} // namespace web