Skip to content

Commit

Permalink
Merge branch 'main' into upgrade-instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
manueliglesias authored Apr 15, 2021
2 parents 32241fa + c1ce5ac commit 80765dc
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 18 deletions.
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
"test": "tslint 'src/**/*.ts' && jest -w 1 --coverage",
"build-with-test": "npm test && npm run build",
"build:cjs": "node ./build es5 && webpack && webpack --config ./webpack.config.dev.js",
"build:esm": "node ./build es6",
"build:esm": "rimraf lib-esm && node ./build es6",
"build:cjs:watch": "node ./build es5 --watch",
"build:esm:watch": "node ./build es6 --watch",
"build:esm:watch": "rimraf lib-esm && node ./build es6 --watch",
"build": "npm run clean && npm run generate-version && npm run build:esm && npm run build:cjs",
"generate-version": "genversion src/Platform/version.ts --es6 --semi",
"clean": "rimraf lib-esm lib dist",
Expand Down
121 changes: 118 additions & 3 deletions packages/storage/__tests__/Storage-unit-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AWSStorageProvider from '../src/providers/AWSS3Provider';
import { Storage as StorageClass } from '../src/Storage';
import { Storage as StorageCategory } from '../src';
import axios from 'axios';

const credentials = {
accessKeyId: 'accessKeyId',
Expand All @@ -16,7 +17,6 @@ const options = {
credentials,
level: 'level',
};

describe('Storage', () => {
describe('constructor test', () => {
test('happy case', () => {
Expand Down Expand Up @@ -465,7 +465,7 @@ describe('Storage', () => {
const get_spyon = jest
.spyOn(AWSStorageProvider.prototype, 'get')
.mockImplementation(() => {
return;
return Promise.resolve('https://this-url-doesnt-exist.gg');
});
const storage = new StorageClass();
const provider = new AWSStorageProvider();
Expand All @@ -482,14 +482,22 @@ describe('Storage', () => {
expect(get_spyon).toBeCalled();
get_spyon.mockClear();
});
test('get without provider', async () => {
const storage = new StorageClass();
try {
await storage.get('key');
} catch (err) {
expect(err).toEqual('No plugin found in Storage for the provider');
}
});
});

describe('put test', () => {
test('put object successfully', async () => {
const put_spyon = jest
.spyOn(AWSStorageProvider.prototype, 'put')
.mockImplementation(() => {
return;
return Promise.resolve({ key: 'new_object' });
});
const storage = new StorageClass();
const provider = new AWSStorageProvider();
Expand All @@ -506,6 +514,14 @@ describe('Storage', () => {
expect(put_spyon).toBeCalled();
put_spyon.mockClear();
});
test('put without provider', async () => {
const storage = new StorageClass();
try {
await storage.put('key', 'test upload');
} catch (err) {
expect(err).toEqual('No plugin found in Storage for the provider');
}
});
});

describe('remove test', () => {
Expand All @@ -530,6 +546,14 @@ describe('Storage', () => {
expect(remove_spyon).toBeCalled();
remove_spyon.mockClear();
});
test('remove without provider', async () => {
const storage = new StorageClass();
try {
await storage.remove('key');
} catch (err) {
expect(err).toEqual('No plugin found in Storage for the provider');
}
});
});

describe('list test', () => {
Expand All @@ -554,5 +578,96 @@ describe('Storage', () => {
expect(list_spyon).toBeCalled();
list_spyon.mockClear();
});
test('list without provider', async () => {
const storage = new StorageClass();
try {
await storage.list('');
} catch (err) {
expect(err).toEqual('No plugin found in Storage for the provider');
}
});
});

describe('cancel test', () => {
let isCancelSpy = null;
let cancelTokenSpy = null;
let cancelMock = null;
let tokenMock = null;

beforeEach(() => {
cancelMock = jest.fn();
tokenMock = jest.fn();
isCancelSpy = jest.spyOn(axios, 'isCancel').mockReturnValue(true);
cancelTokenSpy = jest
.spyOn(axios.CancelToken, 'source')
.mockImplementation(() => {
return { token: tokenMock, cancel: cancelMock };
});
});

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

test('happy case - cancel upload', async () => {
jest.spyOn(AWSStorageProvider.prototype, 'put').mockImplementation(() => {
return Promise.resolve({ key: 'new_object' });
});
const storage = new StorageClass();
const provider = new AWSStorageProvider();
storage.addPluggable(provider);
storage.configure(options);
const request = storage.put('test.txt', 'test upload', {
Storage: {
AWSS3: {
bucket: 'bucket',
region: 'us-east-1',
},
},
});
storage.cancel(request, 'request cancelled');
expect(cancelTokenSpy).toBeCalledTimes(1);
expect(cancelMock).toHaveBeenCalledTimes(1);
try {
await request;
} catch (err) {
expect(err).toEqual('request cancelled');
expect(storage.isCancelError(err)).toBeTruthy();
}
});

test('happy case - cancel download', async () => {
jest.spyOn(AWSStorageProvider.prototype, 'get').mockImplementation(() => {
return Promise.resolve('some_file_content');
});
const storage = new StorageClass();
const provider = new AWSStorageProvider();
storage.addPluggable(provider);
storage.configure(options);
const request = storage.get('test.txt', {
Storage: {
AWSS3: {
bucket: 'bucket',
region: 'us-east-1',
},
},
download: true,
});
storage.cancel(request, 'request cancelled');
expect(cancelTokenSpy).toHaveBeenCalledTimes(1);
expect(cancelMock).toHaveBeenCalledWith('request cancelled');
try {
await request;
} catch (err) {
expect(err).toEqual('request cancelled');
expect(storage.isCancelError(err)).toBeTruthy();
}
});

test('isCancelError called', () => {
const storage = new StorageClass();
storage.isCancelError({});
expect(isCancelSpy).toHaveBeenCalledTimes(1);
});
});
});
68 changes: 60 additions & 8 deletions packages/storage/src/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { ConsoleLogger as Logger, Parser } from '@aws-amplify/core';
import { AWSS3Provider } from './providers';
import { StorageProvider } from './types';
import axios, { CancelTokenSource } from 'axios';

const logger = new Logger('StorageClass');

Expand All @@ -28,6 +29,14 @@ export class Storage {
private _config;
private _pluggables: StorageProvider[];

/**
* Similar to the API module. This weak map allows users to cancel their in-flight request made using the Storage
* module. For every get or put request, a unique cancel token will be generated and injected to it's underlying
* AxiosHttpHandler. This map maintains a mapping of Request to CancelTokenSource. When .cancel is invoked, it will
* attempt to retrieve it's corresponding cancelTokenSource and cancel the in-flight request.
*/
private _cancelTokenSourceMap: WeakMap<Promise<any>, CancelTokenSource>;

/**
* @public
*/
Expand All @@ -40,6 +49,7 @@ export class Storage {
constructor() {
this._config = {};
this._pluggables = [];
this._cancelTokenSourceMap = new WeakMap<Promise<any>, CancelTokenSource>();
logger.debug('Storage Options', this._config);

this.get = this.get.bind(this);
Expand Down Expand Up @@ -159,23 +169,59 @@ export class Storage {
return this._config;
}

private getCancellableTokenSource(): CancelTokenSource {
return axios.CancelToken.source();
}

private updateRequestToBeCancellable(
request: Promise<any>,
cancelTokenSource: CancelTokenSource
) {
this._cancelTokenSourceMap.set(request, cancelTokenSource);
}

/**
* Cancels an inflight request
*
* @param {Promise<any>} request - The request to cancel
* @param {string} [message] - A message to include in the cancelation exception
*/
public cancel(request: Promise<any>, message?: string) {
const cancelTokenSource = this._cancelTokenSourceMap.get(request);
if (cancelTokenSource) {
cancelTokenSource.cancel(message);
} else {
logger.debug('The request does not map to any cancel token');
}
}

/**
* Get a presigned URL of the file or the object data when download:true
*
* @param {string} key - key of the object
* @param {Object} [config] - { level : private|protected|public, download: true|false }
* @return - A promise resolves to either a presigned url or the object
*/
public async get(key: string, config?): Promise<String | Object> {
public get(key: string, config?): Promise<String | Object> {
const { provider = DEFAULT_PROVIDER } = config || {};
const prov = this._pluggables.find(
pluggable => pluggable.getProviderName() === provider
);
if (prov === undefined) {
logger.debug('No plugin found with providerName', provider);
Promise.reject('No plugin found in Storage for the provider');
return Promise.reject('No plugin found in Storage for the provider');
}
return prov.get(key, config);
const cancelTokenSource = this.getCancellableTokenSource();
const responsePromise = prov.get(key, {
...config,
cancelTokenSource,
});
this.updateRequestToBeCancellable(responsePromise, cancelTokenSource);
return responsePromise;
}

public isCancelError(error: any) {
return axios.isCancel(error);
}

/**
Expand All @@ -186,16 +232,22 @@ export class Storage {
* progressCallback: function }
* @return - promise resolves to object on success
*/
public async put(key: string, object, config?): Promise<Object> {
public put(key: string, object, config?): Promise<Object> {
const { provider = DEFAULT_PROVIDER } = config || {};
const prov = this._pluggables.find(
pluggable => pluggable.getProviderName() === provider
);
if (prov === undefined) {
logger.debug('No plugin found with providerName', provider);
Promise.reject('No plugin found in Storage for the provider');
return Promise.reject('No plugin found in Storage for the provider');
}
return prov.put(key, object, config);
const cancelTokenSource = this.getCancellableTokenSource();
const responsePromise = prov.put(key, object, {
...config,
cancelTokenSource,
});
this.updateRequestToBeCancellable(responsePromise, cancelTokenSource);
return responsePromise;
}

/**
Expand All @@ -211,7 +263,7 @@ export class Storage {
);
if (prov === undefined) {
logger.debug('No plugin found with providerName', provider);
Promise.reject('No plugin found in Storage for the provider');
return Promise.reject('No plugin found in Storage for the provider');
}
return prov.remove(key, config);
}
Expand All @@ -229,7 +281,7 @@ export class Storage {
);
if (prov === undefined) {
logger.debug('No plugin found with providerName', provider);
Promise.reject('No plugin found in Storage for the provider');
return Promise.reject('No plugin found in Storage for the provider');
}
return prov.list(path, config);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/storage/src/providers/AWSS3Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ export class AWSS3Provider implements StorageProvider {
const {
region,
credentials,
cancelTokenSource,
dangerouslyConnectToHttpEndpointForTesting,
} = config;
let localTestingConfig = {};
Expand All @@ -524,7 +525,7 @@ export class AWSS3Provider implements StorageProvider {
credentials,
customUserAgent: getAmplifyUserAgent(),
...localTestingConfig,
requestHandler: new AxiosHttpHandler({}, emitter),
requestHandler: new AxiosHttpHandler({}, emitter, cancelTokenSource),
});
return s3client;
}
Expand Down
8 changes: 6 additions & 2 deletions packages/storage/src/providers/AWSS3ProviderManagedUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,11 @@ export class AWSS3ProviderManagedUpload {
*/
protected async _createNewS3Client(config, emitter?) {
const credentials = await this._getCredentials();
const { region, dangerouslyConnectToHttpEndpointForTesting } = config;
const {
region,
dangerouslyConnectToHttpEndpointForTesting,
cancelTokenSource,
} = config;
let localTestingConfig = {};

if (dangerouslyConnectToHttpEndpointForTesting) {
Expand All @@ -368,7 +372,7 @@ export class AWSS3ProviderManagedUpload {
region,
credentials,
...localTestingConfig,
requestHandler: new AxiosHttpHandler({}, emitter),
requestHandler: new AxiosHttpHandler({}, emitter, cancelTokenSource),
customUserAgent: getAmplifyUserAgent(),
});
client.middlewareStack.remove(SET_CONTENT_LENGTH_HEADER);
Expand Down
9 changes: 7 additions & 2 deletions packages/storage/src/providers/axios-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { HttpHandlerOptions } from '@aws-sdk/types';
import { HttpHandler, HttpRequest, HttpResponse } from '@aws-sdk/protocol-http';
import { buildQueryString } from '@aws-sdk/querystring-builder';
import axios, { AxiosRequestConfig, Method } from 'axios';
import axios, { AxiosRequestConfig, Method, CancelTokenSource } from 'axios';
import { ConsoleLogger as Logger } from '@aws-amplify/core';
import { FetchHttpHandlerOptions } from '@aws-sdk/fetch-http-handler';

Expand All @@ -24,7 +24,8 @@ export const SEND_PROGRESS_EVENT = 'sendProgress';
export class AxiosHttpHandler implements HttpHandler {
constructor(
private readonly httpOptions: FetchHttpHandlerOptions = {},
private readonly emitter?: any
private readonly emitter?: any,
private readonly cancelTokenSource?: CancelTokenSource
) {}

destroy(): void {
Expand Down Expand Up @@ -89,6 +90,10 @@ export class AxiosHttpHandler implements HttpHandler {
logger.debug(event);
};
}
// If a cancel token source is passed down from the provider, allows cancellation of in-flight requests
if (this.cancelTokenSource) {
axiosRequest.cancelToken = this.cancelTokenSource.token;
}

// From gamma release, aws-sdk now expects all response type to be of blob or streams
axiosRequest.responseType = 'blob';
Expand Down
Loading

0 comments on commit 80765dc

Please sign in to comment.