Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storage): getProperties API #11378

Merged
merged 24 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7c22a1e
chore: add getProperties in StorageProvider
kvramyasri7 May 16, 2023
2d473d1
chore: add S3ProviderGetPropertiesOutput type
kvramyasri7 May 16, 2023
ab00587
chore: add types in Storage.ts
kvramyasri7 May 16, 2023
7a6fa39
chore: add plugin level getProperties method
kvramyasri7 May 16, 2023
92aee69
feat: add AWSS3Provider getProperties Implementation
kvramyasri7 May 16, 2023
796c11f
chore: add unit test cases
kvramyasri7 May 16, 2023
af5098f
fix: increase size-limit for predictions package
kvramyasri7 May 16, 2023
2251f9b
Merge branch 'aws-amplify:main' into storage/getProperties_API
kvramyasri7 May 17, 2023
d21c027
Update packages/storage/src/providers/AWSS3Provider.ts
kvramyasri7 May 17, 2023
f7a6160
fix: fixed string literal
kvramyasri7 May 17, 2023
f764581
fix: change metaData to metadata
kvramyasri7 May 17, 2023
8f4572e
fix:change reject to Error
kvramyasri7 May 17, 2023
fdea3cc
chore: add extra line for readability
kvramyasri7 May 17, 2023
e6c8a9f
chore: changes to support SSE parameters
kvramyasri7 May 17, 2023
5ad36c4
chore: add assertion for spyon
kvramyasri7 May 17, 2023
2bce874
chore: use getPluggable utility function clean up
kvramyasri7 May 18, 2023
1a3837d
fix: change unit tests for getPluggable function
kvramyasri7 May 18, 2023
cecb214
chore: add unit test for getProperties in storage-unit-tests
kvramyasri7 May 18, 2023
18f9e97
fix: remove Date.now function
kvramyasri7 May 18, 2023
0ad6dec
chore: add contentType in unit tests
kvramyasri7 May 18, 2023
eea6590
Merge branch 'main' into storage/getProperties_API
AllanZhengYP May 18, 2023
68cd516
Merge branch 'aws-amplify:main' into storage/getProperties_API
kvramyasri7 May 22, 2023
405e8f8
fix: revert getPluggable utility changes
kvramyasri7 May 22, 2023
8449227
Merge branch 'main' into storage/getProperties_API
kvramyasri7 May 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/predictions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"name": "Predictions (Identify provider)",
"path": "./lib-esm/index.js",
"import": "{ Amplify, Predictions, AmazonAIIdentifyPredictionsProvider }",
"limit": "104.5 kB"
"limit": "105 kB"
},
{
"name": "Predictions (Interpret provider)",
Expand Down
4 changes: 4 additions & 0 deletions packages/storage/__tests__/Storage-unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class TestCustomProvider implements StorageProvider {
return Promise.resolve({ newKey: 'get' });
}

getProperties(key: string, options?: CustomProviderConfig) {
return Promise.resolve({ newKey: 'getProperties' });
}

put(key: string, object: any, config: CustomProviderConfig) {
return Promise.resolve({ newKey: 'put' });
}
Expand Down
72 changes: 67 additions & 5 deletions packages/storage/__tests__/providers/AWSS3Provider-unit-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ListObjectsV2Command,
CreateMultipartUploadCommand,
UploadPartCommand,
HeadObjectCommand,
} from '@aws-sdk/client-s3';
import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner';

Expand Down Expand Up @@ -52,6 +53,14 @@ S3Client.prototype.send = jest.fn(async command => {
Contents: [resultObj],
IsTruncated: false,
};
} else if (command instanceof HeadObjectCommand) {
return {
ContentLength: '100',
ContentType: 'text/plain',
ETag: 'etag',
LastModified: 'lastmodified',
Metadata: { key: 'value' },
};
}
return 'data';
});
Expand Down Expand Up @@ -519,6 +528,7 @@ describe('StorageProvider test', () => {
return Promise.resolve(credentials);
});
});

test('get existing object with validateObjectExistence option', async () => {
expect.assertions(5);
const options_with_validateObjectExistence = Object.assign(
Expand Down Expand Up @@ -556,11 +566,6 @@ describe('StorageProvider test', () => {

test('get non-existing object with validateObjectExistence option', async () => {
expect.assertions(2);
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return new Promise((res, rej) => {
res({});
});
});
AllanZhengYP marked this conversation as resolved.
Show resolved Hide resolved
const dispatchSpy = jest.spyOn(StorageUtils, 'dispatchStorageEvent');
jest
.spyOn(S3Client.prototype, 'send')
Expand All @@ -586,6 +591,63 @@ describe('StorageProvider test', () => {
});
});

describe('getProperties test', () => {
beforeEach(() => {
jest.spyOn(Credentials, 'get').mockImplementationOnce(() => {
return Promise.resolve(credentials);
});
});

test('getProperties successfully', async () => {
expect.assertions(4);
const spyon = jest.spyOn(S3Client.prototype, 'send');
const dispatchSpy = jest.spyOn(StorageUtils, 'dispatchStorageEvent');
const metadata = { key: 'value' };
expect(await storage.getProperties('key')).toEqual({
contentLength: '100',
contentType: 'text/plain',
eTag: 'etag',
lastModified: 'lastmodified',
metadata,
});
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expect(dispatchSpy).toBeCalledWith(
false,
'getProperties',
{ method: 'getProperties', result: 'success' },
null,
'getProperties successful for key'
);
expect(spyon.mock.calls[0][0].input).toEqual({
Bucket: 'bucket',
Key: 'public/key',
});
spyon.mockClear();
kvramyasri7 marked this conversation as resolved.
Show resolved Hide resolved
});

test('get properties of non-existing object', async () => {
kvramyasri7 marked this conversation as resolved.
Show resolved Hide resolved
expect.assertions(2);
const dispatchSpy = jest.spyOn(StorageUtils, 'dispatchStorageEvent');
jest
.spyOn(S3Client.prototype, 'send')
.mockImplementationOnce(async params => {
throw { $metadata: { httpStatusCode: 404 }, name: 'NotFound' };
});
try {
await storage.getProperties('invalid_key');
} catch (error) {
expect(error.$metadata.httpStatusCode).toBe(404);
expect(dispatchSpy).toBeCalledWith(
false,
'getProperties',
{ method: 'getProperties', result: 'failed' },
null,
'invalid_key not found'
);
}
});
});

describe('put test', () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down
21 changes: 21 additions & 0 deletions packages/storage/src/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
StorageListOutput,
StorageCopyOutput,
UploadTask,
StorageGetPropertiesConfig,
StorageGetPropertiesOutput,
} from './types';
import axios, { CancelTokenSource } from 'axios';
import { PutObjectCommandInput } from '@aws-sdk/client-s3';
Expand Down Expand Up @@ -307,6 +309,25 @@ export class Storage {
return axios.isCancel(error);
}

public getProperties<T extends StorageProvider | { [key: string]: any }>(
key: string,
config?: StorageGetPropertiesConfig<T>
): StorageGetPropertiesOutput<T> {
const provider = config?.provider || DEFAULT_PROVIDER;
const prov = this._pluggables.find(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prov can be made more descriptive? Also there seems to be a util function to get the provider.

pluggable => pluggable.getProviderName() === provider
);
if (prov === undefined) {
logger.debug('No plugin found with providerName', provider);
throw new Error('No plugin found with providerName');
}
const cancelTokenSource = this.getCancellableTokenSource();
const responsePromise = prov.getProperties(key, {
...config,
});
this.updateRequestToBeCancellable(responsePromise, cancelTokenSource);
return responsePromise as StorageGetPropertiesOutput<T>;
}
/**
* Put a file in storage bucket specified to configure method
* @param key - key of the object
Expand Down
86 changes: 85 additions & 1 deletion packages/storage/src/providers/AWSS3Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
GetObjectCommandInput,
ListObjectsV2Request,
HeadObjectCommand,
HeadObjectCommandInput,
} from '@aws-sdk/client-s3';
import { formatUrl } from '@aws-sdk/util-format-url';
import { createRequest } from '@aws-sdk/util-create-request';
Expand Down Expand Up @@ -49,6 +50,8 @@ import {
UploadTask,
S3ClientOptions,
S3ProviderListOutput,
S3ProviderGetPropertiesOutput,
S3ProviderGetPropertiesConfig,
} from '../types';
import { StorageErrorStrings } from '../common/StorageErrorStrings';
import { dispatchStorageEvent } from '../common/StorageUtils';
Expand Down Expand Up @@ -449,7 +452,7 @@ export class AWSS3Provider implements StorageProvider {
}
if (validateObjectExistence) {
const headObjectCommand = new HeadObjectCommand(params);

try {
await s3.send(headObjectCommand);
} catch (error) {
Expand Down Expand Up @@ -498,6 +501,87 @@ export class AWSS3Provider implements StorageProvider {
}
}

/**
* Get Properties of the object
*
* @param {string} key - key of the object
* @param {S3ProviderGetPropertiesConfig} [config] - Optional configuration for the underlying S3 command
* @return {Promise<S3ProviderGetPropertiesOutput>} - A promise resolves to contentType,
* contentLength, eTag, lastModified, metadata
*/
public async getProperties(
key: string,
config?: S3ProviderGetPropertiesConfig
): Promise<S3ProviderGetPropertiesOutput> {
const credentialsOK = await this._ensureCredentials();
if (!credentialsOK || !this._isWithCredentials(this._config)) {
throw new Error(StorageErrorStrings.NO_CREDENTIALS);
}
const opt = Object.assign({}, this._config, config);
const {
bucket,
track = false,
SSECustomerAlgorithm,
SSECustomerKey,
SSECustomerKeyMD5,
} = opt;
const prefix = this._prefix(opt);
const final_key = prefix + key;
const emitter = new events.EventEmitter();
const s3 = this._createNewS3Client(opt, emitter);
logger.debug(`getProperties ${key} from ${final_key}`);

const params: HeadObjectCommandInput = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this API should support SSE to align with the other APIs (HeadObject command supports it it looks like). It would be strange if you can upload/download SSE-enabled objects but can't get the meta-data for them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added SSE support!

Bucket: bucket,
Key: final_key,
};

if (SSECustomerAlgorithm) {
params.SSECustomerAlgorithm = SSECustomerAlgorithm;
}
if (SSECustomerKey) {
params.SSECustomerKey = SSECustomerKey;
}
if (SSECustomerKeyMD5) {
params.SSECustomerKeyMD5 = SSECustomerKeyMD5;
}
// See: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/classes/headobjectcommand.html

const headObjectCommand = new HeadObjectCommand(params);
try {
const response = await s3.send(headObjectCommand);
const getPropertiesResponse: S3ProviderGetPropertiesOutput = {
contentLength: response.ContentLength,
contentType: response.ContentType,
eTag: response.ETag,
lastModified: response.LastModified,
metadata: response.Metadata,
};
dispatchStorageEvent(
track,
'getProperties',
{ method: 'getProperties', result: 'success' },
null,
`getProperties successful for ${key}`
);
return getPropertiesResponse;
} catch (error) {
if (error.$metadata.httpStatusCode === 404) {
dispatchStorageEvent(
track,
'getProperties',
{
method: 'getProperties',
result: 'failed',
},
null,
`${key} not found`
);
}
throw error;
}
}

/**
* Put a file in S3 bucket specified to configure method
* @param key - key of the object
Expand Down
15 changes: 15 additions & 0 deletions packages/storage/src/types/AWSS3Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CopyObjectRequest,
_Object,
DeleteObjectCommandOutput,
HeadObjectRequest,
} from '@aws-sdk/client-s3';
import { StorageOptions, StorageAccessLevel } from './Storage';
import {
Expand Down Expand Up @@ -51,6 +52,12 @@ export type S3ProviderGetConfig = CommonStorageOptions & {
validateObjectExistence?: boolean;
};

export type S3ProviderGetPropertiesConfig = CommonStorageOptions & {
SSECustomerAlgorithm?: HeadObjectRequest['SSECustomerAlgorithm'];
SSECustomerKey?: HeadObjectRequest['SSECustomerKey'];
SSECustomerKeyMD5?: HeadObjectRequest['SSECustomerKeyMD5'];
};

export type S3ProviderGetOuput<T> = T extends { download: true }
? GetObjectCommandOutput
: string;
Expand Down Expand Up @@ -173,6 +180,14 @@ export type S3ProviderCopyOutput = {
key: string;
};

export type S3ProviderGetPropertiesOutput = {
contentType: string;
contentLength: number;
eTag: string;
lastModified: Date;
metadata: Record<string, string>;
};

export type PutResult = {
key: string;
};
Expand Down
11 changes: 10 additions & 1 deletion packages/storage/src/types/Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export interface StorageProvider {
// get object/pre-signed url from storage
get(key: string, options?): Promise<string | Object>;

// get properties of object
getProperties(key: string, options?): Promise<Object>;

// upload storage object
put(key: string, object, options?): Promise<Object> | UploadTask;

Expand Down Expand Up @@ -52,4 +55,10 @@ export interface StorageProviderWithCopy extends StorageProvider {
): Promise<any>;
}

export type StorageProviderApi = 'copy' | 'get' | 'put' | 'remove' | 'list';
export type StorageProviderApi =
| 'copy'
| 'get'
| 'put'
| 'remove'
| 'list'
| 'getProperties';
15 changes: 15 additions & 0 deletions packages/storage/src/types/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
S3ProviderListOutput,
S3ProviderCopyOutput,
S3ProviderPutOutput,
S3ProviderGetPropertiesOutput,
} from '../';

type Tail<T extends any[]> = ((...t: T) => void) extends (
Expand Down Expand Up @@ -87,6 +88,14 @@ export type StorageGetConfig<T extends Record<string, any>> =
T
>;

export type StorageGetPropertiesConfig<T extends Record<string, any>> =
T extends StorageProvider
? StorageOperationConfig<T, 'getProperties'>
: StorageOperationConfigMap<
StorageOperationConfig<AWSS3Provider, 'getProperties'>,
T
>;

export type StoragePutConfig<T extends Record<string, any>> =
T extends StorageProvider
? StorageOperationConfig<T, 'put'>
Expand Down Expand Up @@ -168,6 +177,12 @@ export type StorageCopyOutput<T> = PickProviderOutput<
'copy'
>;

export type StorageGetPropertiesOutput<T> = PickProviderOutput<
Promise<S3ProviderGetPropertiesOutput>,
T,
'getProperties'
>;

/**
* Utility type to allow custom provider to use any config keys, if provider is set to AWSS3 then it should use
* AWSS3Provider's config.
Expand Down