-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
295 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface IProgress { | ||
report(progress: number): void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import { Parsable, RequestAdapter } from "@microsoft/kiota-abstractions"; | ||
import { UploadSession } from "./UploadSession"; | ||
import { UploadSliceRequestBuilder } from "./UploadSliceRequestBuilder"; | ||
import { UploadResult } from "./UploadResult"; | ||
import { IProgress } from "./IProgress"; | ||
|
||
export interface ILargeFileUploadTask<T extends Parsable> { | ||
Upload(progressEventHandler: IProgress): Promise<UploadResult<T>>; | ||
|
||
Resume(progressEventHandler: IProgress): Promise<UploadResult<T>>; | ||
|
||
RefreshUploadStatus(): Promise<void>; | ||
|
||
UpdateSession(): Promise<UploadSession>; | ||
|
||
DeleteSession(): Promise<UploadSession>; | ||
|
||
Cancel(): Promise<void>; | ||
} | ||
|
||
const DefaultSliceSize = 320 * 1024; | ||
|
||
export class LargeFileUploadTask<T extends Parsable> implements ILargeFileUploadTask<T> { | ||
rangesRemaining: number[][] = []; | ||
Session: UploadSession; | ||
|
||
constructor( | ||
readonly uploadSession: Parsable, | ||
readonly uploadStream: ReadableStream<Uint8Array>, | ||
readonly maxSliceSize = -1, | ||
readonly requestAdapter: RequestAdapter, | ||
) { | ||
if (!uploadStream?.locked) { | ||
throw new Error("Please provide stream value"); | ||
} | ||
if (requestAdapter === undefined) { | ||
throw new Error("Request adapter is a required parameter"); | ||
} | ||
if (maxSliceSize <= 0) { | ||
this.maxSliceSize = DefaultSliceSize; | ||
} | ||
|
||
this.Session = this.extractSessionInfo(uploadSession); | ||
this.rangesRemaining = this.GetRangesRemaining(this.Session); | ||
} | ||
|
||
public async Upload(progress?: IProgress, maxTries = 3): Promise<UploadResult<T>> { | ||
let uploadTries = 0; | ||
while (uploadTries < maxTries) { | ||
const sliceRequests = this.GetUploadSliceRequests(); | ||
for (const request of sliceRequests) { | ||
const uploadResult = await request.UploadSlice(this.uploadStream); | ||
progress?.report(request.rangeEnd); | ||
if (uploadResult?.UploadSucceeded()) { | ||
return uploadResult; | ||
} | ||
} | ||
|
||
await this.UpdateSession(); | ||
uploadTries++; | ||
|
||
if (uploadTries < maxTries) { | ||
// Exponential backoff | ||
await this.sleep(2000 * (uploadTries + 1)); | ||
} | ||
} | ||
|
||
throw new Error("Max retries reached"); | ||
} | ||
|
||
public Resume(_?: IProgress): Promise<UploadResult<T>> { | ||
throw new Error("Method not implemented."); | ||
} | ||
|
||
public RefreshUploadStatus(): Promise<void> { | ||
throw new Error("Method not implemented."); | ||
} | ||
|
||
public UpdateSession(): Promise<UploadSession> { | ||
throw new Error("Method not implemented."); | ||
} | ||
|
||
public DeleteSession(): Promise<UploadSession> { | ||
throw new Error("Method not implemented."); | ||
} | ||
|
||
public Cancel(): Promise<void> { | ||
throw new Error("Method not implemented."); | ||
} | ||
|
||
private extractSessionInfo(parsable: Parsable): UploadSession { | ||
const uploadSession: UploadSession = { | ||
expirationDateTime: null, | ||
nextExpectedRanges: null, | ||
odataType: null, | ||
uploadUrl: null, | ||
}; | ||
|
||
if ("expirationDateTime" in parsable) uploadSession.expirationDateTime = parsable.expirationDateTime as Date | null; | ||
if ("nextExpectedRanges" in parsable) | ||
uploadSession.nextExpectedRanges = parsable.nextExpectedRanges as string[] | null; | ||
if ("odataType" in parsable) uploadSession.odataType = parsable.odataType as string | null; | ||
if ("uploadUrl" in parsable) uploadSession.uploadUrl = parsable.uploadUrl as string | null; | ||
|
||
return uploadSession; | ||
} | ||
|
||
private sleep(ms: number): Promise<void> { | ||
return new Promise(resolve => setTimeout(resolve, ms)); | ||
} | ||
|
||
private GetUploadSliceRequests(): UploadSliceRequestBuilder<T>[] { | ||
const uploadSlices: UploadSliceRequestBuilder<T>[] = []; | ||
const rangesRemaining = this.rangesRemaining; | ||
const session = this.Session; | ||
rangesRemaining.forEach(range => { | ||
let currentRangeBegin = range[0]; | ||
while (currentRangeBegin <= range[1]) { | ||
const nextSliceSize = this.nextSliceSize(currentRangeBegin, range[1]); | ||
const uploadRequest = new UploadSliceRequestBuilder<T>( | ||
this.requestAdapter, | ||
session.uploadUrl!, | ||
currentRangeBegin, | ||
currentRangeBegin + nextSliceSize - 1, | ||
range[1] + 1, | ||
); | ||
uploadSlices.push(uploadRequest); | ||
currentRangeBegin += nextSliceSize; | ||
} | ||
}); | ||
return uploadSlices; | ||
} | ||
|
||
private nextSliceSize(currentRangeBegin: number, currentRangeEnd: number): number { | ||
const sizeBasedOnRange = currentRangeEnd - currentRangeBegin + 1; | ||
return sizeBasedOnRange > this.maxSliceSize ? this.maxSliceSize : sizeBasedOnRange; | ||
} | ||
|
||
private GetRangesRemaining(uploadSession: UploadSession): number[][] { | ||
// nextExpectedRanges: https://dev.onedrive.com/items/upload_large_files.htm | ||
// Sample: ["12345-55232","77829-99375"] | ||
// Also, second number in range can be blank, which means 'until the end' | ||
const ranges: number[][] = []; | ||
uploadSession.nextExpectedRanges?.forEach(rangeString => { | ||
const rangeArray = rangeString.split("-"); | ||
ranges.push([parseInt(rangeArray[0], 10), parseInt(rangeArray[1], 10)]); | ||
}); | ||
return ranges; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { ErrorMappings, ResponseHandler } from "@microsoft/kiota-abstractions"; | ||
|
||
export class UploadResponseHandler implements ResponseHandler { | ||
handleResponse<NativeResponseType, ModelType>( | ||
response: NativeResponseType, | ||
_: ErrorMappings | undefined, | ||
): Promise<ModelType | undefined> { | ||
if (response instanceof Response) { | ||
if (response.ok) { | ||
if (response.body != null) { | ||
const body = response.body as unknown as ModelType; | ||
return Promise.resolve(body); | ||
} else { | ||
return Promise.resolve(undefined); | ||
} | ||
} | ||
} | ||
return Promise.resolve(undefined); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { ResponseHandler, ResponseHandlerOption } from "@microsoft/kiota-abstractions"; | ||
import { UploadResponseHandler } from "./UploadResponseHandler"; | ||
|
||
export class UploadResponseHandlerOption extends ResponseHandlerOption { | ||
public responseHandler?: ResponseHandler = new UploadResponseHandler(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { Parsable, ParseNode } from "@microsoft/kiota-abstractions"; | ||
import { UploadSession } from "./UploadSession"; | ||
import { ILargeFileUploadTask } from "./LargeFileUploadTask"; | ||
|
||
export class UploadResult<T extends Parsable> { | ||
uploadSession?: UploadSession; | ||
uploadTask?: ILargeFileUploadTask<T>; | ||
itemResponse?: T; | ||
location?: string; | ||
|
||
UploadSucceeded() { | ||
return this.itemResponse !== undefined || this.location !== undefined; | ||
} | ||
} | ||
|
||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions | ||
export function createUploadResult( | ||
_: ParseNode | undefined, | ||
): (instance?: Parsable) => Record<string, (node: ParseNode) => void> { | ||
return deserializeIntoUploadResult; | ||
} | ||
|
||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions | ||
export function deserializeIntoUploadResult<T extends Parsable>( | ||
uploadResult: Partial<UploadResult<T>> | undefined = {}, | ||
): Record<string, (node: ParseNode) => void> { | ||
return { | ||
uploadSession: _ => { | ||
uploadResult.uploadSession = undefined; | ||
}, | ||
uploadTask: _ => { | ||
uploadResult.uploadSession = undefined; | ||
}, | ||
itemResponse: _ => { | ||
uploadResult.uploadSession = undefined; | ||
}, | ||
location: _ => { | ||
uploadResult.uploadSession = undefined; | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export interface UploadSession { | ||
expirationDateTime?: Date | null; | ||
nextExpectedRanges?: string[] | null; | ||
odataType?: string | null; | ||
uploadUrl?: string | null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Parsable, RequestAdapter, RequestInformation } from "@microsoft/kiota-abstractions"; | ||
import { createUploadResult, UploadResult } from "./UploadResult"; | ||
import { Headers } from "@microsoft/kiota-abstractions/dist/es/src/headers"; | ||
import { HttpMethod } from "@microsoft/kiota-abstractions/dist/es/src/httpMethod"; | ||
import { HeadersInspectionOptions } from "@microsoft/kiota-http-fetchlibrary"; | ||
import { UploadResponseHandlerOption } from "./UploadResponseHandlerOption"; | ||
|
||
const binaryContentType = "application/octet-stream"; | ||
|
||
export class UploadSliceRequestBuilder<T extends Parsable> { | ||
constructor( | ||
readonly requestAdapter: RequestAdapter, | ||
readonly sessionUrl: string, | ||
readonly rangeBegin: number, | ||
readonly rangeEnd: number, | ||
readonly totalSessionLength: number, | ||
) {} | ||
|
||
public async UploadSlice(stream: ReadableStream<Uint8Array>): Promise<UploadResult<T> | undefined> { | ||
const data = await this.readSection(stream, this.rangeBegin, this.rangeEnd); | ||
const requestInformation = this.createPutRequestInformation(data); | ||
|
||
const responseHandler = new UploadResponseHandlerOption(); | ||
|
||
const headerOptions = new HeadersInspectionOptions({ inspectResponseHeaders: true }); | ||
requestInformation.addRequestOptions([headerOptions, responseHandler]); | ||
|
||
return this.requestAdapter.send<UploadResult<T>>(requestInformation, createUploadResult, undefined); | ||
} | ||
|
||
private async readSection(stream: ReadableStream<Uint8Array>, start: number, end: number): Promise<ArrayBuffer> { | ||
const reader = stream.getReader(); | ||
let bytesRead = 0; | ||
const chunks: Uint8Array[] = []; | ||
|
||
while (bytesRead < end - start + 1) { | ||
const { done, value } = await reader.read(); | ||
if (done) break; | ||
chunks.push(value); | ||
bytesRead += value.length; | ||
} | ||
|
||
const result = new Uint8Array(bytesRead); | ||
let offset = 0; | ||
for (const chunk of chunks) { | ||
result.set(chunk, offset); | ||
offset += chunk.length; | ||
} | ||
|
||
return result.buffer; | ||
} | ||
|
||
private createPutRequestInformation(content: ArrayBuffer): RequestInformation { | ||
const header = new Headers(); | ||
header.set("Content-Range", new Set([`bytes ${this.rangeBegin}-${this.rangeEnd - 1}/${this.totalSessionLength}`])); | ||
header.set("Content-Length", new Set([`${this.rangeEnd - this.rangeBegin}`])); | ||
|
||
const request = new RequestInformation(); | ||
request.headers = header; | ||
request.urlTemplate = this.sessionUrl; | ||
request.httpMethod = HttpMethod.PUT; | ||
request.setStreamContent(content, binaryContentType); | ||
return request; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./LargeFileUploadTask"; |
Empty file.