diff --git a/bindings/nodejs/README.md b/bindings/nodejs/README.md index 58d084adac0..f57fe91cb83 100644 --- a/bindings/nodejs/README.md +++ b/bindings/nodejs/README.md @@ -42,7 +42,6 @@ corepack enable `corepack` is distributed with Node.js, so you do not need to specifically look for a way to install it. - ### Build ```bash @@ -62,9 +61,6 @@ yarn test We use [`Cucumber`](https://cucumber.io/) for behavior testing. Refer to [here](https://cucumber.io/docs/guides/overview/) for more information about `Cucumber`. - - ## License Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 - diff --git a/bindings/nodejs/examples/presign.js b/bindings/nodejs/examples/presign.js new file mode 100644 index 00000000000..b5826794012 --- /dev/null +++ b/bindings/nodejs/examples/presign.js @@ -0,0 +1,49 @@ +/* + * 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. + */ + +const http = require('node:http') +const url = require('node:url') +const { Operator } = require('../index') + +const op = new Operator('s3', { + root: '/', + bucket: 'example-bucket', +}) + +const server = http.createServer(async (req, res) => { + res.setHeader('Content-Type', 'text/json; charset=utf-8') + + if (req.url.startsWith('/presign') && req.method === 'GET') { + const urlParts = url.parse(req.url, true) + const path = urlParts.query.path + const expires = urlParts.query.expires + + const presignedRequest = op.presignRead(path, parseInt(expires)) + + res.statusCode = 200 + res.end(JSON.stringify({ url: presignedRequest.uri })) + } else { + res.statusCode = 404 + res.end('Not Found') + } +}) + +server.listen(3000, () => { + console.log('Server is listening on port 3000.') +}) diff --git a/bindings/nodejs/generated.js b/bindings/nodejs/generated.js index c3c427b3f83..03d6bf6e6f2 100644 --- a/bindings/nodejs/generated.js +++ b/bindings/nodejs/generated.js @@ -17,6 +17,12 @@ * under the License. */ +/* tslint:disable */ +/* eslint-disable */ +/* prettier-ignore */ + +/* auto-generated by NAPI-RS */ + const { existsSync, readFileSync } = require('fs') const { join } = require('path') @@ -265,10 +271,11 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Operator, Entry, Metadata, Lister, BlockingLister } = nativeBinding +const { Operator, Entry, Metadata, Lister, BlockingLister, PresignedRequest } = nativeBinding module.exports.Operator = Operator module.exports.Entry = Entry module.exports.Metadata = Metadata module.exports.Lister = Lister module.exports.BlockingLister = BlockingLister +module.exports.PresignedRequest = PresignedRequest diff --git a/bindings/nodejs/index.d.ts b/bindings/nodejs/index.d.ts index 4516d1930c4..34cdbae3f8e 100644 --- a/bindings/nodejs/index.d.ts +++ b/bindings/nodejs/index.d.ts @@ -64,6 +64,24 @@ export class Operator { * An error will be returned if given path doesn't end with `/`. */ listSync(path: string): BlockingLister + /** + * Get a presigned request for read. + * + * Unit of expires is seconds. + */ + presignRead(path: string, expires: number): PresignedRequest + /** + * Get a presigned request for write. + * + * Unit of expires is seconds. + */ + presignWrite(path: string, expires: number): PresignedRequest + /** + * Get a presigned request for stat. + * + * Unit of expires is seconds. + */ + presignStat(path: string, expires: number): PresignedRequest } export class Entry { /** Return the path of this entry. */ @@ -101,3 +119,15 @@ export class Lister { export class BlockingLister { next(): Entry | null } +export class PresignedRequest { + /** Returns the HTTP method of this request. */ + get method(): string + /** Returns the URI of this request. */ + get uri(): string + /** + * Returns the headers of this request. + * + * The key of the map is the header name, and the value is the header value AS bytes. + */ + headers(): Record +} diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs index 2f062cdb387..be3f73930c2 100644 --- a/bindings/nodejs/src/lib.rs +++ b/bindings/nodejs/src/lib.rs @@ -24,6 +24,7 @@ use std::str::FromStr; use futures::TryStreamExt; use napi::bindgen_prelude::*; use time::format_description::well_known::Rfc3339; +use time::Duration; fn build_operator( scheme: opendal::Scheme, @@ -206,6 +207,42 @@ impl Operator { self.0.blocking().scan(&path).map_err(format_napi_error)?, )) } + + /// Get a presigned request for read. + /// + /// Unit of expires is seconds. + #[napi] + pub fn presign_read(&self, path: String, expires: u32) -> Result { + let res = self + .0 + .presign_read(&path, Duration::seconds(expires as i64)) + .map_err(format_napi_error)?; + Ok(PresignedRequest(res)) + } + + /// Get a presigned request for write. + /// + /// Unit of expires is seconds. + #[napi] + pub fn presign_write(&self, path: String, expires: u32) -> Result { + let res = self + .0 + .presign_write(&path, Duration::seconds(expires as i64)) + .map_err(format_napi_error)?; + Ok(PresignedRequest(res)) + } + + /// Get a presigned request for stat. + /// + /// Unit of expires is seconds. + #[napi] + pub fn presign_stat(&self, path: String, expires: u32) -> Result { + let res = self + .0 + .presign_stat(&path, Duration::seconds(expires as i64)) + .map_err(format_napi_error)?; + Ok(PresignedRequest(res)) + } } #[napi] @@ -318,6 +355,43 @@ impl BlockingLister { } } +#[napi] +pub struct PresignedRequest(opendal::raw::PresignedRequest); + +#[napi] +impl PresignedRequest { + /// Returns the HTTP method of this request. + #[napi(getter)] + pub fn method(&self) -> String { + self.0.method().to_string() + } + + /// Returns the URI of this request. + #[napi(getter)] + pub fn uri(&self) -> String { + self.0.uri().to_string() + } + + /// Returns the headers of this request. + /// + /// The key of the map is the header name, and the value is the header value. + #[napi] + pub fn headers(&self) -> HashMap { + self.0 + .header() + .iter() + .map(|(k, v)| { + ( + k.as_str().to_string(), + v.to_str() + .expect("header value contains non visible ascii characters") + .to_string(), + ) + }) + .collect() + } +} + fn format_napi_error(err: opendal::Error) -> Error { Error::from_reason(format!("{}", err)) }