[go: nahoru, domu]

blob: 2d02d6dd54cec5733e26abfae7de784f17dcc26f [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "storage/browser/blob/write_blob_to_file.h"
#include <stdint.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <utility>
#include <vector>
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/numerics/checked_math.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/file_access/scoped_file_access.h"
#include "storage/browser/blob/blob_data_handle.h"
#include "storage/browser/blob/blob_data_item.h"
#include "storage/browser/blob/blob_data_snapshot.h"
#include "storage/browser/blob/shareable_blob_data_item.h"
#include "storage/browser/file_system/file_stream_reader.h"
#include "storage/browser/file_system/file_stream_writer.h"
#include "storage/browser/file_system/file_writer_delegate.h"
#include "storage/common/file_system/file_system_mount_option.h"
#include "third_party/blink/public/common/blob/blob_utils.h"
namespace storage {
namespace {
using DelegateNoProgressWriteCallback = base::OnceCallback<void(
base::File::Error result,
int64_t bytes,
FileWriterDelegate::WriteProgressStatus write_status)>;
using CopyCallback = base::OnceCallback<mojom::WriteBlobToFileResult(
file_access::ScopedFileAccess)>;
struct WriteState {
std::unique_ptr<FileWriterDelegate> delegate;
DelegateNoProgressWriteCallback callback;
base::CheckedNumeric<int64_t> total_bytes;
};
// Utility function to ignore all progress events returned when running
// FileWriterDelegate::Start. This means that there is either a single call for
// a success or failure. This wrapper also handles owning the lifetime of the
// FileWriterDelegate, which it destructs after receiving a success or error.
FileWriterDelegate::DelegateWriteCallback IgnoreProgressWrapper(
std::unique_ptr<FileWriterDelegate> delegate,
DelegateNoProgressWriteCallback callback) {
return base::BindRepeating(
[](WriteState* write_state, base::File::Error result, int64_t bytes,
FileWriterDelegate::WriteProgressStatus write_status) {
DCHECK_GE(bytes, 0);
DCHECK(!write_state->callback.is_null());
DCHECK(write_state->delegate);
if (result == base::File::FILE_OK) {
DCHECK(write_status == FileWriterDelegate::SUCCESS_COMPLETED ||
write_status == FileWriterDelegate::SUCCESS_IO_PENDING);
} else {
DCHECK(write_status == FileWriterDelegate::ERROR_WRITE_STARTED ||
write_status == FileWriterDelegate::ERROR_WRITE_NOT_STARTED);
}
write_state->total_bytes += bytes;
if (write_status == FileWriterDelegate::SUCCESS_IO_PENDING)
return;
std::move(write_state->callback)
.Run(result, write_state->total_bytes.ValueOrDie(), write_status);
// This is necessary because FileWriterDelegate owns this repeating
// callback, so there is a cyclic dependency.
write_state->delegate.reset();
},
base::Owned(new WriteState{std::move(delegate), std::move(callback), 0}));
}
// Copied from file_util_posix.cc, with the addition of |offset| and |max_size|
// parameters. The parameters apply to the |infile|. The |bytes_copied|
// parameter keeps track of the number of bytes written to |outfile|.
// Note - this function can still succeed if the size |infile| is less than
// |max_size|. The caller should use |bytes_copied| to know exactly how many
// bytes were copied.
bool CopyFileContentsWithOffsetAndSize(base::File* infile,
base::File* outfile,
int64_t* bytes_copied,
int64_t offset,
int64_t max_size) {
static constexpr size_t kBufferSize = 32768;
DCHECK_GE(max_size, 0);
*bytes_copied = 0;
base::CheckedNumeric<int64_t> checked_max_size = max_size;
std::vector<char> buffer(
std::min(kBufferSize, base::checked_cast<size_t>(max_size)));
infile->Seek(base::File::FROM_BEGIN, offset);
for (;;) {
size_t bytes_to_read =
std::min(buffer.size(),
base::checked_cast<size_t>(checked_max_size.ValueOrDie()));
int bytes_read = infile->ReadAtCurrentPos(buffer.data(), bytes_to_read);
if (bytes_read < 0)
return false;
if (bytes_read == 0)
return true;
checked_max_size -= bytes_read;
if (!checked_max_size.IsValid())
return false;
// Allow for partial writes
int bytes_written_per_read = 0;
do {
int bytes_written_partial = outfile->WriteAtCurrentPos(
&buffer[bytes_written_per_read], bytes_read - bytes_written_per_read);
if (bytes_written_partial < 0)
return false;
bytes_written_per_read += bytes_written_partial;
*bytes_copied += bytes_written_partial;
} while (bytes_written_per_read < bytes_read);
if (checked_max_size.ValueOrDie() == 0)
return true;
}
NOTREACHED();
return false;
}
// Copies the contents of |copy_from| to |copy_to|, with the given |offset| and
// optional |size| applied to the |copy_from| file. The
// |expected_last_modified_copy_from| must match, within a second, the last
// modified time of the |copy_from| file. Afterwards, the |last_modified| date
// is optionally saved as the last modified & last accessed time of |copy_to|.
// If |flush_on_close| is true, then Flush is called on the |copy_to| file
// before it is closed. The `file_access::ScopedFileAccess` parameter is
// expected to allow access to the source file. It has to be kept in scope of
// this function because it must be alive while the copy operation is happening.
mojom::WriteBlobToFileResult CopyFileAndMaybeWriteTimeModified(
const base::FilePath& copy_from,
base::Time expected_last_modified_copy_from,
const base::FilePath& copy_to,
int64_t offset,
absl::optional<int64_t> size,
absl::optional<base::Time> last_modified,
bool flush_on_close,
file_access::ScopedFileAccess) {
// Do a full file copy if the sizes match and there is no offset.
if (offset == 0) {
base::File::Info info;
base::GetFileInfo(copy_from, &info);
if (!FileStreamReader::VerifySnapshotTime(expected_last_modified_copy_from,
info)) {
return mojom::WriteBlobToFileResult::kInvalidBlob;
}
if (!size || info.size == size.value()) {
bool success = base::CopyFile(copy_from, copy_to);
if (!success)
return mojom::WriteBlobToFileResult::kIOError;
if (last_modified && !base::TouchFile(copy_to, last_modified.value(),
last_modified.value())) {
return mojom::WriteBlobToFileResult::kTimestampError;
}
return mojom::WriteBlobToFileResult::kSuccess;
}
}
// Do a manual file-to-file copy. This will overwrite the file if there
// already is one.
base::File infile =
base::File(copy_from, base::File::FLAG_OPEN | base::File::FLAG_READ);
base::File outfile(copy_to,
base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS);
if (!outfile.IsValid())
return mojom::WriteBlobToFileResult::kIOError;
base::File::Info info;
infile.GetInfo(&info);
if (!FileStreamReader::VerifySnapshotTime(expected_last_modified_copy_from,
info)) {
return mojom::WriteBlobToFileResult::kInvalidBlob;
}
int64_t bytes_copied = 0;
if (!CopyFileContentsWithOffsetAndSize(
&infile, &outfile, &bytes_copied, offset,
size.value_or(std::numeric_limits<int64_t>::max()))) {
return mojom::WriteBlobToFileResult::kIOError;
}
if (size && bytes_copied != size.value())
return mojom::WriteBlobToFileResult::kInvalidBlob;
if (last_modified &&
!outfile.SetTimes(last_modified.value(), last_modified.value())) {
// If the file modification time isn't set correctly, then reading will
// fail.
return mojom::WriteBlobToFileResult::kTimestampError;
}
if (flush_on_close)
outfile.Flush();
outfile.Close();
return mojom::WriteBlobToFileResult::kSuccess;
}
mojom::WriteBlobToFileResult CreateEmptyFileAndMaybeSetModifiedTime(
base::FilePath file_path,
absl::optional<base::Time> last_modified,
bool flush_on_write) {
base::File file(file_path,
base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
bool file_success = file.created();
if (!file_success) {
return mojom::WriteBlobToFileResult::kIOError;
}
if (flush_on_write)
file.Flush();
file.Close();
if (last_modified && !base::TouchFile(file_path, last_modified.value(),
last_modified.value())) {
// If the file modification time isn't set correctly, then reading
// the blob later will fail. Thus, failing to save it is an error.
file.Close();
return mojom::WriteBlobToFileResult::kTimestampError;
}
return mojom::WriteBlobToFileResult::kSuccess;
}
void HandleModifiedTimeOnBlobFileWriteComplete(
base::FilePath file_path,
absl::optional<base::Time> last_modified,
bool flush_on_write,
mojom::BlobStorageContext::WriteBlobToFileCallback callback,
base::File::Error rv,
int64_t bytes_written,
FileWriterDelegate::WriteProgressStatus write_status) {
bool success = write_status == FileWriterDelegate::SUCCESS_COMPLETED;
if (!success) {
std::move(callback).Run(mojom::WriteBlobToFileResult::kIOError);
return;
}
if (success && !bytes_written) {
// Special Case 1: Success but no bytes were written, so just create
// an empty file (LocalFileStreamWriter only creates a file
// if data is actually written).
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(CreateEmptyFileAndMaybeSetModifiedTime,
std::move(file_path), last_modified, flush_on_write),
std::move(callback));
return;
} else if (success && last_modified) {
// Special Case 2: Success and |last_modified| needs to be set. Set
// that before reporting write completion.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(
[](int64_t bytes_written, base::FilePath file_path,
absl::optional<base::Time> last_modified) {
if (!base::TouchFile(file_path, last_modified.value(),
last_modified.value())) {
// If the file modification time isn't set correctly, then
// reading the blob later will fail. Thus, failing to save it is
// an error.
return mojom::WriteBlobToFileResult::kTimestampError;
}
return mojom::WriteBlobToFileResult::kSuccess;
},
bytes_written, std::move(file_path), last_modified),
std::move(callback));
return;
}
std::move(callback).Run(mojom::WriteBlobToFileResult::kSuccess);
}
void PostCopyTaskToFileThreadIfAllowed(
CopyCallback copy_cb,
mojom::BlobStorageContext::WriteBlobToFileCallback callback,
file_access::ScopedFileAccess scoped_file_access) {
if (!scoped_file_access.is_allowed()) {
std::move(callback).Run(mojom::WriteBlobToFileResult::kIOError);
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(std::move(copy_cb), std::move(scoped_file_access)),
std::move(callback));
}
void WriteConstructedBlobToFile(
std::unique_ptr<BlobDataHandle> blob_handle,
const base::FilePath& file_path,
bool flush_on_write,
absl::optional<base::Time> last_modified,
mojom::BlobStorageContext::WriteBlobToFileCallback callback,
BlobStatus status) {
DCHECK(!last_modified || !last_modified.value().is_null());
if (status != BlobStatus::DONE) {
DCHECK(BlobStatusIsError(status));
std::move(callback).Run(mojom::WriteBlobToFileResult::kInvalidBlob);
return;
}
// Check if we can do a copy optimization.
// TODO(dmurph): Optimize the case of IDB blobs, which have a type
// kReadableDataHandle.
std::unique_ptr<BlobDataSnapshot> snapshot = blob_handle->CreateSnapshot();
const auto& items = snapshot->items();
if (items.size() == 1) {
const BlobDataItem& item = *items[0];
if (item.type() == BlobDataItem::Type::kFile) {
// The File API cannot handle uint64_t.
absl::optional<int64_t> optional_size = item.length();
if (item.length() == blink::BlobUtils::kUnknownSize) {
// The blob system uses a special value (max uint64_t) to denote an
// unknown file size. This means the whole file should be copied.
optional_size = absl::nullopt;
} else if (item.length() > std::numeric_limits<int64_t>::max()) {
std::move(callback).Run(mojom::WriteBlobToFileResult::kError);
return;
}
if (item.offset() > std::numeric_limits<int64_t>::max()) {
std::move(callback).Run(mojom::WriteBlobToFileResult::kError);
return;
}
base::OnceCallback<void(file_access::ScopedFileAccess)> post_copy_task =
base::BindOnce(
PostCopyTaskToFileThreadIfAllowed,
base::BindOnce(CopyFileAndMaybeWriteTimeModified, item.path(),
item.expected_modification_time(), file_path,
item.offset(), std::move(optional_size),
std::move(last_modified), flush_on_write),
std::move(callback));
if (item.file_access()) {
item.file_access().Run({item.path()}, std::move(post_copy_task));
} else {
std::move(post_copy_task).Run(file_access::ScopedFileAccess::Allowed());
}
return;
}
}
// If not, copy the BlobReader and FileStreamWriter.
std::unique_ptr<FileStreamWriter> writer =
FileStreamWriter::CreateForLocalFile(
base::ThreadPool::CreateTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE})
.get(),
file_path, /*initial_offset=*/0,
FileStreamWriter::CREATE_NEW_FILE_ALWAYS);
FlushPolicy policy = flush_on_write || last_modified
? FlushPolicy::FLUSH_ON_COMPLETION
: FlushPolicy::NO_FLUSH_ON_COMPLETION;
auto delegate =
std::make_unique<FileWriterDelegate>(std::move(writer), policy);
auto* raw_delegate = delegate.get();
raw_delegate->Start(
blob_handle->CreateReader(),
IgnoreProgressWrapper(
std::move(delegate),
base::BindOnce(HandleModifiedTimeOnBlobFileWriteComplete, file_path,
std::move(last_modified), flush_on_write,
std::move(callback))));
}
} // namespace
void WriteBlobToFile(
std::unique_ptr<BlobDataHandle> blob_handle,
const base::FilePath& file_path,
bool flush_on_write,
absl::optional<base::Time> last_modified,
mojom::BlobStorageContext::WriteBlobToFileCallback callback) {
auto* blob_handle_ptr = blob_handle.get();
blob_handle_ptr->RunOnConstructionComplete(base::BindOnce(
&WriteConstructedBlobToFile, std::move(blob_handle), file_path,
flush_on_write, last_modified, std::move(callback)));
}
} // namespace storage