UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

1 lines 36.1 kB
{"version":3,"sources":["../../../packages/core/data/file-transfer.ts"],"names":[],"mappings":"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","file":"file-transfer.d.ts","sourcesContent":["import { from, lastValueFrom, Observable, of, Subject, throwError } from 'rxjs';\r\nimport { AjaxError, AjaxRequest } from 'rxjs/ajax';\r\nimport { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators';\r\nimport { CancellationToken } from '../async/cancellation';\r\nimport { Progress } from '../async/progress';\r\nimport { ErrorMonitor } from '../diagnostics/error-monitor';\r\nimport { Strings } from '../generated/strings';\r\nimport { EnvironmentModule } from '../manifest/environment-modules';\r\nimport { AuthorizationManager } from '../security/authorization-manager';\r\nimport { Cookie } from './cookie';\r\nimport { GatewayConnection } from './gateway-connection';\r\nimport { ApiVersion } from './gateway-url-builder';\r\nimport { headerConstants, HttpResponseTypes, HttpStatusCode } from './http-constants';\r\nimport { NativeQ } from './native-q';\r\nimport { NodeConnection, NodeRequest } from './node-connection';\r\nimport { UriBuilder } from './uri-builder';\r\n\r\nexport interface FileOptions {\r\n /**\r\n * Indicates that audit logging for this request should be made. Default is false.\r\n */\r\n logAudit?: boolean;\r\n\r\n /**\r\n * Indicates that telemetry logging for this request should be made. Default is false.\r\n */\r\n logTelemetry?: boolean;\r\n}\r\n\r\n/** Represents a object that transfers files */\r\nexport class FileTransfer {\r\n\r\n private moduleName = null;\r\n\r\n /**\r\n * Downloads a blob of data\r\n *\r\n * @param blob the blob of data to download\r\n * @param fileName the name of the file for the user to download.\r\n */\r\n public static downloadBlob(blob: Blob, fileName: string) {\r\n let useAnchorTagForDownload = true;\r\n const windowNagivator: any = window.navigator;\r\n\r\n if (windowNagivator.msSaveOrOpenBlob) {\r\n // This is for IE and Microsoft Edge < 16\r\n // for those cases the download anchor tag doesn't generate the right name so we use the MS download system instead\r\n // \"5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393\"\r\n const ua = navigator.userAgent;\r\n const edgeIndex = ua.indexOf('Edge');\r\n if (edgeIndex > 0) {\r\n const dotIndex = ua.indexOf('.', edgeIndex);\r\n let versionNumber = 0;\r\n if (dotIndex > 0) {\r\n const versionString = ua.substring(edgeIndex + 'Edge'.length + 1, dotIndex);\r\n versionNumber = Number(versionString);\r\n }\r\n\r\n useAnchorTagForDownload = versionNumber > 15;\r\n } else {\r\n useAnchorTagForDownload = false;\r\n }\r\n }\r\n\r\n if (useAnchorTagForDownload) {\r\n const downloadLink = document.createElement('a');\r\n downloadLink.style.display = 'none';\r\n\r\n const url = URL.createObjectURL(blob);\r\n downloadLink.setAttribute('href', url);\r\n downloadLink.setAttribute('download', fileName);\r\n downloadLink.click();\r\n downloadLink.remove();\r\n } else {\r\n windowNagivator.msSaveOrOpenBlob(blob, fileName);\r\n }\r\n }\r\n\r\n /**\r\n * Navigates to a file.\r\n *\r\n * @param filePath the file path we are navigating to.\r\n */\r\n public static navigateToFile(filePath: string): void {\r\n const downloadLink = document.createElement('a');\r\n downloadLink.style.display = 'none';\r\n downloadLink.setAttribute('href', filePath);\r\n downloadLink.click();\r\n downloadLink.remove();\r\n }\r\n\r\n /**\r\n * Initializes a new instance of the FileTransfer class.\r\n *\r\n * @param nodeConnection the NodeConnection class instance.\r\n * @param gatewayConnection the GatewayConnection class instance.\r\n * @param authorizationManager the AuthorizationManager class instance.\r\n */\r\n constructor(\r\n private nodeConnection: NodeConnection,\r\n private gatewayConnection: GatewayConnection,\r\n private authorizationManager: AuthorizationManager) {\r\n }\r\n\r\n private static generateRanges(ranges: ContentRange[], segmentSize: number, totalSize: number): void {\r\n const offset = segmentSize - 1;\r\n const max = totalSize - 1;\r\n\r\n for (let first = 0, last = offset; first < max; first += offset + 1, last = Math.min(++last + offset, max)) {\r\n ranges.push(new ContentRange(first, last, totalSize));\r\n }\r\n }\r\n\r\n private static extractNode(input: string) {\r\n const url = new URL(input);\r\n const segments = url.pathname.split('/');\r\n return segments[3];\r\n }\r\n\r\n /**\r\n * The GET call to file transfer endpoint and return a Blob of the requested file\r\n *\r\n * @param nodeName the node to transfer the file from.\r\n * @param sourcePath the path of the remote file to transfer.\r\n * @param fileOptions the file options for the action.\r\n * @return Observable<Blob> the observable Blob object.\r\n */\r\n public transferBlob(\r\n nodeName: string,\r\n sourcePath: string,\r\n fileOptions?: FileOptions): Observable<Blob> {\r\n const relativeUrl = this.gatewayConnection.url().node(nodeName).makeRelative().fileTransfer().file(sourcePath).build();\r\n const headers = {\r\n Accept: 'application/octet-stream'\r\n };\r\n const token = Cookie.getCrossSiteRequestForgeryToken();\r\n\r\n if (token) {\r\n headers[headerConstants.CROSS_SITE_REQUEST_FORGERY_TOKEN] = token;\r\n }\r\n\r\n const request = <NodeRequest><unknown>{ headers: headers, responseType: 'blob' };\r\n\r\n if (fileOptions) {\r\n request.logAudit = fileOptions.logAudit;\r\n request.logTelemetry = fileOptions.logTelemetry;\r\n }\r\n\r\n return this.nodeConnection.get(nodeName, relativeUrl, request);\r\n }\r\n\r\n /**\r\n * The GET call to file transfer endpoint and manual download of stream\r\n *\r\n * @param nodeName the node to transfer the file from.\r\n * @param sourcePath the path of the remote file to transfer.\r\n * @param targetName the desired name for the downloaded file.\r\n * @param fileOptions the file options for the action.\r\n * @return Observable<Blob> the observable Blob object.\r\n */\r\n public transferFile(\r\n nodeName: string,\r\n sourcePath: string,\r\n targetName: string,\r\n fileOptions?: FileOptions\r\n ): Observable<Blob> {\r\n\r\n return this.transferBlob(nodeName, sourcePath, fileOptions)\r\n .pipe(\r\n catchError((error) => {\r\n if (error.response && error.response.type === HttpResponseTypes.Json) {\r\n return from(error.response.text()).pipe(\r\n mergeMap((errorJson: string) => throwError(JSON.parse(errorJson)?.error))\r\n );\r\n }\r\n\r\n return throwError(error);\r\n }),\r\n map((responseBlob: Blob) => {\r\n FileTransfer.downloadBlob(responseBlob, targetName);\r\n return responseBlob;\r\n })\r\n );\r\n }\r\n\r\n /**\r\n * Upload a file from fileObject.\r\n *\r\n * @deprecated Use upload instead.\r\n * @param nodeName the node to upload the file to.\r\n * @param path the file path to store on the target node.\r\n * @param fileObject the file object created on the UI.\r\n * @param fileOptions the file options for the action.\r\n * @return Observable<any> the observable object.\r\n */\r\n public uploadFile(nodeName: string, path: string, fileObject: File, fileOptions?: FileOptions): Observable<any> {\r\n const deferred = NativeQ.defer<any>();\r\n const formData = new FormData();\r\n formData.append('file-0', fileObject);\r\n const request = new XMLHttpRequest();\r\n const url = this.gatewayConnection.url().node(nodeName).fileTransfer().file(path).build();\r\n const handler = () => {\r\n if (request.readyState === 4 /* complete */) {\r\n if (request.status === HttpStatusCode.Ok || request.status === HttpStatusCode.Created) {\r\n deferred.resolve(request.responseText);\r\n } else {\r\n ErrorMonitor.current.reportErrorFromAjax(\r\n <AjaxError>{\r\n status: request.status,\r\n xhr: request,\r\n request: { url, method: 'POST' }\r\n });\r\n\r\n const uploadError = MsftSme.getStrings<Strings>().MsftSmeShell.Core.DirectoryList.Upload.Error;\r\n const message = request.status === HttpStatusCode.CorsRequestFailed ? uploadError.FileNotFound\r\n : (request.status === HttpStatusCode.BadRequest ? uploadError.OperationBlocked\r\n : uploadError.Unknown.format(request.status));\r\n deferred.reject({ xhr: request, message: message });\r\n }\r\n }\r\n };\r\n\r\n let tokenValue: string;\r\n const ajaxRequest = <AjaxRequest>{ headers: {} };\r\n this.authorizationManager.addAuthorizationRequestHeader(ajaxRequest, nodeName);\r\n request.open('PUT', url);\r\n request.withCredentials = true;\r\n\r\n tokenValue = ajaxRequest.headers[headerConstants.SME_AUTHORIZATION];\r\n if (tokenValue) {\r\n request.setRequestHeader(headerConstants.SME_AUTHORIZATION, tokenValue);\r\n }\r\n\r\n tokenValue = ajaxRequest.headers[headerConstants.USE_LAPS];\r\n if (tokenValue) {\r\n request.setRequestHeader(headerConstants.USE_LAPS, tokenValue);\r\n\r\n // If ajaxRequest.headers[LAPS_LOCALADMINNAME] will always have default of 'administrator',\r\n // so no need to check if it exists and not null\r\n request.setRequestHeader(headerConstants.LAPS_LOCALADMINNAME, ajaxRequest.headers[headerConstants.LAPS_LOCALADMINNAME]);\r\n }\r\n\r\n if (fileOptions) {\r\n if (fileOptions.logAudit === true || fileOptions.logAudit === false) {\r\n request.setRequestHeader(headerConstants.LOG_AUDIT, fileOptions.logAudit ? 'true' : 'false');\r\n }\r\n\r\n if (fileOptions.logTelemetry === true || fileOptions.logTelemetry === false) {\r\n request.setRequestHeader(headerConstants.LOG_TELEMETRY, fileOptions.logTelemetry ? 'true' : 'false');\r\n }\r\n }\r\n\r\n const token = Cookie.getCrossSiteRequestForgeryToken();\r\n if (token) {\r\n request.setRequestHeader(headerConstants.CROSS_SITE_REQUEST_FORGERY_TOKEN, token);\r\n }\r\n\r\n request.setRequestHeader(headerConstants.MODULE_NAME, this.nameOfModule);\r\n request.onreadystatechange = handler;\r\n request.send(formData);\r\n return from(deferred.promise);\r\n }\r\n\r\n /**\r\n * Uploads the specified file.\r\n * @param node The target computer to upload the file to.\r\n * @param path The path on the target computer to upload the file to.\r\n * @param file The file to upload.\r\n * @param options The file upload options.\r\n * @param cancellationToken The token that can be used to cancel the operation.\r\n * @returns An observable object that contains the result of the file transfer.\r\n */\r\n public upload(\r\n node: string,\r\n path: string,\r\n file: File,\r\n options: FileUploadOptions = new FileUploadOptions(),\r\n cancellationToken: CancellationToken = CancellationToken.NONE\r\n ): Observable<FileTransferResult> {\r\n return from(this.uploadAsync(node, path, file, options, cancellationToken));\r\n }\r\n\r\n private uploadAsync(\r\n node: string,\r\n path: string,\r\n file: File,\r\n options: FileUploadOptions,\r\n cancellationToken: CancellationToken): Promise<FileTransferResult> {\r\n\r\n // if the file size <= transfer size, then just transfer the file all at once,\r\n // which will be much faster than the multi - request flow for a single transfer\r\n if (file.size <= options.transferSize) {\r\n return this.uploadAllAtOnce(node, path, file, options, cancellationToken);\r\n }\r\n\r\n return this.uploadInParts(node, path, file, options, cancellationToken);\r\n }\r\n\r\n private async uploadAllAtOnce(\r\n node: string,\r\n path: string,\r\n file: File,\r\n options: FileUploadOptions,\r\n cancellationToken: CancellationToken): Promise<FileTransferResult> {\r\n\r\n const url = this.gatewayConnection.url().node(node).fileTransfer().file(path).build();\r\n const headers = this.newRequestHeaders(node, options);\r\n const transfer = new FileTransferInfo(file.size, 1);\r\n const result = new FileTransferResult(new FileTransferContinuation(null, null, file, transfer));\r\n\r\n do {\r\n const response = await fetch(url, { method: 'PUT', credentials: 'include', headers: headers, body: file });\r\n switch (response.status) {\r\n case HttpStatusCode.Ok:\r\n case HttpStatusCode.Created:\r\n case HttpStatusCode.NoContent:\r\n transfer.incrementCompleted();\r\n return result;\r\n case HttpStatusCode.Forbidden:\r\n case HttpStatusCode.Conflict:\r\n const blockedMessage = MsftSme.getStrings<Strings>().MsftSmeShell.Core.DirectoryList.Upload.Error.OperationBlocked;\r\n throw new Error(blockedMessage);\r\n default:\r\n if (cancellationToken.isCancellationRequested) {\r\n break;\r\n }\r\n const unknownMessage = MsftSme.getStrings<Strings>().MsftSmeShell.Core.DirectoryList.Upload.Error.Unknown;\r\n throw new Error(unknownMessage.format(response.status, response.statusText));\r\n }\r\n }\r\n while (!cancellationToken.isCancellationRequested);\r\n\r\n cancellationToken.throwIfCancellationRequested();\r\n }\r\n\r\n private async uploadInParts(\r\n node: string,\r\n path: string,\r\n file: File,\r\n options: FileUploadOptions,\r\n cancellationToken: CancellationToken = CancellationToken.NONE): Promise<FileTransferResult> {\r\n\r\n const continuation = await this.newUpload(node, path, file, options, cancellationToken);\r\n let canceled = cancellationToken.isCancellationRequested;\r\n let error: Error = null;\r\n\r\n if (!canceled) {\r\n error = await lastValueFrom(this.uploadRanges(continuation, options, cancellationToken));\r\n canceled = error !== null || cancellationToken.isCancellationRequested;\r\n }\r\n\r\n if (canceled && options.cancellationBehavior === FileTransferCancellationBehavior.Cancel) {\r\n try {\r\n // cancel the file transfer if an error occurred or the caller requested cancellation\r\n await this.cancelUpload(continuation);\r\n } catch (e) {\r\n // unable to cancel (e.g. clean up) the file transfer\r\n // if cancellation occurred from an error, rethrow it now\r\n if (error !== null) {\r\n throw error;\r\n }\r\n\r\n throw e;\r\n }\r\n\r\n return new FileTransferResult();\r\n }\r\n\r\n return new FileTransferResult(continuation);\r\n }\r\n\r\n /**\r\n * Resumes a previously started file upload.\r\n * @param continuation The information required to continue the file transfer.\r\n * @param progress The object used to report the progress of the upload.\r\n * @param cancellationToken The token that can be used to cancel the operation.\r\n */\r\n public resumeUpload(\r\n continuation: FileTransferContinuation,\r\n progress: Progress<number> = new NoProgress(),\r\n cancellationToken: CancellationToken = CancellationToken.NONE): Observable<void> {\r\n\r\n return from(this.resumeUploadAsync(continuation, progress, cancellationToken));\r\n }\r\n\r\n private async resumeUploadAsync(\r\n continuation: FileTransferContinuation,\r\n progress: Progress<number> = new NoProgress(),\r\n cancellationToken: CancellationToken = CancellationToken.NONE): Promise<void> {\r\n\r\n const transferSize = continuation.transfer.size;\r\n const options = new FileUploadOptions(transferSize, FileTransferCancellationBehavior.Pause, progress);\r\n let error: Error = null;\r\n error = await lastValueFrom(this.uploadRanges(continuation, options, cancellationToken));\r\n\r\n if (error !== null) {\r\n throw error;\r\n }\r\n\r\n return null;\r\n }\r\n\r\n /**\r\n * Cancels a previously started file upload.\r\n * @param continuation The information required to cancel the file transfer.\r\n */\r\n public cancelUpload(continuation: FileTransferContinuation): Observable<void> {\r\n return from(this.cancelUploadAsync(continuation));\r\n }\r\n\r\n private async cancelUploadAsync(continuation: FileTransferContinuation): Promise<void> {\r\n const url = continuation.location;\r\n const node = FileTransfer.extractNode(url);\r\n const headers = this.newRequestHeaders(node, new FileUploadOptions());\r\n let attempts = 0;\r\n\r\n headers.append(headerConstants.IF_MATCH, continuation.token);\r\n\r\n do {\r\n const response = await fetch(url, { method: 'DELETE', credentials: 'include', headers: headers });\r\n const statusCode = response.status;\r\n\r\n // canceled or a new transfer was started\r\n if (statusCode === HttpStatusCode.NoContent || statusCode === HttpStatusCode.PreconditionFailed) {\r\n break;\r\n }\r\n switch (statusCode) {\r\n case HttpStatusCode.Conflict:\r\n break;\r\n case HttpStatusCode.BadGateway:\r\n case HttpStatusCode.ServiceUnavailable:\r\n if (++attempts > 3) {\r\n const giveUpMessage = MsftSme.getStrings<Strings>().MsftSmeShell.Core.DirectoryList.Upload.Error.Unknown;\r\n throw new Error(giveUpMessage.format(statusCode, response.statusText));\r\n }\r\n break;\r\n default:\r\n const unknownMessage = MsftSme.getStrings<Strings>().MsftSmeShell.Core.DirectoryList.Upload.Error.Unknown;\r\n throw new Error(unknownMessage.format(statusCode, response.statusText));\r\n }\r\n\r\n } while (true);\r\n\r\n return null;\r\n }\r\n\r\n private newRequestHeaders(node: string, options: FileUploadOptions): Headers {\r\n const request = this.gatewayConnection.defaultHttpSecureOptions;\r\n\r\n this.authorizationManager.addAuthorizationRequestHeader(request, node);\r\n\r\n const headers = new Headers();\r\n const authAadToken = request.headers[headerConstants.SME_AAD_AUTHORIZATION];\r\n const authToken = request.headers[headerConstants.SME_AUTHORIZATION];\r\n const useLaps = request.headers[headerConstants.USE_LAPS];\r\n const xsfrToken = Cookie.getCrossSiteRequestForgeryToken();\r\n const { logAudit, logTelemetry } = options.logging;\r\n\r\n headers.append(headerConstants.MODULE_NAME, this.nameOfModule);\r\n\r\n if (authAadToken) {\r\n headers.append(headerConstants.SME_AAD_AUTHORIZATION, authAadToken);\r\n }\r\n\r\n if (authToken) {\r\n headers.append(headerConstants.SME_AUTHORIZATION, authToken);\r\n }\r\n\r\n if (useLaps) {\r\n headers.append(headerConstants.USE_LAPS, useLaps);\r\n headers.append(headerConstants.LAPS_LOCALADMINNAME, request.headers[headerConstants.LAPS_LOCALADMINNAME]);\r\n }\r\n\r\n if (xsfrToken) {\r\n headers.append(headerConstants.CROSS_SITE_REQUEST_FORGERY_TOKEN, xsfrToken);\r\n }\r\n\r\n if (logAudit !== undefined) {\r\n headers.append(headerConstants.LOG_AUDIT, logAudit.toString());\r\n }\r\n\r\n if (logTelemetry !== undefined) {\r\n headers.append(headerConstants.LOG_TELEMETRY, logTelemetry.toString());\r\n }\r\n\r\n return headers;\r\n }\r\n\r\n private async newUpload(\r\n node: string,\r\n path: string,\r\n file: File,\r\n options: FileUploadOptions,\r\n cancellationToken: CancellationToken = CancellationToken.NONE): Promise<FileTransferContinuation> {\r\n\r\n const url = this.gatewayConnection.url().node(node).fileTransfer().file(path).build();\r\n const lastModified = new Date(file.lastModified).toISOString();\r\n const headers = this.newRequestHeaders(node, options);\r\n let response: Response;\r\n let retry = false;\r\n\r\n headers.append(headerConstants.CONTENT_DISPOSITION, `create; size=${file.size}; modification-date=\"${lastModified}\"`);\r\n\r\n do {\r\n response = await fetch(url, { method: 'POST', credentials: 'include', headers: headers });\r\n retry = response.status === HttpStatusCode.Conflict;\r\n } while (retry && !cancellationToken.isCancellationRequested);\r\n\r\n switch (response.status) {\r\n case HttpStatusCode.Created:\r\n const builder = new UriBuilder(response.headers.get(headerConstants.LOCATION));\r\n\r\n // it's the client's job to tell the server which api version they want to use;\r\n // the location header will not contain the query parameter\r\n builder.setQueryParameter('api-version', ApiVersion.Latest);\r\n\r\n const location = builder.toString();\r\n const etag = response.headers.get(headerConstants.ETAG);\r\n const ranges = new Array<ContentRange>();\r\n\r\n FileTransfer.generateRanges(ranges, options.transferSize, file.size);\r\n\r\n const transfer = new FileTransferInfo(options.transferSize, ranges.length);\r\n const continuation = new FileTransferContinuation(location, etag, file, transfer, ranges);\r\n\r\n return continuation;\r\n case HttpStatusCode.Forbidden:\r\n case HttpStatusCode.Conflict:\r\n const blockedMessage = MsftSme.getStrings<Strings>().MsftSmeShell.Core.DirectoryList.Upload.Error.OperationBlocked;\r\n throw new Error(blockedMessage);\r\n default:\r\n const unknownMessage = MsftSme.getStrings<Strings>().MsftSmeShell.Core.DirectoryList.Upload.Error.Unknown;\r\n throw new Error(unknownMessage.format(response.status, response.statusText));\r\n }\r\n }\r\n\r\n private uploadRanges(\r\n continuation: FileTransferContinuation,\r\n options: FileUploadOptions,\r\n cancellationToken: CancellationToken): Observable<Error> {\r\n\r\n const node = FileTransfer.extractNode(continuation.location);\r\n const progress = options.progress;\r\n const ranges = [...continuation.ranges];\r\n const transfer = continuation.transfer;\r\n\r\n const cancellationObservable = new Subject<void>();\r\n\r\n if (cancellationToken.isCancellationRequested) {\r\n // Emit to trigger the cancellation\r\n cancellationObservable.next();\r\n cancellationObservable.complete();\r\n }\r\n // Creating an observable that emits the index and the range\r\n const uploadStream = from(ranges).pipe(\r\n // Limit concurrency to 3 with mergeMap\r\n mergeMap((range, index) => {\r\n return this.uploadPartialFileHelper(index, node, continuation, range, options).pipe(\r\n tap(result => {\r\n switch (result.statusCode) {\r\n // No Content (success)\r\n case 204:\r\n transfer.incrementCompleted();\r\n progress.report(transfer.percentComplete);\r\n break;\r\n case HttpStatusCode.ServiceUnavailable:\r\n if (!cancellationToken.isCancellationRequested) {\r\n // retry upload\r\n return this.uploadPartialFileHelper(index, node, continuation, range, options);\r\n }\r\n break;\r\n case HttpStatusCode.Unauthorized:\r\n case HttpStatusCode.Conflict:\r\n case HttpStatusCode.BadGateway:\r\n case HttpStatusCode.Gone:\r\n case HttpStatusCode.PreconditionFailed:\r\n default:\r\n const unknownMessage = MsftSme.getStrings<Strings>().MsftSmeShell.Core.DirectoryList.Upload.Error.UnknownNoMessage;\r\n throw new Error(unknownMessage.format(result.statusCode));\r\n }\r\n })\r\n );\r\n // 3 parallel uploads\r\n }, 3)\r\n );\r\n\r\n return uploadStream.pipe(\r\n takeUntil(cancellationObservable),\r\n catchError(err => {\r\n // If an error occurs during the upload process, emit it\r\n return of(err);\r\n }));\r\n }\r\n\r\n private uploadPartialFileHelper(index: number,\r\n node: string,\r\n continuation: FileTransferContinuation,\r\n range: ContentRange,\r\n options: FileUploadOptions): Observable<HttpTransferResult> {\r\n return from(this.uploadPartialFile(index, node, continuation, range, options));\r\n }\r\n\r\n private async uploadPartialFile(\r\n index: number,\r\n node: string,\r\n continuation: FileTransferContinuation,\r\n range: ContentRange,\r\n options: FileUploadOptions): Promise<HttpTransferResult> {\r\n\r\n const blob = continuation.file.slice(range.from, range.to + 1);\r\n const headers = this.newRequestHeaders(node, options);\r\n\r\n headers.append(headerConstants.CONTENT_TYPE, 'application/octet-stream');\r\n headers.append(headerConstants.CONTENT_RANGE, range.toString());\r\n headers.append(headerConstants.IF_MATCH, continuation.token);\r\n\r\n const response = await fetch(continuation.location, {\r\n method: 'PATCH',\r\n credentials: 'include',\r\n headers: headers,\r\n body: blob\r\n });\r\n\r\n return new HttpTransferResult(index, response.status, range);\r\n }\r\n\r\n /**\r\n * Gets the name of current shell or module.\r\n */\r\n private get nameOfModule(): string {\r\n if (!this.moduleName) {\r\n this.moduleName = EnvironmentModule.getModuleName();\r\n }\r\n\r\n return this.moduleName;\r\n }\r\n}\r\n\r\n/** Represents the possible cancellation behaviors */\r\nexport enum FileTransferCancellationBehavior {\r\n /** Indicates the file transfer is cancelled or aborted on cancellation */\r\n Cancel,\r\n\r\n /** Indicates the file transfer is paused on cancellation */\r\n Pause,\r\n}\r\n\r\n/** Represents file upload options. */\r\nexport class FileUploadOptions {\r\n constructor(\r\n /** Gets the size of transfer content. The default value is 10MB. */\r\n public readonly transferSize: number = 10485760,\r\n\r\n /** Indicates the behavior to perform if the operation is canceled. */\r\n public readonly cancellationBehavior: FileTransferCancellationBehavior = FileTransferCancellationBehavior.Cancel,\r\n\r\n /** A token that can be used to cancel the operation. */\r\n public readonly progress: Progress<number> = new NoProgress(),\r\n\r\n /** Gets the associated logging options. */\r\n public readonly logging: FileOptions = { logAudit: false, logTelemetry: false }) {\r\n\r\n }\r\n}\r\n\r\n/** Represents the size and progress of a file transfer. */\r\nexport class FileTransferInfo {\r\n private completed = 0;\r\n\r\n constructor(\r\n /** Gets the size used in the file transfer. */\r\n public readonly size: number,\r\n\r\n /** Gets the total number of items in the file transfer. */\r\n public readonly count: number) {\r\n }\r\n\r\n /** Gets the percentage of the file transfer completed. */\r\n public get percentComplete(): number {\r\n return (this.completed / this.count) * 100;\r\n }\r\n\r\n /** Increments the number of completed items in the file transfer. */\r\n public incrementCompleted(): void {\r\n this.completed = Math.min(this.completed + 1, this.count);\r\n }\r\n}\r\n\r\n/** Represents a continuation for a file transfer */\r\nexport class FileTransferContinuation {\r\n constructor(\r\n /** Gets the URl representing the location of the file transfer */\r\n public readonly location: string,\r\n\r\n /** Gets the continuation token for the file transfer */\r\n public readonly token: string,\r\n\r\n /** Gets the file being transferred */\r\n public readonly file: File,\r\n\r\n /** Gets the file transfer information. */\r\n public readonly transfer: FileTransferInfo,\r\n\r\n /** Gets the remaining ranges in the file transfer */\r\n public readonly ranges: ContentRange[] = new Array<ContentRange>()) {\r\n }\r\n}\r\n\r\n/** Represents the result of a file transfer */\r\nexport class FileTransferResult {\r\n constructor(\r\n /** Gets the continuation token associated with the file transfer */\r\n public readonly continuation: FileTransferContinuation =\r\n new FileTransferContinuation(null, null, null, new FileTransferInfo(0, 0))) {\r\n\r\n }\r\n\r\n /** Gets a value indicating whether the file transfer is complete */\r\n public get completed(): boolean {\r\n return this.continuation.ranges.length === 0;\r\n }\r\n\r\n /** Gets a value indicating whether the file transfer is resumable */\r\n public get resumable() {\r\n return !this.completed &&\r\n this.continuation.token !== null &&\r\n this.continuation.location !== null &&\r\n this.continuation.file !== null;\r\n }\r\n}\r\n\r\n/** Represent a range of content in a file transfer */\r\nexport class ContentRange {\r\n constructor(\r\n /** Gets the zero-based start of the content, in bytes */\r\n public readonly from: number,\r\n\r\n /** Gets the zero-based end of the content, in bytes */\r\n public readonly to: number,\r\n\r\n /** Gets the total size of the content in bytes */\r\n public readonly totalSize: number) {\r\n\r\n }\r\n\r\n /** Returns the string representation of the object */\r\n public toString(): string {\r\n return `bytes ${this.from}-${this.to}/${this.totalSize}`;\r\n }\r\n}\r\n\r\nclass NoProgress implements Progress<number> {\r\n public report(): void {\r\n\r\n }\r\n}\r\n\r\nclass HttpTransferResult {\r\n constructor(\r\n public readonly index: number,\r\n public readonly statusCode: number,\r\n public readonly range: ContentRange) {\r\n\r\n }\r\n}\r\n"]}