This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 591d034306 feat(nodejs): add `WriteOptions` for write methods (#4785)
591d034306 is described below
commit 591d034306808e7c942b52297e2c83e51729f4a2
Author: Lemon <[email protected]>
AuthorDate: Mon Jun 24 21:09:10 2024 +0800
feat(nodejs): add `WriteOptions` for write methods (#4785)
* feat(nodejs): add `OpWriteOptions` properties for write methods
close: #4782
* chore
* chore
* refactor: remove trait and rename
---
bindings/nodejs/.prettierignore | 3 +
bindings/nodejs/generated.d.ts | 70 +++++++++-
bindings/nodejs/src/lib.rs | 196 ++++++++++++++++++++++++---
bindings/nodejs/tests/suites/async.suite.mjs | 30 ++++
bindings/nodejs/tests/suites/sync.suite.mjs | 31 +++++
5 files changed, 306 insertions(+), 24 deletions(-)
diff --git a/bindings/nodejs/.prettierignore b/bindings/nodejs/.prettierignore
index 4020394b8d..5bac3c572e 100644
--- a/bindings/nodejs/.prettierignore
+++ b/bindings/nodejs/.prettierignore
@@ -3,3 +3,6 @@ generated.js
generated.d.ts
.yarn
pnpm-lock.yaml
+.devbox
+devbox.json
+devbox.lock
diff --git a/bindings/nodejs/generated.d.ts b/bindings/nodejs/generated.d.ts
index 9048102520..6c795cf52b 100644
--- a/bindings/nodejs/generated.d.ts
+++ b/bindings/nodejs/generated.d.ts
@@ -32,6 +32,64 @@ export interface ListOptions {
limit?: number
recursive?: boolean
}
+export interface WriteOptions {
+ /**
+ * Append bytes into path.
+ *
+ * ### Notes
+ *
+ * - It always appends content to the end of the file.
+ * - It will create file if the path not exists.
+ */
+ append?: boolean
+ /**
+ * Set the chunk of op.
+ *
+ * If chunk is set, the data will be chunked by the underlying writer.
+ *
+ * ## NOTE
+ *
+ * Service could have their own minimum chunk size while perform write
+ * operations like multipart uploads. So the chunk size may be larger than
+ * the given buffer size.
+ */
+ chunk?: bigint
+ /** Set the
[Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)
of op. */
+ contentType?: string
+ /** Set the
[Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
of op. */
+ contentDisposition?: string
+ /** Set the
[Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
of op. */
+ cacheControl?: string
+}
+export interface WriterOptions {
+ /**
+ * Append bytes into path.
+ *
+ * ### Notes
+ *
+ * - It always appends content to the end of the file.
+ * - It will create file if the path not exists.
+ */
+ append?: boolean
+ /**
+ * Set the chunk of op.
+ *
+ * If chunk is set, the data will be chunked by the underlying writer.
+ *
+ * ## NOTE
+ *
+ * Service could have their own minimum chunk size while perform write
+ * operations like multipart uploads. So the chunk size may be larger than
+ * the given buffer size.
+ */
+ chunk?: bigint
+ /** Set the
[Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)
of op. */
+ contentType?: string
+ /** Set the
[Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
of op. */
+ contentDisposition?: string
+ /** Set the
[Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
of op. */
+ cacheControl?: string
+}
/** PresignedRequest is a presigned request return by `presign`. */
export interface PresignedRequest {
/** HTTP method of this request. */
@@ -265,21 +323,23 @@ export class Operator {
* await op.write("path/to/file", Buffer.from("hello world"));
* // or
* await op.write("path/to/file", "hello world");
+ * // or
+ * await op.write("path/to/file", Buffer.from("hello world"), { contentType:
"text/plain" });
* ```
*/
- write(path: string, content: Buffer | string): Promise<void>
+ write(path: string, content: Buffer | string, options?: WriteOptions |
undefined | null): Promise<void>
/**
* Write multiple bytes into path.
*
* It could be used to write large file in a streaming way.
*/
- writer(path: string): Promise<Writer>
+ writer(path: string, options?: WriterOptions | undefined | null):
Promise<Writer>
/**
* Write multiple bytes into path synchronously.
*
* It could be used to write large file in a streaming way.
*/
- writerSync(path: string): BlockingWriter
+ writerSync(path: string, options?: WriterOptions | undefined | null):
BlockingWriter
/**
* Write bytes into path synchronously.
*
@@ -288,9 +348,11 @@ export class Operator {
* op.writeSync("path/to/file", Buffer.from("hello world"));
* // or
* op.writeSync("path/to/file", "hello world");
+ * // or
+ * op.writeSync("path/to/file", Buffer.from("hello world"), { contentType:
"text/plain" });
* ```
*/
- writeSync(path: string, content: Buffer | string): void
+ writeSync(path: string, content: Buffer | string, options?: WriteOptions |
undefined | null): void
/**
* Append bytes into path.
*
diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs
index d50807e784..773b8fe77d 100644
--- a/bindings/nodejs/src/lib.rs
+++ b/bindings/nodejs/src/lib.rs
@@ -18,8 +18,6 @@
#[macro_use]
extern crate napi_derive;
-mod capability;
-
use std::collections::HashMap;
use std::fmt::Display;
use std::io::Read;
@@ -30,6 +28,8 @@ use futures::AsyncReadExt;
use futures::TryStreamExt;
use napi::bindgen_prelude::*;
+mod capability;
+
#[napi]
pub struct Operator(opendal::Operator);
@@ -226,6 +226,7 @@ impl Operator {
})
}
+ //noinspection DuplicatedCode
/// Write bytes into path.
///
/// ### Example
@@ -233,22 +234,66 @@ impl Operator {
/// await op.write("path/to/file", Buffer.from("hello world"));
/// // or
/// await op.write("path/to/file", "hello world");
+ /// // or
+ /// await op.write("path/to/file", Buffer.from("hello world"), {
contentType: "text/plain" });
/// ```
#[napi]
- pub async fn write(&self, path: String, content: Either<Buffer, String>)
-> Result<()> {
+ pub async fn write(
+ &self,
+ path: String,
+ content: Either<Buffer, String>,
+ options: Option<WriteOptions>,
+ ) -> Result<()> {
let c = match content {
Either::A(buf) => buf.as_ref().to_owned(),
Either::B(s) => s.into_bytes(),
};
- self.0.write(&path, c).await.map_err(format_napi_error)
+ let mut writer = self.0.write_with(&path, c);
+ if let Some(options) = options {
+ if let Some(append) = options.append {
+ writer = writer.append(append);
+ }
+ if let Some(chunk) = options.chunk {
+ writer = writer.chunk(chunk.get_u64().1 as usize);
+ }
+ if let Some(ref content_type) = options.content_type {
+ writer = writer.content_type(content_type);
+ }
+ if let Some(ref content_disposition) = options.content_disposition
{
+ writer = writer.content_disposition(content_disposition);
+ }
+ if let Some(ref cache_control) = options.cache_control {
+ writer = writer.cache_control(cache_control);
+ }
+ }
+ writer.await.map_err(format_napi_error)
}
+ //noinspection DuplicatedCode
/// Write multiple bytes into path.
///
/// It could be used to write large file in a streaming way.
#[napi]
- pub async fn writer(&self, path: String) -> Result<Writer> {
- let w = self.0.writer(&path).await.map_err(format_napi_error)?;
+ pub async fn writer(&self, path: String, options: Option<WriterOptions>)
-> Result<Writer> {
+ let mut writer = self.0.writer_with(&path);
+ if let Some(options) = options {
+ if let Some(append) = options.append {
+ writer = writer.append(append);
+ }
+ if let Some(chunk) = options.chunk {
+ writer = writer.chunk(chunk.get_u64().1 as usize);
+ }
+ if let Some(ref content_type) = options.content_type {
+ writer = writer.content_type(content_type);
+ }
+ if let Some(ref content_disposition) = options.content_disposition
{
+ writer = writer.content_disposition(content_disposition);
+ }
+ if let Some(ref cache_control) = options.cache_control {
+ writer = writer.cache_control(cache_control);
+ }
+ }
+ let w = writer.await.map_err(format_napi_error)?;
Ok(Writer(w))
}
@@ -256,11 +301,34 @@ impl Operator {
///
/// It could be used to write large file in a streaming way.
#[napi]
- pub fn writer_sync(&self, path: String) -> Result<BlockingWriter> {
- let w = self.0.blocking().writer(&path).map_err(format_napi_error)?;
+ pub fn writer_sync(
+ &self,
+ path: String,
+ options: Option<WriterOptions>,
+ ) -> Result<BlockingWriter> {
+ let mut writer = self.0.blocking().writer_with(&path);
+ if let Some(options) = options {
+ if let Some(append) = options.append {
+ writer = writer.append(append);
+ }
+ if let Some(chunk) = options.chunk {
+ writer = writer.buffer(chunk.get_u64().1 as usize);
+ }
+ if let Some(ref content_type) = options.content_type {
+ writer = writer.content_type(content_type);
+ }
+ if let Some(ref content_disposition) = options.content_disposition
{
+ writer = writer.content_disposition(content_disposition);
+ }
+ if let Some(ref cache_control) = options.cache_control {
+ writer = writer.cache_control(cache_control);
+ }
+ }
+ let w = writer.call().map_err(format_napi_error)?;
Ok(BlockingWriter(w))
}
+ //noinspection DuplicatedCode
/// Write bytes into path synchronously.
///
/// ### Example
@@ -268,14 +336,39 @@ impl Operator {
/// op.writeSync("path/to/file", Buffer.from("hello world"));
/// // or
/// op.writeSync("path/to/file", "hello world");
+ /// // or
+ /// op.writeSync("path/to/file", Buffer.from("hello world"), {
contentType: "text/plain" });
/// ```
#[napi]
- pub fn write_sync(&self, path: String, content: Either<Buffer, String>) ->
Result<()> {
+ pub fn write_sync(
+ &self,
+ path: String,
+ content: Either<Buffer, String>,
+ options: Option<WriteOptions>,
+ ) -> Result<()> {
let c = match content {
Either::A(buf) => buf.as_ref().to_owned(),
Either::B(s) => s.into_bytes(),
};
- self.0.blocking().write(&path, c).map_err(format_napi_error)
+ let mut writer = self.0.blocking().write_with(&path, c);
+ if let Some(options) = options {
+ if let Some(append) = options.append {
+ writer = writer.append(append);
+ }
+ if let Some(chunk) = options.chunk {
+ writer = writer.chunk(chunk.get_u64().1 as usize);
+ }
+ if let Some(ref content_type) = options.content_type {
+ writer = writer.content_type(content_type);
+ }
+ if let Some(ref content_disposition) = options.content_disposition
{
+ writer = writer.content_disposition(content_disposition);
+ }
+ if let Some(ref cache_control) = options.cache_control {
+ writer = writer.cache_control(cache_control);
+ }
+ }
+ writer.call().map_err(format_napi_error)
}
/// Append bytes into path.
@@ -293,16 +386,15 @@ impl Operator {
/// ```
#[napi]
pub async fn append(&self, path: String, content: Either<Buffer, String>)
-> Result<()> {
- let c = match content {
- Either::A(buf) => buf.as_ref().to_owned(),
- Either::B(s) => s.into_bytes(),
- };
-
- self.0
- .write_with(&path, c)
- .append(true)
- .await
- .map_err(format_napi_error)
+ self.write(
+ path,
+ content,
+ Some(WriteOptions {
+ append: Some(true),
+ ..Default::default()
+ }),
+ )
+ .await
}
/// Copy file according to given `from` and `to` path.
@@ -790,6 +882,70 @@ impl Writer {
}
}
+#[napi(object)]
+#[derive(Default)]
+pub struct WriteOptions {
+ /// Append bytes into path.
+ ///
+ /// ### Notes
+ ///
+ /// - It always appends content to the end of the file.
+ /// - It will create file if the path not exists.
+ pub append: Option<bool>,
+
+ /// Set the chunk of op.
+ ///
+ /// If chunk is set, the data will be chunked by the underlying writer.
+ ///
+ /// ## NOTE
+ ///
+ /// Service could have their own minimum chunk size while perform write
+ /// operations like multipart uploads. So the chunk size may be larger than
+ /// the given buffer size.
+ pub chunk: Option<BigInt>,
+
+ /// Set the
[Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)
of op.
+ pub content_type: Option<String>,
+
+ /// Set the
[Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
of op.
+ pub content_disposition: Option<String>,
+
+ /// Set the
[Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
of op.
+ pub cache_control: Option<String>,
+}
+
+#[napi(object)]
+#[derive(Default)]
+pub struct WriterOptions {
+ /// Append bytes into path.
+ ///
+ /// ### Notes
+ ///
+ /// - It always appends content to the end of the file.
+ /// - It will create file if the path not exists.
+ pub append: Option<bool>,
+
+ /// Set the chunk of op.
+ ///
+ /// If chunk is set, the data will be chunked by the underlying writer.
+ ///
+ /// ## NOTE
+ ///
+ /// Service could have their own minimum chunk size while perform write
+ /// operations like multipart uploads. So the chunk size may be larger than
+ /// the given buffer size.
+ pub chunk: Option<BigInt>,
+
+ /// Set the
[Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type)
of op.
+ pub content_type: Option<String>,
+
+ /// Set the
[Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
of op.
+ pub content_disposition: Option<String>,
+
+ /// Set the
[Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
of op.
+ pub cache_control: Option<String>,
+}
+
/// Lister is designed to list entries at given path in an asynchronous
/// manner.
#[napi]
diff --git a/bindings/nodejs/tests/suites/async.suite.mjs
b/bindings/nodejs/tests/suites/async.suite.mjs
index b021c273d7..9431679a76 100644
--- a/bindings/nodejs/tests/suites/async.suite.mjs
+++ b/bindings/nodejs/tests/suites/async.suite.mjs
@@ -84,5 +84,35 @@ export function run(op) {
op.deleteSync(filename)
})
+
+ test.runIf(op.capability().read && op.capability().write)('write with
behavior', async () => {
+ let c = Buffer.from('hello world')
+ const filename = `random_file_${randomUUID()}`
+
+ const options = { chunk: 1024n * 1024n }
+ if (op.capability().writeCanAppend) {
+ options.append = true
+ }
+ if (op.capability().writeWithContentType) {
+ options.contentType = 'text/plain'
+ }
+ if (op.capability().writeWithContentDisposition) {
+ options.contentDisposition = 'attachment;filename=test.txt'
+ }
+ if (op.capability().writeWithCacheControl) {
+ options.cacheControl = 'public, max-age=31536000, immutable'
+ }
+ await op.write(filename, c, options)
+
+ if (op.capability().writeCanMulti) {
+ const writer = await op.writer(filename, options)
+ await writer.write(c)
+ await writer.close()
+ }
+
+ if (op.capability().delete) {
+ op.deleteSync(filename)
+ }
+ })
})
}
diff --git a/bindings/nodejs/tests/suites/sync.suite.mjs
b/bindings/nodejs/tests/suites/sync.suite.mjs
index 43a7224fff..504e3c67cc 100644
--- a/bindings/nodejs/tests/suites/sync.suite.mjs
+++ b/bindings/nodejs/tests/suites/sync.suite.mjs
@@ -80,5 +80,36 @@ export function run(op) {
op.deleteSync(filename)
})
})
+
+ test.runIf(op.capability().read && op.capability().write)('blocking write
with behavior', () => {
+ let c = Buffer.from('hello world')
+ const filename = `random_file_${randomUUID()}`
+
+ const options = { chunk: 1024n * 1024n }
+ if (op.capability().writeCanAppend) {
+ options.append = true
+ }
+ if (op.capability().writeWithContentType) {
+ options.contentType = 'text/plain'
+ }
+ if (op.capability().writeWithContentDisposition) {
+ options.contentDisposition = 'attachment;filename=test.txt'
+ }
+ if (op.capability().writeWithCacheControl) {
+ options.cacheControl = 'public, max-age=31536000, immutable'
+ }
+
+ op.writeSync(filename, c, options)
+
+ if (op.capability().writeCanMulti) {
+ const w = op.writerSync(filename, options)
+ w.write(c)
+ w.close()
+ }
+
+ if (op.capability().delete) {
+ op.deleteSync(filename)
+ }
+ })
})
}