Commit 8c74d38f authored by Olivier Yiptong's avatar Olivier Yiptong Committed by Commit Bot

[Native File System] Writable File Stream API

This change adds a writable stream output to the `FileSystemFileHandle`.

This follows some
[discussion](https://github.com/WICG/native-file-system/issues/19#issuecomment-490579093) on github.

While this writable stream does not yet feature stream-like behavior, it
carries over some of the interface from `NativeFileSystemWriter`, namely
`truncate` and `write`.

Bug: 955189
Change-Id: I8b8c9ff1ef36adfb30501251563699c6c7973889
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1623534Reviewed-by: default avatarMarijn Kruisselbrink <mek@chromium.org>
Reviewed-by: default avatarJeremy Roman <jbroman@chromium.org>
Commit-Queue: Jeremy Roman <jbroman@chromium.org>
Auto-Submit: Olivier Yiptong <oyiptong@chromium.org>
Cr-Commit-Position: refs/heads/master@{#662620}
parent 65d04a1f
......@@ -215,6 +215,7 @@ modules_idl_files =
"mediastream/media_stream_track_event.idl",
"mediastream/overconstrained_error.idl",
"native_file_system/native_file_system_directory_iterator.idl",
"native_file_system/native_file_system_writable_file_stream.idl",
"native_file_system/file_system_directory_handle.idl",
"native_file_system/file_system_file_handle.idl",
"native_file_system/file_system_handle.idl",
......
......@@ -14,6 +14,8 @@ blink_modules_sources("native_file_system") {
"native_file_system_file_handle.h",
"native_file_system_handle.cc",
"native_file_system_handle.h",
"native_file_system_writable_file_stream.cc",
"native_file_system_writable_file_stream.h",
"native_file_system_writer.cc",
"native_file_system_writer.h",
"window_native_file_system.cc",
......
......@@ -8,5 +8,6 @@
ImplementedAs=NativeFileSystemFileHandle
] interface FileSystemFileHandle : FileSystemHandle {
[CallWith=ScriptState] Promise<FileSystemWriter> createWriter();
[CallWith=ScriptState] Promise<FileSystemWritableFileStream> createWritable();
[CallWith=ScriptState] Promise<File> getFile();
};
......@@ -10,6 +10,7 @@
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/fileapi/file.h"
#include "third_party/blink/renderer/core/fileapi/file_error.h"
#include "third_party/blink/renderer/modules/native_file_system/native_file_system_writable_file_stream.h"
#include "third_party/blink/renderer/modules/native_file_system/native_file_system_writer.h"
#include "third_party/blink/renderer/platform/file_metadata.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
......@@ -33,6 +34,16 @@ ScriptPromise NativeFileSystemFileHandle::createWriter(
return result;
}
ScriptPromise NativeFileSystemFileHandle::createWritable(
ScriptState* script_state) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise result = resolver->Promise();
resolver->Resolve(
MakeGarbageCollected<NativeFileSystemWritableFileStream>(this));
return result;
}
ScriptPromise NativeFileSystemFileHandle::getFile(ScriptState* script_state) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise result = resolver->Promise();
......
......@@ -20,6 +20,7 @@ class NativeFileSystemFileHandle final : public NativeFileSystemHandle {
bool isFile() const override { return true; }
ScriptPromise createWriter(ScriptState*);
ScriptPromise createWritable(ScriptState*);
ScriptPromise getFile(ScriptState*);
mojom::blink::NativeFileSystemTransferTokenPtr Transfer() override;
......
// Copyright 2019 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 "third_party/blink/renderer/modules/native_file_system/native_file_system_writable_file_stream.h"
#include "third_party/blink/renderer/bindings/core/v8/array_buffer_or_array_buffer_view_or_blob_or_usv_string.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_blob.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/fileapi/blob.h"
#include "third_party/blink/renderer/core/fileapi/file_error.h"
#include "third_party/blink/renderer/modules/native_file_system/native_file_system_file_handle.h"
#include "third_party/blink/renderer/platform/blob/blob_data.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
NativeFileSystemWritableFileStream::NativeFileSystemWritableFileStream(
NativeFileSystemFileHandle* file)
: file_(file) {
DCHECK(file_);
}
ScriptPromise NativeFileSystemWritableFileStream::write(
ScriptState* script_state,
uint64_t position,
const ArrayBufferOrArrayBufferViewOrBlobOrUSVString& data,
ExceptionState& exception_state) {
DCHECK(!data.IsNull());
auto blob_data = std::make_unique<BlobData>();
Blob* blob = nullptr;
if (data.IsArrayBuffer()) {
DOMArrayBuffer* array_buffer = data.GetAsArrayBuffer();
blob_data->AppendBytes(array_buffer->Data(), array_buffer->ByteLength());
} else if (data.IsArrayBufferView()) {
DOMArrayBufferView* array_buffer_view = data.GetAsArrayBufferView().View();
blob_data->AppendBytes(array_buffer_view->BaseAddress(),
array_buffer_view->byteLength());
} else if (data.IsBlob()) {
blob = data.GetAsBlob();
} else if (data.IsUSVString()) {
// Let the developer be explicit about line endings.
blob_data->AppendText(data.GetAsUSVString(),
/*normalize_line_endings_to_native=*/false);
}
if (!blob) {
uint64_t size = blob_data->length();
blob = Blob::Create(BlobDataHandle::Create(std::move(blob_data), size));
}
return WriteBlob(script_state, position, blob);
}
ScriptPromise NativeFileSystemWritableFileStream::truncate(
ScriptState* script_state,
uint64_t size) {
if (!file_ || pending_operation_) {
return ScriptPromise::RejectWithDOMException(
script_state, MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError));
}
pending_operation_ =
MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise result = pending_operation_->Promise();
file_->MojoHandle()->Truncate(
size, WTF::Bind(&NativeFileSystemWritableFileStream::TruncateComplete,
WrapPersistent(this)));
return result;
}
void NativeFileSystemWritableFileStream::WriteComplete(
mojom::blink::NativeFileSystemErrorPtr result,
uint64_t bytes_written) {
DCHECK(pending_operation_);
if (result->error_code == base::File::FILE_OK) {
pending_operation_->Resolve();
} else {
pending_operation_->Reject(
file_error::CreateDOMException(result->error_code));
}
pending_operation_ = nullptr;
}
void NativeFileSystemWritableFileStream::TruncateComplete(
mojom::blink::NativeFileSystemErrorPtr result) {
DCHECK(pending_operation_);
if (result->error_code == base::File::FILE_OK) {
pending_operation_->Resolve();
} else {
pending_operation_->Reject(
file_error::CreateDOMException(result->error_code));
}
pending_operation_ = nullptr;
}
ScriptPromise NativeFileSystemWritableFileStream::WriteBlob(
ScriptState* script_state,
uint64_t position,
Blob* blob) {
if (!file_ || pending_operation_) {
return ScriptPromise::RejectWithDOMException(
script_state, MakeGarbageCollected<DOMException>(
DOMExceptionCode::kInvalidStateError));
}
pending_operation_ =
MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise result = pending_operation_->Promise();
file_->MojoHandle()->Write(
position, blob->AsMojoBlob(),
WTF::Bind(&NativeFileSystemWritableFileStream::WriteComplete,
WrapPersistent(this)));
return result;
}
ScriptPromise NativeFileSystemWritableFileStream::abort(
ScriptState* script_state,
ExceptionState& exception_state) {
return ScriptPromise::RejectWithDOMException(
script_state,
MakeGarbageCollected<DOMException>(DOMExceptionCode::kNotSupportedError));
}
ScriptPromise NativeFileSystemWritableFileStream::abort(
ScriptState* script_state,
ScriptValue reason,
ExceptionState& exception_state) {
return ScriptPromise::RejectWithDOMException(
script_state,
MakeGarbageCollected<DOMException>(DOMExceptionCode::kNotSupportedError));
}
ScriptValue NativeFileSystemWritableFileStream::getWriter(
ScriptState* script_state,
ExceptionState& exception_state) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Not Implemented");
return ScriptValue();
}
bool NativeFileSystemWritableFileStream::locked(
ScriptState* script_state,
ExceptionState& exception_state) const {
auto result = IsLocked(script_state, exception_state);
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Not Implemented");
return !result || *result;
}
base::Optional<bool> NativeFileSystemWritableFileStream::IsLocked(
ScriptState* script_state,
ExceptionState& exception_state) const {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Not Implemented");
return base::nullopt;
}
void NativeFileSystemWritableFileStream::Serialize(
ScriptState* script_state,
MessagePort* port,
ExceptionState& exception_state) {
DCHECK(port);
DCHECK(RuntimeEnabledFeatures::TransferableStreamsEnabled());
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Not Implemented");
}
void NativeFileSystemWritableFileStream::Trace(Visitor* visitor) {
ScriptWrappable::Trace(visitor);
visitor->Trace(file_);
visitor->Trace(pending_operation_);
}
} // namespace blink
// Copyright 2019 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_NATIVE_FILE_SYSTEM_NATIVE_FILE_SYSTEM_WRITABLE_FILE_STREAM_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_NATIVE_FILE_SYSTEM_NATIVE_FILE_SYSTEM_WRITABLE_FILE_STREAM_H_
#include "third_party/blink/public/mojom/native_file_system/native_file_system_error.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/array_buffer_or_array_buffer_view_or_blob_or_usv_string.h"
#include "third_party/blink/renderer/core/streams/writable_stream.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
namespace blink {
class Blob;
class ExceptionState;
class ScriptPromise;
class ScriptPromiseResolver;
class ScriptState;
class NativeFileSystemFileHandle;
class NativeFileSystemWritableFileStream final : public WritableStream {
DEFINE_WRAPPERTYPEINFO();
public:
explicit NativeFileSystemWritableFileStream(NativeFileSystemFileHandle*);
void Trace(Visitor* visitor) override;
// IDL defined functions for WritableStream.
bool locked(ScriptState*, ExceptionState&) const override;
ScriptPromise abort(ScriptState*, ExceptionState&) override;
ScriptPromise abort(ScriptState*,
ScriptValue reason,
ExceptionState&) override;
ScriptValue getWriter(ScriptState*, ExceptionState&) override;
void Serialize(ScriptState*, MessagePort* port, ExceptionState&) override;
base::Optional<bool> IsLocked(ScriptState*, ExceptionState&) const override;
// IDL defined functions specific to NativeFileSystemWritableFileStream.
ScriptPromise write(ScriptState*,
uint64_t position,
const ArrayBufferOrArrayBufferViewOrBlobOrUSVString& data,
ExceptionState&);
ScriptPromise truncate(ScriptState*, uint64_t size);
private:
ScriptPromise WriteBlob(ScriptState*, uint64_t position, Blob*);
void WriteComplete(mojom::blink::NativeFileSystemErrorPtr result,
uint64_t bytes_written);
void TruncateComplete(mojom::blink::NativeFileSystemErrorPtr result);
Member<NativeFileSystemFileHandle> file_;
Member<ScriptPromiseResolver> pending_operation_;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_NATIVE_FILE_SYSTEM_NATIVE_FILE_SYSTEM_WRITABLE_FILE_STREAM_H_
// Copyright 2019 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.
// https://wicg.github.io/native-file-system/#filesystemwritablefilestream
[
RuntimeEnabled=NativeFileSystem,
ImplementedAs=NativeFileSystemWritableFileStream
] interface NativeFileSystemWritableFileStream : WritableStream {
[CallWith=ScriptState, RaisesException] Promise<void> write(unsigned long long position, (BufferSource or Blob or USVString) data);
[CallWith=ScriptState] Promise<void> truncate(unsigned long long size);
};
// META: script=resources/test-helpers.js
promise_test(async t => cleanupSandboxedFileSystem(),
'Cleanup to setup test environment');
promise_test(async t => {
const handle = await createEmptyFile(t, 'empty_blob');
const stream = await handle.createWritable();
await stream.write(0, new Blob([]));
assert_equals(await getFileContents(handle), '');
assert_equals(await getFileSize(handle), 0);
}, 'write() with an empty blob to an empty file');
promise_test(async t => {
const handle = await createEmptyFile(t, 'valid_blob');
const stream = await handle.createWritable();
await stream.write(0, new Blob(['1234567890']));
assert_equals(await getFileContents(handle), '1234567890');
assert_equals(await getFileSize(handle), 10);
}, 'write() a blob to an empty file');
promise_test(async t => {
const handle = await createEmptyFile(t, 'blob_with_offset');
const stream = await handle.createWritable();
await stream.write(0, new Blob(['1234567890']));
await stream.write(4, new Blob(['abc']));
assert_equals(await getFileContents(handle), '1234abc890');
assert_equals(await getFileSize(handle), 10);
}, 'write() called with a blob and a valid offset');
promise_test(async t => {
const handle = await createEmptyFile(t, 'bad_offset');
const stream = await handle.createWritable();
await promise_rejects(t, 'InvalidStateError', stream.write(4, new Blob(['abc'])));
assert_equals(await getFileContents(handle), '');
assert_equals(await getFileSize(handle), 0);
}, 'write() called with an invalid offset');
promise_test(async t => {
const handle = await createEmptyFile(t, 'empty_string');
const stream = await handle.createWritable();
await stream.write(0, '');
assert_equals(await getFileContents(handle), '');
assert_equals(await getFileSize(handle), 0);
}, 'write() with an empty string to an empty file');
promise_test(async t => {
const handle = await createEmptyFile(t, 'valid_utf8_string');
const stream = await handle.createWritable();
await stream.write(0, 'foo🤘');
assert_equals(await getFileContents(handle), 'foo🤘');
assert_equals(await getFileSize(handle), 7);
}, 'write() with a valid utf-8 string');
promise_test(async t => {
const handle = await createEmptyFile(t, 'string_with_unix_line_ending');
const stream = await handle.createWritable();
await stream.write(0, 'foo\n');
assert_equals(await getFileContents(handle), 'foo\n');
assert_equals(await getFileSize(handle), 4);
}, 'write() with a string with unix line ending preserved');
promise_test(async t => {
const handle = await createEmptyFile(t, 'string_with_windows_line_ending');
const stream = await handle.createWritable();
await stream.write(0, 'foo\r\n');
assert_equals(await getFileContents(handle), 'foo\r\n');
assert_equals(await getFileSize(handle), 5);
}, 'write() with a string with windows line ending preserved');
promise_test(async t => {
const handle = await createEmptyFile(t, 'empty_array_buffer');
const stream = await handle.createWritable();
let buf = new ArrayBuffer(0);
await stream.write(0, buf);
assert_equals(await getFileContents(handle), '');
assert_equals(await getFileSize(handle), 0);
}, 'write() with an empty array buffer to an empty file');
promise_test(async t => {
const handle = await createEmptyFile(t, 'valid_string_typed_byte_array');
const stream = await handle.createWritable();
let buf = new ArrayBuffer(3);
let intView = new Uint8Array(buf);
intView[0] = 0x66;
intView[1] = 0x6f;
intView[2] = 0x6f;
await stream.write(0, buf);
assert_equals(await getFileContents(handle), 'foo');
assert_equals(await getFileSize(handle), 3);
}, 'write() with a valid typed array buffer');
promise_test(async t => {
const handle = await createEmptyFile(t, 'trunc_shrink');
const stream = await handle.createWritable();
await stream.write(0, new Blob(['1234567890']));
await stream.truncate(5);
assert_equals(await getFileContents(handle), '12345');
assert_equals(await getFileSize(handle), 5);
}, 'truncate() to shrink a file');
promise_test(async t => {
const handle = await createEmptyFile(t, 'trunc_grow');
const stream = await handle.createWritable();
await stream.write(0, new Blob(['abc']));
await stream.truncate(5);
assert_equals(await getFileContents(handle), 'abc\0\0');
assert_equals(await getFileSize(handle), 5);
}, 'truncate() to grow a file');
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment