{"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"]}