bindings/nodejs/src/lib.rs (590 lines of code) (raw):
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#[macro_use]
extern crate napi_derive;
use std::collections::HashMap;
use std::fmt::Display;
use std::io::Read;
use std::str::FromStr;
use std::time::Duration;
use futures::AsyncReadExt;
use futures::TryStreamExt;
use napi::bindgen_prelude::*;
mod capability;
#[napi]
pub struct Operator(opendal::Operator);
#[napi]
impl Operator {
/// @see For the full list of scheme, see https://docs.rs/opendal/latest/opendal/services/index.html
/// And the options,
/// please refer to the documentation of the corresponding service for the corresponding parameters.
/// Note that the current options key is snake_case.
#[napi(constructor)]
pub fn new(scheme: String, options: Option<HashMap<String, String>>) -> Result<Self> {
let scheme = opendal::Scheme::from_str(&scheme)
.map_err(|err| {
opendal::Error::new(opendal::ErrorKind::Unexpected, "not supported scheme")
.set_source(err)
})
.map_err(format_napi_error)?;
let options = options.unwrap_or_default();
let mut op = opendal::Operator::via_iter(scheme, options).map_err(format_napi_error)?;
if !op.info().full_capability().blocking {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let _guard = handle.enter();
op = op.layer(
opendal::layers::BlockingLayer::create()
.expect("blocking layer must be created"),
);
}
}
Ok(Operator(op))
}
/// Get current operator(service)'s full capability.
#[napi]
pub fn capability(&self) -> Result<capability::Capability> {
Ok(capability::Capability::new(self.0.info().full_capability()))
}
/// Get current path's metadata **without cache** directly.
///
/// ### Notes
/// Use stat if you:
///
/// - Want to detect the outside changes of a path.
/// - Don’t want to read from cached metadata.
///
/// You may want to use `metadata` if you are working with entries returned by `Lister`. It’s highly possible that metadata you want has already been cached.
///
/// ### Example
/// ```javascript
/// const meta = await op.stat("test");
/// if (meta.isDir) {
/// // do something
/// }
/// ```
#[napi]
pub async fn stat(&self, path: String) -> Result<Metadata> {
let meta = self.0.stat(&path).await.map_err(format_napi_error)?;
Ok(Metadata(meta))
}
/// Get current path's metadata **without cache** directly and synchronously.
///
/// ### Example
/// ```javascript
/// const meta = op.statSync("test");
/// if (meta.isDir) {
/// // do something
/// }
/// ```
#[napi]
pub fn stat_sync(&self, path: String) -> Result<Metadata> {
let meta = self.0.blocking().stat(&path).map_err(format_napi_error)?;
Ok(Metadata(meta))
}
/// Check if this operator can work correctly.
///
/// We will send a `list` request to the given path and return any errors we met.
///
/// ### Example
/// ```javascript
/// await op.check();
/// ```
#[napi]
pub async fn check(&self) -> Result<()> {
self.0.check().await.map_err(format_napi_error)
}
/// Check the op synchronously.
///
/// ### Example
/// ```javascript
/// op.checkSync();
/// ```
#[napi]
pub fn check_sync(&self) -> Result<()> {
self.0.blocking().check().map_err(format_napi_error)
}
/// Check if this path exists or not.
///
/// ### Example
/// ```javascript
/// await op.isExist("test");
/// ```
#[napi]
pub async fn exists(&self, path: String) -> Result<bool> {
self.0.exists(&path).await.map_err(format_napi_error)
}
/// Check if this path exists or not synchronously.
///
/// ### Example
/// ```javascript
/// op.isExistSync("test");
/// ```
#[napi]
pub fn exists_sync(&self, path: String) -> Result<bool> {
self.0.blocking().exists(&path).map_err(format_napi_error)
}
/// Create dir with a given path.
///
/// ### Example
/// ```javascript
/// await op.createDir("path/to/dir/");
/// ```
#[napi]
pub async fn create_dir(&self, path: String) -> Result<()> {
self.0.create_dir(&path).await.map_err(format_napi_error)
}
/// Create dir with a given path synchronously.
///
/// ### Example
/// ```javascript
/// op.createDirSync("path/to/dir/");
/// ```
#[napi]
pub fn create_dir_sync(&self, path: String) -> Result<()> {
self.0
.blocking()
.create_dir(&path)
.map_err(format_napi_error)
}
/// Read the whole path into a buffer.
///
/// ### Example
/// ```javascript
/// const buf = await op.read("path/to/file");
/// ```
#[napi]
pub async fn read(&self, path: String) -> Result<Buffer> {
let res = self
.0
.read(&path)
.await
.map_err(format_napi_error)?
.to_vec();
Ok(res.into())
}
/// Create a reader to read the given path.
///
/// It could be used to read large file in a streaming way.
#[napi]
pub async fn reader(&self, path: String) -> Result<Reader> {
let r = self.0.reader(&path).await.map_err(format_napi_error)?;
Ok(Reader {
inner: r
.into_futures_async_read(..)
.await
.map_err(format_napi_error)?,
})
}
/// Read the whole path into a buffer synchronously.
///
/// ### Example
/// ```javascript
/// const buf = op.readSync("path/to/file");
/// ```
#[napi]
pub fn read_sync(&self, path: String) -> Result<Buffer> {
let res = self
.0
.blocking()
.read(&path)
.map_err(format_napi_error)?
.to_vec();
Ok(res.into())
}
/// Create a reader to read the given path synchronously.
///
/// It could be used to read large file in a streaming way.
#[napi]
pub fn reader_sync(&self, path: String) -> Result<BlockingReader> {
let r = self.0.blocking().reader(&path).map_err(format_napi_error)?;
Ok(BlockingReader {
inner: r.into_std_read(..).map_err(format_napi_error)?,
})
}
//noinspection DuplicatedCode
/// Write bytes into a path.
///
/// ### Example
/// ```javascript
/// 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>,
options: Option<WriteOptions>,
) -> Result<()> {
let c = match content {
Either::A(buf) => buf.as_ref().to_owned(),
Either::B(s) => s.into_bytes(),
};
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(|_| ()).map_err(format_napi_error)
}
//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<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))
}
/// Write multiple bytes into a path synchronously.
///
/// It could be used to write large file in a streaming way.
#[napi]
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.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.call().map_err(format_napi_error)?;
Ok(BlockingWriter(w))
}
//noinspection DuplicatedCode
/// Write bytes into a path synchronously.
///
/// ### Example
/// ```javascript
/// 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>,
options: Option<WriteOptions>,
) -> Result<()> {
let c = match content {
Either::A(buf) => buf.as_ref().to_owned(),
Either::B(s) => s.into_bytes(),
};
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(|_| ()).map_err(format_napi_error)
}
/// Copy file according to given `from` and `to` path.
///
/// ### Example
/// ```javascript
/// await op.copy("path/to/file", "path/to/dest");
/// ```
#[napi]
pub async fn copy(&self, from: String, to: String) -> Result<()> {
self.0.copy(&from, &to).await.map_err(format_napi_error)
}
/// Copy file according to given `from` and `to` path synchronously.
///
/// ### Example
/// ```javascript
/// op.copySync("path/to/file", "path/to/dest");
/// ```
#[napi]
pub fn copy_sync(&self, from: String, to: String) -> Result<()> {
self.0
.blocking()
.copy(&from, &to)
.map_err(format_napi_error)
}
/// Rename file according to given `from` and `to` path.
///
/// It's similar to `mv` command.
///
/// ### Example
/// ```javascript
/// await op.rename("path/to/file", "path/to/dest");
/// ```
#[napi]
pub async fn rename(&self, from: String, to: String) -> Result<()> {
self.0.rename(&from, &to).await.map_err(format_napi_error)
}
/// Rename file according to given `from` and `to` path synchronously.
///
/// It's similar to `mv` command.
///
/// ### Example
/// ```javascript
/// op.renameSync("path/to/file", "path/to/dest");
/// ```
#[napi]
pub fn rename_sync(&self, from: String, to: String) -> Result<()> {
self.0
.blocking()
.rename(&from, &to)
.map_err(format_napi_error)
}
/// Delete the given path.
///
/// ### Notes
/// Delete not existing error won’t return errors.
///
/// ### Example
/// ```javascript
/// await op.delete("test");
/// ```
#[napi]
pub async fn delete(&self, path: String) -> Result<()> {
self.0.delete(&path).await.map_err(format_napi_error)
}
/// Delete the given path synchronously.
///
/// ### Example
/// ```javascript
/// op.deleteSync("test");
/// ```
#[napi]
pub fn delete_sync(&self, path: String) -> Result<()> {
self.0.blocking().delete(&path).map_err(format_napi_error)
}
/// Remove given paths.
///
/// ### Notes
/// If underlying services support delete in batch, we will use batch delete instead.
///
/// ### Examples
/// ```javascript
/// await op.remove(["abc", "def"]);
/// ```
#[napi]
pub async fn remove(&self, paths: Vec<String>) -> Result<()> {
self.0.delete_iter(paths).await.map_err(format_napi_error)
}
/// Remove the path and all nested dirs and files recursively.
///
/// ### Notes
/// If underlying services support delete in batch, we will use batch delete instead.
///
/// ### Examples
/// ```javascript
/// await op.removeAll("path/to/dir/");
/// ```
#[napi]
pub async fn remove_all(&self, path: String) -> Result<()> {
self.0.remove_all(&path).await.map_err(format_napi_error)
}
/// List the given path.
///
/// This function will return an array of entries.
///
/// An error will be returned if given path doesn't end with `/`.
///
/// ### Example
///
/// ```javascript
/// const list = await op.list("path/to/dir/");
/// for (let entry of list) {
/// let meta = await op.stat(entry.path);
/// if (meta.isFile) {
/// // do something
/// }
/// }
/// ```
///
/// #### List recursively
///
/// With `recursive` option, you can list recursively.
///
/// ```javascript
/// const list = await op.list("path/to/dir/", { recursive: true });
/// for (let entry of list) {
/// let meta = await op.stat(entry.path);
/// if (meta.isFile) {
/// // do something
/// }
/// }
/// ```
#[napi]
pub async fn list(&self, path: String, options: Option<ListOptions>) -> Result<Vec<Entry>> {
let mut l = self.0.list_with(&path);
if let Some(options) = options {
if let Some(limit) = options.limit {
l = l.limit(limit as usize);
}
if let Some(recursive) = options.recursive {
l = l.recursive(recursive);
}
}
Ok(l.await
.map_err(format_napi_error)?
.iter()
.map(|e| Entry(e.to_owned()))
.collect())
}
/// List the given path synchronously.
///
/// This function will return an array of entries.
///
/// An error will be returned if given path doesn't end with `/`.
///
/// ### Example
///
/// ```javascript
/// const list = op.listSync("path/to/dir/");
/// for (let entry of list) {
/// let meta = op.statSync(entry.path);
/// if (meta.isFile) {
/// // do something
/// }
/// }
/// ```
///
/// #### List recursively
///
/// With `recursive` option, you can list recursively.
///
/// ```javascript
/// const list = op.listSync("path/to/dir/", { recursive: true });
/// for (let entry of list) {
/// let meta = op.statSync(entry.path);
/// if (meta.isFile) {
/// // do something
/// }
/// }
/// ```
#[napi]
pub fn list_sync(&self, path: String, options: Option<ListOptions>) -> Result<Vec<Entry>> {
let mut l = self.0.blocking().list_with(&path);
if let Some(options) = options {
if let Some(limit) = options.limit {
l = l.limit(limit as usize);
}
if let Some(recursive) = options.recursive {
l = l.recursive(recursive);
}
}
Ok(l.call()
.map_err(format_napi_error)?
.iter()
.map(|e| Entry(e.to_owned()))
.collect())
}
/// Get a presigned request for read.
///
/// Unit of `expires` is seconds.
///
/// ### Example
///
/// ```javascript
/// const req = await op.presignRead(path, parseInt(expires));
///
/// console.log("method: ", req.method);
/// console.log("url: ", req.url);
/// console.log("headers: ", req.headers);
/// ```
#[napi]
pub async fn presign_read(&self, path: String, expires: u32) -> Result<PresignedRequest> {
let res = self
.0
.presign_read(&path, Duration::from_secs(expires as u64))
.await
.map_err(format_napi_error)?;
Ok(PresignedRequest::new(res))
}
/// Get a presigned request for `write`.
///
/// Unit of `expires` is seconds.
///
/// ### Example
///
/// ```javascript
/// const req = await op.presignWrite(path, parseInt(expires));
///
/// console.log("method: ", req.method);
/// console.log("url: ", req.url);
/// console.log("headers: ", req.headers);
/// ```
#[napi]
pub async fn presign_write(&self, path: String, expires: u32) -> Result<PresignedRequest> {
let res = self
.0
.presign_write(&path, Duration::from_secs(expires as u64))
.await
.map_err(format_napi_error)?;
Ok(PresignedRequest::new(res))
}
/// Get a presigned request for stat.
///
/// Unit of `expires` is seconds.
///
/// ### Example
///
/// ```javascript
/// const req = await op.presignStat(path, parseInt(expires));
///
/// console.log("method: ", req.method);
/// console.log("url: ", req.url);
/// console.log("headers: ", req.headers);
/// ```
#[napi]
pub async fn presign_stat(&self, path: String, expires: u32) -> Result<PresignedRequest> {
let res = self
.0
.presign_stat(&path, Duration::from_secs(expires as u64))
.await
.map_err(format_napi_error)?;
Ok(PresignedRequest::new(res))
}
}
/// Entry returned by Lister or BlockingLister to represent a path, and it's a relative metadata.
#[napi]
pub struct Entry(opendal::Entry);
#[napi]
impl Entry {
/// Return the path of this entry.
#[napi]
pub fn path(&self) -> String {
self.0.path().to_string()
}
}
/// Metadata carries all metadata associated with a path.
#[napi]
pub struct Metadata(opendal::Metadata);
#[napi]
impl Metadata {
/// Returns true if the <op.stat> object describes a file system directory.
#[napi]
pub fn is_directory(&self) -> bool {
self.0.is_dir()
}
/// Returns true if the <op.stat> object describes a regular file.
#[napi]
pub fn is_file(&self) -> bool {
self.0.is_file()
}
/// Content-Disposition of this object
#[napi(getter)]
pub fn content_disposition(&self) -> Option<String> {
self.0.content_disposition().map(|s| s.to_string())
}
/// Content Length of this object
#[napi(getter)]
pub fn content_length(&self) -> Option<u64> {
self.0.content_length().into()
}
/// Content MD5 of this object.
#[napi(getter)]
pub fn content_md5(&self) -> Option<String> {
self.0.content_md5().map(|s| s.to_string())
}
/// Content Type of this object.
#[napi(getter)]
pub fn content_type(&self) -> Option<String> {
self.0.content_type().map(|s| s.to_string())
}
/// ETag of this object.
#[napi(getter)]
pub fn etag(&self) -> Option<String> {
self.0.etag().map(|s| s.to_string())
}
/// Last Modified of this object.
///
/// We will output this time in RFC3339 format like `1996-12-19T16:39:57+08:00`.
#[napi(getter)]
pub fn last_modified(&self) -> Option<String> {
self.0.last_modified().map(|ta| ta.to_rfc3339())
}
}
#[napi(object)]
pub struct ListOptions {
pub limit: Option<u32>,
pub recursive: Option<bool>,
}
/// BlockingReader is designed to read data from a given path in a blocking
/// manner.
#[napi]
pub struct BlockingReader {
inner: opendal::StdReader,
}
#[napi]
impl BlockingReader {
#[napi]
pub fn read(&mut self, mut buf: Buffer) -> Result<usize> {
let buf = buf.as_mut();
let n = self.inner.read(buf).map_err(format_napi_error)?;
Ok(n)
}
}
/// Reader is designed to read data from a given path in an asynchronous
/// manner.
#[napi]
pub struct Reader {
inner: opendal::FuturesAsyncReader,
}
#[napi]
impl Reader {
/// # Safety
///
/// > &mut self in async napi methods should be marked as unsafe
///
/// Read bytes from this reader into given buffer.
#[napi]
pub async unsafe fn read(&mut self, mut buf: Buffer) -> Result<usize> {
let buf = buf.as_mut();
let n = self.inner.read(buf).await.map_err(format_napi_error)?;
Ok(n)
}
}
/// BlockingWriter is designed to write data into a given path in a blocking
/// manner.
#[napi]
pub struct BlockingWriter(opendal::BlockingWriter);
#[napi]
impl BlockingWriter {
/// # Safety
///
/// > &mut self in async napi methods should be marked as unsafe
///
/// Write bytes into this writer.
///
/// ### Example
/// ```javascript
/// const writer = await op.writer("path/to/file");
/// await writer.write(Buffer.from("hello world"));
/// await writer.close();
/// ```
#[napi]
pub unsafe fn write(&mut self, 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(c).map_err(format_napi_error)
}
/// # Safety
///
/// > &mut self in async napi methods should be marked as unsafe
///
/// Close this writer.
///
/// ### Example
///
/// ```javascript
/// const writer = op.writerSync("path/to/file");
/// writer.write(Buffer.from("hello world"));
/// writer.close();
/// ```
#[napi]
pub unsafe fn close(&mut self) -> Result<()> {
self.0.close().map(|_| ()).map_err(format_napi_error)
}
}
/// Writer is designed to write data into a given path in an asynchronous
/// manner.
#[napi]
pub struct Writer(opendal::Writer);
#[napi]
impl Writer {
/// # Safety
///
/// > &mut self in async napi methods should be marked as unsafe
///
/// Write bytes into this writer.
///
/// ### Example
/// ```javascript
/// const writer = await op.writer("path/to/file");
/// await writer.write(Buffer.from("hello world"));
/// await writer.close();
/// ```
#[napi]
pub async unsafe fn write(&mut self, 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(c).await.map_err(format_napi_error)
}
/// # Safety
///
/// > &mut self in async napi methods should be marked as unsafe
///
/// Close this writer.
///
/// ### Example
/// ```javascript
/// const writer = await op.writer("path/to/file");
/// await writer.write(Buffer.from("hello world"));
/// await writer.close();
/// ```
#[napi]
pub async unsafe fn close(&mut self) -> Result<()> {
self.0.close().await.map(|_| ()).map_err(format_napi_error)
}
}
#[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>,
}
#[napi(object)]
#[derive(Default)]
pub struct WriterOptions {
/// 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>,
}
/// Lister is designed to list entries at a given path in an asynchronous
/// manner.
#[napi]
pub struct Lister(opendal::Lister);
#[napi]
impl Lister {
/// # Safety
///
/// > &mut self in async napi methods should be marked as unsafe
///
/// napi will make sure the function is safe, and we didn't do unsafe
/// things internally.
#[napi]
pub async unsafe fn next(&mut self) -> Result<Option<Entry>> {
Ok(self
.0
.try_next()
.await
.map_err(format_napi_error)?
.map(Entry))
}
}
/// BlockingLister is designed to list entries at a given path in a blocking
/// manner.
#[napi]
pub struct BlockingLister(opendal::BlockingLister);
/// Method `next` can be confused for the standard trait method `std::iter::Iterator::next`.
/// But in JavaScript, it is also customary to use the next method directly to get the next element.
/// Therefore, disable this clippy.
/// It can be removed after a complete implementation of `Generator`.
/// FYI: <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator>
#[napi]
#[allow(clippy::should_implement_trait)]
impl BlockingLister {
#[napi]
pub fn next(&mut self) -> Result<Option<Entry>> {
match self.0.next() {
Some(Ok(entry)) => Ok(Some(Entry(entry))),
Some(Err(e)) => Err(format_napi_error(e)),
None => Ok(None),
}
}
}
/// PresignedRequest is a presigned request return by `presign`.
#[napi(object)]
pub struct PresignedRequest {
/// HTTP method of this request.
pub method: String,
/// URL of this request.
pub url: String,
/// HTTP headers of this request.
pub headers: HashMap<String, String>,
}
impl PresignedRequest {
pub fn new(req: opendal::raw::PresignedRequest) -> Self {
let method = req.method().to_string();
let url = req.uri().to_string();
let headers = req
.header()
.iter()
.map(|(k, v)| {
(
k.as_str().to_string(),
v.to_str()
.expect("header value contains non visible ascii characters")
.to_string(),
)
})
.collect();
Self {
method,
url,
headers,
}
}
}
pub trait NodeLayer: Send + Sync {
fn layer(&self, op: opendal::Operator) -> opendal::Operator;
}
/// A public layer wrapper
#[napi]
pub struct Layer {
inner: Box<dyn NodeLayer>,
}
#[napi]
impl Operator {
/// Add a layer to this operator.
#[napi]
pub fn layer(&self, layer: External<Layer>) -> Result<Self> {
Ok(Self(layer.inner.layer(self.0.clone())))
}
}
impl NodeLayer for opendal::layers::RetryLayer {
fn layer(&self, op: opendal::Operator) -> opendal::Operator {
op.layer(self.clone())
}
}
/// Retry layer
///
/// Add retry for temporary failed operations.
///
/// # Notes
///
/// This layer will retry failed operations when [`Error::is_temporary`]
/// returns true.
/// If the operation still failed, this layer will set error to
/// `Persistent` which means error has been retried.
///
/// `write` and `blocking_write` don't support retry so far,
/// visit [this issue](https://github.com/apache/opendal/issues/1223) for more details.
///
/// # Examples
///
/// ```javascript
/// const op = new Operator("file", { root: "/tmp" })
///
/// const retry = new RetryLayer();
/// retry.max_times = 3;
/// retry.jitter = true;
///
/// op.layer(retry.build());
/// ```
#[derive(Default)]
#[napi]
pub struct RetryLayer {
jitter: bool,
max_times: Option<u32>,
factor: Option<f64>,
max_delay: Option<f64>,
min_delay: Option<f64>,
}
#[napi]
impl RetryLayer {
#[napi(constructor)]
pub fn new() -> Self {
Self::default()
}
/// Set jitter of current backoff.
///
/// If jitter is enabled, ExponentialBackoff will add a random jitter in `[0, min_delay)`
/// to current delay.
#[napi(setter)]
pub fn jitter(&mut self, v: bool) {
self.jitter = v;
}
/// Set max_times of current backoff.
///
/// Backoff will return `None` if max times are reached.
#[napi(setter)]
pub fn max_times(&mut self, v: u32) {
self.max_times = Some(v);
}
/// Set factor of current backoff.
///
/// # Panics
///
/// This function will panic if the input factor is smaller than `1.0`.
#[napi(setter)]
pub fn factor(&mut self, v: f64) {
self.factor = Some(v);
}
/// Set max_delay of current backoff.
///
/// Delay will not increase if the current delay is larger than max_delay.
///
/// # Notes
///
/// - The unit of max_delay is millisecond.
#[napi(setter)]
pub fn max_delay(&mut self, v: f64) {
self.max_delay = Some(v);
}
/// Set min_delay of current backoff.
///
/// # Notes
///
/// - The unit of min_delay is millisecond.
#[napi(setter)]
pub fn min_delay(&mut self, v: f64) {
self.min_delay = Some(v);
}
#[napi]
pub fn build(&self) -> External<Layer> {
let mut l = opendal::layers::RetryLayer::default();
if self.jitter {
l = l.with_jitter();
}
if let Some(max_times) = self.max_times {
l = l.with_max_times(max_times as usize);
}
if let Some(factor) = self.factor {
l = l.with_factor(factor as f32);
}
if let Some(max_delay) = self.max_delay {
l = l.with_max_delay(Duration::from_millis(max_delay as u64));
}
if let Some(min_delay) = self.min_delay {
l = l.with_min_delay(Duration::from_millis(min_delay as u64));
}
External::new(Layer { inner: Box::new(l) })
}
}
/// Format opendal error to napi error.
///
/// FIXME: handle error correctly.
fn format_napi_error(err: impl Display) -> Error {
Error::from_reason(format!("{}", err))
}