You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2023/03/30 04:38:23 UTC
[incubator-opendal] branch main updated: feat(bindings/nodejs): Add more APIs and examples (#1799)
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/incubator-opendal.git
The following commit(s) were added to refs/heads/main by this push:
new f199ca0d feat(bindings/nodejs): Add more APIs and examples (#1799)
f199ca0d is described below
commit f199ca0d2fff6522cefa1e129af747b2fef0186d
Author: Suyan <su...@gmail.com>
AuthorDate: Thu Mar 30 12:38:19 2023 +0800
feat(bindings/nodejs): Add more APIs and examples (#1799)
* feat(bindings/nodejs): Add more APIs and examples
Signed-off-by: suyanhanx <su...@gmail.com>
* reformat
Signed-off-by: suyanhanx <su...@gmail.com>
* chore: upgrade typedoc to support TypeScript 5
Signed-off-by: suyanhanx <su...@gmail.com>
* remove example
Signed-off-by: suyanhanx <su...@gmail.com>
* simplify presign docs
Signed-off-by: suyanhanx <su...@gmail.com>
---------
Signed-off-by: suyanhanx <su...@gmail.com>
---
bindings/nodejs/examples/presign.js | 49 -------
bindings/nodejs/index.d.ts | 270 ++++++++++++++++++++++++++++++++++--
bindings/nodejs/package.json | 2 +-
bindings/nodejs/src/lib.rs | 242 +++++++++++++++++++++++++++++++-
bindings/nodejs/yarn.lock | 12 +-
5 files changed, 505 insertions(+), 70 deletions(-)
diff --git a/bindings/nodejs/examples/presign.js b/bindings/nodejs/examples/presign.js
deleted file mode 100644
index 740cb300..00000000
--- a/bindings/nodejs/examples/presign.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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(presignedRequest))
- } else {
- res.statusCode = 404
- res.end('Not Found')
- }
-})
-
-server.listen(3000, () => {
- console.log('Server is listening on port 3000.')
-})
diff --git a/bindings/nodejs/index.d.ts b/bindings/nodejs/index.d.ts
index fd56c055..502cd630 100644
--- a/bindings/nodejs/index.d.ts
+++ b/bindings/nodejs/index.d.ts
@@ -32,36 +32,237 @@ export interface PresignedRequest {
}
export class Operator {
constructor(scheme: string, options?: Record<string, string> | undefined | null)
- /** Get current path's metadata **without cache** directly. */
+ /**
+ * Get current path's metadata **without cache** directly.
+ *
+ * ### Notes
+ * Use stat if you:
+ *
+ * - Want detect the outside changes of 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
+ * }
+ * ```
+ */
stat(path: string): Promise<Metadata>
- /** Get current path's metadata **without cache** directly and synchronously. */
+ /**
+ * Get current path's metadata **without cache** directly and synchronously.
+ *
+ * ### Example
+ * ```javascript
+ * const meta = op.statSync("test");
+ * if (meta.isDir) {
+ * // do something
+ * }
+ * ```
+ */
statSync(path: string): Metadata
- /** Create dir with given path. */
+ /**
+ * Check if this operator can work correctly.
+ *
+ * We will send a `list` request to path and return any errors we met.
+ *
+ * ### Example
+ * ```javascript
+ * await op.check();
+ * ```
+ */
+ check(): Promise<void>
+ /**
+ * Check if this path exists or not.
+ *
+ * ### Example
+ * ```javascript
+ * await op.isExist("test");
+ * ```
+ */
+ isExist(path: string): Promise<boolean>
+ /**
+ * Check if this path exists or not synchronously.
+ *
+ * ### Example
+ * ```javascript
+ * op.isExistSync("test");
+ * ```
+ */
+ isExistSync(path: string): boolean
+ /**
+ * Create dir with given path.
+ *
+ * ### Example
+ * ```javascript
+ * await op.createDir("path/to/dir/");
+ * ```
+ */
createDir(path: string): Promise<void>
- /** Create dir with given path synchronously. */
+ /**
+ * Create dir with given path synchronously.
+ *
+ * ### Example
+ * ```javascript
+ * op.createDirSync("path/to/dir/");
+ * ```
+ */
createDirSync(path: string): void
- /** Write bytes into path. */
+ /**
+ * Write bytes into path.
+ *
+ * ### Example
+ * ```javascript
+ * await op.write("path/to/file", Buffer.from("hello world"));
+ * // or
+ * await op.write("path/to/file", "hello world");
+ * ```
+ */
write(path: string, content: Buffer | string): Promise<void>
- /** Write bytes into path synchronously. */
+ /**
+ * Write bytes into path synchronously.
+ *
+ * ### Example
+ * ```javascript
+ * op.writeSync("path/to/file", Buffer.from("hello world"));
+ * // or
+ * op.writeSync("path/to/file", "hello world");
+ * ```
+ */
writeSync(path: string, content: Buffer | string): void
- /** Read the whole path into a buffer. */
+ /**
+ * Read the whole path into a buffer.
+ *
+ * ### Example
+ * ```javascript
+ * const buf = await op.read("path/to/file");
+ * ```
+ */
read(path: string): Promise<Buffer>
- /** Read the whole path into a buffer synchronously. */
+ /**
+ * Read the whole path into a buffer synchronously.
+ *
+ * ### Example
+ * ```javascript
+ * const buf = op.readSync("path/to/file");
+ * ```
+ */
readSync(path: string): Buffer
- /** List dir in flat way. */
+ /**
+ * List dir in flat way.
+ *
+ * This function will create a new handle to list entries.
+ *
+ * An error will be returned if given path doesn’t end with /.
+ *
+ * ### Example
+ * ```javascript
+ * const lister = await op.scan("/path/to/dir/");
+ * while (true)) {
+ * const entry = await lister.next();
+ * if (entry === null) {
+ * break;
+ * }
+ * let meta = await op.stat(entry.path);
+ * if (meta.is_file) {
+ * // do something
+ * }
+ * }
+ * `````
+ */
scan(path: string): Promise<Lister>
- /** List dir in flat way synchronously. */
+ /**
+ * List dir in flat way synchronously.
+ *
+ * This function will create a new handle to list entries.
+ *
+ * An error will be returned if given path doesn’t end with /.
+ *
+ * ### Example
+ * ```javascript
+ * const lister = op.scan_sync(/path/to/dir/");
+ * while (true)) {
+ * const entry = lister.next();
+ * if (entry === null) {
+ * break;
+ * }
+ * let meta = op.statSync(entry.path);
+ * if (meta.is_file) {
+ * // do something
+ * }
+ * }
+ * `````
+ */
scanSync(path: string): BlockingLister
- /** Delete the given path. */
+ /**
+ * Delete the given path.
+ *
+ * ### Notes
+ * Delete not existing error won’t return errors.
+ *
+ * ### Example
+ * ```javascript
+ * await op.delete("test");
+ * ```
+ */
delete(path: string): Promise<void>
- /** Delete the given path synchronously. */
+ /**
+ * Delete the given path synchronously.
+ *
+ * ### Example
+ * ```javascript
+ * op.deleteSync("test");
+ * ```
+ */
deleteSync(path: string): void
+ /**
+ * Remove given paths.
+ *
+ * ### Notes
+ * If underlying services support delete in batch, we will use batch delete instead.
+ *
+ * ### Examples
+ * ```javascript
+ * await op.remove(["abc", "def"]);
+ * ```
+ */
+ remove(paths: Array<string>): Promise<void>
+ /**
+ * 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/");
+ * ```
+ */
+ removeAll(path: string): Promise<void>
/**
* List given path.
*
* This function will create a new handle to list entries.
*
* An error will be returned if given path doesn't end with `/`.
+ *
+ * ### Example
+ * ```javascript
+ * const lister = await op.list("path/to/dir/");
+ * while (true)) {
+ * const entry = await lister.next();
+ * if (entry === null) {
+ * break;
+ * }
+ * let meta = await op.stat(entry.path);
+ * if (meta.isFile) {
+ * // do something
+ * }
+ * }
+ * ```
*/
list(path: string): Promise<Lister>
/**
@@ -70,24 +271,69 @@ export class Operator {
* This function will create a new handle to list entries.
*
* An error will be returned if given path doesn't end with `/`.
+ *
+ * ### Example
+ * ```javascript
+ * const lister = op.listSync("path/to/dir/");
+ * while (true)) {
+ * const entry = lister.next();
+ * if (entry === null) {
+ * break;
+ * }
+ * let meta = op.statSync(entry.path);
+ * if (meta.isFile) {
+ * // do something
+ * }
+ * }
+ * ```
*/
listSync(path: string): BlockingLister
/**
* Get a presigned request for read.
*
* Unit of expires is seconds.
+ *
+ * ### Example
+ *
+ * ```javascript
+ * const req = op.presignRead(path, parseInt(expires));
+ *
+ * console.log("method: ", req.method)
+ * console.log("url: ", req.url)
+ * console.log("headers: ", req.headers)
+ * ```
*/
presignRead(path: string, expires: number): PresignedRequest
/**
* Get a presigned request for write.
*
* Unit of expires is seconds.
+ *
+ * ### Example
+ *
+ * ```javascript
+ * const req = op.presignWrite(path, parseInt(expires));
+ *
+ * console.log("method: ", req.method)
+ * console.log("url: ", req.url)
+ * console.log("headers: ", req.headers)
+ * ```
*/
presignWrite(path: string, expires: number): PresignedRequest
/**
* Get a presigned request for stat.
*
* Unit of expires is seconds.
+ *
+ * ### Example
+ *
+ * ```javascript
+ * const req = op.presignStat(path, parseInt(expires));
+ *
+ * console.log("method: ", req.method)
+ * console.log("url: ", req.url)
+ * console.log("headers: ", req.headers)
+ * ```
*/
presignStat(path: string, expires: number): PresignedRequest
}
diff --git a/bindings/nodejs/package.json b/bindings/nodejs/package.json
index 798dd208..db8fbdc6 100644
--- a/bindings/nodejs/package.json
+++ b/bindings/nodejs/package.json
@@ -49,7 +49,7 @@
"@swc/core": "^1.3.38",
"@types/node": "^18.14.5",
"prettier": "^2.8.4",
- "typedoc": "^0.23.26",
+ "typedoc": "^0.23.28",
"typescript": "^5.0.2"
},
"engines": {
diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs
index f3178906..d95c7aa2 100644
--- a/bindings/nodejs/src/lib.rs
+++ b/bindings/nodejs/src/lib.rs
@@ -102,6 +102,22 @@ impl Operator {
}
/// Get current path's metadata **without cache** directly.
+ ///
+ /// ### Notes
+ /// Use stat if you:
+ ///
+ /// - Want detect the outside changes of 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)?;
@@ -110,6 +126,14 @@ impl Operator {
}
/// 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)?;
@@ -117,13 +141,58 @@ impl Operator {
Ok(Metadata(meta))
}
+ /// Check if this operator can work correctly.
+ ///
+ /// We will send a `list` request to 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 if this path exists or not.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// await op.isExist("test");
+ /// ```
+ #[napi]
+ pub async fn is_exist(&self, path: String) -> Result<bool> {
+ self.0.is_exist(&path).await.map_err(format_napi_error)
+ }
+
+ /// Check if this path exists or not synchronously.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// op.isExistSync("test");
+ /// ```
+ #[napi]
+ pub fn is_exist_sync(&self, path: String) -> Result<bool> {
+ self.0.blocking().is_exist(&path).map_err(format_napi_error)
+ }
+
/// Create dir with 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 given path synchronously.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// op.createDirSync("path/to/dir/");
+ /// ```
#[napi]
pub fn create_dir_sync(&self, path: String) -> Result<()> {
self.0
@@ -133,20 +202,45 @@ impl Operator {
}
/// Write bytes into path.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// await op.write("path/to/file", Buffer.from("hello world"));
+ /// // or
+ /// await op.write("path/to/file", "hello world");
+ /// ```
#[napi]
pub async fn write(&self, path: String, content: Either<Buffer, String>) -> Result<()> {
- let c = content.as_ref().to_owned();
+ 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)
}
/// Write bytes into path synchronously.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// op.writeSync("path/to/file", Buffer.from("hello world"));
+ /// // or
+ /// op.writeSync("path/to/file", "hello world");
+ /// ```
#[napi]
pub fn write_sync(&self, path: String, content: Either<Buffer, String>) -> Result<()> {
- let c = content.as_ref().to_owned();
+ 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)
}
/// 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)?;
@@ -154,6 +248,11 @@ impl Operator {
}
/// 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)?;
@@ -161,12 +260,50 @@ impl Operator {
}
/// List dir in flat way.
+ ///
+ /// This function will create a new handle to list entries.
+ ///
+ /// An error will be returned if given path doesn’t end with /.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// const lister = await op.scan("/path/to/dir/");
+ /// while (true)) {
+ /// const entry = await lister.next();
+ /// if (entry === null) {
+ /// break;
+ /// }
+ /// let meta = await op.stat(entry.path);
+ /// if (meta.is_file) {
+ /// // do something
+ /// }
+ /// }
+ /// `````
#[napi]
pub async fn scan(&self, path: String) -> Result<Lister> {
Ok(Lister(self.0.scan(&path).await.map_err(format_napi_error)?))
}
/// List dir in flat way synchronously.
+ ///
+ /// This function will create a new handle to list entries.
+ ///
+ /// An error will be returned if given path doesn’t end with /.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// const lister = op.scan_sync(/path/to/dir/");
+ /// while (true)) {
+ /// const entry = lister.next();
+ /// if (entry === null) {
+ /// break;
+ /// }
+ /// let meta = op.statSync(entry.path);
+ /// if (meta.is_file) {
+ /// // do something
+ /// }
+ /// }
+ /// `````
#[napi]
pub fn scan_sync(&self, path: String) -> Result<BlockingLister> {
Ok(BlockingLister(
@@ -175,22 +312,78 @@ impl Operator {
}
/// 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.remove(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 given path.
///
/// This function will create a new handle to list entries.
///
/// An error will be returned if given path doesn't end with `/`.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// const lister = await op.list("path/to/dir/");
+ /// while (true)) {
+ /// const entry = await lister.next();
+ /// if (entry === null) {
+ /// break;
+ /// }
+ /// let meta = await op.stat(entry.path);
+ /// if (meta.isFile) {
+ /// // do something
+ /// }
+ /// }
+ /// ```
#[napi]
pub async fn list(&self, path: String) -> Result<Lister> {
Ok(Lister(self.0.list(&path).await.map_err(format_napi_error)?))
@@ -201,6 +394,21 @@ impl Operator {
/// This function will create a new handle to list entries.
///
/// An error will be returned if given path doesn't end with `/`.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// const lister = op.listSync("path/to/dir/");
+ /// while (true)) {
+ /// const entry = lister.next();
+ /// if (entry === null) {
+ /// break;
+ /// }
+ /// let meta = op.statSync(entry.path);
+ /// if (meta.isFile) {
+ /// // do something
+ /// }
+ /// }
+ /// ```
#[napi]
pub fn list_sync(&self, path: String) -> Result<BlockingLister> {
Ok(BlockingLister(
@@ -211,6 +419,16 @@ impl Operator {
/// Get a presigned request for read.
///
/// Unit of expires is seconds.
+ ///
+ /// ### Example
+ ///
+ /// ```javascript
+ /// const req = op.presignRead(path, parseInt(expires));
+ ///
+ /// console.log("method: ", req.method);
+ /// console.log("url: ", req.url);
+ /// console.log("headers: ", req.headers);
+ /// ```
#[napi]
pub fn presign_read(&self, path: String, expires: u32) -> Result<PresignedRequest> {
let res = self
@@ -223,6 +441,16 @@ impl Operator {
/// Get a presigned request for write.
///
/// Unit of expires is seconds.
+ ///
+ /// ### Example
+ ///
+ /// ```javascript
+ /// const req = op.presignWrite(path, parseInt(expires));
+ ///
+ /// console.log("method: ", req.method);
+ /// console.log("url: ", req.url);
+ /// console.log("headers: ", req.headers);
+ /// ```
#[napi]
pub fn presign_write(&self, path: String, expires: u32) -> Result<PresignedRequest> {
let res = self
@@ -235,6 +463,16 @@ impl Operator {
/// Get a presigned request for stat.
///
/// Unit of expires is seconds.
+ ///
+ /// ### Example
+ ///
+ /// ```javascript
+ /// const req = op.presignStat(path, parseInt(expires));
+ ///
+ /// console.log("method: ", req.method);
+ /// console.log("url: ", req.url);
+ /// console.log("headers: ", req.headers);
+ /// ```
#[napi]
pub fn presign_stat(&self, path: String, expires: u32) -> Result<PresignedRequest> {
let res = self
diff --git a/bindings/nodejs/yarn.lock b/bindings/nodejs/yarn.lock
index 26ef0884..9914c06a 100644
--- a/bindings/nodejs/yarn.lock
+++ b/bindings/nodejs/yarn.lock
@@ -883,7 +883,7 @@ __metadata:
"@swc/core": ^1.3.38
"@types/node": ^18.14.5
prettier: ^2.8.4
- typedoc: ^0.23.26
+ typedoc: ^0.23.28
typescript: ^5.0.2
languageName: unknown
linkType: soft
@@ -1140,19 +1140,19 @@ __metadata:
languageName: node
linkType: hard
-"typedoc@npm:^0.23.26":
- version: 0.23.26
- resolution: "typedoc@npm:0.23.26"
+"typedoc@npm:^0.23.28":
+ version: 0.23.28
+ resolution: "typedoc@npm:0.23.28"
dependencies:
lunr: ^2.3.9
marked: ^4.2.12
minimatch: ^7.1.3
shiki: ^0.14.1
peerDependencies:
- typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x
+ typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x
bin:
typedoc: bin/typedoc
- checksum: 09dbd221b5bd27a7f6c593a6aa7e4efc3c46f20761e109a76bf0ed7239011cca1261357094710c01472582060d75a7558aab5bf5b78db3aff7c52188d146ee65
+ checksum: 40eb4e207aac1b734e09400cf03f543642cc7b11000895198dd5a0d3166315759ccf4ac30a2915153597c5c186101c72bac2f1fc12b428184a9274d3a0e44c5e
languageName: node
linkType: hard