blob: 8c1b613f77a485ee8e267ec916f6e76d067b534e [file] [log] [blame]
// Copyright 2018 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 "remoting/host/file_transfer/local_file_operations.h"
#include <cstdint>
#include "base/bind.h"
#include "base/files/file_proxy.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/path_service.h"
#include "base/sequence_checker.h"
#include "base/task/post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_runner_util.h"
#include "base/task/thread_pool.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "build/build_config.h"
#include "remoting/base/result.h"
#include "remoting/host/file_transfer/ensure_user.h"
#include "remoting/host/file_transfer/file_chooser.h"
#include "remoting/host/file_transfer/get_desktop_directory.h"
#include "remoting/protocol/file_transfer_helpers.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace remoting {
namespace {
constexpr char kTempFileExtension[] = ".part";
remoting::protocol::FileTransfer_Error_Type FileErrorToResponseErrorType(
base::File::Error file_error) {
switch (file_error) {
case base::File::FILE_ERROR_ACCESS_DENIED:
return remoting::protocol::FileTransfer_Error_Type_PERMISSION_DENIED;
case base::File::FILE_ERROR_NO_SPACE:
return remoting::protocol::FileTransfer_Error_Type_OUT_OF_DISK_SPACE;
default:
return remoting::protocol::FileTransfer_Error_Type_IO_ERROR;
}
}
scoped_refptr<base::SequencedTaskRunner> CreateFileTaskRunner() {
#if defined(OS_WIN)
// On Windows, we use user impersonation to write files as the currently
// logged-in user, while the process as a whole runs as SYSTEM. Since user
// impersonation is per-thread on Windows, we need a dedicated thread to
// ensure that no other code is accidentally run with the wrong privileges.
return base::ThreadPool::CreateSingleThreadTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::SingleThreadTaskRunnerThreadMode::DEDICATED);
#else
return base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
#endif
}
class LocalFileReader : public FileOperations::Reader {
public:
explicit LocalFileReader(
scoped_refptr<base::SequencedTaskRunner> ui_task_runner);
LocalFileReader(const LocalFileReader&) = delete;
LocalFileReader& operator=(const LocalFileReader&) = delete;
~LocalFileReader() override;
// FileOperations::Reader implementation.
void Open(OpenCallback callback) override;
void ReadChunk(std::size_t size, ReadCallback callback) override;
const base::FilePath& filename() const override;
std::uint64_t size() const override;
FileOperations::State state() const override;
private:
void OnEnsureUserResult(OpenCallback callback,
protocol::FileTransferResult<Monostate> result);
void OnFileChooserResult(OpenCallback callback, FileChooser::Result result);
void OnOpenResult(OpenCallback callback, base::File::Error error);
void OnGetInfoResult(OpenCallback callback,
base::File::Error error,
const base::File::Info& info);
void OnReadResult(ReadCallback callback,
base::File::Error error,
const char* data,
int bytes_read);
void SetState(FileOperations::State state);
base::FilePath filename_;
std::uint64_t size_ = 0;
std::uint64_t offset_ = 0;
FileOperations::State state_ = FileOperations::kCreated;
scoped_refptr<base::SequencedTaskRunner> ui_task_runner_;
scoped_refptr<base::SequencedTaskRunner> file_task_runner_;
std::unique_ptr<FileChooser> file_chooser_;
absl::optional<base::FileProxy> file_proxy_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<LocalFileReader> weak_ptr_factory_{this};
};
class LocalFileWriter : public FileOperations::Writer {
public:
LocalFileWriter();
LocalFileWriter(const LocalFileWriter&) = delete;
LocalFileWriter& operator=(const LocalFileWriter&) = delete;
~LocalFileWriter() override;
// FileOperations::Writer implementation.
void Open(const base::FilePath& filename, Callback callback) override;
void WriteChunk(std::vector<std::uint8_t> data, Callback callback) override;
void Close(Callback callback) override;
FileOperations::State state() const override;
private:
void Cancel();
// Callbacks for Open().
void OnGetTargetDirectoryResult(
base::FilePath filename,
Callback callback,
protocol::FileTransferResult<base::FilePath> target_directory_result);
void CreateTempFile(Callback callback, base::FilePath temp_filepath);
void OnCreateResult(Callback callback, base::File::Error error);
void OnWriteResult(std::vector<std::uint8_t> data,
Callback callback,
base::File::Error error,
int bytes_written);
// Callbacks for Close().
void OnCloseResult(Callback callback, base::File::Error error);
void MoveToDestination(Callback callback,
base::FilePath destination_filepath);
void OnMoveResult(Callback callback, bool success);
void SetState(FileOperations::State state);
FileOperations::State state_ = FileOperations::kCreated;
base::FilePath destination_filepath_;
base::FilePath temp_filepath_;
std::uint64_t bytes_written_ = 0;
scoped_refptr<base::SequencedTaskRunner> file_task_runner_;
absl::optional<base::FileProxy> file_proxy_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<LocalFileWriter> weak_ptr_factory_{this};
};
LocalFileReader::LocalFileReader(
scoped_refptr<base::SequencedTaskRunner> ui_task_runner)
: ui_task_runner_(std::move(ui_task_runner)) {}
LocalFileReader::~LocalFileReader() = default;
void LocalFileReader::Open(OpenCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(FileOperations::kCreated, state_);
SetState(FileOperations::kBusy);
file_task_runner_ = CreateFileTaskRunner();
file_proxy_.emplace(file_task_runner_.get());
base::PostTaskAndReplyWithResult(
file_task_runner_.get(), FROM_HERE, base::BindOnce(&EnsureUserContext),
base::BindOnce(&LocalFileReader::OnEnsureUserResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void LocalFileReader::ReadChunk(std::size_t size, ReadCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(FileOperations::kReady, state_);
SetState(FileOperations::kBusy);
file_proxy_->Read(
offset_, size,
base::BindOnce(&LocalFileReader::OnReadResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
const base::FilePath& LocalFileReader::filename() const {
return filename_;
}
uint64_t LocalFileReader::size() const {
return size_;
}
FileOperations::State LocalFileReader::state() const {
return state_;
}
void LocalFileReader::OnEnsureUserResult(
FileOperations::Reader::OpenCallback callback,
protocol::FileTransferResult<Monostate> result) {
if (!result) {
SetState(FileOperations::kFailed);
std::move(callback).Run(std::move(result.error()));
return;
}
file_chooser_ = FileChooser::Create(
ui_task_runner_,
base::BindOnce(&LocalFileReader::OnFileChooserResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
file_chooser_->Show();
}
void LocalFileReader::OnFileChooserResult(OpenCallback callback,
FileChooser::Result result) {
file_chooser_.reset();
if (!result) {
SetState(FileOperations::kFailed);
std::move(callback).Run(std::move(result.error()));
return;
}
filename_ = result->BaseName();
file_proxy_->CreateOrOpen(
*result, base::File::FLAG_OPEN | base::File::FLAG_READ,
base::BindOnce(&LocalFileReader::OnOpenResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void LocalFileReader::OnOpenResult(OpenCallback callback,
base::File::Error error) {
if (error != base::File::FILE_OK) {
SetState(FileOperations::kFailed);
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error));
return;
}
file_proxy_->GetInfo(base::BindOnce(&LocalFileReader::OnGetInfoResult,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
}
void LocalFileReader::OnGetInfoResult(OpenCallback callback,
base::File::Error error,
const base::File::Info& info) {
if (error != base::File::FILE_OK) {
SetState(FileOperations::kFailed);
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error));
return;
}
size_ = info.size;
SetState(FileOperations::kReady);
std::move(callback).Run(kSuccessTag);
}
void LocalFileReader::OnReadResult(ReadCallback callback,
base::File::Error error,
const char* data,
int bytes_read) {
if (error != base::File::FILE_OK) {
SetState(FileOperations::kFailed);
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error));
return;
}
offset_ += bytes_read;
SetState(bytes_read > 0 ? FileOperations::kReady : FileOperations::kComplete);
// The read buffer is provided and owned by FileProxy, so there's no way to
// avoid a copy, here.
std::move(callback).Run(std::vector<std::uint8_t>(data, data + bytes_read));
}
void LocalFileReader::SetState(FileOperations::State state) {
switch (state) {
case FileOperations::kCreated:
NOTREACHED(); // Can never return to initial state.
break;
case FileOperations::kReady:
DCHECK_EQ(FileOperations::kBusy, state_);
break;
case FileOperations::kBusy:
DCHECK(state_ == FileOperations::kCreated ||
state_ == FileOperations::kReady);
break;
case FileOperations::kComplete:
DCHECK_EQ(FileOperations::kBusy, state_);
break;
case FileOperations::kFailed:
// Any state can change to kFailed.
break;
}
state_ = state;
}
LocalFileWriter::LocalFileWriter() {}
LocalFileWriter::~LocalFileWriter() {
Cancel();
}
void LocalFileWriter::Open(const base::FilePath& filename, Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(FileOperations::kCreated, state_);
SetState(FileOperations::kBusy);
file_task_runner_ = CreateFileTaskRunner();
file_proxy_.emplace(file_task_runner_.get());
base::PostTaskAndReplyWithResult(
file_task_runner_.get(), FROM_HERE, base::BindOnce([] {
return EnsureUserContext().AndThen(
[](Monostate) { return GetDesktopDirectory(); });
}),
base::BindOnce(&LocalFileWriter::OnGetTargetDirectoryResult,
weak_ptr_factory_.GetWeakPtr(), filename,
std::move(callback)));
}
void LocalFileWriter::WriteChunk(std::vector<std::uint8_t> data,
Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(FileOperations::kReady, state_);
SetState(FileOperations::kBusy);
// TODO(rkjnsn): Under what circumstances can posting the task fail? Is it
// worth checking for? If so, what should we do in that case,
// given that callback is moved into the task and not returned
// on error?
// Ensure buffer pointer is obtained before data is moved.
const std::uint8_t* buffer = data.data();
const std::size_t size = data.size();
file_proxy_->Write(bytes_written_, reinterpret_cast<const char*>(buffer),
size,
base::BindOnce(&LocalFileWriter::OnWriteResult,
weak_ptr_factory_.GetWeakPtr(),
std::move(data), std::move(callback)));
}
void LocalFileWriter::Close(Callback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(FileOperations::kReady, state_);
SetState(FileOperations::kBusy);
file_proxy_->Close(base::BindOnce(&LocalFileWriter::OnCloseResult,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
}
void LocalFileWriter::Cancel() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (state_ == FileOperations::kCreated ||
state_ == FileOperations::kComplete ||
state_ == FileOperations::kFailed) {
return;
}
// Ensure we don't receive further callbacks.
weak_ptr_factory_.InvalidateWeakPtrs();
// Drop FileProxy, which will close the underlying file on the file
// sequence after any possible pending operation is complete.
file_proxy_.reset();
// And finally, queue deletion of the temp file.
if (!temp_filepath_.empty()) {
file_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(base::GetDeleteFileCallback(), temp_filepath_));
}
SetState(FileOperations::kFailed);
}
FileOperations::State LocalFileWriter::state() const {
return state_;
}
void LocalFileWriter::OnGetTargetDirectoryResult(
base::FilePath filename,
Callback callback,
protocol::FileTransferResult<base::FilePath> target_directory) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!target_directory) {
LOG(ERROR) << "Failed to get target directory.";
SetState(FileOperations::kFailed);
std::move(callback).Run(std::move(target_directory.error()));
return;
}
destination_filepath_ =
target_directory.success().Append(filename.BaseName());
// Don't store in temp_filepath_ until we have the final path to make sure
// Cancel can never delete the wrong file.
base::FilePath temp_filepath =
destination_filepath_.AddExtensionASCII(kTempFileExtension);
PostTaskAndReplyWithResult(
file_task_runner_.get(), FROM_HERE,
base::BindOnce(&base::GetUniquePath, temp_filepath),
base::BindOnce(&LocalFileWriter::CreateTempFile,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void LocalFileWriter::CreateTempFile(Callback callback,
base::FilePath temp_filepath) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (temp_filepath.empty()) {
LOG(ERROR) << "Failed to get unique path number.";
SetState(FileOperations::kFailed);
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, protocol::FileTransfer_Error_Type_IO_ERROR));
return;
}
temp_filepath_ = std::move(temp_filepath);
// FLAG_WIN_SHARE_DELETE allows the file to be marked as deleted on Windows
// while the handle is still open. (Other OS's allow this by default.) This
// allows Cancel to clean up the temporary file even if there are writes
// pending.
file_proxy_->CreateOrOpen(
temp_filepath_,
base::File::FLAG_CREATE | base::File::FLAG_WRITE |
base::File::FLAG_WIN_SHARE_DELETE,
base::BindOnce(&LocalFileWriter::OnCreateResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void LocalFileWriter::OnCreateResult(Callback callback,
base::File::Error error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error != base::File::FILE_OK) {
LOG(ERROR) << "Creating temp file failed with error: " << error;
SetState(FileOperations::kFailed);
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error));
return;
}
SetState(FileOperations::kReady);
// Now that the temp file has been created successfully, we could lock it
// using base::File::Lock(), but this would not prevent the file from being
// deleted. When the file is deleted, WriteChunk() will continue to write to
// the file as if the file was still there, and an error will occur when
// calling base::Move() to move the temp file. Chrome exhibits the same
// behavior with its downloads.
std::move(callback).Run(kSuccessTag);
}
void LocalFileWriter::OnWriteResult(std::vector<std::uint8_t> data,
Callback callback,
base::File::Error error,
int bytes_written) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error != base::File::FILE_OK) {
LOG(ERROR) << "Write failed with error: " << error;
Cancel();
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error));
return;
}
SetState(FileOperations::kReady);
bytes_written_ += bytes_written;
// bytes_written should never be negative if error is FILE_OK.
if (static_cast<std::size_t>(bytes_written) != data.size()) {
// Write already makes a "best effort" to write all of the data, so this
// probably means that an error occurred. Unfortunately, the only way to
// find out what went wrong is to try again.
// TODO(rkjnsn): Would it be better just to return a generic error, here?
WriteChunk(
std::vector<std::uint8_t>(data.begin() + bytes_written, data.end()),
std::move(callback));
return;
}
std::move(callback).Run(kSuccessTag);
}
void LocalFileWriter::OnCloseResult(Callback callback,
base::File::Error error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error != base::File::FILE_OK) {
LOG(ERROR) << "Close failed with error: " << error;
Cancel();
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, FileErrorToResponseErrorType(error), error));
return;
}
base::PostTaskAndReplyWithResult(
file_task_runner_.get(), FROM_HERE,
base::BindOnce(&base::GetUniquePath, destination_filepath_),
base::BindOnce(&LocalFileWriter::MoveToDestination,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void LocalFileWriter::MoveToDestination(Callback callback,
base::FilePath destination_filepath) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (destination_filepath.empty()) {
LOG(ERROR) << "Failed to get unique path number.";
SetState(FileOperations::kFailed);
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, protocol::FileTransfer_Error_Type_IO_ERROR));
return;
}
destination_filepath_ = std::move(destination_filepath);
PostTaskAndReplyWithResult(
file_task_runner_.get(), FROM_HERE,
base::BindOnce(&base::Move, temp_filepath_, destination_filepath_),
base::BindOnce(&LocalFileWriter::OnMoveResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void LocalFileWriter::OnMoveResult(Callback callback, bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (success) {
SetState(FileOperations::kComplete);
std::move(callback).Run(kSuccessTag);
} else {
LOG(ERROR) << "Failed to move file to final destination.";
Cancel();
std::move(callback).Run(protocol::MakeFileTransferError(
FROM_HERE, protocol::FileTransfer_Error_Type_IO_ERROR));
}
}
void LocalFileWriter::SetState(FileOperations::State state) {
switch (state) {
case FileOperations::kCreated:
NOTREACHED(); // Can never return to initial state.
break;
case FileOperations::kReady:
DCHECK(state_ == FileOperations::kBusy);
break;
case FileOperations::kBusy:
DCHECK(state_ == FileOperations::kCreated ||
state_ == FileOperations::kReady);
break;
case FileOperations::kComplete:
DCHECK(state_ == FileOperations::kBusy);
break;
case FileOperations::kFailed:
// Any state can change to kFailed.
break;
}
state_ = state;
}
} // namespace
LocalFileOperations::LocalFileOperations(
scoped_refptr<base::SequencedTaskRunner> ui_task_runner)
: ui_task_runner_(std::move(ui_task_runner)) {}
LocalFileOperations::~LocalFileOperations() = default;
std::unique_ptr<FileOperations::Reader> LocalFileOperations::CreateReader() {
return std::make_unique<LocalFileReader>(ui_task_runner_);
}
std::unique_ptr<FileOperations::Writer> LocalFileOperations::CreateWriter() {
return std::make_unique<LocalFileWriter>();
}
} // namespace remoting