diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index d4cfecc01d7..0ab717bc33c 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -461,43 +461,43 @@ "name": "[Storage] copy (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ copy }", - "limit": "14.9 kB" + "limit": "15.28 kB" }, { "name": "[Storage] downloadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ downloadData }", - "limit": "15.49 kB" + "limit": "15.89 kB" }, { "name": "[Storage] getProperties (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getProperties }", - "limit": "14.77 kB" + "limit": "15.14 kB" }, { "name": "[Storage] getUrl (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ getUrl }", - "limit": "15.87 kB" + "limit": "16.27 kB" }, { "name": "[Storage] list (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ list }", - "limit": "15.36 kB" + "limit": "15.74 kB" }, { "name": "[Storage] remove (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ remove }", - "limit": "14.63 kB" + "limit": "15.2 kB" }, { "name": "[Storage] uploadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ uploadData }", - "limit": "19.94 kB" + "limit": "20.30 kB" } ] } diff --git a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts index b7bac2438ed..c313ee8838e 100644 --- a/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/downloadData.test.ts @@ -26,7 +26,15 @@ import { import './testUtils'; jest.mock('../../../../src/providers/s3/utils/client/s3data'); -jest.mock('../../../../src/providers/s3/utils'); +jest.mock('../../../../src/providers/s3/utils', () => { + const utils = jest.requireActual('../../../../src/providers/s3/utils'); + + return { + ...utils, + createDownloadTask: jest.fn(), + validateStorageOperationInput: jest.fn(), + }; +}); jest.mock('@aws-amplify/core', () => ({ ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { return { debug: jest.fn() }; diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts index 43775719dd3..996ad334354 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/index.test.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { Amplify } from '@aws-amplify/core'; + import { uploadData } from '../../../../../src/providers/s3/apis'; import { MAX_OBJECT_SIZE } from '../../../../../src/providers/s3/utils/constants'; import { createUploadTask } from '../../../../../src/providers/s3/utils'; @@ -12,7 +14,25 @@ import { putObjectJob } from '../../../../../src/providers/s3/apis/uploadData/pu import { getMultipartUploadHandlers } from '../../../../../src/providers/s3/apis/uploadData/multipart'; import { UploadDataInput, UploadDataWithPathInput } from '../../../../../src'; -jest.mock('../../../../../src/providers/s3/utils/'); +jest.mock('@aws-amplify/core', () => ({ + ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { + return { debug: jest.fn() }; + }), + Amplify: { + getConfig: jest.fn(), + Auth: { + fetchAuthSession: jest.fn(), + }, + }, +})); +jest.mock('../../../../../src/providers/s3/utils', () => { + const utils = jest.requireActual('../../../../../src/providers/s3/utils'); + + return { + ...utils, + createUploadTask: jest.fn(), + }; +}); jest.mock('../../../../../src/providers/s3/apis/uploadData/putObjectJob'); jest.mock('../../../../../src/providers/s3/apis/uploadData/multipart'); @@ -27,9 +47,21 @@ const mockGetMultipartUploadHandlers = ( onResume: jest.fn(), onCancel: jest.fn(), }); - +const mockGetConfig = Amplify.getConfig as jest.Mock; +const bucket = 'bucket'; +const region = 'region'; /* TODO Remove suite when `key` parameter is removed */ describe('uploadData with key', () => { + beforeAll(() => { + mockGetConfig.mockReturnValue({ + Storage: { + S3: { + bucket, + region, + }, + }, + }); + }); afterEach(() => { jest.clearAllMocks(); }); diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts index ab049042afd..767443337b5 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/multipartHandlers.test.ts @@ -141,8 +141,8 @@ describe('getMultipartUploadHandlers with key', () => { const mockS3Config: S3InternalConfig = { credentialsProvider: mockCredentialsProvider, identityIdProvider: mockIdentityIdProvider, - serviceOptions: mockServiceOptions, - libraryOptions: mockLibraryOptions, + ...mockServiceOptions, + ...mockLibraryOptions, }; beforeAll(() => { mockCredentialsProvider.mockImplementation(async () => credentials); @@ -692,8 +692,8 @@ describe('getMultipartUploadHandlers with path', () => { const mockS3Config: S3InternalConfig = { credentialsProvider: mockCredentialsProvider, identityIdProvider: mockIdentityIdProvider, - serviceOptions: mockServiceOptions, - libraryOptions: mockLibraryOptions, + ...mockServiceOptions, + ...mockLibraryOptions, }; beforeAll(() => { mockCredentialsProvider.mockImplementation(async () => credentials); diff --git a/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts b/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts index f6e06fa1140..0abbdff1aad 100644 --- a/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/uploadData/putObjectJob.test.ts @@ -47,8 +47,8 @@ mockPutObject.mockResolvedValue({ const config: S3InternalConfig = { credentialsProvider: mockCredentialsProvider, identityIdProvider: mockIdentityIdProvider, - serviceOptions: mockServiceOptions, - libraryOptions: mockLibraryOptions, + ...mockServiceOptions, + ...mockLibraryOptions, }; /* TODO Remove suite when `key` parameter is removed */ @@ -124,9 +124,7 @@ describe('putObjectJob with key', () => { const job = putObjectJob({ config: { ...config, - libraryOptions: { - isObjectLockEnabled: true, - }, + isObjectLockEnabled: true, }, input: { key: 'key', @@ -220,9 +218,7 @@ describe('putObjectJob with path', () => { const job = putObjectJob({ config: { ...config, - libraryOptions: { - isObjectLockEnabled: true, - }, + isObjectLockEnabled: true, }, input: { path: testPath, diff --git a/packages/storage/__tests__/providers/s3/apis/utils/config.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/config.test.ts new file mode 100644 index 00000000000..82d581f7eeb --- /dev/null +++ b/packages/storage/__tests__/providers/s3/apis/utils/config.test.ts @@ -0,0 +1,216 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Amplify } from '@aws-amplify/core'; + +import { createStorageConfiguration } from '../../../../../src/providers/s3/utils'; + +jest.mock('@aws-amplify/core', () => ({ + ConsoleLogger: jest.fn().mockImplementation(function ConsoleLogger() { + return { debug: jest.fn() }; + }), + Amplify: { + getConfig: jest.fn(), + Auth: { + fetchAuthSession: jest.fn(), + }, + }, +})); +const mockedBucket = 'mock-bucket'; +const mockedRegion = 'mock-region'; +const mockPermission = 'READ'; +const mockedPath = 'path'; +const mockGetConfig = jest.mocked(Amplify.getConfig); +const mockLocationCredentialsProvider = jest.fn(); +const STORAGE_CONFIG_ERROR_MESSAGE = 'Storage configuration is required.'; +const STORAGE_PATH_ERROR_MESSAGE = 'path option needs to be a string'; + +describe('createStorageConfiguration', () => { + const mockAmplify = Amplify; + + beforeEach(() => { + mockGetConfig.mockReturnValue({ + Storage: { + S3: { + bucket: mockedBucket, + region: mockedRegion, + }, + }, + }); + }); + + afterEach(() => { + mockGetConfig.mockReset(); + }); + + it('should resolve Storage service options from API input', () => { + const apiInput = { + options: { + bucket: mockedBucket, + region: mockedRegion, + }, + }; + const config = createStorageConfiguration( + mockAmplify, + apiInput, + mockPermission, + ); + expect(config).toMatchObject({ + bucket: mockedBucket, + region: mockedRegion, + credentialsProvider: expect.any(Function), + identityIdProvider: expect.any(Function), + }); + }); + + it('should resolve Storage service options from Amplify singleton', () => { + const config = createStorageConfiguration(mockAmplify, {}, mockPermission); + expect(config).toMatchObject({ + bucket: mockedBucket, + region: mockedRegion, + credentialsProvider: expect.any(Function), + identityIdProvider: expect.any(Function), + }); + }); + + it('should throw if Storage service options are not resolved from API input and Amplify singleton', () => { + mockGetConfig.mockReturnValue({}); + expect(() => + createStorageConfiguration(mockAmplify, {}, mockPermission), + ).toThrow(STORAGE_CONFIG_ERROR_MESSAGE); + }); + + it('should create a custom credentials provider if locationCredentialsProvider is defined', () => { + const apiInput = { + path: mockedPath, + options: { + bucket: mockedBucket, + region: mockedRegion, + locationCredentialsProvider: mockLocationCredentialsProvider, + }, + }; + const config = createStorageConfiguration( + mockAmplify, + apiInput, + mockPermission, + ); + expect(config.credentialsProvider).not.toBe(expect.any(Function)); + }); + + it('should throw if bucket is undefined when creating a custom credentials provider', () => { + mockGetConfig.mockReturnValue({ + Storage: { + S3: { + bucket: undefined, + region: mockedRegion, + }, + }, + }); + const apiInput = { + path: mockedPath, + options: { + region: mockedRegion, + locationCredentialsProvider: mockLocationCredentialsProvider, + }, + }; + expect(() => + createStorageConfiguration(mockAmplify, apiInput, mockPermission), + ).toThrow(STORAGE_CONFIG_ERROR_MESSAGE); + }); + + it('should throw if path is undefined when creating a custom credentials provider', () => { + const apiInput = { + options: { + bucket: mockedBucket, + region: mockedRegion, + locationCredentialsProvider: jest.fn(), + }, + }; + expect(() => + createStorageConfiguration(mockAmplify, apiInput, mockPermission), + ).toThrow(STORAGE_PATH_ERROR_MESSAGE); + }); + + it('should throw if path is a function when creating a custom credentials provider', () => { + const apiInput = { + path: jest.fn(), + options: { + bucket: mockedBucket, + region: mockedRegion, + locationCredentialsProvider: jest.fn(), + }, + }; + expect(() => + createStorageConfiguration(mockAmplify, apiInput, mockPermission), + ).toThrow(STORAGE_PATH_ERROR_MESSAGE); + }); + + it('should create paths if API input has a path when creating a custom credentials provider', () => { + const apiInput = { + path: 'mock-path', + options: { + bucket: mockedBucket, + region: mockedRegion, + locationCredentialsProvider: mockLocationCredentialsProvider, + }, + }; + const config = createStorageConfiguration( + mockAmplify, + apiInput, + mockPermission, + ); + expect(config.credentialsProvider).toBeInstanceOf(Function); + }); + + it('should create paths if API input has source and destination when creating a custom credentials provider', () => { + const apiInput = { + source: { path: 'source-path' }, + destination: { path: 'destination-path' }, + options: { + bucket: mockedBucket, + region: mockedRegion, + locationCredentialsProvider: mockLocationCredentialsProvider, + }, + }; + const config = createStorageConfiguration( + mockAmplify, + apiInput, + mockPermission, + ); + expect(config.credentialsProvider).toBeInstanceOf(Function); + }); + + it('should create a default credentials provider if locationCredentialsProvider is not defined and Storage config is passed in the input', () => { + const apiInput = { + options: { + bucket: mockedBucket, + region: mockedRegion, + }, + }; + const config = createStorageConfiguration( + mockAmplify, + apiInput, + mockPermission, + ); + expect(config.credentialsProvider).toBeInstanceOf(Function); + }); + + it('should create a default credentials provider if locationCredentialsProvider is not defined and Storage config is not passed in the input', () => { + const apiInput = {}; + const config = createStorageConfiguration( + mockAmplify, + apiInput, + mockPermission, + ); + expect(config.credentialsProvider).toBeInstanceOf(Function); + }); + + it('should not throw if path is a function when creating a default credentials provider', () => { + const apiInput = { + path: jest.fn(), + }; + expect(() => + createStorageConfiguration(mockAmplify, apiInput, mockPermission), + ).not.toThrow(); + }); +}); diff --git a/packages/storage/__tests__/providers/s3/apis/utils/resolveS3ConfigAndInput.test.ts b/packages/storage/__tests__/providers/s3/apis/utils/resolveS3ConfigAndInput.test.ts index ba527aa8dbf..36b4962683e 100644 --- a/packages/storage/__tests__/providers/s3/apis/utils/resolveS3ConfigAndInput.test.ts +++ b/packages/storage/__tests__/providers/s3/apis/utils/resolveS3ConfigAndInput.test.ts @@ -32,8 +32,8 @@ describe('resolveS3ConfigAndInput', () => { const config: S3InternalConfig = { credentialsProvider: mockCredentialsProvider, identityIdProvider: mockIdentityIdProvider, - serviceOptions: mockServiceOptions, - libraryOptions: mockLibraryOptions, + ...mockServiceOptions, + ...mockLibraryOptions, }; beforeEach(() => { mockCredentialsProvider.mockImplementation(async () => credentials); @@ -88,9 +88,7 @@ describe('resolveS3ConfigAndInput', () => { resolveS3ConfigAndInput({ config: { ...config, - serviceOptions: { - bucket: undefined, - }, + bucket: undefined, }, }), ).rejects.toMatchObject( @@ -108,9 +106,7 @@ describe('resolveS3ConfigAndInput', () => { resolveS3ConfigAndInput({ config: { ...config, - serviceOptions: { - bucket, - }, + region: undefined, }, }), ).rejects.toMatchObject( @@ -126,7 +122,7 @@ describe('resolveS3ConfigAndInput', () => { }; const { s3Config } = await resolveS3ConfigAndInput({ - config: { ...config, serviceOptions }, + config: { ...config, ...serviceOptions }, }); expect(s3Config.customEndpoint).toEqual('http://localhost:20005'); expect(s3Config.forcePathStyle).toEqual(true); @@ -136,7 +132,7 @@ describe('resolveS3ConfigAndInput', () => { const { isObjectLockEnabled } = await resolveS3ConfigAndInput({ config: { ...config, - libraryOptions: { isObjectLockEnabled: true }, + isObjectLockEnabled: true, }, }); expect(isObjectLockEnabled).toEqual(true); @@ -154,9 +150,7 @@ describe('resolveS3ConfigAndInput', () => { const { keyPrefix } = await resolveS3ConfigAndInput({ config: { ...config, - libraryOptions: { - prefixResolver: customResolvePrefix, - }, + prefixResolver: customResolvePrefix, }, }); expect(customResolvePrefix).toHaveBeenCalled(); @@ -184,9 +178,7 @@ describe('resolveS3ConfigAndInput', () => { const { keyPrefix } = await resolveS3ConfigAndInput({ config: { ...config, - libraryOptions: { - defaultAccessLevel: 'someLevel' as any, - }, + defaultAccessLevel: 'someLevel' as any, }, }); expect(mockDefaultResolvePrefix).toHaveBeenCalledWith({ diff --git a/packages/storage/src/errors/constants.ts b/packages/storage/src/errors/constants.ts new file mode 100644 index 00000000000..f50dcf12896 --- /dev/null +++ b/packages/storage/src/errors/constants.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const NO_STORAGE_CONFIG = 'NoStorageConfig'; +export const INVALID_STORAGE_PATH = 'InvalidStoragePath'; diff --git a/packages/storage/src/providers/s3/apis/copy.ts b/packages/storage/src/providers/s3/apis/copy.ts index 763ff45829b..a04b45091e1 100644 --- a/packages/storage/src/providers/s3/apis/copy.ts +++ b/packages/storage/src/providers/s3/apis/copy.ts @@ -9,6 +9,7 @@ import { CopyWithPathInput, CopyWithPathOutput, } from '../types'; +import { createStorageConfiguration } from '../utils'; import { copy as copyInternal } from './internal/copy'; @@ -38,5 +39,7 @@ export function copy(input: CopyWithPathInput): Promise; export function copy(input: CopyInput): Promise; export function copy(input: CopyInput | CopyWithPathInput) { - return copyInternal(Amplify, input); + const config = createStorageConfiguration(Amplify, input, 'READWRITE'); + + return copyInternal(config, input); } diff --git a/packages/storage/src/providers/s3/apis/downloadData.ts b/packages/storage/src/providers/s3/apis/downloadData.ts index e67ab7f6b25..4f068e93ce9 100644 --- a/packages/storage/src/providers/s3/apis/downloadData.ts +++ b/packages/storage/src/providers/s3/apis/downloadData.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { Amplify } from '@aws-amplify/core'; -import { StorageAction } from '@aws-amplify/core/internals/utils'; import { DownloadDataInput, @@ -10,18 +9,9 @@ import { DownloadDataWithPathInput, DownloadDataWithPathOutput, } from '../types'; -import { resolveS3ConfigAndInput } from '../utils/resolveS3ConfigAndInput'; -import { createDownloadTask, validateStorageOperationInput } from '../utils'; -import { getObject } from '../utils/client/s3data'; -import { createStorageConfiguration } from '../utils/config'; -import { getStorageUserAgentValue } from '../utils/userAgent'; -import { logger } from '../../../utils'; -import { - StorageDownloadDataOutput, - StorageItemWithKey, - StorageItemWithPath, -} from '../../../types'; -import { STORAGE_INPUT_KEY } from '../utils/constants'; +import { createStorageConfiguration } from '../utils'; + +import { internalDownloadData } from './internal/downloadData'; /** * Download S3 object data to memory @@ -94,78 +84,7 @@ 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); - }, - }); + const config = createStorageConfiguration(Amplify, input, 'READ'); - return downloadTask; + return internalDownloadData(input, config); } - -const downloadDataJob = - ( - downloadDataInput: DownloadDataInput | DownloadDataWithPathInput, - abortSignal: AbortSignal, - ) => - async (): Promise< - StorageDownloadDataOutput - > => { - const { options: downloadDataOptions } = downloadDataInput; - const config = createStorageConfiguration(Amplify); - - const { bucket, keyPrefix, s3Config, identityId } = - await resolveS3ConfigAndInput({ - config, - apiOptions: downloadDataOptions, - }); - 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 }; - }; diff --git a/packages/storage/src/providers/s3/apis/getProperties.ts b/packages/storage/src/providers/s3/apis/getProperties.ts index 630d0b1c467..51702e3e585 100644 --- a/packages/storage/src/providers/s3/apis/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/getProperties.ts @@ -9,6 +9,7 @@ import { GetPropertiesWithPathInput, GetPropertiesWithPathOutput, } from '../types'; +import { createStorageConfiguration } from '../utils'; import { getProperties as getPropertiesInternal } from './internal/getProperties'; @@ -43,5 +44,7 @@ export function getProperties( export function getProperties( input: GetPropertiesInput | GetPropertiesWithPathInput, ) { - return getPropertiesInternal(Amplify, input); + const config = createStorageConfiguration(Amplify, input, 'READ'); + + return getPropertiesInternal(config, input); } diff --git a/packages/storage/src/providers/s3/apis/getUrl.ts b/packages/storage/src/providers/s3/apis/getUrl.ts index aafe1f282b3..b09e74f11a4 100644 --- a/packages/storage/src/providers/s3/apis/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/getUrl.ts @@ -9,6 +9,7 @@ import { GetUrlWithPathInput, GetUrlWithPathOutput, } from '../types'; +import { createStorageConfiguration } from '../utils'; import { getUrl as getUrlInternal } from './internal/getUrl'; @@ -53,5 +54,7 @@ export function getUrl( export function getUrl(input: GetUrlInput): Promise; export function getUrl(input: GetUrlInput | GetUrlWithPathInput) { - return getUrlInternal(Amplify, input); + const config = createStorageConfiguration(Amplify, input, 'READ'); + + return getUrlInternal(config, input); } diff --git a/packages/storage/src/providers/s3/apis/internal/copy.ts b/packages/storage/src/providers/s3/apis/internal/copy.ts index 22bdc1bac6a..ac06b23a719 100644 --- a/packages/storage/src/providers/s3/apis/internal/copy.ts +++ b/packages/storage/src/providers/s3/apis/internal/copy.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -12,7 +11,6 @@ import { } from '../../types'; import { ResolvedS3Config } from '../../types/options'; import { - createStorageConfiguration, isInputWithPath, resolveS3ConfigAndInput, validateStorageOperationInput, @@ -23,25 +21,27 @@ import { copyObject } from '../../utils/client/s3data'; import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; +import { S3InternalConfig } from './types'; + const isCopyInputWithPath = ( input: CopyInput | CopyWithPathInput, ): input is CopyWithPathInput => isInputWithPath(input.source); export const copy = async ( - amplify: AmplifyClassV6, + config: S3InternalConfig, input: CopyInput | CopyWithPathInput, ): Promise => { return isCopyInputWithPath(input) - ? copyWithPath(amplify, input) - : copyWithKey(amplify, input); + ? copyWithPath(config, input) + : copyWithKey(config, input); }; const copyWithPath = async ( - amplify: AmplifyClassV6, + config: S3InternalConfig, input: CopyWithPathInput, ): Promise => { const { source, destination } = input; - const config = createStorageConfiguration(amplify); + const { s3Config, bucket, identityId } = await resolveS3ConfigAndInput({ config, }); @@ -77,7 +77,7 @@ const copyWithPath = async ( /** @deprecated Use {@link copyWithPath} instead. */ export const copyWithKey = async ( - amplify: AmplifyClassV6, + config: S3InternalConfig, input: CopyInput, ): Promise => { const { @@ -90,7 +90,6 @@ export const copyWithKey = async ( !!destinationKey, StorageValidationErrorCode.NoDestinationKey, ); - const config = createStorageConfiguration(amplify); const { s3Config, bucket, diff --git a/packages/storage/src/providers/s3/apis/internal/downloadData.ts b/packages/storage/src/providers/s3/apis/internal/downloadData.ts new file mode 100644 index 00000000000..fe501d099eb --- /dev/null +++ b/packages/storage/src/providers/s3/apis/internal/downloadData.ts @@ -0,0 +1,102 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { StorageAction } from '@aws-amplify/core/internals/utils'; + +import { + StorageDownloadDataOutput, + StorageItemWithKey, + StorageItemWithPath, +} from '../../../../types'; +import { logger } from '../../../../utils'; +import { DownloadDataInput, DownloadDataWithPathInput } from '../../types'; +import { + createDownloadTask, + resolveS3ConfigAndInput, + validateStorageOperationInput, +} from '../../utils'; +import { getObject } from '../../utils/client/s3data'; +import { STORAGE_INPUT_KEY } from '../../utils/constants'; +import { getStorageUserAgentValue } from '../../utils/userAgent'; + +import { S3InternalConfig } from './types'; + +export function internalDownloadData( + input: DownloadDataInput | DownloadDataWithPathInput, + config: S3InternalConfig, +) { + const abortController = new AbortController(); + + const downloadTask = createDownloadTask({ + job: downloadDataJob(input, abortController.signal, config), + onCancel: (message?: string) => { + abortController.abort(message); + }, + }); + + return downloadTask; +} + +const downloadDataJob = + ( + downloadDataInput: DownloadDataInput | DownloadDataWithPathInput, + abortSignal: AbortSignal, + config: S3InternalConfig, + ) => + async (): Promise< + StorageDownloadDataOutput + > => { + const { options: downloadDataOptions } = downloadDataInput; + + const { bucket, keyPrefix, s3Config, identityId } = + await resolveS3ConfigAndInput({ + config, + apiOptions: downloadDataOptions, + }); + 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 }; + }; diff --git a/packages/storage/src/providers/s3/apis/internal/getProperties.ts b/packages/storage/src/providers/s3/apis/internal/getProperties.ts index f06f6bcab69..2193a361cce 100644 --- a/packages/storage/src/providers/s3/apis/internal/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/internal/getProperties.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -11,7 +10,6 @@ import { GetPropertiesWithPathOutput, } from '../../types'; import { - createStorageConfiguration, resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../utils'; @@ -20,13 +18,14 @@ import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; import { STORAGE_INPUT_KEY } from '../../utils/constants'; +import { S3InternalConfig } from './types'; + export const getProperties = async ( - amplify: AmplifyClassV6, + config: S3InternalConfig, input: GetPropertiesInput | GetPropertiesWithPathInput, action?: StorageAction, ): Promise => { const { options: getPropertiesOptions } = input; - const config = createStorageConfiguration(amplify); const { s3Config, bucket, keyPrefix, identityId } = await resolveS3ConfigAndInput({ config, diff --git a/packages/storage/src/providers/s3/apis/internal/getUrl.ts b/packages/storage/src/providers/s3/apis/internal/getUrl.ts index 755a2028e4c..b23b20aedd6 100644 --- a/packages/storage/src/providers/s3/apis/internal/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/internal/getUrl.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -13,7 +12,6 @@ import { import { StorageValidationErrorCode } from '../../../../errors/types/validation'; import { getPresignedGetObjectUrl } from '../../utils/client/s3data'; import { - createStorageConfiguration, resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../utils'; @@ -25,13 +23,13 @@ import { } from '../../utils/constants'; import { getProperties } from './getProperties'; +import { S3InternalConfig } from './types'; export const getUrl = async ( - amplify: AmplifyClassV6, + config: S3InternalConfig, input: GetUrlInput | GetUrlWithPathInput, ): Promise => { const { options: getUrlOptions } = input; - const config = createStorageConfiguration(amplify); const { s3Config, keyPrefix, bucket, identityId } = await resolveS3ConfigAndInput({ config, @@ -46,7 +44,7 @@ export const getUrl = async ( inputType === STORAGE_INPUT_KEY ? keyPrefix + objectKey : objectKey; if (getUrlOptions?.validateObjectExistence) { - await getProperties(amplify, input, StorageAction.GetUrl); + await getProperties(config, input, StorageAction.GetUrl); } let urlExpirationInSec = diff --git a/packages/storage/src/providers/s3/apis/internal/list.ts b/packages/storage/src/providers/s3/apis/internal/list.ts index 0da1742aac3..70a6bc90d86 100644 --- a/packages/storage/src/providers/s3/apis/internal/list.ts +++ b/packages/storage/src/providers/s3/apis/internal/list.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -17,7 +16,6 @@ import { ListPaginateWithPathOutput, } from '../../types'; import { - createStorageConfiguration, resolveS3ConfigAndInput, validateStorageOperationInputWithPrefix, } from '../../utils'; @@ -32,6 +30,8 @@ import { logger } from '../../../../utils'; import { STORAGE_INPUT_PREFIX } from '../../utils/constants'; import { CommonPrefix } from '../../utils/client/s3data/types'; +import { S3InternalConfig } from './types'; + const MAX_PAGE_SIZE = 1000; interface ListInputArgs { @@ -41,7 +41,7 @@ interface ListInputArgs { } export const list = async ( - amplify: AmplifyClassV6, + config: S3InternalConfig, input: | ListAllInput | ListPaginateInput @@ -55,7 +55,6 @@ export const list = async ( > => { const { options = {} } = input; - const config = createStorageConfiguration(amplify); const { s3Config, bucket, diff --git a/packages/storage/src/providers/s3/apis/internal/remove.ts b/packages/storage/src/providers/s3/apis/internal/remove.ts index 5a6add5ce67..374aa364eee 100644 --- a/packages/storage/src/providers/s3/apis/internal/remove.ts +++ b/packages/storage/src/providers/s3/apis/internal/remove.ts @@ -1,7 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageAction } from '@aws-amplify/core/internals/utils'; import { @@ -11,7 +10,6 @@ import { RemoveWithPathOutput, } from '../../types'; import { - createStorageConfiguration, resolveS3ConfigAndInput, validateStorageOperationInput, } from '../../utils'; @@ -20,12 +18,13 @@ import { getStorageUserAgentValue } from '../../utils/userAgent'; import { logger } from '../../../../utils'; import { STORAGE_INPUT_KEY } from '../../utils/constants'; +import { S3InternalConfig } from './types'; + export const remove = async ( - amplify: AmplifyClassV6, + config: S3InternalConfig, input: RemoveInput | RemoveWithPathInput, ): Promise => { const { options = {} } = input ?? {}; - const config = createStorageConfiguration(amplify); const { s3Config, keyPrefix, bucket, identityId } = await resolveS3ConfigAndInput({ config, diff --git a/packages/storage/src/providers/s3/apis/internal/types/index.ts b/packages/storage/src/providers/s3/apis/internal/types/index.ts index fb20b5da08d..b43054d657a 100644 --- a/packages/storage/src/providers/s3/apis/internal/types/index.ts +++ b/packages/storage/src/providers/s3/apis/internal/types/index.ts @@ -4,12 +4,15 @@ import { LibraryOptions, StorageConfig } from '@aws-amplify/core'; import { AWSCredentials } from '@aws-amplify/core/internals/utils'; +import { StorageOperationOptionsInput } from '../../../../../types/inputs'; +import { CommonOptions } from '../../../types/options'; + /** * Internal S3 service options. * * @internal */ -type S3ServiceOptions = StorageConfig['S3']; +export type S3ServiceOptions = StorageConfig['S3']; /** * Internal S3 library options. @@ -23,9 +26,25 @@ type S3LibraryOptions = NonNullable['S3']; * * @internal */ -export interface S3InternalConfig { - serviceOptions: S3ServiceOptions; - libraryOptions: S3LibraryOptions; +export interface S3InternalConfig extends S3ServiceOptions, S3LibraryOptions { credentialsProvider(): Promise; identityIdProvider(): Promise; } + +export type StorageCredentialsProvider = () => Promise; +export type StorageIdentityIdProvider = () => Promise; + +/** + * This interface is used to resolve the main options for the locationCredentialsProvider + * + * @internal + */ +export interface InternalStorageAPIConfig + extends StorageOperationOptionsInput< + CommonOptions & { + bucket?: string; + region?: string; + } + > { + paths?: string[]; +} diff --git a/packages/storage/src/providers/s3/apis/list.ts b/packages/storage/src/providers/s3/apis/list.ts index cd58dbdaacd..a05bbfda0d4 100644 --- a/packages/storage/src/providers/s3/apis/list.ts +++ b/packages/storage/src/providers/s3/apis/list.ts @@ -12,6 +12,7 @@ import { ListPaginateWithPathInput, ListPaginateWithPathOutput, } from '../types'; +import { createStorageConfiguration } from '../utils'; import { list as listInternal } from './internal/list'; @@ -65,5 +66,7 @@ export function list( | ListAllWithPathInput | ListPaginateWithPathInput, ) { - return listInternal(Amplify, input ?? {}); + const config = createStorageConfiguration(Amplify, input, 'READ'); + + return listInternal(config, input ?? {}); } diff --git a/packages/storage/src/providers/s3/apis/remove.ts b/packages/storage/src/providers/s3/apis/remove.ts index c0526df854c..0c25860eecc 100644 --- a/packages/storage/src/providers/s3/apis/remove.ts +++ b/packages/storage/src/providers/s3/apis/remove.ts @@ -9,6 +9,7 @@ import { RemoveWithPathInput, RemoveWithPathOutput, } from '../types'; +import { createStorageConfiguration } from '../utils'; import { remove as removeInternal } from './internal/remove'; @@ -37,5 +38,7 @@ export function remove( export function remove(input: RemoveInput): Promise; export function remove(input: RemoveInput | RemoveWithPathInput) { - return removeInternal(Amplify, input); + const config = createStorageConfiguration(Amplify, input, 'WRITE'); + + return removeInternal(config, input); } diff --git a/packages/storage/src/providers/s3/apis/server/copy.ts b/packages/storage/src/providers/s3/apis/server/copy.ts index e9486e10431..c645042ee4f 100644 --- a/packages/storage/src/providers/s3/apis/server/copy.ts +++ b/packages/storage/src/providers/s3/apis/server/copy.ts @@ -12,6 +12,7 @@ import { CopyWithPathOutput, } from '../../types'; import { copy as copyInternal } from '../internal/copy'; +import { createServerStorageConfiguration } from '../../utils/config'; /** * Copy an object from a source to a destination object within the same bucket. @@ -50,5 +51,9 @@ export function copy( contextSpec: AmplifyServer.ContextSpec, input: CopyInput | CopyWithPathInput, ) { - return copyInternal(getAmplifyServerContext(contextSpec).amplify, input); + const config = createServerStorageConfiguration( + getAmplifyServerContext(contextSpec).amplify, + ); + + return copyInternal(config, input); } diff --git a/packages/storage/src/providers/s3/apis/server/getProperties.ts b/packages/storage/src/providers/s3/apis/server/getProperties.ts index 87a77a297a4..3c21f2610f6 100644 --- a/packages/storage/src/providers/s3/apis/server/getProperties.ts +++ b/packages/storage/src/providers/s3/apis/server/getProperties.ts @@ -13,6 +13,7 @@ import { GetPropertiesWithPathOutput, } from '../../types'; import { getProperties as getPropertiesInternal } from '../internal/getProperties'; +import { createServerStorageConfiguration } from '../../utils'; /** * Gets the properties of a file. The properties include S3 system metadata and @@ -50,8 +51,9 @@ export function getProperties( contextSpec: AmplifyServer.ContextSpec, input: GetPropertiesInput | GetPropertiesWithPathInput, ) { - return getPropertiesInternal( + const config = createServerStorageConfiguration( getAmplifyServerContext(contextSpec).amplify, - input, ); + + return getPropertiesInternal(config, input); } diff --git a/packages/storage/src/providers/s3/apis/server/getUrl.ts b/packages/storage/src/providers/s3/apis/server/getUrl.ts index f9f4e80d07c..0761af64be2 100644 --- a/packages/storage/src/providers/s3/apis/server/getUrl.ts +++ b/packages/storage/src/providers/s3/apis/server/getUrl.ts @@ -13,6 +13,7 @@ import { GetUrlWithPathOutput, } from '../../types'; import { getUrl as getUrlInternal } from '../internal/getUrl'; +import { createServerStorageConfiguration } from '../../utils'; /** * Get a temporary presigned URL to download the specified S3 object. @@ -64,5 +65,9 @@ export function getUrl( contextSpec: AmplifyServer.ContextSpec, input: GetUrlInput | GetUrlWithPathInput, ) { - return getUrlInternal(getAmplifyServerContext(contextSpec).amplify, input); + const config = createServerStorageConfiguration( + getAmplifyServerContext(contextSpec).amplify, + ); + + return getUrlInternal(config, input); } diff --git a/packages/storage/src/providers/s3/apis/server/list.ts b/packages/storage/src/providers/s3/apis/server/list.ts index 66d0ad4cd22..593a9d88ab9 100644 --- a/packages/storage/src/providers/s3/apis/server/list.ts +++ b/packages/storage/src/providers/s3/apis/server/list.ts @@ -16,6 +16,7 @@ import { ListPaginateWithPathOutput, } from '../../types'; import { list as listInternal } from '../internal/list'; +import { createServerStorageConfiguration } from '../../utils'; /** * List files in pages with the given `path`. @@ -78,8 +79,9 @@ export function list( | ListAllWithPathInput | ListPaginateWithPathInput, ) { - return listInternal( + const config = createServerStorageConfiguration( getAmplifyServerContext(contextSpec).amplify, - input ?? {}, ); + + return listInternal(config, input ?? {}); } diff --git a/packages/storage/src/providers/s3/apis/server/remove.ts b/packages/storage/src/providers/s3/apis/server/remove.ts index 5b788447f64..dab88e94f89 100644 --- a/packages/storage/src/providers/s3/apis/server/remove.ts +++ b/packages/storage/src/providers/s3/apis/server/remove.ts @@ -13,6 +13,7 @@ import { RemoveWithPathOutput, } from '../../types'; import { remove as removeInternal } from '../internal/remove'; +import { createServerStorageConfiguration } from '../../utils'; /** * Remove a file from your S3 bucket. @@ -48,5 +49,9 @@ export function remove( contextSpec: AmplifyServer.ContextSpec, input: RemoveInput | RemoveWithPathInput, ) { - return removeInternal(getAmplifyServerContext(contextSpec).amplify, input); + const config = createServerStorageConfiguration( + getAmplifyServerContext(contextSpec).amplify, + ); + + return removeInternal(config, input); } diff --git a/packages/storage/src/providers/s3/apis/uploadData/index.ts b/packages/storage/src/providers/s3/apis/uploadData/index.ts index f32b90425dc..02f855da1c7 100644 --- a/packages/storage/src/providers/s3/apis/uploadData/index.ts +++ b/packages/storage/src/providers/s3/apis/uploadData/index.ts @@ -123,7 +123,7 @@ export function uploadData( export function uploadData(input: UploadDataInput): UploadDataOutput; export function uploadData(input: UploadDataInput | UploadDataWithPathInput) { - const config = createStorageConfiguration(Amplify); + const config = createStorageConfiguration(Amplify, input, 'WRITE'); return internalUploadData(config, input); } diff --git a/packages/storage/src/providers/s3/types/inputs.ts b/packages/storage/src/providers/s3/types/inputs.ts index f7bd6c5db44..26e6af4ae3e 100644 --- a/packages/storage/src/providers/s3/types/inputs.ts +++ b/packages/storage/src/providers/s3/types/inputs.ts @@ -35,6 +35,8 @@ import { UploadDataOptionsWithPath, } from '../types'; +import { CommonOptions } from './options'; + // TODO: support use accelerate endpoint option /** * @deprecated Use {@link CopyWithPathInput} instead. @@ -42,12 +44,15 @@ import { */ export type CopyInput = StorageCopyInputWithKey< CopySourceOptionsWithKey, - CopyDestinationOptionsWithKey + CopyDestinationOptionsWithKey, + Pick >; /** * Input type with path for S3 copy API. */ -export type CopyWithPathInput = StorageCopyInputWithPath; +export type CopyWithPathInput = StorageCopyInputWithPath< + Pick +>; /** * @deprecated Use {@link GetPropertiesWithPathInput} instead. diff --git a/packages/storage/src/providers/s3/types/options.ts b/packages/storage/src/providers/s3/types/options.ts index 9a908890352..270908c3efc 100644 --- a/packages/storage/src/providers/s3/types/options.ts +++ b/packages/storage/src/providers/s3/types/options.ts @@ -33,7 +33,7 @@ export type LocationCredentialsProvider = (options: { permission: Permission; }) => Promise<{ credentials: AWSCredentials }>; -interface CommonOptions { +export interface CommonOptions { /** * Whether to use accelerate endpoint. * @default false diff --git a/packages/storage/src/providers/s3/utils/config.ts b/packages/storage/src/providers/s3/utils/config.ts index 49258d0e04d..b56f11fc481 100644 --- a/packages/storage/src/providers/s3/utils/config.ts +++ b/packages/storage/src/providers/s3/utils/config.ts @@ -5,9 +5,31 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { StorageValidationErrorCode } from '../../../errors/types/validation'; import { assertValidationError } from '../../../errors/utils/assertValidationError'; -import { S3InternalConfig } from '../apis/internal/types'; +import { + InternalStorageAPIConfig, + S3InternalConfig, + S3ServiceOptions, + StorageCredentialsProvider, + StorageIdentityIdProvider, +} from '../apis/internal/types'; +import { + BucketLocation, + LocationCredentialsProvider, + Permission, +} from '../types/options'; +import { + StorageOperationInput, + StorageOperationInputWithPath, +} from '../../../types/inputs'; +import { StorageError } from '../../../errors/StorageError'; +import { + INVALID_STORAGE_PATH, + NO_STORAGE_CONFIG, +} from '../../../errors/constants'; -const createDefaultCredentialsProvider = (amplify: AmplifyClassV6) => { +const createDefaultCredentialsProvider = ( + amplify: AmplifyClassV6, +): StorageCredentialsProvider => { /** * A credentials provider function instead of a static credentials object is * used because the long-running tasks like multipart upload may span over the @@ -25,7 +47,9 @@ const createDefaultCredentialsProvider = (amplify: AmplifyClassV6) => { }; }; -const createDefaultIdentityIdProvider = (amplify: AmplifyClassV6) => { +const createDefaultIdentityIdProvider = ( + amplify: AmplifyClassV6, +): StorageIdentityIdProvider => { return async () => { const { identityId } = await amplify.Auth.fetchAuthSession(); assertValidationError( @@ -44,6 +68,67 @@ const createDefaultIdentityIdProvider = (amplify: AmplifyClassV6) => { */ export const createStorageConfiguration = ( amplify: AmplifyClassV6, + apiInput: unknown, + permission: Permission, +): S3InternalConfig => { + const options = getOptionsFromAPIInput(apiInput) ?? {}; + const { locationCredentialsProvider } = options; + const libraryOptions = amplify.libraryOptions?.Storage?.S3 ?? {}; + const serviceOptions = getServiceOptions(amplify, options); + const identityIdProvider = createDefaultIdentityIdProvider(amplify); + const { bucket } = serviceOptions; + + const credentialsProvider = locationCredentialsProvider + ? createCustomCredentialsProvider({ + bucket: resolveBucket(bucket), + paths: resolvePathsFromAPIInput(apiInput), + locationCredentialsProvider, + permission, + }) + : createDefaultCredentialsProvider(amplify); + + return { + ...libraryOptions, + ...serviceOptions, + credentialsProvider, + identityIdProvider, + }; +}; + +/** + * This util will get the main storage service options used by the storage APIs + * + * @internal + */ +const getServiceOptions = ( + amplify: AmplifyClassV6, + options: InternalStorageAPIConfig['options'], +): S3ServiceOptions => { + const { Storage } = amplify.getConfig() ?? {}; + const { dangerouslyConnectToHttpEndpointForTesting } = Storage?.S3 ?? {}; + + if (isConfigFromApiInput(options)) { + return { + bucket: options?.bucket, + region: options?.region, + dangerouslyConnectToHttpEndpointForTesting, + }; + } else if (isConfigFromAmplifySingleton(Storage?.S3)) { + return { + bucket: Storage?.S3.bucket, + region: Storage?.S3.region, + dangerouslyConnectToHttpEndpointForTesting, + }; + } + + throw new StorageError({ + name: NO_STORAGE_CONFIG, + message: 'Storage configuration is required.', + }); +}; + +export const createServerStorageConfiguration = ( + amplify: AmplifyClassV6, ): S3InternalConfig => { const libraryOptions = amplify.libraryOptions?.Storage?.S3 ?? {}; const serviceOptions = amplify.getConfig()?.Storage?.S3 ?? {}; @@ -51,9 +136,130 @@ export const createStorageConfiguration = ( const identityIdProvider = createDefaultIdentityIdProvider(amplify); return { - libraryOptions, - serviceOptions, + ...libraryOptions, + ...serviceOptions, credentialsProvider, identityIdProvider, }; }; + +const isInputWithOptions = ( + input: unknown, +): input is { options: InternalStorageAPIConfig['options'] } => { + return !!input && (input as any).options; +}; +const isInputWithPath = ( + input: unknown, +): input is StorageOperationInputWithPath => { + return !!input && (input as any).path; +}; + +const isInputWithSourceAndDestination = ( + input: unknown, +): input is { + source: StorageOperationInputWithPath; + destination: StorageOperationInputWithPath; +} => { + return !!input && (input as any).destination && (input as any).source; +}; + +/** + * This function is independent from the different input permutations and is used to get the common Storage options. + * + * @internal + */ +const getOptionsFromAPIInput = ( + input: unknown, +): InternalStorageAPIConfig['options'] => { + return isInputWithOptions(input) ? input.options : undefined; +}; +/** + * This function is independent from the different input permutations and is used mainly to resolve the + * the main options for the locationCredentialsProvider. + * + * @internal + */ +const resolvePathsFromAPIInput = (input: unknown): string[] => { + if (isInputWithPath(input)) { + const { path } = input; + + return [resolvePath({ path })]; + } else if (isInputWithSourceAndDestination(input)) { + const { + destination: { path: destinationPath }, + source: { path: sourcePath }, + } = input; + + return [ + resolvePath({ path: destinationPath }), + resolvePath({ path: sourcePath }), + ]; + } + + throw new StorageError({ + name: INVALID_STORAGE_PATH, + message: 'path option needs to be a string', + }); +}; + +const resolveBucket = (bucket?: string): string => { + assertValidationError(!!bucket, StorageValidationErrorCode.NoBucket); + + return bucket; +}; + +interface CreateCustomCredentialsProviderParams { + paths: string[]; + bucket: string; + locationCredentialsProvider: LocationCredentialsProvider; + permission: Permission; +} +const createCustomCredentialsProvider = ({ + bucket, + paths, + permission, + locationCredentialsProvider, +}: CreateCustomCredentialsProviderParams): StorageCredentialsProvider => { + return async () => { + const locations = getLocations(paths, bucket); + const { credentials } = await locationCredentialsProvider({ + locations, + permission, + }); + + return credentials; + }; +}; + +interface ResolvePathProps { + path: StorageOperationInput['path']; +} +const resolvePath = ({ path }: ResolvePathProps): string => { + if (!path || typeof path === 'function') { + throw new StorageError({ + name: INVALID_STORAGE_PATH, + message: 'path option needs to be a string', + }); + } + + return path; +}; + +const getLocations: (paths: string[], bucket: string) => BucketLocation[] = ( + paths, + bucket, +) => { + return paths.map(path => ({ bucket, path })); +}; + +const isConfigFromApiInput = ( + options: InternalStorageAPIConfig['options'], +): boolean => { + return !!(options?.bucket && options?.region); +}; + +const isConfigFromAmplifySingleton = ( + s3Options?: S3ServiceOptions, +): s3Options is S3ServiceOptions => { + return !!(s3Options && s3Options.bucket && s3Options.region); +}; diff --git a/packages/storage/src/providers/s3/utils/index.ts b/packages/storage/src/providers/s3/utils/index.ts index 1f43bb3f5d9..6a98924f8b3 100644 --- a/packages/storage/src/providers/s3/utils/index.ts +++ b/packages/storage/src/providers/s3/utils/index.ts @@ -7,4 +7,7 @@ export { createDownloadTask, createUploadTask } from './transferTask'; export { validateStorageOperationInput } from './validateStorageOperationInput'; export { validateStorageOperationInputWithPrefix } from './validateStorageOperationInputWithPrefix'; export { isInputWithPath } from './isInputWithPath'; -export { createStorageConfiguration } from './config'; +export { + createStorageConfiguration, + createServerStorageConfiguration, +} from './config'; diff --git a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts index ece08ea9223..dd096f95999 100644 --- a/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts +++ b/packages/storage/src/providers/s3/utils/resolveS3ConfigAndInput.ts @@ -38,20 +38,19 @@ export const resolveS3ConfigAndInput = async ({ }: ResolveS3ConfigAndInputParams): Promise => { const { credentialsProvider, - serviceOptions, - libraryOptions, + bucket, + region, + dangerouslyConnectToHttpEndpointForTesting, + defaultAccessLevel, + prefixResolver = defaultPrefixResolver, + isObjectLockEnabled, identityIdProvider, } = config; - const { bucket, region, dangerouslyConnectToHttpEndpointForTesting } = - serviceOptions ?? {}; + assertValidationError(!!bucket, StorageValidationErrorCode.NoBucket); assertValidationError(!!region, StorageValidationErrorCode.NoRegion); const identityId = await identityIdProvider(); - const { - defaultAccessLevel, - prefixResolver = defaultPrefixResolver, - isObjectLockEnabled, - } = libraryOptions ?? {}; + const keyPrefix = await prefixResolver({ accessLevel: apiOptions?.accessLevel ?? defaultAccessLevel ?? DEFAULT_ACCESS_LEVEL, diff --git a/packages/storage/src/types/inputs.ts b/packages/storage/src/types/inputs.ts index 403a2a14332..f3f34fadcee 100644 --- a/packages/storage/src/types/inputs.ts +++ b/packages/storage/src/types/inputs.ts @@ -85,12 +85,14 @@ export type StorageUploadDataInputWithPath = export interface StorageCopyInputWithKey< SourceOptions extends StorageOptions, DestinationOptions extends StorageOptions, -> { + Options, +> extends StorageOperationOptionsInput { source: SourceOptions; destination: DestinationOptions; } -export interface StorageCopyInputWithPath { +export interface StorageCopyInputWithPath + extends StorageOperationOptionsInput { source: StorageOperationInputWithPath; destination: StorageOperationInputWithPath; }