@cirrobio/sdk 0.12.11 → 0.12.12
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/index.esm.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/api/config.ts +17 -0
- package/src/api/error-handler.ts +28 -0
- package/src/api/error.ts +8 -0
- package/src/api.ts +2 -0
- package/src/auth/authentication-provider.ts +26 -0
- package/src/auth/current-user.ts +26 -0
- package/src/auth/static-token-auth.ts +29 -0
- package/src/auth.ts +3 -0
- package/src/data/data.service.ts +128 -0
- package/src/data.ts +1 -0
- package/src/file/__test__/credentials-mutex.spec.ts +44 -0
- package/src/file/__test__/extensions.spec.ts +36 -0
- package/src/file/__test__/manifest-parser.spec.ts +67 -0
- package/src/file/__test__/s3-utils.spec.ts +17 -0
- package/src/file/__test__/utils.spec.ts +9 -0
- package/src/file/actions/delete.fn.ts +18 -0
- package/src/file/actions/sign-url.fn.ts +58 -0
- package/src/file/actions/upload.fn.ts +33 -0
- package/src/file/calculate-size.ts +14 -0
- package/src/file/extensions.fn.ts +88 -0
- package/src/file/file.service.ts +90 -0
- package/src/file/manifest-parser.ts +63 -0
- package/src/file/models/assets.ts +27 -0
- package/src/file/models/file-object.model.ts +61 -0
- package/src/file/models/file.ts +43 -0
- package/src/file/models/folder.ts +27 -0
- package/src/file/project-access-context.ts +26 -0
- package/src/file/shared.ts +9 -0
- package/src/file/util/credentials-mutex.so.ts +33 -0
- package/src/file/util/get-display.fn.ts +6 -0
- package/src/file/util/get-parent.fn.ts +7 -0
- package/src/file/util/s3-client.ts +43 -0
- package/src/file/util/s3-utils.ts +14 -0
- package/src/file.ts +16 -0
- package/src/formatters/__tests__/formatters.spec.ts +101 -0
- package/src/formatters/bytes-to-string.ts +32 -0
- package/src/formatters/json-pretty-print.ts +8 -0
- package/src/formatters/normalize-date.ts +10 -0
- package/src/formatters/normalize-string.ts +8 -0
- package/src/formatters/slash.ts +18 -0
- package/src/formatters/to-date-format.ts +12 -0
- package/src/formatters/to-friendly-name.ts +14 -0
- package/src/formatters/to-money.ts +13 -0
- package/src/formatters/to-pascal-case.ts +16 -0
- package/src/formatters/to-title-case.ts +9 -0
- package/src/formatters.ts +10 -0
- package/src/index.ts +6 -0
- package/src/util/__tests__/extract-from-object.spec.ts +29 -0
- package/src/util/download.ts +18 -0
- package/src/util/extract-from-object.ts +11 -0
- package/src/util/get-resource-name.ts +7 -0
- package/src/util/handle-promise.ts +7 -0
- package/src/util.ts +4 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AWSCredentials, GenerateProjectFileAccessTokenRequest, ProjectAccessType } from "@cirrobio/api-client";
|
|
2
|
+
import { ProjectFileAccessContext } from "./project-access-context";
|
|
3
|
+
import { credentialsCache, credentialsMutex } from "./util/credentials-mutex.so";
|
|
4
|
+
import { GetFileUrlParams, getSignedUrl, GetSignedUrlOptions } from "./actions/sign-url.fn";
|
|
5
|
+
import { getProjectS3Bucket } from "./shared";
|
|
6
|
+
import { DownloadableFile } from "./models/file-object.model";
|
|
7
|
+
|
|
8
|
+
export interface IFileCredentialsApi {
|
|
9
|
+
generateProjectFileAccessToken: (params: GenerateProjectFileAccessTokenRequest) => Promise<AWSCredentials>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Service for viewing files in Cirro
|
|
14
|
+
* currently this only operates on files within a project
|
|
15
|
+
*/
|
|
16
|
+
export class FileService {
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly fileCredsApi: IFileCredentialsApi
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get contents of a file
|
|
23
|
+
*/
|
|
24
|
+
async getProjectFile(file: DownloadableFile, params?: GetSignedUrlOptions): Promise<Response> {
|
|
25
|
+
const url = await this.getSignedUrlFromProjectFile(file, params);
|
|
26
|
+
return fetch(url);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a signed URL for a file
|
|
31
|
+
*/
|
|
32
|
+
async getSignedUrlFromProjectFile(file: DownloadableFile, params?: GetSignedUrlOptions): Promise<string> {
|
|
33
|
+
const credentials = await this.getProjectAccessCredentials(file.fileAccessContext);
|
|
34
|
+
const _params: GetFileUrlParams = {
|
|
35
|
+
...params,
|
|
36
|
+
filename: file.name,
|
|
37
|
+
url: file.url,
|
|
38
|
+
credentials,
|
|
39
|
+
};
|
|
40
|
+
return getSignedUrl(_params);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get a signed URL for a file given a path
|
|
45
|
+
*/
|
|
46
|
+
async getSignedUrlFromProjectPath(fileAccessContext: ProjectFileAccessContext, path: string, params?: GetSignedUrlOptions): Promise<string> {
|
|
47
|
+
const credentials = await this.getProjectAccessCredentials(fileAccessContext);
|
|
48
|
+
const _params: GetFileUrlParams = {
|
|
49
|
+
...params,
|
|
50
|
+
filename: path.split('/').pop(),
|
|
51
|
+
url: `s3://${getProjectS3Bucket(fileAccessContext.project.id)}/${path}`,
|
|
52
|
+
credentials,
|
|
53
|
+
};
|
|
54
|
+
return getSignedUrl(_params);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get credentials for accessing a project file
|
|
59
|
+
*/
|
|
60
|
+
async getProjectAccessCredentials(fileAccessContext: ProjectFileAccessContext): Promise<AWSCredentials> {
|
|
61
|
+
const accessType = fileAccessContext.fileAccessRequest.accessType;
|
|
62
|
+
// Special case for project download, since we can cache the credentials
|
|
63
|
+
if (accessType === ProjectAccessType.ProjectDownload || accessType === ProjectAccessType.SharedDatasetDownload) {
|
|
64
|
+
return this.getProjectReadCredentials(fileAccessContext);
|
|
65
|
+
}
|
|
66
|
+
return this.fileCredsApi.generateProjectFileAccessToken({ projectId: fileAccessContext.project.id, projectFileAccessRequest: fileAccessContext.fileAccessRequest });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async getProjectReadCredentials(fileAccessContext: ProjectFileAccessContext): Promise<AWSCredentials> {
|
|
70
|
+
const projectId = fileAccessContext.project.id;
|
|
71
|
+
// Append datasetId to cache key for shared dataset downloads since we need to generate a new token for each dataset
|
|
72
|
+
let cacheKey = projectId;
|
|
73
|
+
if (fileAccessContext.fileAccessRequest.accessType === ProjectAccessType.SharedDatasetDownload) {
|
|
74
|
+
cacheKey = `${projectId}-${fileAccessContext.fileAccessRequest.datasetId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return credentialsMutex.dispatch(async () => {
|
|
78
|
+
const cachedCredentials = credentialsCache.get(cacheKey);
|
|
79
|
+
const expirationTime = cachedCredentials ? cachedCredentials?.expiration : null;
|
|
80
|
+
const fetchNewCredentials = !expirationTime || expirationTime < new Date();
|
|
81
|
+
if (fetchNewCredentials) {
|
|
82
|
+
const projectFileAccessRequest = fileAccessContext.fileAccessRequest;
|
|
83
|
+
const credentials = await this.fileCredsApi.generateProjectFileAccessToken({ projectId, projectFileAccessRequest });
|
|
84
|
+
credentialsCache.set(cacheKey, credentials);
|
|
85
|
+
return credentials;
|
|
86
|
+
}
|
|
87
|
+
return cachedCredentials;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DatasetAssetsManifest, FileEntry } from '@cirrobio/api-client';
|
|
2
|
+
import { Assets } from "./models/assets";
|
|
3
|
+
import { Folder } from "./models/folder";
|
|
4
|
+
import { File } from "./models/file";
|
|
5
|
+
import { ProjectFileAccessContext } from "./project-access-context";
|
|
6
|
+
import { removeEndingSlash } from "../formatters";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export class ManifestParser {
|
|
10
|
+
private readonly manifest: DatasetAssetsManifest;
|
|
11
|
+
private readonly accessContext: ProjectFileAccessContext;
|
|
12
|
+
|
|
13
|
+
constructor(manifest: DatasetAssetsManifest, accessContext?: ProjectFileAccessContext) {
|
|
14
|
+
this.manifest = manifest;
|
|
15
|
+
this.accessContext = accessContext;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private get files(): Array<FileEntry> {
|
|
19
|
+
return this.manifest?.files ?? [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generates asset objects from the manifest, optionally including folder entries.
|
|
24
|
+
*/
|
|
25
|
+
public generateAssets(generateFolders = false): Assets {
|
|
26
|
+
const domain = removeEndingSlash(this.manifest?.domain);
|
|
27
|
+
const assets = new Assets();
|
|
28
|
+
|
|
29
|
+
for (const file of this.files) {
|
|
30
|
+
assets.push(new File(domain, file.path, file.size, file.metadata, this.accessContext));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Optionally add folder entries
|
|
34
|
+
if (generateFolders) {
|
|
35
|
+
assets.push(...this.generateFolderAssets(assets, domain));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return assets;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create folder entries, useful for visualizing folders as items in a file browser
|
|
42
|
+
private generateFolderAssets(assets: Assets, domain: string): Folder[] {
|
|
43
|
+
const folderSet = new Set<string>();
|
|
44
|
+
|
|
45
|
+
// Collect all unique folder paths
|
|
46
|
+
for (const asset of assets) {
|
|
47
|
+
const parts = asset.path.split('/').filter(p => p.length > 0);
|
|
48
|
+
let currentPath = '';
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
51
|
+
folderSet.add(currentPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create Folder objects from unique paths
|
|
56
|
+
return Array.from(folderSet)
|
|
57
|
+
.map(folderPath => {
|
|
58
|
+
const name = folderPath.substring(folderPath.lastIndexOf('/') + 1);
|
|
59
|
+
const parentPath = folderPath.includes('/') ? folderPath.substring(0, folderPath.lastIndexOf('/')) : '';
|
|
60
|
+
return new Folder(folderPath, name, parentPath, domain);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { calculateTotalSize } from "../calculate-size";
|
|
2
|
+
import { bytesToString } from "../../formatters";
|
|
3
|
+
import { FileSystemObject } from "./file-object.model";
|
|
4
|
+
|
|
5
|
+
export class Assets extends Array<FileSystemObject> {
|
|
6
|
+
private _totalSize: string;
|
|
7
|
+
private _totalSizeBytes: number;
|
|
8
|
+
|
|
9
|
+
// Total size of all files in human-readable format
|
|
10
|
+
get totalSize(): string {
|
|
11
|
+
if (!this._totalSize) {
|
|
12
|
+
this.calculateSize();
|
|
13
|
+
}
|
|
14
|
+
return this._totalSize;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get totalSizeBytes(): number {
|
|
18
|
+
if (!this._totalSizeBytes) {
|
|
19
|
+
this.calculateSize();
|
|
20
|
+
}
|
|
21
|
+
return this._totalSizeBytes;
|
|
22
|
+
}
|
|
23
|
+
private calculateSize(): void {
|
|
24
|
+
this._totalSizeBytes = calculateTotalSize(this);
|
|
25
|
+
this._totalSize = bytesToString(this._totalSizeBytes)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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 Display Path
|
|
35
|
+
*/
|
|
36
|
+
displayPath: string;
|
|
37
|
+
/**
|
|
38
|
+
* Object Name
|
|
39
|
+
*/
|
|
40
|
+
name: string;
|
|
41
|
+
/**
|
|
42
|
+
* Object Size
|
|
43
|
+
*/
|
|
44
|
+
size: number;
|
|
45
|
+
/**
|
|
46
|
+
* Object Kind (PNG, TXT)
|
|
47
|
+
*/
|
|
48
|
+
kind: string;
|
|
49
|
+
/**
|
|
50
|
+
* Object Type (File or Folder)
|
|
51
|
+
*/
|
|
52
|
+
type: FileSystemObjectType;
|
|
53
|
+
/**
|
|
54
|
+
* Metadata
|
|
55
|
+
*/
|
|
56
|
+
metadata?: Record<string, any>;
|
|
57
|
+
/**
|
|
58
|
+
* Access context
|
|
59
|
+
*/
|
|
60
|
+
fileAccessContext: ProjectFileAccessContext;
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { FileSystemObject, FileSystemObjectType } from "./file-object.model";
|
|
2
|
+
import { ProjectFileAccessContext } from "../project-access-context";
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import { getDisplayPath } from "../util/get-display.fn";
|
|
5
|
+
|
|
6
|
+
export class File implements FileSystemObject {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly url: string
|
|
9
|
+
readonly size: number;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly metadata: Record<string, any>;
|
|
13
|
+
readonly fileAccessContext: ProjectFileAccessContext;
|
|
14
|
+
|
|
15
|
+
get type() {
|
|
16
|
+
return FileSystemObjectType.FILE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get kind() {
|
|
20
|
+
return this.name.slice(this.name.lastIndexOf('.') + 1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get displayPath() {
|
|
24
|
+
return getDisplayPath(this);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor(domain: string, file: string, size: number, metadata: Record<string, any>, accessContext: ProjectFileAccessContext) {
|
|
28
|
+
this.id = uuidv4();
|
|
29
|
+
this.url = `${domain}/${file}`;
|
|
30
|
+
this.size = size;
|
|
31
|
+
this.metadata = metadata;
|
|
32
|
+
this.fileAccessContext = accessContext;
|
|
33
|
+
|
|
34
|
+
const fileFolderIdx = file.lastIndexOf('/');
|
|
35
|
+
if (fileFolderIdx === -1) {
|
|
36
|
+
this.path = '';
|
|
37
|
+
this.name = file;
|
|
38
|
+
} else {
|
|
39
|
+
this.path = file.substring(0, fileFolderIdx);
|
|
40
|
+
this.name = file.substring(fileFolderIdx + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { FileSystemObject, FileSystemObjectType } from "./file-object.model";
|
|
3
|
+
import { getDisplayPath } from "../util/get-display.fn";
|
|
4
|
+
|
|
5
|
+
export class Folder implements FileSystemObject {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly url: string;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly domain: string;
|
|
11
|
+
readonly kind = 'Folder';
|
|
12
|
+
readonly fileAccessContext = null;
|
|
13
|
+
readonly size = 0;
|
|
14
|
+
readonly type = FileSystemObjectType.FOLDER;
|
|
15
|
+
|
|
16
|
+
get displayPath() {
|
|
17
|
+
return getDisplayPath(this);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
constructor(url: string, name: string, path: string, domain: string) {
|
|
21
|
+
this.id = uuidv4();
|
|
22
|
+
this.url = url;
|
|
23
|
+
this.name = name;
|
|
24
|
+
this.path = path;
|
|
25
|
+
this.domain = domain;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ProjectAccessType, DatasetDetail, ProjectFileAccessRequest, 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: ProjectFileAccessRequest
|
|
13
|
+
) {
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static projectDownload(project: ProjectIdentifiable): ProjectFileAccessContext {
|
|
17
|
+
const request: ProjectFileAccessRequest = { accessType: ProjectAccessType.ProjectDownload };
|
|
18
|
+
return new ProjectFileAccessContext(project, null, request);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static datasetDownload(project: ProjectIdentifiable, dataset: DatasetDetail): ProjectFileAccessContext {
|
|
22
|
+
const accessType = dataset.share ? ProjectAccessType.SharedDatasetDownload : ProjectAccessType.ProjectDownload;
|
|
23
|
+
const request: ProjectFileAccessRequest = { accessType, datasetId: dataset.id };
|
|
24
|
+
return new ProjectFileAccessContext(project, dataset, request);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getResourceName } from "../util/get-resource-name";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get the S3 bucket name for a project.
|
|
5
|
+
* Will be deprecated in the future, use domain from manifest instead.
|
|
6
|
+
*/
|
|
7
|
+
export function getProjectS3Bucket(projectId: string): string {
|
|
8
|
+
return getResourceName(projectId)
|
|
9
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AWSCredentials } from '@cirrobio/api-client';
|
|
2
|
+
|
|
3
|
+
export 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,6 @@
|
|
|
1
|
+
import { removeStartingSlash } from "../../formatters/slash";
|
|
2
|
+
import { FileSystemObject } from "../models/file-object.model";
|
|
3
|
+
|
|
4
|
+
export function getDisplayPath(file: FileSystemObject, dataPrefix: string = "data/") {
|
|
5
|
+
return removeStartingSlash(`${file.path}/${file.name}`.replace(dataPrefix, ''));
|
|
6
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { AWSCredentials } from "@cirrobio/api-client";
|
|
3
|
+
import { S3ClientConfigType } from "@aws-sdk/client-s3/dist-types/S3Client";
|
|
4
|
+
|
|
5
|
+
const clientCache = new Map<string, S3Client>();
|
|
6
|
+
|
|
7
|
+
const clientDefaults: S3ClientConfigType = {
|
|
8
|
+
// Support ipv6 endpoints
|
|
9
|
+
useDualstackEndpoint: true,
|
|
10
|
+
// Protection against not having the correct region (does not work for signed url fn)
|
|
11
|
+
followRegionRedirects: true,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createS3ClientInternal(credentials: AWSCredentials, clientOverrides: S3ClientConfigType): S3Client {
|
|
15
|
+
return new S3Client({
|
|
16
|
+
credentials: {
|
|
17
|
+
accessKeyId: credentials.accessKeyId,
|
|
18
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
19
|
+
sessionToken: credentials.sessionToken,
|
|
20
|
+
},
|
|
21
|
+
region: credentials.region,
|
|
22
|
+
...clientDefaults,
|
|
23
|
+
...clientOverrides
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates an S3 client using the provided credentials.
|
|
29
|
+
*/
|
|
30
|
+
export function createS3Client(credentials: AWSCredentials, clientOverrides: S3ClientConfigType = {}): S3Client {
|
|
31
|
+
// Bypass cache when creating s3 client with overrides, since these values may be changed.
|
|
32
|
+
if (clientOverrides && Object.keys(clientOverrides).length > 0) {
|
|
33
|
+
return createS3ClientInternal(credentials, clientOverrides);
|
|
34
|
+
}
|
|
35
|
+
const cacheKey = `${credentials.accessKeyId}-${credentials.region}`;
|
|
36
|
+
const cachedClient = clientCache.get(cacheKey);
|
|
37
|
+
if (cachedClient) {
|
|
38
|
+
return cachedClient;
|
|
39
|
+
}
|
|
40
|
+
const newClient = createS3ClientInternal(credentials, clientOverrides)
|
|
41
|
+
clientCache.set(cacheKey, newClient);
|
|
42
|
+
return newClient;
|
|
43
|
+
}
|
|
@@ -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,16 @@
|
|
|
1
|
+
export * from './file/extensions.fn'
|
|
2
|
+
export { FileService, IFileCredentialsApi } 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, DownloadableFile } from './file/models/file-object.model'
|
|
8
|
+
export { s3UriToParams } from './file/util/s3-utils'
|
|
9
|
+
export { createS3Client } from './file/util/s3-client'
|
|
10
|
+
export { credentialsCache, credentialsMutex } from './file/util/credentials-mutex.so';
|
|
11
|
+
export { getProjectS3Bucket } from './file/shared'
|
|
12
|
+
export { calculateTotalSize } from './file/calculate-size'
|
|
13
|
+
export { Progress, Upload } from '@aws-sdk/lib-storage';
|
|
14
|
+
export { Assets } from './file/models/assets';
|
|
15
|
+
export { ManifestParser } from './file/manifest-parser';
|
|
16
|
+
export { getParentPath } from './file/util/get-parent.fn';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { bytesToString } from "../bytes-to-string";
|
|
2
|
+
import { normalizeDate } from "../normalize-date";
|
|
3
|
+
import { removeEndingSlash, removeStartingSlash } from "../slash";
|
|
4
|
+
import { toFriendlyName } from "../to-friendly-name";
|
|
5
|
+
import { toMoney } from "../to-money";
|
|
6
|
+
import { toTitleCase } from "../to-title-case";
|
|
7
|
+
import { normalizeString } from "../normalize-string";
|
|
8
|
+
import { toDateFormat } from "../to-date-format";
|
|
9
|
+
import { toPascalCase } from "../to-pascal-case";
|
|
10
|
+
|
|
11
|
+
describe('bytesToString', () => {
|
|
12
|
+
it('should convert IEC bytes to string', () => {
|
|
13
|
+
expect(bytesToString(12521, false)).toBe('12.2 KiB');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should convert SI bytes to string', () => {
|
|
17
|
+
expect(bytesToString(12521, true, 2)).toBe('12.52 kB');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('normalizeDate', () => {
|
|
22
|
+
it('should work on a string input', () => {
|
|
23
|
+
const dateString = '2022-12-01'
|
|
24
|
+
const output = normalizeDate(dateString).toISOString().split('T')[0]
|
|
25
|
+
expect(output).toEqual(dateString);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should work on a Date object input', () => {
|
|
29
|
+
const dateString = '2022-12-01'
|
|
30
|
+
const output = normalizeDate(new Date(dateString)).toISOString().split('T')[0]
|
|
31
|
+
expect(output).toEqual(dateString);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('normalizeString', () => {
|
|
36
|
+
it('should trim extra spaces', () => {
|
|
37
|
+
expect(normalizeString(' test ')).toEqual('test');
|
|
38
|
+
expect(normalizeString(' test')).toEqual('test');
|
|
39
|
+
});
|
|
40
|
+
it('should convert blank to null', () => {
|
|
41
|
+
expect(normalizeString(' ')).toEqual(null);
|
|
42
|
+
expect(normalizeString('')).toEqual(null);
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('removeStartingSlash', () => {
|
|
47
|
+
it('should remove the starting slash from a string', () => {
|
|
48
|
+
expect(removeStartingSlash('/example/path/')).toBe('example/path/');
|
|
49
|
+
expect(removeStartingSlash('./example/path/')).toBe('example/path/');
|
|
50
|
+
});
|
|
51
|
+
it('should not do anything with no starting slash', () => {
|
|
52
|
+
expect(removeStartingSlash('path/')).toBe('path/');
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('removeEndingSlash', () => {
|
|
57
|
+
it('should remove the ending slash from a string', () => {
|
|
58
|
+
expect(removeEndingSlash('/example/path/')).toBe('/example/path');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('toDateFormat', () => {
|
|
63
|
+
it('should display proper date format', () => {
|
|
64
|
+
expect(toDateFormat("2023-10-01 05:00:00 PST")).toBe('10/01/23');
|
|
65
|
+
});
|
|
66
|
+
it('should display proper date format', () => {
|
|
67
|
+
expect(toDateFormat(new Date(Date.parse("2023-10-01 05:00:00 PST")))).toBe('10/01/23');
|
|
68
|
+
});
|
|
69
|
+
it('should work with null', () => {
|
|
70
|
+
expect(toDateFormat(null)).toBe(null);
|
|
71
|
+
});
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('toFriendlyName', () => {
|
|
75
|
+
it('should add correct spaces to a string', () => {
|
|
76
|
+
expect(toFriendlyName('ExampleUnfriendly|Name_here')).toBe('Example unfriendly name here');
|
|
77
|
+
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('toMoney', () => {
|
|
82
|
+
it('convert an integer value to a money-formatted string', () => {
|
|
83
|
+
expect(toMoney(25)).toBe('$25.00');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('convert a float value to a money-formatted string', () => {
|
|
87
|
+
expect(toMoney(117.15)).toBe('$117.15');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('toTitleCase', () => {
|
|
92
|
+
it('should convert a string to title case', () => {
|
|
93
|
+
expect(toTitleCase('convert To Title case')).toBe('Convert to title case');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('toPascalCase', () => {
|
|
98
|
+
it('should convert a string to pascal case', () => {
|
|
99
|
+
expect(toPascalCase('convert to pascal case')).toBe('ConvertToPascalCase');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format bytes as human-readable text.
|
|
3
|
+
* @param bytes Number of bytes.
|
|
4
|
+
* @param si True to use metric(SI) units, aka powers of 1000. False to use
|
|
5
|
+
* binary(IEC), aka powers of 1024.
|
|
6
|
+
* @param dp Number of decimal places to display.
|
|
7
|
+
*
|
|
8
|
+
* @return Formatted string.
|
|
9
|
+
*/
|
|
10
|
+
export function bytesToString(bytes: number, si = false, dp = 1): string {
|
|
11
|
+
const thresh = si ? 1000 : 1024;
|
|
12
|
+
|
|
13
|
+
if (Math.abs(bytes) < thresh) {
|
|
14
|
+
return `${bytes} B`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const units = si
|
|
18
|
+
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
19
|
+
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
|
20
|
+
let u = -1;
|
|
21
|
+
const r = 10 ** dp;
|
|
22
|
+
|
|
23
|
+
do {
|
|
24
|
+
bytes /= thresh;
|
|
25
|
+
++u;
|
|
26
|
+
} while (
|
|
27
|
+
Math.round(Math.abs(bytes) * r) / r >= thresh
|
|
28
|
+
&& u < units.length - 1
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return `${bytes.toFixed(dp)} ${units[u]}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty prints a JSON object with the specified indentation level.
|
|
3
|
+
* @param obj JSON object to be pretty printed.
|
|
4
|
+
* @param indentLevel Number of spaces to use for indentation.
|
|
5
|
+
*/
|
|
6
|
+
export function jsonPrettyPrint(obj: object, indentLevel=2): string {
|
|
7
|
+
return JSON.stringify(obj, null, indentLevel);
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a date to UTC by adjusting for the timezone offset.
|
|
3
|
+
* Useful when you are working with dates that may not include time information.
|
|
4
|
+
* @param date
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeDate(date: Date | string | undefined | null): Date {
|
|
7
|
+
if (!date) throw new Error("Attempt to normalize undefined");
|
|
8
|
+
if (!(date instanceof Date)) date = new Date(date);
|
|
9
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * -60000);
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes the starting slash from a path.
|
|
3
|
+
* @param path Input path
|
|
4
|
+
*/
|
|
5
|
+
export function removeStartingSlash(path: string): string {
|
|
6
|
+
if (!path) return path;
|
|
7
|
+
if (path.startsWith('./')) return path.substring(2);
|
|
8
|
+
if (path.startsWith('/')) return path.substring(1);
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Removes the ending slash from a path.
|
|
14
|
+
* @param path Input path
|
|
15
|
+
*/
|
|
16
|
+
export function removeEndingSlash(path: string): string {
|
|
17
|
+
return path?.endsWith('/') ? path.slice(0, -1) : path;
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const dateFormat = new Intl.DateTimeFormat('en-US', { month: '2-digit', day: '2-digit', year: '2-digit' });
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a date string or Date object to MM/DD/YY date string.
|
|
5
|
+
* Ex., "2023-10-01" -> "10/01/23"
|
|
6
|
+
* @param date Input date string or Date object.
|
|
7
|
+
*/
|
|
8
|
+
export function toDateFormat(date: string | Date): string {
|
|
9
|
+
if (!date) return null;
|
|
10
|
+
const d: Date = (typeof date === 'string') ? new Date(date) : date
|
|
11
|
+
return dateFormat.format(d);
|
|
12
|
+
}
|