Skip to content

Large file upload task - Add UploadResult object #397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/tasks/LargeFileUploadTask.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,19 @@ async function largeFileUpload(client, file) {
}
}
```

## Cancelling a largeFileUpload task

_Cancelling an upload session sends a DELETE request to the upload session URL_

```typescript
const cancelResponse = await uploadTask.cancel();
```

## Get the largeFileUpload session

_Returns the largeFileUpload session information containing the URL, expiry date and cancellation status of the task_

```typescript
const uploadsession: LargeFileUploadSession = uploadTask.getUploadSession();
```
40 changes: 24 additions & 16 deletions src/middleware/AuthenticationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* @module AuthenticationHandler
*/

import { isGraphURL } from "../GraphRequestUtil";
import { AuthenticationProvider } from "../IAuthenticationProvider";
import { AuthenticationProviderOptions } from "../IAuthenticationProviderOptions";
import { Context } from "../IContext";
Expand Down Expand Up @@ -60,23 +61,30 @@ export class AuthenticationHandler implements Middleware {
* @returns A Promise that resolves to nothing
*/
public async execute(context: Context): Promise<void> {
let options: AuthenticationHandlerOptions;
if (context.middlewareControl instanceof MiddlewareControl) {
options = context.middlewareControl.getMiddlewareOptions(AuthenticationHandlerOptions) as AuthenticationHandlerOptions;
const url = typeof context.request === "string" ? context.request : context.request.url;
if (isGraphURL(url)) {
let options: AuthenticationHandlerOptions;
if (context.middlewareControl instanceof MiddlewareControl) {
options = context.middlewareControl.getMiddlewareOptions(AuthenticationHandlerOptions) as AuthenticationHandlerOptions;
}
let authenticationProvider: AuthenticationProvider;
let authenticationProviderOptions: AuthenticationProviderOptions;
if (options) {
authenticationProvider = options.authenticationProvider;
authenticationProviderOptions = options.authenticationProviderOptions;
}
if (!authenticationProvider) {
authenticationProvider = this.authenticationProvider;
}
const token: string = await authenticationProvider.getAccessToken(authenticationProviderOptions);
const bearerKey = `Bearer ${token}`;
appendRequestHeader(context.request, context.options, AuthenticationHandler.AUTHORIZATION_HEADER, bearerKey);
TelemetryHandlerOptions.updateFeatureUsageFlag(context, FeatureUsageFlag.AUTHENTICATION_HANDLER_ENABLED);
} else {
if (context.options.headers) {
delete context.options.headers[AuthenticationHandler.AUTHORIZATION_HEADER];
}
}
let authenticationProvider: AuthenticationProvider;
let authenticationProviderOptions: AuthenticationProviderOptions;
if (typeof options !== "undefined") {
authenticationProvider = options.authenticationProvider;
authenticationProviderOptions = options.authenticationProviderOptions;
}
if (typeof authenticationProvider === "undefined") {
authenticationProvider = this.authenticationProvider;
}
const token: string = await authenticationProvider.getAccessToken(authenticationProviderOptions);
const bearerKey = `Bearer ${token}`;
appendRequestHeader(context.request, context.options, AuthenticationHandler.AUTHORIZATION_HEADER, bearerKey);
TelemetryHandlerOptions.updateFeatureUsageFlag(context, FeatureUsageFlag.AUTHENTICATION_HANDLER_ENABLED);
return await this.nextMiddleware.execute(context);
}

Expand Down
77 changes: 77 additions & 0 deletions src/tasks/FileUploadUtil/UploadResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/

/**
* Class representing a successful file upload result
*/
export class UploadResult {
/**
* @private
* Location value looked up in the response header
*/
private _location: string;

/**
* @private
* Response body of the final raw response
*/
private _responseBody: unknown;

/**
* @public
* Get of the location value.
* Location value is looked up in the response header
*/
public get location(): string {
return this._location;
}

/**
* @public
* Set the location value
* Location value is looked up in the response header
*/
public set location(location: string) {
this._location = location;
}

/**
* @public
* Get The response body from the completed upload response
*/
public get responseBody() {
return this._responseBody;
}

/**
* @public
* Set the response body from the completed upload response
*/
public set responseBody(responseBody: unknown) {
this._responseBody = responseBody;
}

/**
* @public
* @param {responseBody} responsebody - The response body from the completed upload response
* @param {location} location - The location value from the headers from the completed upload response
*/
public constructor(responseBody: unknown, location: string) {
// Response body or the location parameter can be undefined.
this._location = location;
this._responseBody = responseBody;
}

/**
* @public
* @param {responseBody} responseBody - The response body from the completed upload response
* @param {responseHeaders} responseHeaders - The headers from the completed upload response
*/
public static CreateUploadResult(responseBody?: unknown, responseHeaders?: Headers) {
return new UploadResult(responseBody, responseHeaders.get("location"));
}
}
76 changes: 67 additions & 9 deletions src/tasks/LargeFileUploadTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
* @module LargeFileUploadTask
*/

import { GraphClientError } from "../GraphClientError";
import { GraphResponseHandler } from "../GraphResponseHandler";
import { Client } from "../index";
import { Range } from "../Range";
import { ResponseType } from "../ResponseType";
import { UploadResult } from "./FileUploadUtil/UploadResult";

/**
* @interface
Expand Down Expand Up @@ -50,6 +54,7 @@ export interface LargeFileUploadTaskOptions {
export interface LargeFileUploadSession {
url: string;
expiry: Date;
isCancelled?: boolean;
}

/**
Expand Down Expand Up @@ -124,6 +129,7 @@ export class LargeFileUploadTask {
const largeFileUploadSession: LargeFileUploadSession = {
url: session.uploadUrl,
expiry: new Date(session.expirationDateTime),
isCancelled: false,
};
return largeFileUploadSession;
}
Expand Down Expand Up @@ -215,22 +221,37 @@ export class LargeFileUploadTask {
* @returns The promise resolves to uploaded response
*/
public async upload(): Promise<any> {
// eslint-disable-next-line no-constant-condition
while (true) {
while (!this.uploadSession.isCancelled) {
const nextRange = this.getNextRange();
if (nextRange.maxValue === -1) {
const err = new Error("Task with which you are trying to upload is already completed, Please check for your uploaded file");
err.name = "Invalid Session";
throw err;
}
const fileSlice = this.sliceFile(nextRange);
const response = await this.uploadSlice(fileSlice, nextRange, this.file.size);
// Upon completion of upload process incase of onedrive, driveItem is returned, which contains id
if (response.id !== undefined) {
return response;
} else {
this.updateTaskStatus(response);
const rawResponse = await this.uploadSliceGetRawResponse(fileSlice, nextRange, this.file.size);
if (!rawResponse) {
throw new GraphClientError("Something went wrong! Large file upload slice response is null.");
}

const responseBody = await GraphResponseHandler.getResponse(rawResponse);
/**
* (rawResponse.status === 201) -> This condition is applicable for OneDrive, PrintDocument and Outlook APIs.
* (rawResponse.status === 200 && responseBody.id) -> This additional condition is applicable only for OneDrive API.
*/
if (rawResponse.status === 201 || (rawResponse.status === 200 && responseBody.id)) {
return UploadResult.CreateUploadResult(responseBody, rawResponse.headers);
}

/* Handling the API issue where the case of Outlook upload response property -'nextExpectedRanges' is not uniform.
* https://github.com/microsoftgraph/msgraph-sdk-serviceissues/issues/39
*/
const res: UploadStatusResponse = {
expirationDateTime: responseBody.expirationDateTime,
nextExpectedRanges: responseBody.NextExpectedRanges || responseBody.nextExpectedRanges,
};

this.updateTaskStatus(res);
}
}

Expand All @@ -241,6 +262,7 @@ export class LargeFileUploadTask {
* @param {ArrayBuffer | Blob | File} fileSlice - The file slice
* @param {Range} range - The range value
* @param {number} totalSize - The total size of a complete file
* @returns The response body of the upload slice result
*/
public async uploadSlice(fileSlice: ArrayBuffer | Blob | File, range: Range, totalSize: number): Promise<any> {
return await this.client
Expand All @@ -251,6 +273,25 @@ export class LargeFileUploadTask {
})
.put(fileSlice);
}
/**
* @public
* @async
* Uploads given slice to the server
* @param {unknown} fileSlice - The file slice
* @param {Range} range - The range value
* @param {number} totalSize - The total size of a complete file
* @returns The raw response of the upload slice result
*/
public async uploadSliceGetRawResponse(fileSlice: unknown, range: Range, totalSize: number): Promise<Response> {
return await this.client
.api(this.uploadSession.url)
.headers({
"Content-Length": `${range.maxValue - range.minValue + 1}`,
"Content-Range": `bytes ${range.minValue}-${range.maxValue}/${totalSize}`,
})
.responseType(ResponseType.RAW)
.put(fileSlice);
}

/**
* @public
Expand All @@ -259,7 +300,14 @@ export class LargeFileUploadTask {
* @returns The promise resolves to cancelled response
*/
public async cancel(): Promise<any> {
return await this.client.api(this.uploadSession.url).delete();
const cancelResponse = await this.client
.api(this.uploadSession.url)
.responseType(ResponseType.RAW)
.delete();
if (cancelResponse.status === 204) {
this.uploadSession.isCancelled = true;
}
return cancelResponse;
}

/**
Expand All @@ -284,4 +332,14 @@ export class LargeFileUploadTask {
await this.getStatus();
return await this.upload();
}

/**
* @public
* @async
* Get the upload session information
* @returns The large file upload session
*/
public getUploadSession(): LargeFileUploadSession {
return this.uploadSession;
}
}
58 changes: 57 additions & 1 deletion test/common/tasks/LargeFileUploadTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/

import { assert } from "chai";
import * as sinon from "sinon";

import { UploadResult } from "../../../src/tasks/FileUploadUtil/UploadResult";
import { LargeFileUploadTask } from "../../../src/tasks/LargeFileUploadTask";
import { getClient } from "../../test-helper";

Expand Down Expand Up @@ -157,12 +159,66 @@ describe("LargeFileUploadTask.ts", () => {
const options = {
rangeSize: 327680,
};
const uploadTask = new LargeFileUploadTask(getClient(), fileObj, uploadSession, options);

it("Should return a Upload Result object after a completed task with 201 status", async () => {
const location = "TEST_URL";
const body = {
id: "TEST_ID",
};
const uploadTask = new LargeFileUploadTask(getClient(), fileObj, uploadSession, options);
const status201 = {
status: 200,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

201?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise this test is exactly the same as the one below.

stautsText: "OK",
headers: {
"Content-Type": "application/json",
location,
},
};
const rawResponse = new Response(JSON.stringify(body), status201);

const moq = sinon.mock(uploadTask);
moq.expects("uploadSliceGetRawResponse").resolves(rawResponse);
const result = await uploadTask.upload();
assert.isDefined(result);
assert.instanceOf(result, UploadResult);
assert.equal(result["location"], location);
const responseBody = result["responseBody"];
assert.isDefined(responseBody);
assert.equal(responseBody["id"], "TEST_ID");
});

it("Should return a Upload Result object after a completed task with 200 status and id", async () => {
const location = "TEST_URL";
const body = {
id: "TEST_ID",
};
const uploadTask = new LargeFileUploadTask(getClient(), fileObj, uploadSession, options);
const status200 = {
status: 200,
stautsText: "OK",
headers: {
"Content-Type": "application/json",
location,
},
};
const rawResponse = new Response(JSON.stringify(body), status200);

const moq = sinon.mock(uploadTask);
moq.expects("uploadSliceGetRawResponse").resolves(rawResponse);
const result = await uploadTask.upload();
assert.isDefined(result);
assert.instanceOf(result, UploadResult);
assert.equal(result["location"], location);
const responseBody = result["responseBody"];
assert.isDefined(responseBody);
assert.equal(responseBody["id"], "TEST_ID");
});
it("Should return an exception while trying to upload the file upload completed task", (done) => {
const statusResponse = {
expirationDateTime: "2018-08-06T09:05:45.195Z",
nextExpectedRanges: [],
};
const uploadTask = new LargeFileUploadTask(getClient(), fileObj, uploadSession, options);
uploadTask["updateTaskStatus"](statusResponse);
uploadTask
.upload()
Expand Down