Skip to content

Commit

Permalink
feat(storage): internal uploadData implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
AllanZhengYP committed Oct 4, 2024
1 parent cced539 commit 6b59aca
Show file tree
Hide file tree
Showing 24 changed files with 236 additions and 101 deletions.
67 changes: 67 additions & 0 deletions packages/storage/__tests__/internals/apis/uploadData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { uploadData as advancedUploadData } from '../../../src/internals';
import { uploadData as uploadDataInternal } from '../../../src/providers/s3/apis/internal/uploadData';

jest.mock('../../../src/providers/s3/apis/internal/uploadData');
const mockedUploadDataInternal = jest.mocked(uploadDataInternal);
const mockedUploadTask = 'UPLOAD_TASK';

describe('uploadData (internal)', () => {
beforeEach(() => {
mockedUploadDataInternal.mockReturnValue(mockedUploadTask as any);
});

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

it('should pass advanced option locationCredentialsProvider to internal remove', 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 contentDisposition = { type: 'attachment', filename: 'foo' } as const;
const onProgress = jest.fn();
const metadata = { foo: 'bar' };

const result = advancedUploadData({
path: 'input/path/to/mock/object',
data: 'data',
options: {
useAccelerateEndpoint,
bucket,
locationCredentialsProvider,
contentDisposition,
contentEncoding: 'gzip',
contentType: 'text/html',
onProgress,
metadata,
},
});

expect(mockedUploadDataInternal).toHaveBeenCalledTimes(1);
expect(mockedUploadDataInternal).toHaveBeenCalledWith({
path: 'input/path/to/mock/object',
data: 'data',
options: {
useAccelerateEndpoint,
bucket,
locationCredentialsProvider,
contentDisposition,
contentEncoding: 'gzip',
contentType: 'text/html',
onProgress,
metadata,
},
});
expect(result).toEqual(mockedUploadTask);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { byteLength } from '../../../../../src/providers/s3/apis/uploadData/byteLength';
import { byteLength } from '../../../../../src/providers/s3/apis/internal/uploadData/byteLength';

describe('byteLength', () => {
it('returns 0 for null or undefined', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import {
StorageValidationErrorCode,
validationErrorMap,
} from '../../../../../src/errors/types/validation';
import { putObjectJob } from '../../../../../src/providers/s3/apis/uploadData/putObjectJob';
import { getMultipartUploadHandlers } from '../../../../../src/providers/s3/apis/uploadData/multipart';
import { putObjectJob } from '../../../../../src/providers/s3/apis/internal/uploadData/putObjectJob';
import { getMultipartUploadHandlers } from '../../../../../src/providers/s3/apis/internal/uploadData/multipart';
import { UploadDataInput, UploadDataWithPathInput } from '../../../../../src';

jest.mock('../../../../../src/providers/s3/utils/');
jest.mock('../../../../../src/providers/s3/apis/uploadData/putObjectJob');
jest.mock('../../../../../src/providers/s3/apis/uploadData/multipart');
jest.mock(
'../../../../../src/providers/s3/apis/internal/uploadData/putObjectJob',
);
jest.mock('../../../../../src/providers/s3/apis/internal/uploadData/multipart');

const testPath = 'testPath/object';
const mockCreateUploadTask = createUploadTask as jest.Mock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import {
listParts,
uploadPart,
} from '../../../../../src/providers/s3/utils/client/s3data';
import { getMultipartUploadHandlers } from '../../../../../src/providers/s3/apis/uploadData/multipart';
import { getMultipartUploadHandlers } from '../../../../../src/providers/s3/apis/internal/uploadData/multipart';
import {
StorageValidationErrorCode,
validationErrorMap,
} from '../../../../../src/errors/types/validation';
import { UPLOADS_STORAGE_KEY } from '../../../../../src/providers/s3/utils/constants';
import { byteLength } from '../../../../../src/providers/s3/apis/uploadData/byteLength';
import { byteLength } from '../../../../../src/providers/s3/apis/internal/uploadData/byteLength';
import { CanceledError } from '../../../../../src/errors/CanceledError';
import { StorageOptions } from '../../../../../src/types';
import '../testUtils';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '../../../../../src/providers/s3/utils/client/s3data';
import { calculateContentMd5 } from '../../../../../src/providers/s3/utils';
import * as CRC32 from '../../../../../src/providers/s3/utils/crc32';
import { putObjectJob } from '../../../../../src/providers/s3/apis/uploadData/putObjectJob';
import { putObjectJob } from '../../../../../src/providers/s3/apis/internal/uploadData/putObjectJob';
import '../testUtils';

global.Blob = BlobPolyfill as any;
Expand Down
7 changes: 3 additions & 4 deletions packages/storage/src/internals/apis/getProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import { GetPropertiesOutput } from '../types/outputs';
*
* @internal
*/
export function getProperties(
export const getProperties = (
input: GetPropertiesInput,
): Promise<GetPropertiesOutput> {
return getPropertiesInternal(Amplify, {
): Promise<GetPropertiesOutput> =>
getPropertiesInternal(Amplify, {
path: input.path,
options: {
useAccelerateEndpoint: input?.options?.useAccelerateEndpoint,
Expand All @@ -30,4 +30,3 @@ export function getProperties(
// Type casting is necessary because `getPropertiesInternal` supports both Gen1 and Gen2 signatures, but here
// given in input can only be Gen2 signature, the return can only ben Gen2 signature.
}) as Promise<GetPropertiesOutput>;
}
5 changes: 2 additions & 3 deletions packages/storage/src/internals/apis/getUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { GetUrlOutput } from '../types/outputs';
/**
* @internal
*/
export function getUrl(input: GetUrlInput) {
return getUrlInternal(Amplify, {
export const getUrl = (input: GetUrlInput) =>
getUrlInternal(Amplify, {
path: input.path,
options: {
useAccelerateEndpoint: input?.options?.useAccelerateEndpoint,
Expand All @@ -27,4 +27,3 @@ export function getUrl(input: GetUrlInput) {
// Type casting is necessary because `getPropertiesInternal` supports both Gen1 and Gen2 signatures, but here
// given in input can only be Gen2 signature, the return can only ben Gen2 signature.
}) as Promise<GetUrlOutput>;
}
33 changes: 33 additions & 0 deletions packages/storage/src/internals/apis/uploadData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { UploadDataInput } from '../types/inputs';
import { UploadDataOutput } from '../types/outputs';
import { uploadData as uploadDataInternal } from '../../providers/s3/apis/internal/uploadData';

/**
* @internal
*/
export const uploadData = (input: UploadDataInput) => {
const { data, path, options } = input;

return uploadDataInternal({
path,
data,
options: {
useAccelerateEndpoint: options?.useAccelerateEndpoint,
bucket: options?.bucket,
onProgress: options?.onProgress,
contentDisposition: options?.contentDisposition,
contentEncoding: options?.contentEncoding,
contentType: options?.contentType,
metadata: options?.metadata,
preventOverwrite: options?.preventOverwrite,

// Advanced options
locationCredentialsProvider: options?.locationCredentialsProvider,
},
// Type casting is necessary because `uploadDataInternal` supports both Gen1 and Gen2 signatures, but here
// given in input can only be Gen2 signature, the return can only ben Gen2 signature.
}) as UploadDataOutput;
};
3 changes: 3 additions & 0 deletions packages/storage/src/internals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ export {
GetUrlInput,
CopyInput,
RemoveInput,
UploadDataInput,
} from './types/inputs';
export {
GetDataAccessOutput,
ListCallerAccessGrantsOutput,
GetPropertiesOutput,
GetUrlOutput,
RemoveOutput,
UploadDataOutput,
} from './types/outputs';

export { getDataAccess } from './apis/getDataAccess';
export { listCallerAccessGrants } from './apis/listCallerAccessGrants';
export { getProperties } from './apis/getProperties';
export { getUrl } from './apis/getUrl';
export { remove } from './apis/remove';
export { uploadData } from './apis/uploadData';

/*
CredentialsStore exports
Expand Down
11 changes: 9 additions & 2 deletions packages/storage/src/internals/types/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
GetPropertiesWithPathInput,
GetUrlWithPathInput,
RemoveWithPathInput,
UploadDataWithPathInput,
} from '../../providers/s3';

import { CredentialsProvider, ListLocationsInput } from './credentials';
Expand Down Expand Up @@ -79,15 +80,21 @@ export type CopyInput = ExtendCopyInputWithAdvancedOptions<
}
>;

export type UploadDataInput = ExtendInputWithAdvancedOptions<
UploadDataWithPathInput,
{
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.
*/
type ExtendInputWithAdvancedOptions<InputType, ExtendedOptionsType> =
InputType extends StorageOperationInputWithPath &
StorageOperationOptionsInput<infer PublicInputOptionsType>
? {
path: InputType['path'];
? InputType & {
options?: PublicInputOptionsType & ExtendedOptionsType;
}
: never;
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 @@ -5,6 +5,7 @@ import {
GetPropertiesWithPathOutput,
GetUrlWithPathOutput,
RemoveWithPathOutput,
UploadDataWithPathOutput,
} from '../../providers/s3/types';

import { ListLocationsOutput, LocationCredentials } from './credentials';
Expand Down Expand Up @@ -33,3 +34,8 @@ export type GetUrlOutput = GetUrlWithPathOutput;
* @internal
*/
export type RemoveOutput = RemoveWithPathOutput;

/**
* @internal
*/
export type UploadDataOutput = UploadDataWithPathOutput;
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 { UploadDataInput } from '../../../types';
import { UploadDataInput as UploadDataWithPathInputWithAdvancedOptions } from '../../../../../internals/types/inputs';
import { createUploadTask } from '../../../utils';
import { assertValidationError } from '../../../../../errors/utils/assertValidationError';
import { StorageValidationErrorCode } from '../../../../../errors/types/validation';
import { DEFAULT_PART_SIZE, MAX_OBJECT_SIZE } from '../../../utils/constants';

import { byteLength } from './byteLength';
import { putObjectJob } from './putObjectJob';
import { getMultipartUploadHandlers } from './multipart';

export const uploadData = (
input: UploadDataInput | UploadDataWithPathInputWithAdvancedOptions,
) => {
const { data } = input;

const dataByteLength = byteLength(data);
assertValidationError(
dataByteLength === undefined || dataByteLength <= MAX_OBJECT_SIZE,
StorageValidationErrorCode.ObjectIsTooLarge,
);

if (dataByteLength && dataByteLength <= DEFAULT_PART_SIZE) {
// Single part upload
const abortController = new AbortController();

return createUploadTask({
isMultipartUpload: false,
job: putObjectJob(input, abortController.signal, dataByteLength),
onCancel: (message?: string) => {
abortController.abort(message);
},
});
} else {
// Multipart upload
const { multipartUploadJob, onPause, onResume, onCancel } =
getMultipartUploadHandlers(input, dataByteLength);

return createUploadTask({
isMultipartUpload: true,
job: multipartUploadJob,
onCancel: (message?: string) => {
onCancel(message);
},
onPause,
onResume,
});
}
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { DEFAULT_PART_SIZE, MAX_PARTS_COUNT } from '../../../utils/constants';
import {
DEFAULT_PART_SIZE,
MAX_PARTS_COUNT,
} from '../../../../utils/constants';

export const calculatePartSize = (totalSize?: number): number => {
if (!totalSize) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { StorageUploadDataPayload } from '../../../../../types';
import { StorageUploadDataPayload } from '../../../../../../types';
import {
StorageValidationErrorCode,
validationErrorMap,
} from '../../../../../errors/types/validation';
import { StorageError } from '../../../../../errors/StorageError';
} from '../../../../../../errors/types/validation';
import { StorageError } from '../../../../../../errors/StorageError';

import { calculatePartSize } from './calculatePartSize';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@

import { StorageAccessLevel } from '@aws-amplify/core';

import { ContentDisposition, ResolvedS3Config } from '../../../types/options';
import { StorageUploadDataPayload } from '../../../../../types';
import { Part, createMultipartUpload } from '../../../utils/client/s3data';
import { logger } from '../../../../../utils';
import { calculateContentCRC32 } from '../../../utils/crc32';
import { constructContentDisposition } from '../../../utils/constructContentDisposition';
import {
ContentDisposition,
ResolvedS3Config,
} from '../../../../types/options';
import { StorageUploadDataPayload } from '../../../../../../types';
import { Part, createMultipartUpload } from '../../../../utils/client/s3data';
import { logger } from '../../../../../../utils';
import { calculateContentCRC32 } from '../../../../utils/crc32';
import { constructContentDisposition } from '../../../../utils/constructContentDisposition';

import {
cacheMultipartUpload,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { TransferProgressEvent } from '../../../../../types';
import { TransferProgressEvent } from '../../../../../../types';

interface ConcurrentUploadsProgressTrackerOptions {
size?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
defaultStorage,
} from '@aws-amplify/core';

import { UPLOADS_STORAGE_KEY } from '../../../utils/constants';
import { ResolvedS3Config } from '../../../types/options';
import { Part, listParts } from '../../../utils/client/s3data';
import { logger } from '../../../../../utils';
import { UPLOADS_STORAGE_KEY } from '../../../../utils/constants';
import { ResolvedS3Config } from '../../../../types/options';
import { Part, listParts } from '../../../../utils/client/s3data';
import { logger } from '../../../../../../utils';

const ONE_HOUR = 1000 * 60 * 60;

Expand Down
Loading

0 comments on commit 6b59aca

Please sign in to comment.