diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index ef74d972717..b4f4be3f1b7 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -466,7 +466,7 @@ "name": "[Storage] downloadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ downloadData }", - "limit": "14.00 kB" + "limit": "14.10 kB" }, { "name": "[Storage] getProperties (S3)", diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index 62f1704398b..88e9cb21eb7 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -5,8 +5,18 @@ import { AWSCredentials } from '@aws-amplify/core/internals/utils'; import { Amplify } from '@aws-amplify/core'; import { getObject } from '../../../../src/providers/s3/utils/client'; import { downloadData } from '../../../../src/providers/s3'; -import { createDownloadTask } from '../../../../src/providers/s3/utils'; -import { DownloadDataOptions } from '../../../../src/providers/s3/types'; +import { + createDownloadTask, + validateStorageOperationInput, +} from '../../../../src/providers/s3/utils'; +import { + DownloadDataOptionsKey, + DownloadDataOptionsPath, +} from '../../../../src/providers/s3/types'; +import { + STORAGE_INPUT_KEY, + STORAGE_INPUT_PATH, +} from '../../../../src/providers/s3/utils/constants'; jest.mock('../../../../src/providers/s3/utils/client'); jest.mock('../../../../src/providers/s3/utils'); @@ -34,9 +44,10 @@ const defaultIdentityId = 'defaultIdentityId'; const mockFetchAuthSession = Amplify.Auth.fetchAuthSession as jest.Mock; const mockCreateDownloadTask = createDownloadTask as jest.Mock; +const mockValidateStorageInput = validateStorageOperationInput as jest.Mock; const mockGetConfig = Amplify.getConfig as jest.Mock; -describe('downloadData', () => { +describe('downloadData with key', () => { beforeAll(() => { mockFetchAuthSession.mockResolvedValue({ credentials, @@ -52,16 +63,20 @@ describe('downloadData', () => { }); }); mockCreateDownloadTask.mockReturnValue('downloadTask'); + mockValidateStorageInput.mockReturnValue({ + inputType: STORAGE_INPUT_KEY, + objectKey: key, + }); beforeEach(() => { jest.clearAllMocks(); }); - it('should return a download task', async () => { + it('should return a download task with key', async () => { expect(downloadData({ key: 'key' })).toBe('downloadTask'); }); - [ + test.each([ { expectedKey: `public/${key}`, }, @@ -81,14 +96,9 @@ describe('downloadData', () => { options: { accessLevel: 'protected', targetIdentityId }, expectedKey: `protected/${targetIdentityId}/${key}`, }, - ].forEach(({ options, expectedKey }) => { - const accessLevelMsg = options?.accessLevel ?? 'default'; - const targetIdentityIdMsg = options?.targetIdentityId - ? `and targetIdentityId` - : ''; - - it(`should supply the correct parameters to getObject API handler with ${accessLevelMsg} accessLevel ${targetIdentityIdMsg}`, async () => { - expect.assertions(2); + ])( + 'should supply the correct parameters to getObject API handler with $expectedKey accessLevel', + async ({ options, expectedKey }) => { (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); const onProgress = jest.fn(); downloadData({ @@ -97,7 +107,7 @@ describe('downloadData', () => { ...options, useAccelerateEndpoint: true, onProgress, - } as DownloadDataOptions, + } as DownloadDataOptionsKey, }); const job = mockCreateDownloadTask.mock.calls[0][0].job; await job(); @@ -116,11 +126,10 @@ describe('downloadData', () => { Key: expectedKey, }, ); - }); - }); + }, + ); - it('should assign the getObject API handler response to the result', async () => { - expect.assertions(2); + it('should assign the getObject API handler response to the result with key', async () => { const lastModified = 'lastModified'; const contentLength = 'contentLength'; const eTag = 'eTag'; @@ -177,3 +186,131 @@ describe('downloadData', () => { ); }); }); + +describe('downloadData with path', () => { + beforeAll(() => { + mockFetchAuthSession.mockResolvedValue({ + credentials, + identityId: defaultIdentityId, + }); + mockGetConfig.mockReturnValue({ + Storage: { + S3: { + bucket, + region, + }, + }, + }); + mockCreateDownloadTask.mockReturnValue('downloadTask'); + mockValidateStorageInput.mockReturnValue({ + inputType: STORAGE_INPUT_PATH, + objectKey: 'path', + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return a download task with path', async () => { + expect(downloadData({ path: 'path' })).toBe('downloadTask'); + }); + + test.each([ + { + path: 'path', + expectedKey: 'path', + }, + { + path: () => 'path', + expectedKey: 'path', + }, + ])( + 'should call getObject API with $expectedKey when path provided is $path', + async ({ path, expectedKey }) => { + (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); + const onProgress = jest.fn(); + downloadData({ + path: path, + options: { + useAccelerateEndpoint: true, + onProgress, + } as DownloadDataOptionsPath, + }); + const job = mockCreateDownloadTask.mock.calls[0][0].job; + await job(); + expect(getObject).toHaveBeenCalledTimes(1); + expect(getObject).toHaveBeenCalledWith( + { + credentials, + region, + useAccelerateEndpoint: true, + onDownloadProgress: onProgress, + abortSignal: expect.any(AbortSignal), + userAgentValue: expect.any(String), + }, + { + Bucket: bucket, + Key: expectedKey, + }, + ); + }, + ); + + it('should assign the getObject API handler response to the result with path', async () => { + const lastModified = 'lastModified'; + const contentLength = 'contentLength'; + const eTag = 'eTag'; + const metadata = 'metadata'; + const versionId = 'versionId'; + const contentType = 'contentType'; + const body = 'body'; + const path = 'path'; + (getObject as jest.Mock).mockResolvedValueOnce({ + Body: body, + LastModified: lastModified, + ContentLength: contentLength, + ETag: eTag, + Metadata: metadata, + VersionId: versionId, + ContentType: contentType, + }); + downloadData({ path }); + const job = mockCreateDownloadTask.mock.calls[0][0].job; + const result = await job(); + expect(getObject).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + path, + body, + lastModified, + size: contentLength, + eTag, + metadata, + versionId, + contentType, + }); + }); + + it('should forward the bytes range option to the getObject API', async () => { + const start = 1; + const end = 100; + (getObject as jest.Mock).mockResolvedValueOnce({ Body: 'body' }); + + downloadData({ + path: 'mockPath', + options: { + bytesRange: { start, end }, + }, + }); + + const job = mockCreateDownloadTask.mock.calls[0][0].job; + await job(); + + expect(getObject).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + Range: `bytes=${start}-${end}`, + }), + ); + }); +}); diff --git a/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts new file mode 100644 index 00000000000..83388bd3457 --- /dev/null +++ b/packages/storage/__tests__/providers/s3/apis/utils/validateStorageOperationInput.test.ts @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + StorageValidationErrorCode, + validationErrorMap, +} from '../../../../../src/errors/types/validation'; +import { validateStorageOperationInput } from '../../../../../src/providers/s3/utils'; +import { + STORAGE_INPUT_KEY, + STORAGE_INPUT_PATH, +} from '../../../../../src/providers/s3/utils/constants'; + +describe('validateStorageOperationInput', () => { + it('should return inputType as STORAGE_INPUT_PATH and objectKey as testPath when input is path as string', () => { + const input = { path: 'testPath' }; + const result = validateStorageOperationInput(input); + expect(result).toEqual({ + inputType: STORAGE_INPUT_PATH, + objectKey: 'testPath', + }); + }); + + it('should return inputType as STORAGE_INPUT_PATH and objectKey as result of path function when input is path as function', () => { + const input = { + path: ({ identityId }: { identityId?: string }) => + `testPath/${identityId}`, + }; + const result = validateStorageOperationInput(input, '123'); + expect(result).toEqual({ + inputType: STORAGE_INPUT_PATH, + objectKey: 'testPath/123', + }); + }); + + it('should return inputType as STORAGE_INPUT_KEY and objectKey as testKey when input is key', () => { + const input = { key: 'testKey' }; + const result = validateStorageOperationInput(input); + expect(result).toEqual({ + inputType: STORAGE_INPUT_KEY, + objectKey: 'testKey', + }); + }); + + it('should throw an error when input is invalid', () => { + const input = { invalid: 'test' } as any; + expect(() => validateStorageOperationInput(input)).toThrow( + validationErrorMap[ + StorageValidationErrorCode.InvalidStorageOperationInput + ].message, + ); + }); +}); diff --git a/packages/storage/src/errors/types/validation.ts b/packages/storage/src/errors/types/validation.ts index f596b8d8e25..d63e3d21b34 100644 --- a/packages/storage/src/errors/types/validation.ts +++ b/packages/storage/src/errors/types/validation.ts @@ -14,6 +14,7 @@ export enum StorageValidationErrorCode { UrlExpirationMaxLimitExceed = 'UrlExpirationMaxLimitExceed', ObjectIsTooLarge = 'ObjectIsTooLarge', InvalidUploadSource = 'InvalidUploadSource', + InvalidStorageOperationInput = 'InvalidStorageOperationInput', } export const validationErrorMap: AmplifyErrorMap = { @@ -49,4 +50,7 @@ export const validationErrorMap: AmplifyErrorMap = { message: 'Upload source type can only be a `Blob`, `File`, `ArrayBuffer`, or `string`.', }, + [StorageValidationErrorCode.InvalidStorageOperationInput]: { + message: 'Missing path or key parameter in Input', + }, }; diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index b10fc35d8ea..68f335d0843 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -4,45 +4,93 @@ import { Amplify } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; -import { DownloadDataInput, DownloadDataOutput, S3Exception } from '../types'; +import { + DownloadDataInput, + DownloadDataInputKey, + DownloadDataInputPath, + DownloadDataOutput, + DownloadDataOutputKey, + DownloadDataOutputPath, +} from '../types'; import { resolveS3ConfigAndInput } from '../utils/resolveS3ConfigAndInput'; -import { StorageValidationErrorCode } from '../../../errors/types/validation'; -import { createDownloadTask } from '../utils'; +import { createDownloadTask, validateStorageOperationInput } from '../utils'; import { getObject } from '../utils/client'; import { getStorageUserAgentValue } from '../utils/userAgent'; import { logger } from '../../../utils'; +import { + StorageDownloadDataOutput, + StorageItemKey, + StorageItemPath, +} from '../../../types'; +import { STORAGE_INPUT_KEY } from '../utils/constants'; -/** - * Download S3 object data to memory - * - * @param input - The DownloadDataInput object. - * @returns A cancelable task exposing result promise from `result` property. - * @throws service: {@link S3Exception} - thrown when checking for existence of the object - * @throws validation: {@link StorageValidationErrorCode } - Validation errors - * - * @example - * ```ts - * // Download a file from s3 bucket - * const { body, eTag } = await downloadData({ key, data: file, options: { - * onProgress, // Optional progress callback. - * } }).result; - * ``` - * @example - * ```ts - * // Cancel a task - * const downloadTask = downloadData({ key, data: file }); - * //... - * downloadTask.cancel(); - * try { - * await downloadTask.result; - * } catch (error) { - * if(isCancelError(error)) { - * // Handle error thrown by task cancelation. - * } - * } - *``` - */ -export const downloadData = (input: DownloadDataInput): DownloadDataOutput => { +interface DownloadData { + /** + * Download S3 object data to memory + * + * @param input - The DownloadDataInputPath object. + * @returns A cancelable task exposing result promise from `result` property. + * + * @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. + * } + * } + *``` + */ + (input: DownloadDataInputPath): DownloadDataOutputPath; + /** + * @deprecated The `key` and `accessLevel` parameters are deprecated and will be removed in next major version. + * Please use {@link https://docs.amplify.aws/react/build-a-backend/storage/download/#downloaddata | path} instead. + * + * Download S3 object data to memory + * + * @param input - The DownloadDataInputKey object. + * @returns A cancelable task exposing result promise from `result` property. + * + * @example + * ```ts + * // Download a file from s3 bucket + * const { body, eTag } = await downloadData({ key, options: { + * onProgress, // Optional progress callback. + * } }).result; + * ``` + * @example + * ```ts + * // Cancel a task + * const downloadTask = downloadData({ key }); + * //... + * downloadTask.cancel(); + * try { + * await downloadTask.result; + * } catch (error) { + * if(isCancelError(error)) { + * // Handle error thrown by task cancelation. + * } + * } + *``` + */ + (input: DownloadDataInputKey): DownloadDataOutputKey; +} + +export const downloadData: DownloadData = ( + input: DownloadDataInput, +): Output => { const abortController = new AbortController(); const downloadTask = createDownloadTask({ @@ -52,22 +100,25 @@ export const downloadData = (input: DownloadDataInput): DownloadDataOutput => { }, }); - return downloadTask; + return downloadTask as Output; }; const downloadDataJob = - ( - { options: downloadDataOptions, key }: DownloadDataInput, - abortSignal: AbortSignal, - ) => - async () => { - const { bucket, keyPrefix, s3Config } = await resolveS3ConfigAndInput( - Amplify, - downloadDataOptions, + (downloadDataInput: DownloadDataInput, abortSignal: AbortSignal) => + async (): Promise< + StorageDownloadDataOutput + > => { + const { options: downloadDataOptions } = downloadDataInput; + const { bucket, keyPrefix, s3Config, identityId } = + await resolveS3ConfigAndInput(Amplify, downloadDataOptions); + const { inputType, objectKey } = validateStorageOperationInput( + downloadDataInput, + identityId, ); - const finalKey = keyPrefix + key; + const finalKey = + inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey; - logger.debug(`download ${key} from ${finalKey}.`); + logger.debug(`download ${objectKey} from ${finalKey}.`); const { Body: body, @@ -93,8 +144,7 @@ const downloadDataJob = }, ); - return { - key, + const result = { body, lastModified, size, @@ -103,4 +153,8 @@ const downloadDataJob = metadata, versionId, }; + + return inputType === STORAGE_INPUT_KEY + ? { key: objectKey, ...result } + : { path: finalKey, ...result }; }; diff --git a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts index 587ee33c434..7aa62f08390 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/multipart/uploadHandlers.ts @@ -6,7 +6,7 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { UploadDataInput } from '../../../types'; import { resolveS3ConfigAndInput } from '../../../utils'; -import { Item as S3Item } from '../../../types/outputs'; +import { ItemKey as S3Item } from '../../../types/outputs'; import { DEFAULT_ACCESS_LEVEL, DEFAULT_QUEUE_SIZE, diff --git a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts index 5d1a40786ad..30819b854d8 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/putObjectJob.ts @@ -6,7 +6,7 @@ import { StorageAction } from '@aws-amplify/core/internals/utils'; import { UploadDataInput } from '../../types'; import { calculateContentMd5, resolveS3ConfigAndInput } from '../../utils'; -import { Item as S3Item } from '../../types/outputs'; +import { ItemKey as S3Item } from '../../types/outputs'; import { putObject } from '../../utils/client'; import { getStorageUserAgentValue } from '../../utils/userAgent'; diff --git a/packages/storage/src/providers/s3/types/index.ts b/packages/storage/src/providers/s3/types/index.ts index 4366ee48383..773dde39361 100644 --- a/packages/storage/src/providers/s3/types/index.ts +++ b/packages/storage/src/providers/s3/types/index.ts @@ -8,12 +8,15 @@ export { ListAllOptions, ListPaginateOptions, RemoveOptions, - DownloadDataOptions, + DownloadDataOptionsPath, + DownloadDataOptionsKey, CopyDestinationOptions, CopySourceOptions, } from './options'; export { DownloadDataOutput, + DownloadDataOutputKey, + DownloadDataOutputPath, GetUrlOutput, UploadDataOutput, ListOutputItem, @@ -31,6 +34,8 @@ export { ListPaginateInput, RemoveInput, DownloadDataInput, + DownloadDataInputKey, + DownloadDataInputPath, UploadDataInput, } from './inputs'; export { S3Exception } from './errors'; diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index 9a360d0bfe3..3ec41b7d47a 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -1,9 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { StrictUnion } from '@aws-amplify/core/internals/utils'; + import { StorageCopyInput, - StorageDownloadDataInput, + StorageDownloadDataInputKey, + StorageDownloadDataInputPath, StorageGetPropertiesInput, StorageGetUrlInput, StorageListInput, @@ -13,7 +16,8 @@ import { import { CopyDestinationOptions, CopySourceOptions, - DownloadDataOptions, + DownloadDataOptionsKey, + DownloadDataOptionsPath, GetPropertiesOptions, GetUrlOptions, ListAllOptions, @@ -60,7 +64,15 @@ export type RemoveInput = StorageRemoveInput; /** * Input type for S3 downloadData API. */ -export type DownloadDataInput = StorageDownloadDataInput; +export type DownloadDataInput = StrictUnion< + DownloadDataInputKey | DownloadDataInputPath +>; + +/** @deprecated Use {@link DownloadDataInputPath} instead. */ +export type DownloadDataInputKey = + StorageDownloadDataInputKey; +export type DownloadDataInputPath = + StorageDownloadDataInputPath; /** * Input type for S3 uploadData API. diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index dad92d72f1b..93f83eebea4 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -18,11 +18,14 @@ interface CommonOptions { useAccelerateEndpoint?: boolean; } +/** @deprecated This will be removed in next major version. */ type ReadOptions = | { accessLevel?: 'guest' | 'private' } | { accessLevel: 'protected'; targetIdentityId?: string }; +/** @deprecated This will be removed in next major version. */ interface WriteOptions { + /** @deprecated This will be removed in next major version. */ accessLevel?: StorageAccessLevel; } @@ -87,11 +90,14 @@ export type GetUrlOptions = ReadOptions & /** * Input options type for S3 downloadData API. */ -export type DownloadDataOptions = ReadOptions & - CommonOptions & +export type DownloadDataOptions = CommonOptions & TransferOptions & BytesRangeOptions; +/** @deprecated Use {@link DownloadDataOptionsPath} instead. */ +export type DownloadDataOptionsKey = ReadOptions & DownloadDataOptions; +export type DownloadDataOptionsPath = DownloadDataOptions; + export type UploadDataOptions = WriteOptions & CommonOptions & TransferOptions & { diff --git a/packages/storage/src/providers/s3/types/outputs.ts b/packages/storage/src/providers/s3/types/outputs.ts index bbcb9fc75b1..35b6784af93 100644 --- a/packages/storage/src/providers/s3/types/outputs.ts +++ b/packages/storage/src/providers/s3/types/outputs.ts @@ -5,15 +5,16 @@ import { DownloadTask, StorageDownloadDataOutput, StorageGetUrlOutput, - StorageItem, + StorageItemKey, + StorageItemPath, StorageListOutput, UploadTask, } from '../../../types'; /** - * type for S3 item. + * Base type for an S3 item. */ -export interface Item extends StorageItem { +export interface ItemBase { /** * VersionId used to reference a specific version of the object. */ @@ -24,15 +25,29 @@ export interface Item extends StorageItem { contentType?: string; } +/** + * @deprecated Use {@link ItemPath} instead. + */ +export type ItemKey = ItemBase & StorageItemKey; +export type ItemPath = ItemBase & StorageItemPath; + /** * type for S3 list item. */ -export type ListOutputItem = Omit; +export type ListOutputItem = Omit; + +/** @deprecated Use {@link DownloadDataOutputPath} instead. */ +export type DownloadDataOutputKey = DownloadTask< + StorageDownloadDataOutput +>; +export type DownloadDataOutputPath = DownloadTask< + StorageDownloadDataOutput +>; /** * Output type for S3 downloadData API. */ -export type DownloadDataOutput = DownloadTask>; +export type DownloadDataOutput = DownloadDataOutputKey | DownloadDataOutputPath; /** * Output type for S3 getUrl API. @@ -42,12 +57,12 @@ export type GetUrlOutput = StorageGetUrlOutput; /** * Output type for S3 uploadData API. */ -export type UploadDataOutput = UploadTask; +export type UploadDataOutput = UploadTask; /** * Output type for S3 getProperties API. */ -export type GetPropertiesOutput = Item; +export type GetPropertiesOutput = ItemKey; /** * Output type for S3 list API. Lists all bucket objects. @@ -64,9 +79,9 @@ export type ListPaginateOutput = StorageListOutput & { /** * Output type for S3 copy API. */ -export type CopyOutput = Pick; +export type CopyOutput = Pick; /** * Output type for S3 remove API. */ -export type RemoveOutput = Pick; +export type RemoveOutput = Pick; diff --git a/packages/storage/src/providers/s3/utils/constants.ts b/packages/storage/src/providers/s3/utils/constants.ts index 9e48b80047f..0aa063cef4d 100644 --- a/packages/storage/src/providers/s3/utils/constants.ts +++ b/packages/storage/src/providers/s3/utils/constants.ts @@ -19,3 +19,6 @@ export const MAX_PARTS_COUNT = 10000; export const DEFAULT_QUEUE_SIZE = 4; export const UPLOADS_STORAGE_KEY = '__uploadInProgress'; + +export const STORAGE_INPUT_KEY = 'key'; +export const STORAGE_INPUT_PATH = 'path'; diff --git a/packages/storage/src/providers/s3/utils/index.ts b/packages/storage/src/providers/s3/utils/index.ts index fe8ee9db247..a4c451c5105 100644 --- a/packages/storage/src/providers/s3/utils/index.ts +++ b/packages/storage/src/providers/s3/utils/index.ts @@ -4,3 +4,4 @@ export { calculateContentMd5 } from './md5'; export { resolveS3ConfigAndInput } from './resolveS3ConfigAndInput'; export { createDownloadTask, createUploadTask } from './transferTask'; +export { validateStorageOperationInput } from './validateStorageOperationInput'; diff --git a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts index d99149f6c0a..8eb28ef8681 100644 --- a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts +++ b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts @@ -22,6 +22,7 @@ interface ResolvedS3ConfigAndInput { bucket: string; keyPrefix: string; isObjectLockEnabled?: boolean; + identityId?: string; } /** @@ -84,6 +85,7 @@ export const resolveS3ConfigAndInput = async ( }, bucket, keyPrefix, + identityId, isObjectLockEnabled, }; }; diff --git a/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts new file mode 100644 index 00000000000..038df38f931 --- /dev/null +++ b/packages/storage/src/providers/s3/utils/validateStorageOperationInput.ts @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { StrictUnion } from '@aws-amplify/core/internals/utils'; + +import { + StorageOperationInputKey, + StorageOperationInputPath, +} from '../../../types/inputs'; +import { assertValidationError } from '../../../errors/utils/assertValidationError'; +import { StorageValidationErrorCode } from '../../../errors/types/validation'; + +import { STORAGE_INPUT_KEY, STORAGE_INPUT_PATH } from './constants'; + +type Input = StrictUnion; + +const isInputWithPath = (input: Input): input is StorageOperationInputPath => { + return input.path !== undefined; +}; + +export const validateStorageOperationInput = ( + input: Input, + identityId?: string, +) => { + assertValidationError( + !!(input as Input).key || !!(input as Input).path, + StorageValidationErrorCode.InvalidStorageOperationInput, + ); + + if (isInputWithPath(input)) { + const { path } = input; + + return { + inputType: STORAGE_INPUT_PATH, + objectKey: typeof path === 'string' ? path : path({ identityId }), + }; + } else { + return { inputType: STORAGE_INPUT_KEY, objectKey: input.key }; + } +}; diff --git a/packages/storage/src/types/index.ts b/packages/storage/src/types/index.ts index 39bb1c049a3..78569a76f36 100644 --- a/packages/storage/src/types/index.ts +++ b/packages/storage/src/types/index.ts @@ -12,7 +12,8 @@ export { StorageListInput, StorageGetPropertiesInput, StorageRemoveInput, - StorageDownloadDataInput, + StorageDownloadDataInputKey, + StorageDownloadDataInputPath, StorageUploadDataInput, StorageCopyInput, StorageGetUrlInput, @@ -26,6 +27,8 @@ export { } from './options'; export { StorageItem, + StorageItemKey, + StorageItemPath, StorageListOutput, StorageDownloadDataOutput, StorageGetUrlOutput, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 861ebf53876..7e1e1ea15dc 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -7,6 +7,26 @@ import { StorageOptions, } from './options'; +/** @deprecated Use {@link StorageOperationInputPath} instead. */ +export interface StorageOperationInputKey { + /** @deprecated Use `path` instead. */ + key: string; +} +export interface StorageOperationInputPath { + path: string | (({ identityId }: { identityId?: string }) => string); +} +export interface StorageOperationOptions { + options?: Options; +} + +/** @deprecated Use {@link StorageDownloadDataInputPath} instead. */ +export type StorageDownloadDataInputKey = + StorageOperationInputKey & StorageOperationOptions; + +export type StorageDownloadDataInputPath = StorageOperationInputPath & + StorageOperationOptions; + +// TODO: This needs to be removed after refactor of all storage APIs export interface StorageOperationInput { key: string; options?: Options; @@ -30,9 +50,6 @@ export interface StorageListInput< export type StorageGetUrlInput = StorageOperationInput; -export type StorageDownloadDataInput = - StorageOperationInput; - export type StorageUploadDataInput = StorageOperationInput & { data: StorageUploadDataPayload; diff --git a/packages/storage/src/types/options.ts b/packages/storage/src/types/options.ts index df992df5838..29bdd1cd43d 100644 --- a/packages/storage/src/types/options.ts +++ b/packages/storage/src/types/options.ts @@ -4,6 +4,7 @@ import { StorageAccessLevel } from '@aws-amplify/core'; export interface StorageOptions { + /** @deprecated This will be removed in next major version. */ accessLevel?: StorageAccessLevel; } diff --git a/packages/storage/src/types/outputs.ts b/packages/storage/src/types/outputs.ts index b5b3a9236c0..0d4307fd73b 100644 --- a/packages/storage/src/types/outputs.ts +++ b/packages/storage/src/types/outputs.ts @@ -3,11 +3,10 @@ import { ResponseBodyMixin } from '@aws-amplify/core/internals/aws-client-utils'; -export interface StorageItem { - /** - * Key of the object - */ - key: string; +/** + * Base type for a storage item. + */ +export interface StorageItemBase { /** * Creation date of the object. */ @@ -28,7 +27,27 @@ export interface StorageItem { metadata?: Record; } -export type StorageDownloadDataOutput = T & { +/** @deprecated Use {@link StorageItemPath} instead. */ +export type StorageItemKey = StorageItemBase & { + /** + * Key of the object. + */ + key: string; +}; + +export type StorageItemPath = StorageItemBase & { + /** + * Path of the object. + */ + path: string; +}; + +/** + * A storage item can be identified either by a key or a path. + */ +export type StorageItem = StorageItemKey | StorageItemPath; + +export type StorageDownloadDataOutput = Item & { body: ResponseBodyMixin; };