diff --git a/docs/tasks/LargeFileUploadTask.md b/docs/tasks/LargeFileUploadTask.md index 981eff3f7..f0e2d401f 100644 --- a/docs/tasks/LargeFileUploadTask.md +++ b/docs/tasks/LargeFileUploadTask.md @@ -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(); +``` diff --git a/src/middleware/AuthenticationHandler.ts b/src/middleware/AuthenticationHandler.ts index 121c5e0b1..267f8543f 100644 --- a/src/middleware/AuthenticationHandler.ts +++ b/src/middleware/AuthenticationHandler.ts @@ -9,6 +9,7 @@ * @module AuthenticationHandler */ +import { isGraphURL } from "../GraphRequestUtil"; import { AuthenticationProvider } from "../IAuthenticationProvider"; import { AuthenticationProviderOptions } from "../IAuthenticationProviderOptions"; import { Context } from "../IContext"; @@ -60,23 +61,30 @@ export class AuthenticationHandler implements Middleware { * @returns A Promise that resolves to nothing */ public async execute(context: Context): Promise { - 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); } diff --git a/src/tasks/FileUploadUtil/UploadResult.ts b/src/tasks/FileUploadUtil/UploadResult.ts new file mode 100644 index 000000000..e2d88dd02 --- /dev/null +++ b/src/tasks/FileUploadUtil/UploadResult.ts @@ -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")); + } +} diff --git a/src/tasks/LargeFileUploadTask.ts b/src/tasks/LargeFileUploadTask.ts index 185390f9d..8b37e8fa6 100644 --- a/src/tasks/LargeFileUploadTask.ts +++ b/src/tasks/LargeFileUploadTask.ts @@ -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 @@ -50,6 +54,7 @@ export interface LargeFileUploadTaskOptions { export interface LargeFileUploadSession { url: string; expiry: Date; + isCancelled?: boolean; } /** @@ -124,6 +129,7 @@ export class LargeFileUploadTask { const largeFileUploadSession: LargeFileUploadSession = { url: session.uploadUrl, expiry: new Date(session.expirationDateTime), + isCancelled: false, }; return largeFileUploadSession; } @@ -215,8 +221,7 @@ export class LargeFileUploadTask { * @returns The promise resolves to uploaded response */ public async upload(): Promise { - // 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"); @@ -224,13 +229,29 @@ export class LargeFileUploadTask { 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); } } @@ -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 { return await this.client @@ -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 { + 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 @@ -259,7 +300,14 @@ export class LargeFileUploadTask { * @returns The promise resolves to cancelled response */ public async cancel(): Promise { - 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; } /** @@ -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; + } } diff --git a/test/common/tasks/LargeFileUploadTask.ts b/test/common/tasks/LargeFileUploadTask.ts index 62fceefbe..edf45857a 100644 --- a/test/common/tasks/LargeFileUploadTask.ts +++ b/test/common/tasks/LargeFileUploadTask.ts @@ -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"; @@ -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, + 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()