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)
+      }
+    })
   })
 }

Reply via email to