@cirrobio/sdk 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/error-handler.d.ts +8 -0
- package/dist/api/error-handler.js +37 -0
- package/dist/api.d.ts +1 -8
- package/dist/api.js +4 -103
- package/dist/file/__test__/s3-utils.test.d.ts +1 -0
- package/dist/file/__test__/s3-utils.test.js +16 -0
- package/dist/file/actions/delete.fn.d.ts +9 -0
- package/dist/file/actions/delete.fn.js +15 -0
- package/dist/file/actions/sign-url.fn.d.ts +15 -0
- package/dist/file/actions/sign-url.fn.js +26 -0
- package/dist/file/actions/upload.fn.d.ts +13 -0
- package/dist/file/actions/upload.fn.js +23 -0
- package/dist/{extensions.fn.js → file/extensions.fn.js} +21 -16
- package/dist/file/file-object.model.d.ts +54 -0
- package/dist/file/file-object.model.js +8 -0
- package/dist/file/file.service.d.ts +29 -0
- package/dist/file/file.service.js +75 -0
- package/dist/file/project-access-context.d.ts +14 -0
- package/dist/file/project-access-context.js +24 -0
- package/dist/file/shared.d.ts +5 -0
- package/dist/file/shared.js +11 -0
- package/dist/file/util/credentials-mutex.so.d.ts +15 -0
- package/dist/file/util/credentials-mutex.so.js +32 -0
- package/dist/file/util/s3-client.d.ts +6 -0
- package/dist/file/util/s3-client.js +19 -0
- package/dist/file/util/s3-utils.d.ts +7 -0
- package/dist/file/util/s3-utils.js +17 -0
- package/dist/file.d.ts +11 -0
- package/dist/file.js +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +16 -3
- package/src/api/error-handler.ts +36 -0
- package/src/api.ts +1 -36
- package/src/file/__test__/s3-utils.test.ts +17 -0
- package/src/file/actions/delete.fn.ts +18 -0
- package/src/file/actions/sign-url.fn.ts +35 -0
- package/src/file/actions/upload.fn.ts +30 -0
- package/src/{extensions.fn.ts → file/extensions.fn.ts} +1 -2
- package/src/file/file-object.model.ts +57 -0
- package/src/file/file.service.ts +79 -0
- package/src/file/project-access-context.ts +26 -0
- package/src/file/shared.ts +7 -0
- package/src/file/util/credentials-mutex.so.ts +33 -0
- package/src/file/util/s3-client.ts +17 -0
- package/src/file/util/s3-utils.ts +14 -0
- package/src/file.ts +11 -0
- package/src/index.ts +2 -2
- package/tsconfig.json +3 -3
- /package/dist/{extensions.fn.d.ts → file/extensions.fn.d.ts} +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createS3Client = void 0;
|
|
4
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
5
|
+
/**
|
|
6
|
+
* Creates an S3 client using the provided credentials.
|
|
7
|
+
*/
|
|
8
|
+
function createS3Client(credentials) {
|
|
9
|
+
return new client_s3_1.S3Client({
|
|
10
|
+
credentials: {
|
|
11
|
+
accessKeyId: credentials.accessKeyId,
|
|
12
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
13
|
+
sessionToken: credentials.sessionToken,
|
|
14
|
+
},
|
|
15
|
+
region: credentials.region,
|
|
16
|
+
useDualstackEndpoint: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
exports.createS3Client = createS3Client;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.s3UriToParams = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Converts an S3 URI to a bucket and key
|
|
6
|
+
*/
|
|
7
|
+
function s3UriToParams(uri) {
|
|
8
|
+
const matches = /^s3:\/\/([^/]+)\/(.+)$/.exec(uri);
|
|
9
|
+
if (!matches) {
|
|
10
|
+
throw new Error(`Received invalid uri: '${uri}'`);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
Bucket: matches[1],
|
|
14
|
+
Key: matches[2],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
exports.s3UriToParams = s3UriToParams;
|
package/dist/file.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './file/extensions.fn';
|
|
2
|
+
export { FileService } from './file/file.service';
|
|
3
|
+
export { deleteFile, DeleteFileParams } from './file/actions/delete.fn';
|
|
4
|
+
export { getSignedUrl, GetSignedUrlOptions } from './file/actions/sign-url.fn';
|
|
5
|
+
export { uploadFile, UploadFileParams } from './file/actions/upload.fn';
|
|
6
|
+
export { ProjectFileAccessContext } from './file/project-access-context';
|
|
7
|
+
export { FileSystemObject, FileSystemObjectType } from './file/file-object.model';
|
|
8
|
+
export { s3UriToParams } from './file/util/s3-utils';
|
|
9
|
+
export { createS3Client } from './file/util/s3-client';
|
|
10
|
+
export { getProjectS3Bucket } from './file/shared';
|
|
11
|
+
export { Progress, Upload } from '@aws-sdk/lib-storage';
|
package/dist/file.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.Upload = exports.getProjectS3Bucket = exports.createS3Client = exports.s3UriToParams = exports.FileSystemObjectType = exports.ProjectFileAccessContext = exports.uploadFile = exports.getSignedUrl = exports.deleteFile = exports.FileService = void 0;
|
|
18
|
+
__exportStar(require("./file/extensions.fn"), exports);
|
|
19
|
+
var file_service_1 = require("./file/file.service");
|
|
20
|
+
Object.defineProperty(exports, "FileService", { enumerable: true, get: function () { return file_service_1.FileService; } });
|
|
21
|
+
var delete_fn_1 = require("./file/actions/delete.fn");
|
|
22
|
+
Object.defineProperty(exports, "deleteFile", { enumerable: true, get: function () { return delete_fn_1.deleteFile; } });
|
|
23
|
+
var sign_url_fn_1 = require("./file/actions/sign-url.fn");
|
|
24
|
+
Object.defineProperty(exports, "getSignedUrl", { enumerable: true, get: function () { return sign_url_fn_1.getSignedUrl; } });
|
|
25
|
+
var upload_fn_1 = require("./file/actions/upload.fn");
|
|
26
|
+
Object.defineProperty(exports, "uploadFile", { enumerable: true, get: function () { return upload_fn_1.uploadFile; } });
|
|
27
|
+
var project_access_context_1 = require("./file/project-access-context");
|
|
28
|
+
Object.defineProperty(exports, "ProjectFileAccessContext", { enumerable: true, get: function () { return project_access_context_1.ProjectFileAccessContext; } });
|
|
29
|
+
var file_object_model_1 = require("./file/file-object.model");
|
|
30
|
+
Object.defineProperty(exports, "FileSystemObjectType", { enumerable: true, get: function () { return file_object_model_1.FileSystemObjectType; } });
|
|
31
|
+
var s3_utils_1 = require("./file/util/s3-utils");
|
|
32
|
+
Object.defineProperty(exports, "s3UriToParams", { enumerable: true, get: function () { return s3_utils_1.s3UriToParams; } });
|
|
33
|
+
var s3_client_1 = require("./file/util/s3-client");
|
|
34
|
+
Object.defineProperty(exports, "createS3Client", { enumerable: true, get: function () { return s3_client_1.createS3Client; } });
|
|
35
|
+
var shared_1 = require("./file/shared");
|
|
36
|
+
Object.defineProperty(exports, "getProjectS3Bucket", { enumerable: true, get: function () { return shared_1.getProjectS3Bucket; } });
|
|
37
|
+
var lib_storage_1 = require("@aws-sdk/lib-storage");
|
|
38
|
+
Object.defineProperty(exports, "Upload", { enumerable: true, get: function () { return lib_storage_1.Upload; } });
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -14,5 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./file"), exports);
|
|
17
18
|
__exportStar(require("./api"), exports);
|
|
18
|
-
__exportStar(require("./extensions.fn"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cirrobio/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "SDK for Cirro",
|
|
5
5
|
"author": "CirroBio",
|
|
6
6
|
"repository": {
|
|
@@ -16,10 +16,23 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"prepare": "npm run build"
|
|
18
18
|
},
|
|
19
|
+
"jest": {
|
|
20
|
+
"coverageReporters": [
|
|
21
|
+
"lcov"
|
|
22
|
+
],
|
|
23
|
+
"preset": "ts-jest"
|
|
24
|
+
},
|
|
19
25
|
"devDependencies": {
|
|
20
|
-
"typescript": "^4.0"
|
|
26
|
+
"typescript": "^4.0",
|
|
27
|
+
"@types/jest": "^29.5.5",
|
|
28
|
+
"jest": "^29.7.0",
|
|
29
|
+
"ts-jest": "^29.1.1",
|
|
30
|
+
"@types/node": "^22.13.1"
|
|
21
31
|
},
|
|
22
32
|
"dependencies": {
|
|
23
|
-
"@cirrobio/api-client": "^0.
|
|
33
|
+
"@cirrobio/api-client": "^0.2.4",
|
|
34
|
+
"@aws-sdk/client-s3": "^3.34.0",
|
|
35
|
+
"@aws-sdk/lib-storage": "^3.34.0",
|
|
36
|
+
"@aws-sdk/s3-request-presigner": "^3.34.0"
|
|
24
37
|
}
|
|
25
38
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Middleware, ResponseContext } from "@cirrobio/api-client";
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
errors: string[];
|
|
5
|
+
|
|
6
|
+
constructor(message: string, errors: string[]) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.errors = errors;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class PortalErrorHandler implements Middleware {
|
|
13
|
+
async post(context: ResponseContext): Promise<Response | void> {
|
|
14
|
+
const { response } = context;
|
|
15
|
+
if (response && (response.status >= 200 && response.status < 300)) {
|
|
16
|
+
return response;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Handle Error
|
|
20
|
+
let errorMessage: string;
|
|
21
|
+
const errors = [];
|
|
22
|
+
try {
|
|
23
|
+
const err = await response.json();
|
|
24
|
+
console.warn(err);
|
|
25
|
+
if ('errorDetail' in err) {
|
|
26
|
+
errorMessage = err.errorDetail;
|
|
27
|
+
errors.push(err.errors.map((e: any) => e.message));
|
|
28
|
+
} else {
|
|
29
|
+
errorMessage = err.message;
|
|
30
|
+
}
|
|
31
|
+
} catch (ignore) {
|
|
32
|
+
errorMessage = "Unknown Error";
|
|
33
|
+
}
|
|
34
|
+
throw new ApiError(errorMessage, errors);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/api.ts
CHANGED
|
@@ -1,36 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export class ApiError extends Error {
|
|
4
|
-
errors: string[];
|
|
5
|
-
|
|
6
|
-
constructor(message: string, errors: string[]) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.errors = errors;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class PortalErrorHandler implements Middleware {
|
|
13
|
-
async post(context: ResponseContext): Promise<Response | void> {
|
|
14
|
-
const { response } = context;
|
|
15
|
-
if (response && (response.status >= 200 && response.status < 300)) {
|
|
16
|
-
return response;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Handle Error
|
|
20
|
-
let errorMessage: string;
|
|
21
|
-
const errors = [];
|
|
22
|
-
try {
|
|
23
|
-
const err = await response.json();
|
|
24
|
-
console.warn(err);
|
|
25
|
-
if ('errorDetail' in err) {
|
|
26
|
-
errorMessage = err.errorDetail;
|
|
27
|
-
errors.push(err.errors.map((e: any) => e.message));
|
|
28
|
-
} else {
|
|
29
|
-
errorMessage = err.message;
|
|
30
|
-
}
|
|
31
|
-
} catch (ignore) {
|
|
32
|
-
errorMessage = "Unknown Error";
|
|
33
|
-
}
|
|
34
|
-
throw new ApiError(errorMessage, errors);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
export { PortalErrorHandler, ApiError } from './api/error-handler';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { s3UriToParams } from "../../file";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
describe("s3-utils", () => {
|
|
5
|
+
describe("s3UriToParams", () => {
|
|
6
|
+
it("should parse a valid s3 URI", () => {
|
|
7
|
+
const uri = "s3://my-bucket/my-key/nested/folder";
|
|
8
|
+
const result = s3UriToParams(uri);
|
|
9
|
+
expect(result).toEqual({ Bucket: "my-bucket", Key: "my-key/nested/folder" });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should throw an error for an invalid URI", () => {
|
|
13
|
+
const uri = "invalid-uri";
|
|
14
|
+
expect(() => s3UriToParams(uri)).toThrowError(`Received invalid uri: '${uri}'`);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AWSCredentials } from "@cirrobio/api-client";
|
|
2
|
+
import { DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
3
|
+
import { s3UriToParams } from "../util/s3-utils";
|
|
4
|
+
import { createS3Client } from "../util/s3-client";
|
|
5
|
+
|
|
6
|
+
export interface DeleteFileParams {
|
|
7
|
+
url: string;
|
|
8
|
+
credentials: AWSCredentials;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Delete a file from S3 given its URL and credentials.
|
|
13
|
+
*/
|
|
14
|
+
export async function deleteFile({ url, credentials }: DeleteFileParams): Promise<void> {
|
|
15
|
+
const { Bucket, Key } = s3UriToParams(url);
|
|
16
|
+
const cmd = new DeleteObjectCommand({ Bucket, Key });
|
|
17
|
+
await createS3Client(credentials).send(cmd);
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AWSCredentials } from "@cirrobio/api-client";
|
|
2
|
+
import { GetObjectCommand, GetObjectCommandInput } from "@aws-sdk/client-s3";
|
|
3
|
+
import { getSignedUrl as getSignedUrlInternal } from "@aws-sdk/s3-request-presigner";
|
|
4
|
+
import { createS3Client } from "../util/s3-client";
|
|
5
|
+
import { s3UriToParams } from "../util/s3-utils";
|
|
6
|
+
|
|
7
|
+
export interface GetFileUrlParams extends GetSignedUrlOptions {
|
|
8
|
+
url: string;
|
|
9
|
+
credentials?: AWSCredentials;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GetSignedUrlOptions {
|
|
13
|
+
download?: boolean;
|
|
14
|
+
gzip?: boolean;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
filename?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get a signed URL for a file in S3 given its S3 URI.
|
|
21
|
+
*/
|
|
22
|
+
export function getSignedUrl({ url, credentials, ...params }: GetFileUrlParams): Promise<string> {
|
|
23
|
+
const client = createS3Client(credentials);
|
|
24
|
+
const { Bucket, Key } = s3UriToParams(url);
|
|
25
|
+
const fileName = params.filename ?? Key.split('/').pop();
|
|
26
|
+
const args: GetObjectCommandInput = { Bucket, Key };
|
|
27
|
+
if (params?.download) {
|
|
28
|
+
args.ResponseContentDisposition = `attachment; filename="${fileName}"`
|
|
29
|
+
}
|
|
30
|
+
if (params?.gzip) {
|
|
31
|
+
args.ResponseContentEncoding = 'gzip'
|
|
32
|
+
}
|
|
33
|
+
const command = new GetObjectCommand(args);
|
|
34
|
+
return getSignedUrlInternal(client, command, { expiresIn: 60 * (params?.timeout ?? 5) });
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Upload } from "@aws-sdk/lib-storage";
|
|
2
|
+
import { AWSCredentials } from '@cirrobio/api-client';
|
|
3
|
+
import { createS3Client } from "../util/s3-client";
|
|
4
|
+
|
|
5
|
+
export interface UploadFileParams {
|
|
6
|
+
bucket: string;
|
|
7
|
+
path: string;
|
|
8
|
+
file: File;
|
|
9
|
+
credentials: AWSCredentials;
|
|
10
|
+
metadata?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Upload a file to S3
|
|
15
|
+
*/
|
|
16
|
+
export function uploadFile({ bucket, path, file, credentials, metadata }: UploadFileParams): Upload {
|
|
17
|
+
const params = {
|
|
18
|
+
Bucket: bucket,
|
|
19
|
+
Key: path,
|
|
20
|
+
Body: file,
|
|
21
|
+
ContentType: file.type,
|
|
22
|
+
Metadata: metadata,
|
|
23
|
+
};
|
|
24
|
+
return new Upload({
|
|
25
|
+
client: createS3Client(credentials),
|
|
26
|
+
queueSize: 4,
|
|
27
|
+
params,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* An array of file extensions that can be rendered in a genome viewer
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
export const FILE_TRACK_ANNOTATION = ['bed', 'bed.gz', 'gtf', 'gtf.gz'];
|
|
7
6
|
export const FILE_TRACK_ALIGNMENTS = ['cram', 'cram.gz']; // TODO: put back bam
|
|
8
7
|
export const FILE_TRACK_VARIANT = ['vcf', 'vcf.gz'];
|
|
@@ -86,4 +85,4 @@ export function matchesExtension(filePath: string, extensions: string[]): boolea
|
|
|
86
85
|
|
|
87
86
|
// Check if the fileExtension is in the list of extensions
|
|
88
87
|
return extensions.includes(fileExtension);
|
|
89
|
-
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ProjectFileAccessContext } from "./project-access-context";
|
|
2
|
+
|
|
3
|
+
export enum FileSystemObjectType {
|
|
4
|
+
FILE = 'file',
|
|
5
|
+
FOLDER = 'folder'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents a file that can be downloaded
|
|
10
|
+
*/
|
|
11
|
+
export interface DownloadableFile {
|
|
12
|
+
url: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
fileAccessContext: ProjectFileAccessContext;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Represents a file in Cirro
|
|
19
|
+
*/
|
|
20
|
+
export interface FileSystemObject extends DownloadableFile {
|
|
21
|
+
/**
|
|
22
|
+
* Unique Id For Object
|
|
23
|
+
*/
|
|
24
|
+
id: string;
|
|
25
|
+
/**
|
|
26
|
+
* Object S3 Url
|
|
27
|
+
*/
|
|
28
|
+
url: string;
|
|
29
|
+
/**
|
|
30
|
+
* Object Path
|
|
31
|
+
*/
|
|
32
|
+
path: string;
|
|
33
|
+
/**
|
|
34
|
+
* Object Name
|
|
35
|
+
*/
|
|
36
|
+
name: string;
|
|
37
|
+
/**
|
|
38
|
+
* Object Size
|
|
39
|
+
*/
|
|
40
|
+
size: number;
|
|
41
|
+
/**
|
|
42
|
+
* Object Kind (PNG, TXT)
|
|
43
|
+
*/
|
|
44
|
+
kind: string;
|
|
45
|
+
/**
|
|
46
|
+
* Object Type (File or Folder)
|
|
47
|
+
*/
|
|
48
|
+
type: FileSystemObjectType;
|
|
49
|
+
/**
|
|
50
|
+
* Metadata
|
|
51
|
+
*/
|
|
52
|
+
metadata?: Record<string, any>;
|
|
53
|
+
/**
|
|
54
|
+
* Access context
|
|
55
|
+
*/
|
|
56
|
+
fileAccessContext: ProjectFileAccessContext;
|
|
57
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { AccessType, AWSCredentials, FileApi } from "@cirrobio/api-client";
|
|
2
|
+
import { ProjectFileAccessContext } from "./project-access-context";
|
|
3
|
+
import { DownloadableFile } from "./file-object.model";
|
|
4
|
+
import { credentialsCache, credentialsMutex } from "./util/credentials-mutex.so";
|
|
5
|
+
import { GetSignedUrlOptions, GetFileUrlParams, getSignedUrl } from "./actions/sign-url.fn";
|
|
6
|
+
import { getProjectS3Bucket } from "./shared";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Service for viewing files in Cirro
|
|
10
|
+
* currently this only operates on files within a project
|
|
11
|
+
*/
|
|
12
|
+
export class FileService {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly fileApi: FileApi
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get contents of a file
|
|
19
|
+
*/
|
|
20
|
+
async getProjectFile(file: DownloadableFile): Promise<Response> {
|
|
21
|
+
const url = await this.getSignedUrlFromProjectFile(file);
|
|
22
|
+
return fetch(url);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get a signed URL for a file
|
|
27
|
+
*/
|
|
28
|
+
async getSignedUrlFromProjectFile(file: DownloadableFile, params?: GetSignedUrlOptions): Promise<string> {
|
|
29
|
+
const credentials = await this.getProjectAccessCredentials(file.fileAccessContext);
|
|
30
|
+
const _params: GetFileUrlParams = {
|
|
31
|
+
...params,
|
|
32
|
+
filename: file.name,
|
|
33
|
+
url: file.url,
|
|
34
|
+
credentials,
|
|
35
|
+
};
|
|
36
|
+
return getSignedUrl(_params);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a signed URL for a file given a path
|
|
41
|
+
*/
|
|
42
|
+
async getSignedUrlFromProjectPath(fileAccessContext: ProjectFileAccessContext, path: string, params?: GetFileUrlParams): Promise<string> {
|
|
43
|
+
const credentials = await this.getProjectAccessCredentials(fileAccessContext);
|
|
44
|
+
const _params: GetFileUrlParams = {
|
|
45
|
+
...params,
|
|
46
|
+
filename: path.split('/').pop(),
|
|
47
|
+
url: `s3://${getProjectS3Bucket(fileAccessContext.project.id)}/${path}`,
|
|
48
|
+
credentials,
|
|
49
|
+
};
|
|
50
|
+
return getSignedUrl(_params);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get credentials for accessing a project file
|
|
55
|
+
*/
|
|
56
|
+
async getProjectAccessCredentials(fileAccessContext: ProjectFileAccessContext): Promise<AWSCredentials> {
|
|
57
|
+
// Special case for project download, since we can cache the credentials
|
|
58
|
+
if (fileAccessContext.fileAccessRequest.accessType === AccessType.ProjectDownload) {
|
|
59
|
+
return this.getProjectReadCredentials(fileAccessContext);
|
|
60
|
+
}
|
|
61
|
+
return this.fileApi.generateProjectFileAccessToken({ projectId: fileAccessContext.project.id, fileAccessRequest: fileAccessContext.fileAccessRequest });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async getProjectReadCredentials(fileAccessContext: ProjectFileAccessContext): Promise<AWSCredentials> {
|
|
65
|
+
const projectId = fileAccessContext.project.id;
|
|
66
|
+
return credentialsMutex.dispatch(async () => {
|
|
67
|
+
const cachedCredentials = credentialsCache.get(projectId);
|
|
68
|
+
const expirationTime = cachedCredentials ? cachedCredentials?.expiration : null;
|
|
69
|
+
const fetchNewCredentials = !expirationTime || expirationTime < new Date();
|
|
70
|
+
if (fetchNewCredentials) {
|
|
71
|
+
const fileAccessRequest = fileAccessContext.fileAccessRequest;
|
|
72
|
+
const credentials = await this.fileApi.generateProjectFileAccessToken({ projectId, fileAccessRequest });
|
|
73
|
+
credentialsCache.set(projectId, credentials);
|
|
74
|
+
return credentials;
|
|
75
|
+
}
|
|
76
|
+
return cachedCredentials;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AccessType, DatasetDetail, FileAccessRequest, Project } from "@cirrobio/api-client";
|
|
2
|
+
|
|
3
|
+
type ProjectIdentifiable = Pick<Project, 'id'>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper class to encapsulate the file access for a project.
|
|
7
|
+
*/
|
|
8
|
+
export class ProjectFileAccessContext {
|
|
9
|
+
constructor(
|
|
10
|
+
readonly project: ProjectIdentifiable,
|
|
11
|
+
readonly dataset: DatasetDetail,
|
|
12
|
+
readonly fileAccessRequest: FileAccessRequest
|
|
13
|
+
) {
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static projectDownload(project: ProjectIdentifiable): ProjectFileAccessContext {
|
|
17
|
+
const request: FileAccessRequest = { accessType: AccessType.ProjectDownload };
|
|
18
|
+
return new ProjectFileAccessContext(project, null, request);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static datasetDownload(project: ProjectIdentifiable, dataset: DatasetDetail): ProjectFileAccessContext {
|
|
22
|
+
const accessType = dataset.share ? AccessType.SharedDatasetDownload : AccessType.ProjectDownload;
|
|
23
|
+
const request: FileAccessRequest = { accessType, datasetId: dataset.id };
|
|
24
|
+
return new ProjectFileAccessContext(project, dataset, request);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AWSCredentials } from '@cirrobio/api-client';
|
|
2
|
+
|
|
3
|
+
class Mutex<T> {
|
|
4
|
+
private mutex = Promise.resolve();
|
|
5
|
+
|
|
6
|
+
lock(): PromiseLike<() => void> {
|
|
7
|
+
let begin: (unlock: () => void) => void = () => { /* Do nothing */ };
|
|
8
|
+
|
|
9
|
+
this.mutex = this.mutex.then(() => new Promise(begin));
|
|
10
|
+
|
|
11
|
+
return new Promise((res) => {
|
|
12
|
+
begin = res;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async dispatch(fn: (() => T) | (() => PromiseLike<T>)): Promise<T> {
|
|
17
|
+
const unlock = await this.lock();
|
|
18
|
+
try {
|
|
19
|
+
return await Promise.resolve(fn());
|
|
20
|
+
} finally {
|
|
21
|
+
unlock();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A mutex to ensure that only one request for credentials is made at a time.
|
|
28
|
+
*/
|
|
29
|
+
export const credentialsMutex = new Mutex<AWSCredentials>();
|
|
30
|
+
/**
|
|
31
|
+
* A cache of credentials to avoid making multiple requests for the same credentials.
|
|
32
|
+
*/
|
|
33
|
+
export const credentialsCache: Map<string, AWSCredentials> = new Map<string, AWSCredentials>();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { AWSCredentials } from "@cirrobio/api-client";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates an S3 client using the provided credentials.
|
|
6
|
+
*/
|
|
7
|
+
export function createS3Client(credentials: AWSCredentials): S3Client {
|
|
8
|
+
return new S3Client({
|
|
9
|
+
credentials: {
|
|
10
|
+
accessKeyId: credentials.accessKeyId,
|
|
11
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
12
|
+
sessionToken: credentials.sessionToken,
|
|
13
|
+
},
|
|
14
|
+
region: credentials.region,
|
|
15
|
+
useDualstackEndpoint: true,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an S3 URI to a bucket and key
|
|
3
|
+
*/
|
|
4
|
+
export function s3UriToParams(uri: string): { Bucket: string, Key: string } {
|
|
5
|
+
const matches = /^s3:\/\/([^/]+)\/(.+)$/.exec(uri);
|
|
6
|
+
if (!matches) {
|
|
7
|
+
throw new Error(`Received invalid uri: '${uri}'`);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
Bucket: matches[1],
|
|
11
|
+
Key: matches[2],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
package/src/file.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './file/extensions.fn'
|
|
2
|
+
export { FileService } from './file/file.service'
|
|
3
|
+
export { deleteFile, DeleteFileParams } from './file/actions/delete.fn'
|
|
4
|
+
export { getSignedUrl, GetSignedUrlOptions } from './file/actions/sign-url.fn'
|
|
5
|
+
export { uploadFile, UploadFileParams } from './file/actions/upload.fn'
|
|
6
|
+
export { ProjectFileAccessContext } from './file/project-access-context'
|
|
7
|
+
export { FileSystemObject, FileSystemObjectType } from './file/file-object.model'
|
|
8
|
+
export { s3UriToParams } from './file/util/s3-utils'
|
|
9
|
+
export { createS3Client } from './file/util/s3-client'
|
|
10
|
+
export { getProjectS3Bucket } from './file/shared'
|
|
11
|
+
export { Progress, Upload } from '@aws-sdk/lib-storage';
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './
|
|
1
|
+
export * from './file';
|
|
2
|
+
export * from './api';
|
package/tsconfig.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"declaration": true,
|
|
4
|
-
"target": "
|
|
4
|
+
"target": "es2018",
|
|
5
5
|
"module": "commonjs",
|
|
6
6
|
"moduleResolution": "node",
|
|
7
7
|
"outDir": "dist",
|
|
8
8
|
"lib": [
|
|
9
|
-
"
|
|
9
|
+
"es2018",
|
|
10
10
|
"dom"
|
|
11
11
|
],
|
|
12
12
|
"typeRoots": [
|
|
@@ -17,4 +17,4 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"node_modules"
|
|
19
19
|
]
|
|
20
|
-
}
|
|
20
|
+
}
|
|
File without changes
|