UNPKG

12.4 kBPlain TextView Raw
1/**
2 * -------------------------------------------------------------------------------------------
3 * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4 * See License in the project root for license information.
5 * -------------------------------------------------------------------------------------------
6 */
7
8/**
9 * @module LargeFileUploadTask
10 */
11
12import { GraphClientError } from "../GraphClientError";
13import { GraphResponseHandler } from "../GraphResponseHandler";
14import { Client } from "../index";
15import { ResponseType } from "../ResponseType";
16import { UploadEventHandlers } from "./FileUploadTask/Interfaces/IUploadEventHandlers";
17import { Range } from "./FileUploadTask/Range";
18import { UploadResult } from "./FileUploadTask/UploadResult";
19
20/**
21 * @interface
22 * Signature to representing key value pairs
23 * @property {[key: string] : string | number} - The Key value pair
24 */
25interface KeyValuePairObjectStringNumber {
26 [key: string]: string | number;
27}
28
29/**
30 * @interface
31 * Signature to represent the resulting response in the status enquiry request
32 * @property {string} expirationDateTime - The expiration time of the upload session
33 * @property {string[]} nextExpectedRanges - The ranges expected in next consecutive request in the upload
34 */
35interface UploadStatusResponse {
36 expirationDateTime: string;
37 nextExpectedRanges: string[];
38}
39
40/**
41 * @interface
42 * Signature to define options for upload task
43 * @property {number} [rangeSize = LargeFileUploadTask.DEFAULT_FILE_SIZE] - Specifies the range chunk size
44 * @property {UploadEventHandlers} uploadEventHandlers - UploadEventHandlers attached to an upload task
45 */
46export interface LargeFileUploadTaskOptions {
47 rangeSize?: number;
48 uploadEventHandlers?: UploadEventHandlers;
49}
50
51/**
52 * @interface
53 * Signature to represent upload session resulting from the session creation in the server
54 * @property {string} url - The URL to which the file upload is made
55 * @property {Date} expiry - The expiration of the time of the upload session
56 */
57export interface LargeFileUploadSession {
58 url: string;
59 expiry: Date;
60 isCancelled?: boolean;
61}
62
63/**
64 * @type
65 * Representing the return type of the sliceFile function that is type of the slice of a given range.
66 */
67export type SliceType = ArrayBuffer | Blob | Uint8Array;
68
69/**
70 * @interface
71 * Signature to define the properties and content of the file in upload task
72 * @property {ArrayBuffer | File} content - The actual file content
73 * @property {string} name - Specifies the file name with extension
74 * @property {number} size - Specifies size of the file
75 */
76export interface FileObject<T> {
77 content: T;
78 name: string;
79 size: number;
80 sliceFile(range: Range): SliceType | Promise<SliceType>;
81}
82
83/**
84 * @class
85 * Class representing LargeFileUploadTask
86 */
87export class LargeFileUploadTask<T> {
88 /**
89 * @private
90 * Default value for the rangeSize
91 */
92 private DEFAULT_FILE_SIZE: number = 5 * 1024 * 1024;
93
94 /**
95 * @protected
96 * The GraphClient instance
97 */
98 protected client: Client;
99
100 /**
101 * @protected
102 * The object holding file details
103 */
104 protected file: FileObject<T>;
105
106 /**
107 * @protected
108 * The object holding options for the task
109 */
110 protected options: LargeFileUploadTaskOptions;
111
112 /**
113 * @protected
114 * The object for upload session
115 */
116 protected uploadSession: LargeFileUploadSession;
117
118 /**
119 * @protected
120 * The next range needs to be uploaded
121 */
122 protected nextRange: Range;
123
124 /**
125 * @public
126 * @static
127 * @async
128 * Makes request to the server to create an upload session
129 * @param {Client} client - The GraphClient instance
130 * @param {string} requestUrl - The URL to create the upload session
131 * @param {any} payload - The payload that needs to be sent
132 * @param {KeyValuePairObjectStringNumber} headers - The headers that needs to be sent
133 * @returns The promise that resolves to LargeFileUploadSession
134 */
135 public static async createUploadSession(client: Client, requestUrl: string, payload: any, headers: KeyValuePairObjectStringNumber = {}): Promise<LargeFileUploadSession> {
136 const session = await client
137 .api(requestUrl)
138 .headers(headers)
139 .post(payload);
140 const largeFileUploadSession: LargeFileUploadSession = {
141 url: session.uploadUrl,
142 expiry: new Date(session.expirationDateTime),
143 isCancelled: false,
144 };
145 return largeFileUploadSession;
146 }
147
148 /**
149 * @public
150 * @constructor
151 * Constructs a LargeFileUploadTask
152 * @param {Client} client - The GraphClient instance
153 * @param {FileObject} file - The FileObject holding details of a file that needs to be uploaded
154 * @param {LargeFileUploadSession} uploadSession - The upload session to which the upload has to be done
155 * @param {LargeFileUploadTaskOptions} options - The upload task options
156 * @returns An instance of LargeFileUploadTask
157 */
158 public constructor(client: Client, file: FileObject<T>, uploadSession: LargeFileUploadSession, options: LargeFileUploadTaskOptions = {}) {
159 this.client = client;
160
161 if (!file.sliceFile) {
162 throw new GraphClientError("Please pass the FileUpload object, StreamUpload object or any custom implementation of the FileObject interface");
163 } else {
164 this.file = file;
165 }
166 this.file = file;
167 if (!options.rangeSize) {
168 options.rangeSize = this.DEFAULT_FILE_SIZE;
169 }
170
171 this.options = options;
172 this.uploadSession = uploadSession;
173 this.nextRange = new Range(0, this.options.rangeSize - 1);
174 }
175
176 /**
177 * @private
178 * Parses given range string to the Range instance
179 * @param {string[]} ranges - The ranges value
180 * @returns The range instance
181 */
182 private parseRange(ranges: string[]): Range {
183 const rangeStr = ranges[0];
184 if (typeof rangeStr === "undefined" || rangeStr === "") {
185 return new Range();
186 }
187 const firstRange = rangeStr.split("-");
188 const minVal = parseInt(firstRange[0], 10);
189 let maxVal = parseInt(firstRange[1], 10);
190 if (Number.isNaN(maxVal)) {
191 maxVal = this.file.size - 1;
192 }
193 return new Range(minVal, maxVal);
194 }
195
196 /**
197 * @private
198 * Updates the expiration date and the next range
199 * @param {UploadStatusResponse} response - The response of the upload status
200 * @returns Nothing
201 */
202 private updateTaskStatus(response: UploadStatusResponse): void {
203 this.uploadSession.expiry = new Date(response.expirationDateTime);
204 this.nextRange = this.parseRange(response.nextExpectedRanges);
205 }
206
207 /**
208 * @public
209 * Gets next range that needs to be uploaded
210 * @returns The range instance
211 */
212 public getNextRange(): Range {
213 if (this.nextRange.minValue === -1) {
214 return this.nextRange;
215 }
216 const minVal = this.nextRange.minValue;
217 let maxValue = minVal + this.options.rangeSize - 1;
218 if (maxValue >= this.file.size) {
219 maxValue = this.file.size - 1;
220 }
221 return new Range(minVal, maxValue);
222 }
223
224 /**
225 * @deprecated This function has been moved into FileObject interface.
226 * @public
227 * Slices the file content to the given range
228 * @param {Range} range - The range value
229 * @returns The sliced ArrayBuffer or Blob
230 */
231 public sliceFile(range: Range): ArrayBuffer | Blob {
232 console.warn("The LargeFileUploadTask.sliceFile() function has been deprecated and moved into the FileObject interface.");
233 if (this.file.content instanceof ArrayBuffer || this.file.content instanceof Blob || this.file.content instanceof Uint8Array) {
234 return this.file.content.slice(range.minValue, range.maxValue + 1);
235 }
236 throw new GraphClientError("The LargeFileUploadTask.sliceFile() function expects only Blob, ArrayBuffer or Uint8Array file content. Please note that the sliceFile() function is deprecated.");
237 }
238
239 /**
240 * @public
241 * @async
242 * Uploads file to the server in a sequential order by slicing the file
243 * @returns The promise resolves to uploaded response
244 */
245 public async upload(): Promise<UploadResult> {
246 const uploadEventHandlers = this.options && this.options.uploadEventHandlers;
247 while (!this.uploadSession.isCancelled) {
248 const nextRange = this.getNextRange();
249 if (nextRange.maxValue === -1) {
250 const err = new Error("Task with which you are trying to upload is already completed, Please check for your uploaded file");
251 err.name = "Invalid Session";
252 throw err;
253 }
254 const fileSlice = await this.file.sliceFile(nextRange);
255 const rawResponse = await this.uploadSliceGetRawResponse(fileSlice, nextRange, this.file.size);
256 if (!rawResponse) {
257 throw new GraphClientError("Something went wrong! Large file upload slice response is null.");
258 }
259
260 const responseBody = await GraphResponseHandler.getResponse(rawResponse);
261 /**
262 * (rawResponse.status === 201) -> This condition is applicable for OneDrive, PrintDocument and Outlook APIs.
263 * (rawResponse.status === 200 && responseBody.id) -> This additional condition is applicable only for OneDrive API.
264 */
265 if (rawResponse.status === 201 || (rawResponse.status === 200 && responseBody.id)) {
266 this.reportProgress(uploadEventHandlers, nextRange);
267 return UploadResult.CreateUploadResult(responseBody, rawResponse.headers);
268 }
269
270 /* Handling the API issue where the case of Outlook upload response property -'nextExpectedRanges' is not uniform.
271 * https://github.com/microsoftgraph/msgraph-sdk-serviceissues/issues/39
272 */
273 const res: UploadStatusResponse = {
274 expirationDateTime: responseBody.expirationDateTime || responseBody.ExpirationDateTime,
275 nextExpectedRanges: responseBody.NextExpectedRanges || responseBody.nextExpectedRanges,
276 };
277 this.updateTaskStatus(res);
278 this.reportProgress(uploadEventHandlers, nextRange);
279 }
280 }
281
282 private reportProgress(uploadEventHandlers: UploadEventHandlers, nextRange: Range) {
283 if (uploadEventHandlers && uploadEventHandlers.progress) {
284 uploadEventHandlers.progress(nextRange, uploadEventHandlers.extraCallbackParam);
285 }
286 }
287
288 /**
289 * @public
290 * @async
291 * Uploads given slice to the server
292 * @param {ArrayBuffer | Blob | File} fileSlice - The file slice
293 * @param {Range} range - The range value
294 * @param {number} totalSize - The total size of a complete file
295 * @returns The response body of the upload slice result
296 */
297 public async uploadSlice(fileSlice: ArrayBuffer | Blob | File, range: Range, totalSize: number): Promise<unknown> {
298 return await this.client
299 .api(this.uploadSession.url)
300 .headers({
301 "Content-Length": `${range.maxValue - range.minValue + 1}`,
302 "Content-Range": `bytes ${range.minValue}-${range.maxValue}/${totalSize}`,
303 "Content-Type": "application/octet-stream",
304 })
305 .put(fileSlice);
306 }
307
308 /**
309 * @public
310 * @async
311 * Uploads given slice to the server
312 * @param {unknown} fileSlice - The file slice
313 * @param {Range} range - The range value
314 * @param {number} totalSize - The total size of a complete file
315 * @returns The raw response of the upload slice result
316 */
317 public async uploadSliceGetRawResponse(fileSlice: unknown, range: Range, totalSize: number): Promise<Response> {
318 return await this.client
319 .api(this.uploadSession.url)
320 .headers({
321 "Content-Length": `${range.maxValue - range.minValue + 1}`,
322 "Content-Range": `bytes ${range.minValue}-${range.maxValue}/${totalSize}`,
323 "Content-Type": "application/octet-stream",
324 })
325 .responseType(ResponseType.RAW)
326 .put(fileSlice);
327 }
328
329 /**
330 * @public
331 * @async
332 * Deletes upload session in the server
333 * @returns The promise resolves to cancelled response
334 */
335 public async cancel(): Promise<unknown> {
336 const cancelResponse = await this.client
337 .api(this.uploadSession.url)
338 .responseType(ResponseType.RAW)
339 .delete();
340 if (cancelResponse.status === 204) {
341 this.uploadSession.isCancelled = true;
342 }
343 return cancelResponse;
344 }
345
346 /**
347 * @public
348 * @async
349 * Gets status for the upload session
350 * @returns The promise resolves to the status enquiry response
351 */
352 public async getStatus(): Promise<unknown> {
353 const response = await this.client.api(this.uploadSession.url).get();
354 this.updateTaskStatus(response);
355 return response;
356 }
357
358 /**
359 * @public
360 * @async
361 * Resumes upload session and continue uploading the file from the last sent range
362 * @returns The promise resolves to the uploaded response
363 */
364 public async resume(): Promise<unknown> {
365 await this.getStatus();
366 return await this.upload();
367 }
368
369 /**
370 * @public
371 * @async
372 * Get the upload session information
373 * @returns The large file upload session
374 */
375 public getUploadSession(): LargeFileUploadSession {
376 return this.uploadSession;
377 }
378}