Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 77 additions & 13 deletions bindings/nodejs/generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,17 +269,6 @@ export interface ListOptions {
*/
deleted?: boolean
}
export interface DeleteOptions {
version?: string
}
export const enum EntryMode {
/** FILE means the path has data to read. */
FILE = 0,
/** DIR means the path can be listed. */
DIR = 1,
/** Unknown means we don't know what we can do on this path. */
Unknown = 2
}
export interface WriteOptions {
/**
* Append bytes into a path.
Expand Down Expand Up @@ -308,6 +297,65 @@ export interface WriteOptions {
contentDisposition?: string
/** Set the [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) of op. */
cacheControl?: string
/** Set the [Content-Encoding] https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding of op. */
contentEncoding?: string
/**
* Sets user metadata of op.
*
* If chunk is set, the user metadata will be attached to the object during write.
*
* ## NOTE
*
* - Services may have limitations for user metadata, for example:
* - Key length is typically limited (e.g., 1024 bytes)
* - Value length is typically limited (e.g., 4096 bytes)
* - Total metadata size might be limited
* - Some characters might be forbidden in keys
*/
userMetadata?: Record<string, string>
/**
* Sets if-match condition of op.
*
* This operation provides conditional write functionality based on ETag matching,
* helping prevent unintended overwrites in concurrent scenarios.
*/
ifMatch?: string
/**
* Sets if-none-match condition of op.
*
* This operation provides conditional write functionality based on ETag non-matching,
* useful for preventing overwriting existing resources or ensuring unique writes.
*/
ifNoneMatch?: string
/**
* Sets if_not_exists condition of op.
*
* This operation provides a way to ensure write operations only create new resources
* without overwriting existing ones, useful for implementing "create if not exists" logic.
*/
ifNotExists?: boolean
/**
* Sets concurrent of op.
*
* - By default, OpenDAL writes files sequentially
* - When concurrent is set:
* - Multiple write operations can execute in parallel
* - Write operations return immediately without waiting if tasks space are available
* - Close operation ensures all writes complete in order
* - Memory usage increases with concurrency level
*/
concurrent?: number
}
export interface DeleteOptions {
version?: string
}
export const enum EntryMode {
/** FILE means the path has data to read. */
FILE = 0,
/** DIR means the path can be listed. */
DIR = 1,
/** Unknown means we don't know what we can do on this path. */
Unknown = 2
}
/** PresignedRequest is a presigned request return by `presign`. */
export interface PresignedRequest {
Expand Down Expand Up @@ -381,6 +429,16 @@ export class Capability {
get writeWithContentDisposition(): boolean
/** If operator supports write with cache control. */
get writeWithCacheControl(): boolean
/** If operator supports write with content encoding. */
get writeWithContentEncoding(): boolean
/** If operator supports write with user metadata. */
get writeWithUserMetadata(): boolean
/** If operator supports write with if match. */
get writeWithIfMatch(): boolean
/** If operator supports write with if none match. */
get writeWithIfNoneMatch(): boolean
/** If operator supports write with if not exists. */
get writeWithIfNotExists(): boolean
/**
* write_multi_max_size is the max size that services support in write_multi.
*
Expand Down Expand Up @@ -572,7 +630,7 @@ export class Operator {
* await op.write("path/to/file", Buffer.from("hello world"), { contentType: "text/plain" });
* ```
*/
write(path: string, content: Buffer | string, options?: WriteOptions | undefined | null): Promise<void>
write(path: string, content: Buffer | string, options?: WriteOptions | undefined | null): Promise<Metadata>
/**
* Write multiple bytes into a path.
*
Expand All @@ -597,7 +655,7 @@ export class Operator {
* op.writeSync("path/to/file", Buffer.from("hello world"), { contentType: "text/plain" });
* ```
*/
writeSync(path: string, content: Buffer | string, options?: WriteOptions | undefined | null): void
writeSync(path: string, content: Buffer | string, options?: WriteOptions | undefined | null): Metadata
/**
* Copy file according to given `from` and `to` path.
*
Expand Down Expand Up @@ -845,14 +903,20 @@ export class Metadata {
* deletion or has been permanently deleted.
*/
isDeleted(): boolean
/** Cache-Control of this object. */
get cacheControl(): string | null
/** Content-Disposition of this object */
get contentDisposition(): string | null
/** Content Length of this object */
get contentLength(): bigint | null
/** Content Encoding of this object */
get contentEncoding(): string | null
/** Content MD5 of this object. */
get contentMd5(): string | null
/** Content Type of this object. */
get contentType(): string | null
/** User Metadata of this object. */
get userMetadata(): Record<string, string> | null
/** ETag of this object. */
get etag(): string | null
/**
Expand Down
30 changes: 30 additions & 0 deletions bindings/nodejs/src/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,36 @@ impl Capability {
self.0.write_with_cache_control
}

/// If operator supports write with content encoding.
#[napi(getter)]
pub fn write_with_content_encoding(&self) -> bool {
self.0.write_with_content_encoding
}

/// If operator supports write with user metadata.
#[napi(getter)]
pub fn write_with_user_metadata(&self) -> bool {
self.0.write_with_user_metadata
}

/// If operator supports write with if match.
#[napi(getter)]
pub fn write_with_if_match(&self) -> bool {
self.0.write_with_if_match
}

/// If operator supports write with if none match.
#[napi(getter)]
pub fn write_with_if_none_match(&self) -> bool {
self.0.write_with_if_none_match
}

/// If operator supports write with if not exists.
#[napi(getter)]
pub fn write_with_if_not_exists(&self) -> bool {
self.0.write_with_if_not_exists
}

/// write_multi_max_size is the max size that services support in write_multi.
///
/// For example, AWS S3 supports 5GiB as max in write_multi.
Expand Down
118 changes: 43 additions & 75 deletions bindings/nodejs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ use std::time::Duration;
use futures::AsyncReadExt;
use futures::TryStreamExt;
use napi::bindgen_prelude::*;
use opendal::options::{DeleteOptions, ListOptions, ReadOptions, ReaderOptions, StatOptions};
use opendal::options::{
DeleteOptions, ListOptions, ReadOptions, ReaderOptions, StatOptions, WriteOptions,
};

mod capability;
mod options;
Expand Down Expand Up @@ -304,39 +306,31 @@ impl Operator {
&self,
path: String,
content: Either<Buffer, String>,
options: Option<WriteOptions>,
) -> Result<()> {
options: Option<options::WriteOptions>,
) -> Result<Metadata> {
let c = match content {
Either::A(buf) => buf.as_ref().to_owned(),
Either::B(s) => s.into_bytes(),
};
let mut writer = self.async_op.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(|_| ()).map_err(format_napi_error)
let options = options.map_or_else(WriteOptions::default, WriteOptions::from);
let metadata = self
.async_op
.write_options(&path, c, options)
.await
.map_err(format_napi_error)?;
Ok(Metadata(metadata))
}

//noinspection DuplicatedCode
/// Write multiple bytes into a path.
///
/// It could be used to write large file in a streaming way.
#[napi]
pub async fn writer(&self, path: String, options: Option<WriteOptions>) -> Result<Writer> {
pub async fn writer(
&self,
path: String,
options: Option<options::WriteOptions>,
) -> Result<Writer> {
let options = options.unwrap_or_default();
let writer = self
.async_op
Expand All @@ -353,7 +347,7 @@ impl Operator {
pub fn writer_sync(
&self,
path: String,
options: Option<WriteOptions>,
options: Option<options::WriteOptions>,
) -> Result<BlockingWriter> {
let options = options.unwrap_or_default();
let writer = self
Expand All @@ -379,17 +373,18 @@ impl Operator {
&self,
path: String,
content: Either<Buffer, String>,
options: Option<WriteOptions>,
) -> Result<()> {
options: Option<options::WriteOptions>,
) -> Result<Metadata> {
let c = match content {
Either::A(buf) => buf.as_ref().to_owned(),
Either::B(s) => s.into_bytes(),
};
let options = options.unwrap_or_default();
self.blocking_op
.write_options(&path, c, options.into())
let options = options.map_or_else(WriteOptions::default, WriteOptions::from);
let metadata = self
.blocking_op
.write_options(&path, c, options)
.map_err(format_napi_error)?;
Ok(())
Ok(Metadata(metadata))
}

/// Copy file according to given `from` and `to` path.
Expand Down Expand Up @@ -778,6 +773,12 @@ impl Metadata {
self.0.is_deleted()
}

/// Cache-Control of this object.
#[napi(getter)]
pub fn cache_control(&self) -> Option<String> {
self.0.cache_control().map(|s| s.to_string())
}

/// Content-Disposition of this object
#[napi(getter)]
pub fn content_disposition(&self) -> Option<String> {
Expand All @@ -790,6 +791,12 @@ impl Metadata {
self.0.content_length().into()
}

/// Content Encoding of this object
#[napi(getter)]
pub fn content_encoding(&self) -> Option<String> {
self.0.content_encoding().map(|s| s.to_string())
}

/// Content MD5 of this object.
#[napi(getter)]
pub fn content_md5(&self) -> Option<String> {
Expand All @@ -802,6 +809,12 @@ impl Metadata {
self.0.content_type().map(|s| s.to_string())
}

/// User Metadata of this object.
#[napi(getter)]
pub fn user_metadata(&self) -> Option<HashMap<String, String>> {
self.0.user_metadata().cloned()
}

/// ETag of this object.
#[napi(getter)]
pub fn etag(&self) -> Option<String> {
Expand Down Expand Up @@ -961,51 +974,6 @@ impl Writer {
}
}

#[napi(object)]
#[derive(Default)]
pub struct WriteOptions {
/// Append bytes into a path.
///
/// ### Notes
///
/// - It always appends content to the end of the file.
/// - It will create file if the path does not exist.
pub append: Option<bool>,

/// Set the chunk of op.
///
/// If chunk is set, the data will be chunked by the underlying writer.
///
/// ## NOTE
///
/// A 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>,
}

impl From<WriteOptions> for opendal::options::WriteOptions {
fn from(value: WriteOptions) -> Self {
Self {
append: value.append.unwrap_or_default(),
chunk: value.chunk.map(|v| v.get_u64().1 as usize),
content_type: value.content_type,
content_disposition: value.content_disposition,
cache_control: value.cache_control,
..Default::default()
}
}
}

/// Lister is designed to list entries at a given path in an asynchronous
/// manner.
#[napi]
Expand Down
Loading
Loading