diff --git a/.gitignore b/.gitignore index bb88442b2..486fbb303 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules *.exe pangea-node-sdk-*.tgz *.gz +packages/pangea-node-sdk/download/ \ No newline at end of file diff --git a/examples/.examples-ci.yml b/examples/.examples-ci.yml index e8db113c4..220a4ff8d 100644 --- a/examples/.examples-ci.yml +++ b/examples/.examples-ci.yml @@ -14,6 +14,7 @@ examples-tests: - "intel" - "redact" - "vault" + - "share" image: node:${NODE_VERSION} before_script: - export PANGEA_AUDIT_CONFIG_ID="${PANGEA_AUDIT_CONFIG_ID_1_LVE_AWS}" @@ -34,6 +35,7 @@ examples-tests: - export PANGEA_URL_INTEL_TOKEN="${PANGEA_INTEGRATION_TOKEN_LVE_AWS}" - export PANGEA_USER_INTEL_TOKEN="${PANGEA_INTEGRATION_TOKEN_LVE_AWS}" - export PANGEA_VAULT_TOKEN="${PANGEA_INTEGRATION_TOKEN_LVE_AWS}" + - export PANGEA_SHARE_TOKEN="${PANGEA_INTEGRATION_TOKEN_LVE_AWS}" - pushd packages/pangea-node-sdk - tar -xf pangea-node-sdk-v*.tgz --strip-components 1 -C . - popd diff --git a/examples/file_scan/package.json b/examples/file_scan/package.json index 597c224fc..162336179 100644 --- a/examples/file_scan/package.json +++ b/examples/file_scan/package.json @@ -7,7 +7,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Pangea", - "license": "ISC", + "license": "MIT", "dependencies": { "pangea-node-sdk": "3.7.0" } diff --git a/examples/intel/package.json b/examples/intel/package.json index 597c224fc..162336179 100644 --- a/examples/intel/package.json +++ b/examples/intel/package.json @@ -7,7 +7,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Pangea", - "license": "ISC", + "license": "MIT", "dependencies": { "pangea-node-sdk": "3.7.0" } diff --git a/examples/redact/package.json b/examples/redact/package.json index c83333594..964570095 100644 --- a/examples/redact/package.json +++ b/examples/redact/package.json @@ -7,7 +7,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Pangea", - "license": "ISC", + "license": "MIT", "dependencies": { "pangea-node-sdk": "3.7.0" } diff --git a/examples/share/README.md b/examples/share/README.md new file mode 100644 index 000000000..7f9e4a521 --- /dev/null +++ b/examples/share/README.md @@ -0,0 +1,12 @@ +# Pangea Store Service Example + +## Setup + +Set up environment variables ([Instructions](https://pangea.cloud/docs/getting-started/integrate/#set-environment-variables)) `PANGEA_SHARE_TOKEN` and `PANGEA_DOMAIN` with your project token configured on Pangea User Console (token should have access to Store service [Instructions](https://pangea.cloud/docs/getting-started/configure-services/#configure-a-pangea-service)) and with your Pangea domain. + +## Run example + +``` +yarn install +node folder_create_n_delete.mjs +``` diff --git a/examples/share/folder_create_and_delete.mjs b/examples/share/folder_create_and_delete.mjs new file mode 100644 index 000000000..1e9ea821f --- /dev/null +++ b/examples/share/folder_create_and_delete.mjs @@ -0,0 +1,35 @@ +/* eslint-disable no-console */ + +import { PangeaConfig, ShareService } from "pangea-node-sdk"; + +// Load Pangea token and domain from environment variables +const token = process.env.PANGEA_SHARE_TOKEN; +const config = new PangeaConfig({ domain: process.env.PANGEA_DOMAIN }); + +// Create Share client +const client = new ShareService(token, config); + +// Create unique folder path +const time = Math.round(Date.now() / 1000); +const folderPath = "/sdk_examples/node/delete/" + time; + +(async () => { + try { + console.log("Creating folder..."); + const respCreate = await client.folderCreate({ + path: folderPath, + }); + + const id = respCreate.result.object.id; + console.log(`Folder create success. Folder ID: ${id}`); + + console.log("Deleting folder..."); + const respDelete = await client.delete({ + id: id, + }); + + console.log(`Deleted ${respDelete.result.count} item(s)`); + } catch (e) { + console.log(e); + } +})(); diff --git a/examples/share/item_life_cycle.mjs b/examples/share/item_life_cycle.mjs new file mode 100644 index 000000000..8caec0eab --- /dev/null +++ b/examples/share/item_life_cycle.mjs @@ -0,0 +1,215 @@ +/* eslint-disable no-console */ + +import { + PangeaConfig, + ShareService, + TransferMethod, + Share, +} from "pangea-node-sdk"; +import * as fs from "fs"; + +// Load Pangea token and domain from environment variables +const token = process.env.PANGEA_SHARE_TOKEN; +const config = new PangeaConfig({ domain: process.env.PANGEA_DOMAIN }); + +// Create Share client +const client = new ShareService(token, config); + +// Create unique folder path +const time = Math.round(Date.now() / 1000); +const folderPath = "/sdk_examples/node/delete/" + time; +const filepath = "./testfile.pdf"; + +(async () => { + try { + console.log("Creating folder..."); + // Create a folder + const respCreate = await client.folderCreate({ + path: folderPath, + }); + const folderID = respCreate.result.object.id; + console.log(`Create folder success. Folder ID: ${folderID}`); + + // # Upload a file with path as unique param + // Read file content as buffer + const data = fs.readFileSync(filepath); + + console.log("\nUploading file with path as unique field..."); + const path1 = folderPath + "/" + time + "_file_multipart_1"; + const respPutPath = await client.put( + { + path: path1, + transfer_method: TransferMethod.MULTIPART, + }, + { + file: data, + name: path1, + } + ); + + console.log(`Upload success. Item ID: ${respPutPath.result.object.id}`); + console.log(`\tParent ID: ${respPutPath.result.object.parent_id}`); + console.log(`\tMetadata: ${respPutPath.result.object.metadata}`); + console.log(`\tTags: ${respPutPath.result.object.tags}`); + + console.log("\nUploading file with parent_id and name..."); + // Upload a file with parent id and name + const name2 = time + "_file_multipart_2"; + const metadata = { field1: "value1", field2: "value2" }; + const tags = ["tag1", "tag2"]; + const respPutId = await client.put( + { + parent_id: folderID, + name: name2, + transfer_method: TransferMethod.MULTIPART, + metadata: metadata, + tags: tags, + }, + { + file: data, + name: path1, + } + ); + + console.log(`Upload success. Item ID: ${respPutId.result.object.id}`); + console.log(`\tParent ID: ${respPutId.result.object.parent_id}`); + console.log(`\tMetadata: ${respPutId.result.object.metadata}`); + console.log(`\tTags: ${respPutId.result.object.tags}`); + + console.log("\nUpdating file with full metadata and tags..."); + // Update file with full metadata and tags + const respUpdate = await client.update({ + id: respPutPath.result.object.id, + metadata: metadata, + tags: tags, + }); + + console.log(`Upload success. Item ID: ${respUpdate.result.object.id}`); + console.log(`\tParent ID: ${respUpdate.result.object.parent_id}`); + console.log(`\tMetadata: ${respUpdate.result.object.metadata}`); + console.log(`\tTags: ${respUpdate.result.object.tags}`); + + console.log("\nUpdating file with additional metadata and tags..."); + // Update file with added metadata and tags + const addMetadata = { field3: "value3" }; + const addTags = ["tag3"]; + + const respUpdateAdd = await client.update({ + id: respPutPath.result.object.id, + add_metadata: addMetadata, + add_tags: addTags, + }); + + console.log(`Upload success. Item ID: ${respUpdateAdd.result.object.id}`); + console.log(`\tParent ID: ${respUpdateAdd.result.object.parent_id}`); + console.log(`\tMetadata: ${respUpdateAdd.result.object.metadata}`); + console.log(`\tTags: ${respUpdateAdd.result.object.tags}`); + + console.log("\nGetting archive with multipart transfer method..."); + // Get archive + const respGetArchive1 = await client.getArchive({ + ids: [folderID], + format: Share.ArchiveFormat.ZIP, + transfer_method: TransferMethod.MULTIPART, + }); + + // Using multipart as transfer method it should return just 1 file and no dest url + console.log( + `Got ${respGetArchive1.attachedFiles.length} attached file(s).` + ); + console.log(`Got URL: ${respGetArchive1.result.dest_url}`); + + // Saving attached files + respGetArchive1.attachedFiles.forEach((file) => { + file.save("./"); + }); + + console.log("\nGetting archive with dest-url transfer method..."); + const respGetArchive2 = await client.getArchive({ + ids: [folderID], + format: Share.ArchiveFormat.TAR, + transfer_method: TransferMethod.DEST_URL, + }); + + // Using dest-url as transfer method it should return no attahched files but dest url + console.log( + `Got ${respGetArchive2.attachedFiles.length} attached file(s).` + ); + console.log(`Got URL: ${respGetArchive2.result.dest_url}`); + + console.log("\nDownloading file..."); + // Download file + const url = respGetArchive2.result.dest_url ?? ""; + let downloadedFile = await client.downloadFile(url); + + console.log("\nCreating share link..."); + // Create share link + // Create authenticators list to access share link + const authenticators = [ + { + auth_type: Share.AuthenticatorType.PASSWORD, + auth_context: "somepassword", + }, + ]; + + // Create a link's list, each link should have targets (folder or objects), link_type, etc. + const linkList = [ + { + targets: [folderID], + link_type: Share.LinkType.EDITOR, + max_access_count: 3, + authenticators: authenticators, + }, + ]; + + // Send share link create request + const respCreateLink = await client.shareLinkCreate({ + links: linkList, + }); + + const links = respCreateLink.result.share_link_objects; + console.log(`Created ${links.length} share links`); + + const link = links[0]; + console.log(`Link ID: ${link.id}. Link: ${link.link}`); + + console.log("\nGetting already created link..."); + // Get share link + const linkID = link?.id ?? ""; + const respGetLink = await client.shareLinkGet({ + id: linkID, + }); + console.log( + `Got link ID: ${respGetLink.result.share_link_object.id}. Link: ${respGetLink.result.share_link_object.link}` + ); + + console.log("\nListing share links..."); + // List share link + const respListLink = await client.shareLinkList(); + console.log(`Got ${respListLink.result.count} link(s)`); + + console.log("\nDeleting link..."); + // Delete share link + const respDeleteLink = await client.shareLinkDelete({ + ids: [linkID], + }); + console.log( + `Deleted ${respDeleteLink.result.share_link_objects.length} link(s)` + ); + + console.log("\nListing files..."); + // List files in folder + // Create a ListFilter and set its possible values + const listFilter = { + folder: folderPath, + }; + + const respList = await client.list({ + filter: listFilter, + }); + + console.log(`Got ${respList.result.count} item(s)`); + } catch (e) { + console.log(e.toString()); + } +})(); diff --git a/examples/share/package.json b/examples/share/package.json new file mode 100644 index 000000000..309a74fe7 --- /dev/null +++ b/examples/share/package.json @@ -0,0 +1,14 @@ +{ + "name": "share_examples", + "version": "2.0.0", + "description": "Pangea Share example", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Pangea", + "license": "MIT", + "dependencies": { + "pangea-node-sdk": "file:../../packages/pangea-node-sdk" + } +} diff --git a/examples/share/put_split_upload.mjs b/examples/share/put_split_upload.mjs new file mode 100644 index 000000000..2c45a801e --- /dev/null +++ b/examples/share/put_split_upload.mjs @@ -0,0 +1,93 @@ +/* eslint-disable no-console */ + +import { + PangeaConfig, + ShareService, + TransferMethod, + FileUploader, +} from "pangea-node-sdk"; +import * as fs from "fs"; + +// Load Pangea token and domain from environment variables +const token = process.env.PANGEA_SHARE_TOKEN; +const config = new PangeaConfig({ domain: process.env.PANGEA_DOMAIN }); + +// Create Share client +const client = new ShareService(token, config); + +// Create unique folder path +const time = Math.round(Date.now() / 1000); +const filepath = "./testfile.pdf"; + +// Auxiliary function +const delay = async (ms) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +(async () => { + try { + console.log("Request upload url with put transfer method..."); + let response; + // Create a file unique name + const name = time + "_file_split_put_url"; + try { + // Request upload url with put-url transfer method + response = await client.requestUploadURL({ + transfer_method: TransferMethod.PUT_URL, + name: name, + }); + } catch (e) { + console.log(e.toString()); + process.exit(1); + } + + const url = response.accepted_result?.put_url || ""; + console.log(`Got URL: ${url}`); + + // Create FileUploader client + const uploader = new FileUploader(); + + // Read file content as buffer + const data = fs.readFileSync(filepath); + + console.log("Uploading file..."); + // Upload the file to received url + await uploader.uploadFile( + url, + { + file: data, + name: "file", + }, + { + transfer_method: TransferMethod.PUT_URL, + } + ); + + const maxRetry = 12; + let retry; + for (retry = 0; retry < maxRetry; retry++) { + try { + console.log(`Polling result. Retry: ${retry}`); + // Wait until result could be ready + await delay(10 * 1000); + const request_id = response.request_id || ""; + response = await client.pollResult(request_id); + + console.log( + `Poll result success. Item ID: ${response.result.object.id}` + ); + break; + } catch { + console.log("Result is not ready yet."); + } + } + + if (retry >= maxRetry) { + console.log("Failed to poll result. Reached max retry."); + } + } catch (e) { + console.log(e.toString()); + process.exit(1); + } +})(); diff --git a/examples/share/put_transfer_method_multipart.mjs b/examples/share/put_transfer_method_multipart.mjs new file mode 100644 index 000000000..77100bc14 --- /dev/null +++ b/examples/share/put_transfer_method_multipart.mjs @@ -0,0 +1,42 @@ +/* eslint-disable no-console */ + +import { PangeaConfig, ShareService, TransferMethod } from "pangea-node-sdk"; +import * as fs from "fs"; + +// Load Pangea token and domain from environment variables +const token = process.env.PANGEA_SHARE_TOKEN; +const config = new PangeaConfig({ domain: process.env.PANGEA_DOMAIN }); + +// Create Share client +const client = new ShareService(token, config); + +// Create unique folder path +const time = Math.round(Date.now() / 1000); +const filepath = "./testfile.pdf"; + +(async () => { + try { + console.log("Uploading file with multipart transfer method..."); + // Create a unique name + const name = time + "_file_multipart"; + + // Read file content as buffer + const data = fs.readFileSync(filepath); + + // Send Put request setting transfer_method to multipart + const respPut = await client.put( + { + name: name, + transfer_method: TransferMethod.MULTIPART, + }, + { + file: data, + name: name, + } + ); + + console.log(`Upload success. Item ID: ${respPut.result.object.id}`); + } catch (e) { + console.log(e); + } +})(); diff --git a/examples/share/put_transfer_method_post_url.mjs b/examples/share/put_transfer_method_post_url.mjs new file mode 100644 index 000000000..ee1f58462 --- /dev/null +++ b/examples/share/put_transfer_method_post_url.mjs @@ -0,0 +1,43 @@ +/* eslint-disable no-console */ + +import { PangeaConfig, ShareService, TransferMethod } from "pangea-node-sdk"; +import * as fs from "fs"; + +// Load Pangea token and domain from environment variables +const token = process.env.PANGEA_SHARE_TOKEN; +const config = new PangeaConfig({ domain: process.env.PANGEA_DOMAIN }); + +// Create Share client +const client = new ShareService(token, config); + +// Create unique folder path +const time = Math.round(Date.now() / 1000); +const filepath = "./testfile.pdf"; + +(async () => { + try { + console.log("Uploading file with post-url transfer method..."); + // Create a unique name + const name = time + "_file_post_url"; + + // Read file content as buffer + const data = fs.readFileSync(filepath); + + // Send Put request setting transfer_method to post-url + // SDK will request an upload url, post the file to that url and then poll the upload result to Share service + const respPut = await client.put( + { + name: name, + transfer_method: TransferMethod.POST_URL, + }, + { + file: data, + name: name, + } + ); + + console.log(`Upload success. Item ID: ${respPut.result.object.id}`); + } catch (e) { + console.log(e.toString()); + } +})(); diff --git a/examples/share/testfile.pdf b/examples/share/testfile.pdf new file mode 100644 index 000000000..267047742 Binary files /dev/null and b/examples/share/testfile.pdf differ diff --git a/examples/share/yarn.lock b/examples/share/yarn.lock new file mode 100644 index 000000000..f3065a1bc --- /dev/null +++ b/examples/share/yarn.lock @@ -0,0 +1,269 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aws-crypto/crc32c@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz#4e34aab7f419307821509a98b9b08e84e0c1917e" + integrity sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag== + dependencies: + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + tslib "^2.6.2" + +"@aws-crypto/util@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" + integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== + dependencies: + "@aws-sdk/types" "^3.222.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" + +"@aws-sdk/types@^3.222.0": + version "3.533.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.533.0.tgz#4c4ade8f41f153295c69f1dea812dcd6154613e3" + integrity sha512-mFb0701oLRcJ7Y2unlrszzk9rr2P6nt2A4Bdz4K5WOsY4f4hsdbcYkrzA1NPmIUTEttU9JT0YG+8z0XxLEX4Aw== + dependencies: + "@smithy/types" "^2.11.0" + tslib "^2.5.0" + +"@sindresorhus/is@^5.2.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" + integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== + +"@smithy/is-array-buffer@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz#07b4c77ae67ed58a84400c76edd482271f9f957b" + integrity sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ== + dependencies: + tslib "^2.5.0" + +"@smithy/types@^2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.11.0.tgz#d40c27302151be243d3a7319a154b7d7d5775021" + integrity sha512-AR0SXO7FuAskfNhyGfSTThpLRntDI5bOrU0xrpVYU0rZyjl3LBXInZFMTP/NNSd7IS6Ksdtar0QvnrPRIhVrLQ== + dependencies: + tslib "^2.5.0" + +"@smithy/util-buffer-from@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz#f9346bf8b23c5ba6f6bdb61dd9db779441ba8d08" + integrity sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg== + dependencies: + "@smithy/is-array-buffer" "^2.1.1" + tslib "^2.5.0" + +"@smithy/util-utf8@^2.0.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.2.0.tgz#e352a81adc0491fbdc0086a00950d7e8333e211f" + integrity sha512-hBsKr5BqrDrKS8qy+YcV7/htmMGxriA1PREOf/8AGBhHIZnfilVv1Waf1OyKhSbFW15U/8+gcMUQ9/Kk5qwpHQ== + dependencies: + "@smithy/util-buffer-from" "^2.1.1" + tslib "^2.5.0" + +"@szmarczak/http-timer@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" + integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== + dependencies: + defer-to-connect "^2.0.1" + +"@types/http-cache-semantics@^4.0.2": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +cacheable-lookup@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" + integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + +cacheable-request@^10.2.8: + version "10.2.14" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" + integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== + dependencies: + "@types/http-cache-semantics" "^4.0.2" + get-stream "^6.0.1" + http-cache-semantics "^4.1.1" + keyv "^4.5.3" + mimic-response "^4.0.0" + normalize-url "^8.0.0" + responselike "^3.0.0" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +defer-to-connect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +form-data-encoder@^2.1.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" + integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +got@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/got/-/got-13.0.0.tgz#a2402862cef27a5d0d1b07c0fb25d12b58175422" + integrity sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA== + dependencies: + "@sindresorhus/is" "^5.2.0" + "@szmarczak/http-timer" "^5.0.1" + cacheable-lookup "^7.0.0" + cacheable-request "^10.2.8" + decompress-response "^6.0.0" + form-data-encoder "^2.1.2" + get-stream "^6.0.1" + http2-wrapper "^2.1.10" + lowercase-keys "^3.0.0" + p-cancelable "^3.0.0" + responselike "^3.0.0" + +http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http2-wrapper@^2.1.10: + version "2.2.1" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" + integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.2.0" + +js-sha3@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +lowercase-keys@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" + integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== + +merkle-tools@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merkle-tools/-/merkle-tools-1.4.1.tgz#d08799886a6d51f5ee2bf0195f967b3cc3afd62c" + integrity sha512-QhO1/eDvAnyn0oXgRWlydVWYVMrVJwrdNICYvQXYhBU1Bjj1LoxsQxdAKJ5ttN3L6pkKhjcK6O4k927kgTMdqw== + dependencies: + js-sha3 "^0.8.0" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +mimic-response@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" + integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== + +normalize-url@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.0.tgz#593dbd284f743e8dcf6a5ddf8fadff149c82701a" + integrity sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw== + +p-cancelable@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" + integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== + +"pangea-node-sdk@file:../../packages/pangea-node-sdk": + version "3.7.0" + dependencies: + "@aws-crypto/crc32c" "^5.2.0" + crypto-js "^4.2.0" + form-data "^4.0.0" + got "^13.0.0" + merkle-tools "^1.4.1" + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +resolve-alpn@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + +responselike@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" + integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== + dependencies: + lowercase-keys "^3.0.0" + +tslib@^2.5.0, tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== diff --git a/packages/pangea-node-sdk/.gitignore b/packages/pangea-node-sdk/.gitignore index 87c4ddb28..462a3b820 100644 --- a/packages/pangea-node-sdk/.gitignore +++ b/packages/pangea-node-sdk/.gitignore @@ -11,6 +11,7 @@ .DS_Store *.pem yarn-error.log +*.tar # Editors .idea diff --git a/packages/pangea-node-sdk/.gitlab-ci.yml b/packages/pangea-node-sdk/.gitlab-ci.yml index ff32dbc46..2428ac49c 100644 --- a/packages/pangea-node-sdk/.gitlab-ci.yml +++ b/packages/pangea-node-sdk/.gitlab-ci.yml @@ -30,6 +30,7 @@ pangea-node-sdk-integration-tests: SERVICE_USER_INTEL_ENV: LVE SERVICE_REDACT_ENV: LVE SERVICE_VAULT_ENV: LVE + SERVICE_SHARE_ENV: LVE NODE_OPTIONS: "--experimental-vm-modules --openssl-legacy-provider" before_script: - echo $ENV @@ -83,7 +84,7 @@ pangea-node-sdk-integration-tests: - CLOUD: [AWS, GCP] ENV: ${SERVICE_FILE_INTEL_ENV} TEST: intel.file - - CLOUD: [AWS] + - CLOUD: [AWS, GCP] ENV: ${SERVICE_FILE_SCAN_ENV} TEST: file_scan - CLOUD: [AWS, GCP] @@ -92,6 +93,9 @@ pangea-node-sdk-integration-tests: - CLOUD: [AWS, GCP] ENV: ${SERVICE_VAULT_ENV} TEST: vault + - CLOUD: [AWS, GCP] + ENV: ${SERVICE_SHARE_ENV} + TEST: share rules: - if: $CI_COMMIT_BRANCH changes: @@ -100,15 +104,6 @@ pangea-node-sdk-integration-tests: script: - npx jest --testPathPattern=tests/integration/${TEST}.test.ts -pangea-node-sdk-integration-tests-may-fail: - extends: pangea-node-sdk-integration-tests - parallel: - matrix: - - CLOUD: [GCP] - ENV: ${SERVICE_FILE_SCAN_ENV} - TEST: file_scan - allow_failure: true - pangea-node-sdk-build: extends: .pangea-node-sdk-base stage: build diff --git a/packages/pangea-node-sdk/src/file_uploader.ts b/packages/pangea-node-sdk/src/file_uploader.ts new file mode 100644 index 000000000..8ed923618 --- /dev/null +++ b/packages/pangea-node-sdk/src/file_uploader.ts @@ -0,0 +1,43 @@ +import PangeaRequest from "@src/request.js"; +import { FileData, TransferMethod } from "@src/types.js"; +import PangeaConfig from "@src/config.js"; + +export class FileUploader { + protected serviceName: string = "FileUploader"; + protected request_: PangeaRequest | undefined = undefined; + + constructor() {} + + private get request(): PangeaRequest { + if (this.request_) { + return this.request_; + } + + this.request_ = new PangeaRequest( + this.serviceName, + "notatoken", + new PangeaConfig() + ); + return this.request_; + } + + // TODO: Docs + public async uploadFile( + url: string, + fileData: FileData, + options: { + transfer_method?: TransferMethod; + } + ) { + if ( + !options.transfer_method || + options.transfer_method === TransferMethod.PUT_URL + ) { + await this.request.putPresignedURL(url, fileData); + } else if (options.transfer_method === TransferMethod.POST_URL) { + await this.request.postPresignedURL(url, fileData); + } + } +} + +export default FileUploader; diff --git a/packages/pangea-node-sdk/src/index.ts b/packages/pangea-node-sdk/src/index.ts index e53af2292..0540feeec 100755 --- a/packages/pangea-node-sdk/src/index.ts +++ b/packages/pangea-node-sdk/src/index.ts @@ -22,6 +22,7 @@ export { } from "./utils/utils.js"; export { FileScanUploader } from "./services/file_scan.js"; +export { FileUploader } from "./file_uploader.js"; export const PangeaConfig = _PangeaConfig; export const PangeaRequest = _PangeaRequest; @@ -39,3 +40,4 @@ export const URLIntelService = services.URLIntelService; export const UserIntelService = services.UserIntelService; export const VaultService = services.VaultService; export const FileScanService = services.FileScanService; +export const ShareService = services.ShareService; diff --git a/packages/pangea-node-sdk/src/request.ts b/packages/pangea-node-sdk/src/request.ts index 601cb6d4e..c8e369d85 100644 --- a/packages/pangea-node-sdk/src/request.ts +++ b/packages/pangea-node-sdk/src/request.ts @@ -95,8 +95,12 @@ class PangeaRequest { contentDispositionHeader: string | string[] | undefined ): string | undefined { let contentDisposition = ""; - if (Array.isArray(contentDispositionHeader)) { + if (contentDispositionHeader === undefined) { + return undefined; + } else if (Array.isArray(contentDispositionHeader)) { contentDisposition = contentDispositionHeader[0] ?? contentDisposition; + } else { + contentDisposition = contentDispositionHeader; } return getHeaderField(contentDisposition, "filename", undefined); @@ -180,6 +184,10 @@ class PangeaRequest { fileData: FileData ): Promise { const response = await this.requestPresignedURL(endpoint, data); + if (response.success && response.gotResponse) { + return response.gotResponse; + } + if (!response.gotResponse || !response.accepted_result?.post_url) { throw new PangeaErrors.PangeaError( "Failed to request post presigned URL" @@ -212,6 +220,7 @@ class PangeaRequest { } } + // Right now, only accept the file with name "file" form.append("file", this.getFileToForm(fileData.file), { contentType: "application/octet-stream", }); @@ -272,10 +281,9 @@ class PangeaRequest { } try { - await this.post(endpoint, data, { + return await this.post(endpoint, data, { pollResultSync: false, }); - throw new PangeaErrors.PangeaError("This call should return 202"); } catch (error) { if (!(error instanceof PangeaErrors.AcceptedRequestException)) { throw error; diff --git a/packages/pangea-node-sdk/src/response.ts b/packages/pangea-node-sdk/src/response.ts index c65abd678..5e1e726d8 100644 --- a/packages/pangea-node-sdk/src/response.ts +++ b/packages/pangea-node-sdk/src/response.ts @@ -40,7 +40,7 @@ export class AttachedFile { destFolder = "."; } if (!filename) { - filename = this.filename ? this.filename : "defaultName.txt"; + filename = this.filename ? this.filename : "defaultSaveFilename"; } if (!fs.existsSync(destFolder)) { // If it doesn't exist, create it diff --git a/packages/pangea-node-sdk/src/services/index.ts b/packages/pangea-node-sdk/src/services/index.ts index 8c970ecee..12b26b16f 100644 --- a/packages/pangea-node-sdk/src/services/index.ts +++ b/packages/pangea-node-sdk/src/services/index.ts @@ -12,6 +12,7 @@ import { } from "./intel.js"; import VaultService from "./vault.js"; import { FileScanService } from "./file_scan.js"; +import ShareService from "./share.js"; export default { AuditService, @@ -26,4 +27,5 @@ export default { UserIntelService, VaultService, FileScanService, + ShareService: ShareService, }; diff --git a/packages/pangea-node-sdk/src/services/share.ts b/packages/pangea-node-sdk/src/services/share.ts new file mode 100644 index 000000000..ae15fb83d --- /dev/null +++ b/packages/pangea-node-sdk/src/services/share.ts @@ -0,0 +1,351 @@ +import PangeaResponse from "@src/response.js"; +import BaseService from "./base.js"; +import PangeaConfig from "@src/config.js"; +import { + Share, + FileData, + TransferMethod, + FileUploadParams, +} from "@src/types.js"; +import { PangeaErrors } from "@src/errors.js"; +import { getFileUploadParams } from "@src/index.js"; +import { getFileSize } from "@src/utils/utils.js"; + +/** + * ShareService class provides methods for interacting with the Share Service + * @extends BaseService + */ +class ShareService extends BaseService { + constructor(token: string, config: PangeaConfig) { + super("share", token, config); + } + + /** + * @summary Delete + * @description Delete object by ID or path. If both are supplied, the path must match that of the object represented by the ID. Beta API. + * @operationId share_post_v1beta_delete + * @param {Share.DeleteRequest} request + * @returns {Promise} - A promise representing an async call to the delete endpoint. + * @example + * ```js + * const request = { id: "pos_3djfmzg2db4c6donarecbyv5begtj2bm" }; + * const response = await client.delete(request); + * ``` + */ + delete( + request: Share.DeleteRequest + ): Promise> { + return this.post("v1beta/delete", request); + } + + /** + * @summary Create a folder + * @description Create a folder, either by name or path and parent_id. Beta API. + * @operationId share_post_v1beta_folder_create + * @param {Share.FolderCreateRequest} request + * @returns {Promise} - A promise representing an async call to the folder create endpoint. + * @example + * ```js + * const request = { + * metadata: { + * created_by: "jim", + * priority: "medium", + * }, + * parent_id: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", + * path: "/", + * tags: ["irs_2023", "personal"], + * }; + * + * const response = await client.folderCreate(request); + * ``` + */ + folderCreate( + request: Share.FolderCreateRequest + ): Promise> { + return this.post("v1beta/folder/create", request); + } + + /** + * @summary Get an object + * @description Get object. If both ID and path are supplied, the call will fail if the target object doesn't match both properties. Beta API. + * @operationId share_post_v1beta_get + * @param {Share.GetRequest} request + * @returns {Promise} - A promise representing an async call to the get item endpoint. + * @example + * ```js + * const request = { + * id: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", + * path: "/", + * }; + * + * const response = await client.getItem(request); + * ``` + */ + getItem(request: Share.GetRequest): Promise> { + return this.post("v1beta/get", request); + } + + /** + * @summary Get archive + * @description Get an archive file of multiple objects. Beta API. + * @operationId share_post_v1beta_get_archive + * @param {Share.GetArchiveRequest} request + * @returns {Promise} - A promise representing an async call to the get archive endpoint. + * @example + * ```js + * const request = { ids: ["pos_3djfmzg2db4c6donarecbyv5begtj2bm"] }; + * const response = await client.getArchive(request); + * ``` + */ + getArchive( + request: Share.GetArchiveRequest + ): Promise> { + return this.post("v1beta/get_archive", request); + } + + /** + * @summary List + * @description List or filter/search records. Beta API. + * @operationId share_post_v1beta_list + * @param {Share.ListRequest} request + * @returns {Promise} - A promise representing an async call to the list endpoint. + * @example + * ```js + * const request = {}; + * const response = await client.list(request); + * ``` + */ + list( + request: Share.ListRequest = {} + ): Promise> { + return this.post("v1beta/list", request); + } + + /** + * @summary Upload a file + * @description Upload a file. Beta API. + * @operationId share_post_v1beta_put + * @param {Share.PutRequest} request + * @param {FileData} fileData + * @returns {Promise} - A promise representing an async call to the put endpoint. + * @example + * ```js + * const request = { + * transfer_method: TransferMethod.MULTIPART, + * Metadata: { + * created_by: "jim", + * priority: "medium", + * }, + * parent_id: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", + * path: "/", + * tags: ["irs_2023", "personal"], + * }; + * const file = fs.readFileSync("./path/to/file.pdf"); + * const fileData = { + * file, + * name: "file", + * }; + * + * const response = await client.put(request, fileData); + * ``` + */ + put( + request: Share.PutRequest, + fileData: FileData + ): Promise> { + let fsData = {} as FileUploadParams; + + if ( + !request.transfer_method || + request.transfer_method === TransferMethod.POST_URL + ) { + fsData = getFileUploadParams(fileData.file); + request.crc32c = fsData.crc32c; + request.sha256 = fsData.sha256; + request.size = fsData.size; + } else if (getFileSize(fileData.file) == 0) { + request.size = 0; + } + + return this.post("v1beta/put", request, { + files: { + file: fileData, + }, + }); + } + + /** + * @summary Request upload URL + * @description Request an upload URL. Beta API. + * @operationId share_post_v1beta_put 2 + * @param {Share.PutRequest} request + * @returns {Promise} - A promise representing an async call to the put endpoint. + * @example + * ```js + * const { crc32c, sha256, size } = getFileUploadParams("./path/to/file.pdf"); + * + * const request = { + * transfer_method: TransferMethod.POST_URL, + * crc32c, + * sha256, + * size, + * Metadata: { + * created_by: "jim", + * priority: "medium", + * }, + * parent_id: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", + * path: "/", + * tags: ["irs_2023", "personal"], + * }; + * + * const response = await client.requestUploadURL(request); + * ``` + */ + requestUploadURL( + request: Share.PutRequest + ): Promise> { + if ( + request.transfer_method === TransferMethod.POST_URL && + (!request.size || !request.crc32c || !request.sha256) + ) { + throw new PangeaErrors.PangeaError( + `When transfer_method is ${request.transfer_method}, crc32c, sha256 and size must be set. Set them or use transfer_method ${TransferMethod.PUT_URL}` + ); + } + + return this.request.requestPresignedURL("v1beta/put", request); + } + + /** + * @summary Update a file + * @description Update a file. Beta API. + * @operationId share_post_v1beta_update + * @param {Share.UpdateRequest} request + * @returns {Promise} - A promise representing an async call to the update endpoint. + * @example + * ```js + * const request = { + * id: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", + * path: "/", + * remove_metadata: { + * created_by: "jim", + * priority: "medium", + * } + * remove_tags: ["irs_2023", "personal"], + * }; + * + * const response = await client.update(request); + * ``` + */ + update( + request: Share.UpdateRequest + ): Promise> { + return this.post("v1beta/update", request); + } + + /** + * @summary Create share links + * @description Create a share link. Beta API. + * @operationId share_post_v1beta_share_link_create + * @param {Share.ShareLinkCreateRequest} request + * @returns {Promise} - A promise representing an async call to the share link create endpoint. + * @example + * ```js + * const authenticator = { + * auth_type: Share.AuthenticatorType.PASSWORD, + * auth_context: "my_fav_Pa55word", + * }; + * const link = { + * targets: ["pos_3djfmzg2db4c6donarecbyv5begtj2bm"], + * link_type: Share.LinkType.DOWNLOAD, + * authenticators: [authenticator], + * }; + * const request = { links: [link] }; + * const response = await client.shareLinkCreate(request); + * ``` + */ + shareLinkCreate( + request: Share.ShareLinkCreateRequest + ): Promise> { + return this.post("v1beta/share/link/create", request); + } + + /** + * @summary Get share link + * @description Get a share link. Beta API. + * @operationId share_post_v1beta_share_link_get + * @param {Share.ShareLinkGetRequest} request + * @returns {Promise} - A promise representing an async call to the share link get endpoint. + * @example + * ```js + * const request = { id: "psl_3djfmzg2db4c6donarecbyv5begtj2bm" }; + * const response = await client.shareLinkGet(request); + * ``` + */ + shareLinkGet( + request: Share.ShareLinkGetRequest + ): Promise> { + return this.post("v1beta/share/link/get", request); + } + + /** + * @summary List share links + * @description Look up share links by filter options. Beta API. + * @operationId share_post_v1beta_share_link_list + * @param {Share.ShareLinkListRequest} request + * @returns {Promise} - A promise representing an async call to the share link list endpoint. + * @example + * ```js + * const request = {}; + * const response = await client.shareLinkList(request); + * ``` + */ + shareLinkList( + request: Share.ShareLinkListRequest = {} + ): Promise> { + return this.post("v1beta/share/link/list", request); + } + + /** + * @summary Delete share links + * @description Delete share links. Beta API. + * @operationId share_post_v1beta_share_link_delete + * @param {Share.ShareLinkDeleteRequest} request + * @returns {Promise} - A promise representing an async call to the delete share links endpoint. + * @example + * ```js + * const request = { ids: ["psl_3djfmzg2db4c6donarecbyv5begtj2bm"] }; + * const response = await client.shareLinkDelete(request); + * ``` + */ + shareLinkDelete( + request: Share.ShareLinkDeleteRequest + ): Promise> { + return this.post("v1beta/share/link/delete", request); + } + + /** + * @summary Send share links + * @description Send share links. Beta API. + * @operationId share_post_v1beta_share_link_send + * @param {Share.ShareLinkDeleteRequest} request + * @returns {Promise} - A promise representing an async call to the send share links endpoint. + * @example + * ```js + * const resp = await client.shareLinkSend({ + * links: [{ + * id: linkID, + * email: "user@email.com", + * }], + * sender_email: "sender@email.com", + * sender_name: "Sender Name" + * }) + */ + shareLinkSend( + request: Share.ShareLinkSendRequest + ): Promise> { + return this.post("v1beta/share/link/send", request); + } +} + +export default ShareService; diff --git a/packages/pangea-node-sdk/src/types.ts b/packages/pangea-node-sdk/src/types.ts index 9b770559c..1d859df12 100644 --- a/packages/pangea-node-sdk/src/types.ts +++ b/packages/pangea-node-sdk/src/types.ts @@ -1989,3 +1989,812 @@ export namespace AuthN { export interface UpdateResult extends UserItem {} } } + +export namespace Share { + export enum FileFormat { + F3G2 = "3G2", + F3GP = "3GP", + F3MF = "3MF", + F7Z = "7Z", + A = "A", + AAC = "AAC", + ACCDB = "ACCDB", + AIFF = "AIFF", + AMF = "AMF", + AMR = "AMR", + APE = "APE", + ASF = "ASF", + ATOM = "ATOM", + AU = "AU", + AVI = "AVI", + AVIF = "AVIF", + BIN = "BIN", + BMP = "BMP", + BPG = "BPG", + BZ2 = "BZ2", + CAB = "CAB", + CLASS = "CLASS", + CPIO = "CPIO", + CRX = "CRX", + CSV = "CSV", + DAE = "DAE", + DBF = "DBF", + DCM = "DCM", + DEB = "DEB", + DJVU = "DJVU", + DLL = "DLL", + DOC = "DOC", + DOCX = "DOCX", + DWG = "DWG", + EOT = "EOT", + EPUB = "EPUB", + EXE = "EXE", + FDF = "FDF", + FITS = "FITS", + FLAC = "FLAC", + FLV = "FLV", + GBR = "GBR", + GEOJSON = "GEOJSON", + GIF = "GIF", + GLB = "GLB", + GML = "GML", + GPX = "GPX", + GZ = "GZ", + HAR = "HAR", + HDR = "HDR", + HEIC = "HEIC", + HEIF = "HEIF", + HTML = "HTML", + ICNS = "ICNS", + ICO = "ICO", + ICS = "ICS", + ISO = "ISO", + JAR = "JAR", + JP2 = "JP2", + JPF = "JPF", + JPG = "JPG", + JPM = "JPM", + JS = "JS", + JSON = "JSON", + JXL = "JXL", + JXR = "JXR", + KML = "KML", + LIT = "LIT", + LNK = "LNK", + LUA = "LUA", + LZ = "LZ", + M3U = "M3U", + M4A = "M4A", + MACHO = "MACHO", + MDB = "MDB", + MIDI = "MIDI", + MKV = "MKV", + MOBI = "MOBI", + MOV = "MOV", + MP3 = "MP3", + MP4 = "MP4", + MPC = "MPC", + MPEG = "MPEG", + MQV = "MQV", + MRC = "MRC", + MSG = "MSG", + MSI = "MSI", + NDJSON = "NDJSON", + NES = "NES", + ODC = "ODC", + ODF = "ODF", + ODG = "ODG", + ODP = "ODP", + ODS = "ODS", + ODT = "ODT", + OGA = "OGA", + OGV = "OGV", + OTF = "OTF", + OTG = "OTG", + OTP = "OTP", + OTS = "OTS", + OTT = "OTT", + OWL = "OWL", + P7S = "P7S", + PAT = "PAT", + PDF = "PDF", + PHP = "PHP", + PL = "PL", + PNG = "PNG", + PPT = "PPT", + PPTX = "PPTX", + PS = "PS", + PSD = "PSD", + PUB = "PUB", + PY = "PY", + QCP = "QCP", + RAR = "RAR", + RMVB = "RMVB", + RPM = "RPM", + RSS = "RSS", + RTF = "RTF", + SHP = "SHP", + SHX = "SHX", + SO = "SO", + SQLITE = "SQLITE", + SRT = "SRT", + SVG = "SVG", + SWF = "SWF", + SXC = "SXC", + TAR = "TAR", + TCL = "TCL", + TCX = "TCX", + TIFF = "TIFF", + TORRENT = "TORRENT", + TSV = "TSV", + TTC = "TTC", + TTF = "TTF", + TXT = "TXT", + VCF = "VCF", + VOC = "VOC", + VTT = "VTT", + WARC = "WARC", + WASM = "WASM", + WAV = "WAV", + WEBM = "WEBM", + WEBP = "WEBP", + WOFF = "WOFF", + WOFF2 = "WOFF2", + X3D = "X3D", + XAR = "XAR", + XCF = "XCF", + XFDF = "XFDF", + XLF = "XLF", + XLS = "XLS", + XLSX = "XLSX", + XML = "XML", + XPM = "XPM", + XZ = "XZ", + ZIP = "ZIP", + ZST = "ZST", + } + + export enum ArchiveFormat { + TAR = "tar", + ZIP = "zip", + } + + export enum LinkType { + UPLOAD = "upload", + DOWNLOAD = "download", + EDITOR = "editor", + } + export enum AuthenticatorType { + EMAIL_OTP = "email_otp", + PASSWORD = "password", + SMS_OTP = "sms_otp", + SOCIAL = "social", + } + + export enum ItemOrder { + ASC = "asc", + DESC = "desc", + } + + export enum ItemOrderBy { + ID = "id", + CREATED_AT = "created_at", + NAME = "name", + PARENT_ID = "parent_id", + TYPE = "type", + UPDATED_AT = "updated_at", + } + + export interface Metadata { + [key: string]: string; + } + + export type Tags = string[]; + + export interface ItemData { + /** + * The number of billable bytes (includes Metadata, Tags, etc.) for the + * object. + */ + billable_size?: number; + + /** The date and time the object was created. */ + created_at: string; + + /** The ID of a stored object. */ + id: string; + + /** The MD5 hash of the file contents. */ + md5?: string; + + /** + * A set of string-based key/value pairs used to provide additional data + * about an object. + */ + metadata?: Metadata; + name: string; + parent_id: string; + + /** The SHA256 hash of the file contents. */ + sha256?: string; + + /** The SHA512 hash of the file contents. */ + sha512?: string; + + /** The size of the object in bytes. */ + size?: number; + + /** A list of user-defined tags. */ + tags?: Tags; + + /** The type of the item (file or dir). */ + type: string; + + /** The date and time the object was last updated. */ + updated_at: string; + } + + export interface DeleteRequest { + /** + * The ID of the object to delete. + */ + id?: string; + + /** + * If true, delete a folder even if it's not empty. Deletes the contents of + * folder as well. + */ + force?: boolean; + + /** + * The path of the object to delete. + */ + path?: string; + } + + export interface Authenticator { + /** + * An authentication mechanism. + */ + auth_type: AuthenticatorType; + + /** + * An email address. + */ + auth_context: string; + } + + export interface DeleteResult { + /** + * Number of objects deleted. + */ + count: number; + } + + export interface FolderCreateRequest { + /** + * The name of an object. + */ + name?: string; + + /** + * A set of string-based key/value pairs used to provide additional data + * about an object. + */ + metadata?: Metadata; + + /** + * The ID of a stored object. + */ + parent_id?: string; + + /** + * A case-sensitive path to an object. Contains a sequence of path segments + * delimited by the the `/` character. Any path ending in a `/` character + * refers to a folder. + */ + path?: string; + + /** + * A list of user-defined tags. + */ + tags?: Tags; + } + + export interface FolderCreateResult { + object: ItemData; + } + + export interface GetRequest { + /** The ID of the object to retrieve. */ + id?: string; + + /** The path of the object to retrieve. */ + path?: string; + + /** The requested transfer method for the file data. */ + transfer_method?: TransferMethod; + } + + export interface GetResult { + object: ItemData; + + /** A URL where the file can be downloaded from. */ + dest_url?: string; + } + + export interface PutRequest { + /** + * The hexadecimal-encoded CRC32C hash of the file data, which will be + * verified by the server if provided. + */ + crc32c?: string; + + /** + * The format of the file, which will be verified by the server if provided. + * Uploads not matching the supplied format will be rejected. + */ + format?: FileFormat; + + /** + * The hexadecimal-encoded MD5 hash of the file data, which will be verified + * by the server if provided. + */ + md5?: string; + + /** + * A set of string-based key/value pairs used to provide additional data + * about an object. + */ + metadata?: Metadata; + + /** + * The MIME type of the file, which will be verified by the server if + * provided. Uploads not matching the supplied MIME type will be rejected. + */ + mimetype?: string; + + /** The name of the object to store. */ + name?: string; + + /** + * The parent ID of the object (a folder). Leave blank to keep in the root + * folder. + */ + parent_id?: string; + + /** + * An optional path where the file should be placed. It will auto-create + * directories if necessary. + */ + path?: string; + + /** + * The hexadecimal-encoded SHA1 hash of the file data, which will be + * verified by the server if provided. + */ + sha1?: string; + + /** + * The SHA256 hash of the file data, which will be verified by the server + * if provided. + */ + sha256?: string; + + /** + * The hexadecimal-encoded SHA512 hash of the file data, which will be + * verified by the server if provided. + */ + sha512?: string; + + /** + * The size (in bytes) of the file. If the upload doesn't match, the call + * will fail. + */ + size?: number; + + /** A list of user-defined tags */ + tags?: Tags; + + /** The transfer method used to upload the file data. */ + transfer_method?: TransferMethod; + } + + export interface PutResult { + object: ItemData; + } + + export interface UpdateRequest { + /** + * A list of metadata key/values to set in the object. If a provided key + * exists, the value will be replaced. + */ + add_metadata?: Metadata; + + /** + * A list of tags to add. It is not an error to provide a tag which already + * exists. + */ + add_tags?: Tags; + + /** An identifier for the file to update. */ + id: string; + + /** Set the object's metadata. */ + metadata?: Metadata; + + /** Set the parent (folder) of the object. */ + parent_id?: string; + + /** An alternative to ID for identifying the target file. */ + path?: string; + + /** + * A list of metadata key/values to remove in the object. It is not an + * error for a provided key to not exist. If a provided key exists but + * doesn't match the provided value, it will not be removed. + */ + remove_metadata?: Metadata; + + /** + * A list of tags to remove. It is not an error to provide a tag which is + * not present. + */ + remove_tags?: Tags; + + /** Set the object's tags. */ + tags?: Tags; + + /** + * The date and time the object was last updated. If included, the update + * will fail if this doesn't match the date and time of the last update for + * the object. + */ + updated_at?: string; + } + + export interface UpdateResult { + object: ItemData; + } + + export interface ListFilter { + /** + * Only records where the object exists in the supplied parent folder path + * name. + */ + folder?: string; + } + + export interface ListRequest { + filter?: ListFilter; + + /** + * Reflected value from a previous response to obtain the next page of + * results. + */ + last?: string; + + /** Order results asc(ending) or desc(ending). */ + order?: ItemOrder; + + /** Which field to order results by. */ + order_by?: ItemOrderBy; + + /** Maximum results to include in the response. */ + size?: number; + } + + export interface ListResult { + /** The total number of objects matched by the list request. */ + count: number; + + /** + * Used to fetch the next page of the current listing when provided in a + * repeated request's last parameter. + */ + last?: string; + objects: ItemData[]; + } + + export interface GetArchiveRequest { + /** + * The IDs of the objects to include in the archive. Folders include all + * children. + */ + ids: string[]; + + /** The format to use to build the archive. */ + format?: ArchiveFormat; + + /** The requested transfer method for the file data. */ + transfer_method?: TransferMethod; + } + + export interface GetArchiveResult { + /** A location where the archive can be downloaded from. */ + dest_url?: string; + + /** Number of objects included in the archive. */ + count: number; + } + + export interface Authenticator { + /** An authentication mechanism. */ + auth_type: AuthenticatorType; + + /** An email address. */ + auth_context: string; + } + + export interface ShareLinkCreateItem { + /** List of storage IDs. */ + targets: string[]; + + /** Type of link. */ + link_type?: LinkType; + + /** The date and time the share link expires. */ + expires_at?: string; + + /** + * The maximum number of times a user can be authenticated to access the + * share link. + */ + max_access_count?: number; + + /** A list of authenticators. */ + authenticators: Authenticator[]; + + /** An optional message to use in accessing shares. */ + message?: string; + + /** An optional title to use in accessing shares. */ + title?: string; + + /** An email address. */ + notify_email?: string; + + /** A list of user-defined tags. */ + tags?: Tags; + } + + export interface ShareLinkCreateRequest { + links: ShareLinkCreateItem[]; + } + + export interface ShareLinkItem { + /** The ID of a share link. */ + id: string; + + /** The ID of a bucket resource. */ + storage_pool_id: string; + + /** List of storage IDs. */ + targets: string[]; + + /** Type of link. */ + link_type: string; + + /** + * The number of times a user has authenticated to access the share link. + */ + access_count: number; + + /** + * The maximum number of times a user can be authenticated to access the + * share link. + */ + max_access_count: number; + + /** The date and time the share link was created. */ + created_at: string; + /** The date and time the share link expires. */ + expires_at: string; + + /** The date and time the share link was last accessed. */ + last_accessed_at?: string; + + /** A list of authenticators */ + authenticators: Authenticator[]; + + /** A URL to access the file/folders shared with a link. */ + link: string; + + /** An optional message to use in accessing shares. */ + message?: string; + + /** An optional title to use in accessing shares. */ + title?: string; + + /** An email address. */ + notify_email?: string; + + /** A list of user-defined tags. */ + tags?: Tags; + } + + export interface ShareLinkCreateResult { + share_link_objects: ShareLinkItem[]; + } + + export interface ShareLinkGetRequest { + /** The ID of a share link. */ + id: string; + } + + export interface ShareLinkGetResult { + share_link_object: ShareLinkItem; + } + + export interface ShareLinkListFilter { + /** Only records where id equals this value. */ + id?: string; + + /** Only records where id includes each substring. */ + id__contains?: string[]; + + /** Only records where id equals one of the provided substrings. */ + id__in?: string[]; + + /** Only records where storage_pool_id equals this value. */ + storage_pool_id?: string; + + /** Only records where storage_pool_id includes each substring. */ + storage_pool_id__contains?: string[]; + + /** Only records where storage_pool_id equals one of the provided substrings. */ + storage_pool_id__in?: string[]; + + /** Only records where target_id equals this value. */ + target_id?: string; + + /** Only records where target_id includes each substring. */ + target_id__contains?: string[]; + + /** Only records where target_id equals one of the provided substrings. */ + target_id__in?: string[]; + + /** Only records where link_type equals this value. */ + link_type?: string; + + /** Only records where link_type includes each substring. */ + link_type__contains?: string[]; + + /** Only records where link_type equals one of the provided substrings. */ + link_type__in?: string[]; + + /** Only records where access_count equals this value. */ + access_count?: number; + + /** Only records where access_count is greater than this value. */ + access_count__gt?: number; + + /** Only records where access_count is greater than or equal to this value. */ + access_count__gte?: number; + + /** Only records where access_count is less than this value. */ + access_count__lt?: number; + + /** Only records where access_count is less than or equal to this value. */ + access_count__lte?: number; + + /** Only records where max_access_count equals this value. */ + max_access_count?: number; + + /** Only records where max_access_count is greater than this value. */ + max_access_count__gt?: number; + + /** Only records where max_access_count is greater than or equal to this value. */ + max_access_count__gte?: number; + + /** Only records where max_access_count is less than this value. */ + max_access_count__lt?: number; + + /** Only records where max_access_count is less than or equal to this value. */ + max_access_count__lte?: number; + + /** Only records where created_at equals this value. */ + created_at?: string; + + /** Only records where created_at is greater than this value. */ + created_at__gt?: string; + + /** Only records where created_at is greater than or equal to this value. */ + created_at__gte?: string; + + /** Only records where created_at is less than this value. */ + created_at__lt?: string; + + /** Only records where created_at is less than or equal to this value. */ + created_at__lte?: string; + + /** Only records where expires_at equals this value. */ + expires_at?: string; + + /** Only records where expires_at is greater than this value. */ + expires_at__gt?: string; + + /** Only records where expires_at is greater than or equal to this value. */ + expires_at__gte?: string; + + /** Only records where expires_at is less than this value. */ + expires_at__lt?: string; + + /** Only records where expires_at is less than or equal to this value. */ + expires_at__lte?: string; + + /** Only records where last_accessed_at equals this value. */ + last_accessed_at?: string; + + /** Only records where last_accessed_at is greater than this value. */ + last_accessed_at__gt?: string; + + /** Only records where last_accessed_at is greater than or equal to this value. */ + last_accessed_at__gte?: string; + + /** Only records where last_accessed_at is less than this value. */ + last_accessed_at__lt?: string; + + /** Only records where last_accessed_at is less than or equal to this value. */ + last_accessed_at__lte?: string; + + /** Only records where link equals this value. */ + link?: string; + + /** Only records where link includes each substring. */ + link__contains?: string[]; + + /** Only records where link equals one of the provided substrings. */ + link__in?: string[]; + } + + export interface ShareLinkListRequest { + filter?: ShareLinkListFilter; + + /** Reflected value from a previous response to obtain the next page of results. */ + last?: string; + + /** Order results asc(ending) or desc(ending). */ + order?: ItemOrder; + + /** Which field to order results by. */ + order_by?: ItemOrderBy; + + /** Maximum results to include in the response. */ + size?: number; + } + + export interface ShareLinkListResult { + /** The total number of share links matched by the list request. */ + count: number; + share_link_objects: ShareLinkItem[]; + } + + export interface ShareLinkDeleteRequest { + ids: string[]; + } + + export interface ShareLinkDeleteResult { + share_link_objects: ShareLinkItem[]; + } + + export interface ShareLinkSendItem { + /** The ID of a share link. */ + id: string; + + /** An email address. */ + email: string; + } + + export interface ShareLinkSendRequest { + links: ShareLinkSendItem[]; + + /** An email address. */ + sender_email: string; + sender_name?: string; + } + + export interface ShareLinkSendResult { + share_link_objects: ShareLinkItem[]; + } +} diff --git a/packages/pangea-node-sdk/src/utils/multipart.ts b/packages/pangea-node-sdk/src/utils/multipart.ts index b6cc9e711..f3f4adfc2 100644 --- a/packages/pangea-node-sdk/src/utils/multipart.ts +++ b/packages/pangea-node-sdk/src/utils/multipart.ts @@ -194,8 +194,11 @@ export function getHeaderField( const parts = header.split(field + "="); if (parts.length > 1 && parts[1]) { const valueParts = parts[1].split(";"); - if (valueParts[0]) { - return valueParts[0].trim().replace(/['"]+/g, ""); + if (valueParts[0] !== undefined) { + const value = valueParts[0].trim().replace(/['"]+/g, ""); + if (value.length > 0) { + return value; + } } } return defaultValue; diff --git a/packages/pangea-node-sdk/src/utils/utils.ts b/packages/pangea-node-sdk/src/utils/utils.ts index 4c83170ba..f40297681 100644 --- a/packages/pangea-node-sdk/src/utils/utils.ts +++ b/packages/pangea-node-sdk/src/utils/utils.ts @@ -167,7 +167,20 @@ export function getFileUploadParams( const crcValue = crc32c(data); return { sha256: sha256hex, - crc32c: crcValue.toString(16), + crc32c: crcValue.toString(16).padStart(8, "0"), size: size, }; } + +export function getFileSize(file: string | Buffer): number { + let data: Buffer; + if (typeof file === "string") { + data = fs.readFileSync(file); + } else if (Buffer.isBuffer(file)) { + data = file; + } else { + throw new PangeaErrors.PangeaError("Invalid file type"); + } + + return data.length; +} diff --git a/packages/pangea-node-sdk/tests/integration/share.test.ts b/packages/pangea-node-sdk/tests/integration/share.test.ts new file mode 100644 index 000000000..e9a785dc1 --- /dev/null +++ b/packages/pangea-node-sdk/tests/integration/share.test.ts @@ -0,0 +1,508 @@ +import PangeaConfig from "../../src/config.js"; +import { it, expect, jest } from "@jest/globals"; +import { + TestEnvironment, + getFileUploadParams, + getTestDomain, + getTestToken, +} from "../../src/utils/utils.js"; +import { ShareService, PangeaErrors } from "../../src/index.js"; +import { Share, TransferMethod } from "../../src/types.js"; +import { FileUploader } from "../../src/file_uploader.js"; +import { loadTestEnvironment } from "./utils.js"; + +const environment = loadTestEnvironment("share", TestEnvironment.LIVE); +const token = getTestToken(environment); +const testHost = getTestDomain(environment); +const config = new PangeaConfig({ + domain: testHost, + customUserAgent: "sdk-test", +}); +const client = new ShareService(token, config); + +const TIME = Math.round(Date.now() / 1000); +const FOLDER_DELETE = "/sdk_tests/node/delete/" + TIME; +const FOLDER_FILES = "/sdk_tests/node/files/" + TIME; +const METADATA = { field1: "value1", field2: "value2" }; +const ADD_METADATA = { field3: "value3" }; +const TAGS = ["tag1", "tag2"]; +const ADD_TAGS = ["tag3"]; + +const testFilePath = "./tests/testdata/testfile.pdf"; +const zeroBytesFilePath = "./tests/testdata/zerobytes.txt"; +jest.setTimeout(120000); + +const delay = async (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +it("Folder create/delete", async () => { + try { + const respCreate = await client.folderCreate({ + path: FOLDER_DELETE, + }); + expect(respCreate.success).toBeTruthy(); + expect(respCreate.result.object.id).toBeDefined(); + expect(respCreate.result.object.type).toBe("folder"); + expect(respCreate.result.object.name).toBeDefined(); + expect(respCreate.result.object.created_at).toBeDefined(); + expect(respCreate.result.object.updated_at).toBeDefined(); + + const id = respCreate.result.object.id; + const respDelete = await client.delete({ + id: id, + }); + expect(respDelete.success).toBeTruthy(); + expect(respDelete.result.count).toBe(1); + } catch (e) { + e instanceof PangeaErrors.APIError + ? console.log(e.toString()) + : console.log(e); + expect(false).toBeTruthy(); + } +}); + +it("Put file. Multipart transfer_method", async () => { + try { + const name = TIME + "_file_multipart"; + const respPut = await client.put( + { + name: name, + transfer_method: TransferMethod.MULTIPART, + }, + { + file: testFilePath, + name: name, + } + ); + expect(respPut.success).toBeTruthy(); + + let respGet = await client.getItem({ + id: respPut.result.object.id, + transfer_method: TransferMethod.MULTIPART, + }); + + expect(respGet.success).toBeTruthy(); + expect(respGet.result.dest_url).toBeUndefined(); + expect(respGet.attachedFiles.length).toBe(1); + expect(respGet.attachedFiles[0]).toBeDefined(); + respGet.attachedFiles[0]?.save("./download/"); + + respGet = await client.getItem({ + id: respPut.result.object.id, + transfer_method: TransferMethod.DEST_URL, + }); + + expect(respGet.success).toBeTruthy(); + expect(respGet.attachedFiles.length).toBe(0); + expect(respGet.result.dest_url).toBeDefined(); + } catch (e) { + e instanceof PangeaErrors.APIError + ? console.log(e.toString()) + : console.log(e); + expect(false).toBeTruthy(); + } +}); + +it("Put zero bytes file. Multipart transfer_method", async () => { + try { + const name = TIME + "_file_zero_bytes_multipart"; + const respPut = await client.put( + { + name: name, + transfer_method: TransferMethod.MULTIPART, + }, + { + file: zeroBytesFilePath, + name: name, + } + ); + expect(respPut.success).toBeTruthy(); + + let respGet = await client.getItem({ + id: respPut.result.object.id, + transfer_method: TransferMethod.MULTIPART, + }); + + expect(respGet.success).toBeTruthy(); + expect(respGet.result.dest_url).toBeUndefined(); + expect(respGet.attachedFiles.length).toBe(1); + expect(respGet.attachedFiles[0]).toBeDefined(); + respGet.attachedFiles[0]?.save("./download/"); + + respGet = await client.getItem({ + id: respPut.result.object.id, + transfer_method: TransferMethod.DEST_URL, + }); + + expect(respGet.success).toBeTruthy(); + expect(respGet.attachedFiles.length).toBe(0); + expect(respGet.result.dest_url).toBeUndefined(); + } catch (e) { + e instanceof PangeaErrors.APIError + ? console.log(e.toString()) + : console.log(e); + throw e; + } +}); + +it("Put file. post-url transfer_method", async () => { + try { + const name = TIME + "_file_post_url"; + const respPut = await client.put( + { + name: name, + transfer_method: TransferMethod.POST_URL, + }, + { + file: testFilePath, + name: name, + } + ); + expect(respPut.success).toBeTruthy(); + + let respGet = await client.getItem({ + id: respPut.result.object.id, + transfer_method: TransferMethod.MULTIPART, + }); + + expect(respGet.success).toBeTruthy(); + expect(respGet.result.dest_url).toBeUndefined(); + expect(respGet.attachedFiles.length).toBe(1); + expect(respGet.attachedFiles[0]).toBeDefined(); + respGet.attachedFiles[0]?.save("./download/"); + + respGet = await client.getItem({ + id: respPut.result.object.id, + transfer_method: TransferMethod.DEST_URL, + }); + + expect(respGet.success).toBeTruthy(); + expect(respGet.attachedFiles.length).toBe(0); + expect(respGet.result.dest_url).toBeDefined(); + } catch (e) { + e instanceof PangeaErrors.APIError + ? console.log(e.toString()) + : console.log(e); + throw e; + } +}); + +it("Put zero bytes file. post-url transfer_method", async () => { + try { + const name = TIME + "_file_zero_bytes_post_url"; + const respPut = await client.put( + { + name: name, + transfer_method: TransferMethod.POST_URL, + }, + { + file: zeroBytesFilePath, + name: name, + } + ); + expect(respPut.success).toBeTruthy(); + } catch (e) { + e instanceof PangeaErrors.APIError + ? console.log(e.toString()) + : console.log(e); + throw e; + } +}); + +it("get url and put upload", async () => { + let response; + const name = TIME + "_file_split_put_url"; + try { + const request: Share.PutRequest = { + transfer_method: TransferMethod.PUT_URL, + name: name, + }; + response = await client.requestUploadURL(request); + } catch (e) { + e instanceof PangeaErrors.APIError + ? console.log(e.toString()) + : console.log(e); + throw e; + } + + const url = response.accepted_result?.put_url || ""; + + const uploader = new FileUploader(); + await uploader.uploadFile( + url, + { + file: testFilePath, + name: name, + }, + { + transfer_method: TransferMethod.PUT_URL, + } + ); + + const maxRetry = 12; + for (let retry = 0; retry < maxRetry; retry++) { + try { + // Wait until result could be ready + await delay(10 * 1000); + const request_id = response.request_id || ""; + response = await client.pollResult(request_id); + expect(response.status).toBe("Success"); + break; + } catch { + expect(retry).toBeLessThan(maxRetry - 1); + } + } +}); + +it("get url and post upload", async () => { + let response; + const name = TIME + "_file_split_post_url"; + try { + const params = getFileUploadParams(testFilePath); + + const request: Share.PutRequest = { + transfer_method: TransferMethod.POST_URL, + name: name, + crc32c: params.crc32c, + sha256: params.sha256, + size: params.size, + }; + + response = await client.requestUploadURL(request); + } catch (e) { + e instanceof PangeaErrors.APIError + ? console.log(e.toString()) + : console.log(e); + throw e; + } + + console.log(response.request_id); + const url = response.accepted_result?.post_url || ""; + const file_details = response.accepted_result?.post_form_data; + + const uploader = new FileUploader(); + await uploader.uploadFile( + url, + { + file: testFilePath, + name: name, + file_details: file_details, + }, + { + transfer_method: TransferMethod.POST_URL, + } + ); + + const maxRetry = 12; + for (let retry = 0; retry < maxRetry; retry++) { + try { + // Wait until result could be ready + await delay(10 * 1000); + const request_id = response.request_id || ""; + response = await client.pollResult(request_id); + expect(response.status).toBe("Success"); + break; + } catch { + expect(retry).toBeLessThan(maxRetry - 1); + } + } +}); + +it("Item life cycle", async () => { + // Create a folder + const respCreate = await client.folderCreate({ + path: FOLDER_FILES, + }); + expect(respCreate.success).toBeTruthy(); + const folderID = respCreate.result.object.id; + + // # Upload a file with path as unique param + const path1 = FOLDER_FILES + "/" + TIME + "_file_multipart_1"; + const respPutPath = await client.put( + { + path: path1, + transfer_method: TransferMethod.MULTIPART, + }, + { + file: testFilePath, + name: path1, + } + ); + + expect(respPutPath.success).toBeTruthy(); + expect(respPutPath.result.object.parent_id).toBe(folderID); + expect(respPutPath.result.object.metadata).toBeUndefined(); + expect(respPutPath.result.object.tags).toBeUndefined(); + expect(respPutPath.result.object.md5).toBeUndefined(); + expect(respPutPath.result.object.sha512).toBeUndefined(); + expect(respPutPath.result.object.sha256).toBeDefined(); + + // Upload a file with parent id and name + const name2 = TIME + "_file_multipart_2"; + const respPutId = await client.put( + { + parent_id: folderID, + name: name2, + transfer_method: TransferMethod.MULTIPART, + metadata: METADATA, + tags: TAGS, + }, + { + file: testFilePath, + name: path1, + } + ); + + expect(respPutId.success).toBeTruthy(); + expect(respPutId.result.object.parent_id).toBe(folderID); + expect(respPutId.result.object.metadata).toStrictEqual(METADATA); + expect(respPutId.result.object.tags).toStrictEqual(TAGS); + expect(respPutId.result.object.sha256).toBeDefined(); + expect(respPutId.result.object.md5).toBeUndefined(); + expect(respPutId.result.object.sha512).toBeUndefined(); + + // Update file. full metadata and tags + const respUpdate = await client.update({ + id: respPutPath.result.object.id, + metadata: METADATA, + tags: TAGS, + }); + expect(respUpdate.success).toBeTruthy(); + expect(respUpdate.result.object.metadata).toStrictEqual(METADATA); + expect(respUpdate.result.object.tags).toStrictEqual(TAGS); + + // Update file. add metadata and tags + const respUpdateAdd = await client.update({ + id: respPutPath.result.object.id, + add_metadata: ADD_METADATA, + add_tags: ADD_TAGS, + }); + + const metadataFinal = { + ...METADATA, + ...ADD_METADATA, + }; + const tagsFinal = [...TAGS, ...ADD_TAGS]; + expect(respUpdateAdd.success).toBeTruthy(); + expect(respUpdateAdd.result.object.metadata).toStrictEqual(metadataFinal); + expect(respUpdateAdd.result.object.tags).toStrictEqual(tagsFinal); + + // Get archive + const respGetArchive1 = await client.getArchive({ + ids: [folderID], + format: Share.ArchiveFormat.ZIP, + transfer_method: TransferMethod.MULTIPART, + }); + expect(respGetArchive1.success).toBeTruthy(); + expect(respGetArchive1.result.dest_url).toBeUndefined(); + expect(respGetArchive1.attachedFiles.length).toBe(1); + respGetArchive1.attachedFiles.forEach((file) => { + file.save("./download/"); + }); + + const respGetArchive2 = await client.getArchive({ + ids: [folderID], + format: Share.ArchiveFormat.TAR, + transfer_method: TransferMethod.DEST_URL, + }); + expect(respGetArchive2.success).toBeTruthy(); + expect(respGetArchive2.result.dest_url).toBeDefined(); + expect(respGetArchive2.attachedFiles.length).toBe(0); + + // Download file + const url = respGetArchive2.result.dest_url ?? ""; + let downloadedFile = await client.downloadFile(url); + downloadedFile.save("./download/"); + expect(downloadedFile.file.length).toBeGreaterThan(0); + + // Create share link + const authenticators: Share.Authenticator[] = [ + { + auth_type: Share.AuthenticatorType.PASSWORD, + auth_context: "somepassword", + }, + ]; + const linkList: Share.ShareLinkCreateItem[] = [ + { + targets: [folderID], + link_type: Share.LinkType.EDITOR, + max_access_count: 3, + authenticators: authenticators, + }, + ]; + + const respCreateLink = await client.shareLinkCreate({ + links: linkList, + }); + expect(respCreateLink.success).toBeTruthy(); + const links = respCreateLink.result.share_link_objects; + expect(links.length).toBe(1); + + const link = links[0]; + expect(link).toBeDefined(); + expect(link?.access_count).toBe(0); + expect(link?.max_access_count).toBe(3); + expect(link?.authenticators.length).toBe(1); + expect(link?.authenticators[0]?.auth_type).toBe( + Share.AuthenticatorType.PASSWORD + ); + expect(link?.link).toBeDefined(); + expect(link?.id).toBeDefined(); + expect(link?.targets.length).toBe(1); + + const linkID = link?.id ?? ""; + + // Send share link + const respSendLink = await client.shareLinkSend({ + links: [ + { + id: linkID, + email: "user@email.com", + }, + ], + sender_email: "sender@email.com", + sender_name: "Sender Name", + }); + expect(respSendLink.result.share_link_objects.length).toBe(1); + + // Get share link + const respGetLink = await client.shareLinkGet({ + id: linkID, + }); + expect(respGetLink.success).toBeTruthy(); + expect(respGetLink.result.share_link_object.link).toBe(link?.link); + expect(respGetLink.result.share_link_object.access_count).toBe(0); + expect(respGetLink.result.share_link_object.max_access_count).toBe(3); + expect(respGetLink.result.share_link_object.created_at).toBe( + link?.created_at + ); + expect(respGetLink.result.share_link_object.expires_at).toBe( + link?.expires_at + ); + + // List share link + const respListLink = await client.shareLinkList(); + expect(respListLink.success).toBeTruthy(); + expect(respListLink.result.count).toBeGreaterThan(0); + expect(respListLink.result.share_link_objects.length).toBeGreaterThan(0); + + // Delete share link + const respDeleteLink = await client.shareLinkDelete({ + ids: [linkID], + }); + expect(respDeleteLink.success).toBeTruthy(); + expect(respDeleteLink.result.share_link_objects.length).toBe(1); + + // List files in folder + const listFilter = { + folder: FOLDER_FILES, + }; + const respList = await client.list({ + filter: listFilter, + }); + expect(respList.success).toBeTruthy(); + expect(respList.result.count).toBe(2); + expect(respList.result.objects.length).toBe(2); +}); diff --git a/packages/pangea-node-sdk/tests/testdata/zerobytes.txt b/packages/pangea-node-sdk/tests/testdata/zerobytes.txt new file mode 100644 index 000000000..e69de29bb