Skip to content

Commit

Permalink
feat(storage): add new internal downloadData api (#13887)
Browse files Browse the repository at this point in the history
* feat(internal-remove): add new internal downloadData api

* code cleanup

* code cleanup

* chore: fix ts doc

---------

Co-authored-by: Ashwin Kumar <[email protected]>
  • Loading branch information
ashwinkumar6 and Ashwin Kumar authored Oct 7, 2024
1 parent 20f62a6 commit 7318ba2
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 86 deletions.
76 changes: 76 additions & 0 deletions packages/storage/__tests__/internals/apis/downloadData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { downloadData as advancedDownloadData } from '../../../src/internals';
import { downloadData as downloadDataInternal } from '../../../src/providers/s3/apis/internal/downloadData';

jest.mock('../../../src/providers/s3/apis/internal/downloadData');
const mockedDownloadDataInternal = jest.mocked(downloadDataInternal);

describe('downloadData (internal)', () => {
beforeEach(() => {
mockedDownloadDataInternal.mockReturnValue({
result: Promise.resolve({
path: 'output/path/to/mock/object',
body: {
blob: () => Promise.resolve(new Blob()),
json: () => Promise.resolve(''),
text: () => Promise.resolve(''),
},
}),
cancel: jest.fn(),
state: 'SUCCESS',
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('should pass advanced option locationCredentialsProvider to internal downloadData', async () => {
const useAccelerateEndpoint = true;
const bucket = { bucketName: 'bucket', region: 'us-east-1' };
const locationCredentialsProvider = async () => ({
credentials: {
accessKeyId: 'akid',
secretAccessKey: 'secret',
sessionToken: 'token',
expiration: new Date(),
},
});
const onProgress = jest.fn();
const bytesRange = { start: 1024, end: 2048 };

const output = await advancedDownloadData({
path: 'input/path/to/mock/object',
options: {
useAccelerateEndpoint,
bucket,
locationCredentialsProvider,
onProgress,
bytesRange,
},
});

expect(mockedDownloadDataInternal).toHaveBeenCalledTimes(1);
expect(mockedDownloadDataInternal).toHaveBeenCalledWith({
path: 'input/path/to/mock/object',
options: {
useAccelerateEndpoint,
bucket,
locationCredentialsProvider,
onProgress,
bytesRange,
},
});

expect(await output.result).toEqual({
path: 'output/path/to/mock/object',
body: {
blob: expect.any(Function),
json: expect.any(Function),
text: expect.any(Function),
},
});
});
});
52 changes: 52 additions & 0 deletions packages/storage/src/internals/apis/downloadData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { downloadData as downloadDataInternal } from '../../providers/s3/apis/internal/downloadData';
import { DownloadDataInput } from '../types/inputs';
import { DownloadDataOutput } from '../types/outputs';

/**
* Download S3 object data to memory
*
* @param input - The `DownloadDataInput` object.
* @returns A cancelable task exposing result promise from `result` property.
* @throws service: `S3Exception` - thrown when checking for existence of the object
* @throws validation: `StorageValidationErrorCode` - Validation errors
*
* @example
* ```ts
* // Download a file from s3 bucket
* const { body, eTag } = await downloadData({ path, options: {
* onProgress, // Optional progress callback.
* } }).result;
* ```
* @example
* ```ts
* // Cancel a task
* const downloadTask = downloadData({ path });
* //...
* downloadTask.cancel();
* try {
* await downloadTask.result;
* } catch (error) {
* if(isCancelError(error)) {
* // Handle error thrown by task cancelation.
* }
* }
*```
*
* @internal
*/
export const downloadData = (input: DownloadDataInput): DownloadDataOutput =>
downloadDataInternal({
path: input.path,
options: {
useAccelerateEndpoint: input?.options?.useAccelerateEndpoint,
bucket: input?.options?.bucket,
locationCredentialsProvider: input?.options?.locationCredentialsProvider,
bytesRange: input?.options?.bytesRange,
onProgress: input?.options?.onProgress,
},
// Type casting is necessary because `downloadDataInternal` supports both Gen1 and Gen2 signatures, but here
// given in input can only be Gen2 signature, the return can only ben Gen2 signature.
}) as DownloadDataOutput;
2 changes: 1 addition & 1 deletion packages/storage/src/internals/apis/getProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { GetPropertiesOutput } from '../types/outputs';
/**
* Gets the properties of a file. The properties include S3 system metadata and
* the user metadata that was provided when uploading the file.
* @param input - The `GetPropertiesWithPathInput` object.
* @param input - The `GetPropertiesInput` object.
* @returns Requested object properties.
* @throws An `S3Exception` when the underlying S3 service returned error.
* @throws A `StorageValidationErrorCode` when API call parameters are invalid.
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/src/internals/apis/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { RemoveOutput } from '../types/outputs';

/**
* Remove a file from your S3 bucket.
* @param input - The `RemoveWithPathInput` object.
* @param input - The `RemoveInput` object.
* @return Output containing the removed object path.
* @throws service: `S3Exception` - S3 service errors thrown while while removing the object.
* @throws validation: `StorageValidationErrorCode` - Validation errors thrown
Expand Down
3 changes: 3 additions & 0 deletions packages/storage/src/internals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ export {
CopyInput,
ListInputWithPath,
RemoveInput,
DownloadDataInput,
} from './types/inputs';
export {
GetDataAccessOutput,
ListCallerAccessGrantsOutput,
GetPropertiesOutput,
GetUrlOutput,
RemoveOutput,
DownloadDataOutput,
} from './types/outputs';

export { getDataAccess } from './apis/getDataAccess';
Expand All @@ -31,6 +33,7 @@ export { list } from './apis/list';
export { getProperties } from './apis/getProperties';
export { getUrl } from './apis/getUrl';
export { remove } from './apis/remove';
export { downloadData } from './apis/downloadData';

/*
CredentialsStore exports
Expand Down
11 changes: 11 additions & 0 deletions packages/storage/src/internals/types/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../../types/inputs';
import {
CopyWithPathInput,
DownloadDataWithPathInput,
GetPropertiesWithPathInput,
GetUrlWithPathInput,
RemoveWithPathInput,
Expand Down Expand Up @@ -93,6 +94,16 @@ export type CopyInput = ExtendCopyInputWithAdvancedOptions<
}
>;

/**
* @internal
*/
export type DownloadDataInput = ExtendInputWithAdvancedOptions<
DownloadDataWithPathInput,
{
locationCredentialsProvider?: CredentialsProvider;
}
>;

/**
* Generic types that extend the public non-copy API input types with extended
* options. This is a temporary solution to support advanced options from internal APIs.
Expand Down
6 changes: 6 additions & 0 deletions packages/storage/src/internals/types/outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import {
DownloadDataWithPathOutput,
GetPropertiesWithPathOutput,
GetUrlWithPathOutput,
RemoveWithPathOutput,
Expand Down Expand Up @@ -33,3 +34,8 @@ export type GetUrlOutput = GetUrlWithPathOutput;
* @internal
*/
export type RemoveOutput = RemoveWithPathOutput;

/**
* @internal
*/
export type DownloadDataOutput = DownloadDataWithPathOutput;
87 changes: 3 additions & 84 deletions packages/storage/src/providers/s3/apis/downloadData.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Amplify } from '@aws-amplify/core';
import { StorageAction } from '@aws-amplify/core/internals/utils';

import {
DownloadDataInput,
DownloadDataOutput,
DownloadDataWithPathInput,
DownloadDataWithPathOutput,
} from '../types';
import { resolveS3ConfigAndInput } from '../utils/resolveS3ConfigAndInput';
import { createDownloadTask, validateStorageOperationInput } from '../utils';
import { getObject } from '../utils/client/s3data';
import { getStorageUserAgentValue } from '../utils/userAgent';
import { logger } from '../../../utils';
import {
StorageDownloadDataOutput,
StorageItemWithKey,
StorageItemWithPath,
} from '../../../types';
import { STORAGE_INPUT_KEY } from '../utils/constants';

import { downloadData as downloadDataInternal } from './internal/downloadData';

/**
* Download S3 object data to memory
Expand Down Expand Up @@ -89,77 +77,8 @@ export function downloadData(
*```
*/
export function downloadData(input: DownloadDataInput): DownloadDataOutput;

export function downloadData(
input: DownloadDataInput | DownloadDataWithPathInput,
) {
const abortController = new AbortController();

const downloadTask = createDownloadTask({
job: downloadDataJob(input, abortController.signal),
onCancel: (message?: string) => {
abortController.abort(message);
},
});

return downloadTask;
return downloadDataInternal(input);
}

const downloadDataJob =
(
downloadDataInput: DownloadDataInput | DownloadDataWithPathInput,
abortSignal: AbortSignal,
) =>
async (): Promise<
StorageDownloadDataOutput<StorageItemWithKey | StorageItemWithPath>
> => {
const { options: downloadDataOptions } = downloadDataInput;
const { bucket, keyPrefix, s3Config, identityId } =
await resolveS3ConfigAndInput(Amplify, downloadDataInput);
const { inputType, objectKey } = validateStorageOperationInput(
downloadDataInput,
identityId,
);
const finalKey =
inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey;

logger.debug(`download ${objectKey} from ${finalKey}.`);

const {
Body: body,
LastModified: lastModified,
ContentLength: size,
ETag: eTag,
Metadata: metadata,
VersionId: versionId,
ContentType: contentType,
} = await getObject(
{
...s3Config,
abortSignal,
onDownloadProgress: downloadDataOptions?.onProgress,
userAgentValue: getStorageUserAgentValue(StorageAction.DownloadData),
},
{
Bucket: bucket,
Key: finalKey,
...(downloadDataOptions?.bytesRange && {
Range: `bytes=${downloadDataOptions.bytesRange.start}-${downloadDataOptions.bytesRange.end}`,
}),
},
);

const result = {
body,
lastModified,
size,
contentType,
eTag,
metadata,
versionId,
};

return inputType === STORAGE_INPUT_KEY
? { key: objectKey, ...result }
: { path: objectKey, ...result };
};
Loading

0 comments on commit 7318ba2

Please sign in to comment.