diff --git a/portal-ui/package.json b/portal-ui/package.json index a50b05e8e..14e46fa55 100644 --- a/portal-ui/package.json +++ b/portal-ui/package.json @@ -13,7 +13,6 @@ "@mui/styles": "^5.12.0", "@mui/x-date-pickers": "^5.0.20", "@reduxjs/toolkit": "^1.9.5", - "@types/streamsaver": "^2.0.1", "@uiw/react-textarea-code-editor": "^2.1.1", "kbar": "^0.1.0-beta.39", "local-storage-fallback": "^4.1.1", @@ -33,7 +32,6 @@ "react-window": "^1.8.9", "react-window-infinite-loader": "^1.0.9", "recharts": "^2.6.2", - "streamsaver": "^2.0.6", "styled-components": "^5.3.11", "superagent": "^8.0.8", "tinycolor2": "^1.6.0", diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts index 21079c314..b50693355 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts @@ -17,7 +17,6 @@ import { BucketObjectItem } from "./ListObjects/types"; import { encodeURLString } from "../../../../../common/utils"; import { removeTrace } from "../../../ObjectBrowser/transferManager"; -import streamSaver from "streamsaver"; import store from "../../../../../store"; import { PermissionResource } from "api/consoleApi"; @@ -50,146 +49,101 @@ export const download = ( if (versionID) { path = path.concat(`&version_id=${versionID}`); } - return new DownloadHelper( - path, - id, - anonymousMode, - fileSize, - progressCallback, - completeCallback, - errorCallback, - abortCallback, - toastCallback, + + // If file is greater than 50GiB then we force browser download, if not then we use HTTP Request for Object Manager + if (fileSize > 53687091200) { + return new BrowserDownload(path, id, completeCallback, toastCallback); + } + + let req = new XMLHttpRequest(); + req.open("GET", path, true); + if (anonymousMode) { + req.setRequestHeader("X-Anonymous", "1"); + } + req.addEventListener( + "progress", + function (evt) { + let percentComplete = Math.round((evt.loaded / fileSize) * 100); + + if (progressCallback) { + progressCallback(percentComplete); + } + }, + false, ); + + req.responseType = "blob"; + req.onreadystatechange = () => { + if (req.readyState === 4) { + if (req.status === 200) { + const rspHeader = req.getResponseHeader("Content-Disposition"); + + let filename = "download"; + if (rspHeader) { + let rspHeaderDecoded = decodeURIComponent(rspHeader); + filename = rspHeaderDecoded.split('"')[1]; + } + + if (completeCallback) { + completeCallback(); + } + + removeTrace(id); + + var link = document.createElement("a"); + link.href = window.URL.createObjectURL(req.response); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } else { + if (req.getResponseHeader("Content-Type") === "application/json") { + const rspBody: { detailedMessage?: string } = JSON.parse( + req.response, + ); + if (rspBody.detailedMessage) { + errorCallback(rspBody.detailedMessage); + return; + } + } + errorCallback(`Unexpected response status code (${req.status}).`); + } + } + }; + req.onerror = () => { + if (errorCallback) { + errorCallback("A network error occurred."); + } + }; + req.onabort = () => { + if (abortCallback) { + abortCallback(); + } + }; + + return req; }; -class DownloadHelper { - aborter: AbortController; +class BrowserDownload { path: string; id: string; - filename: string = ""; - anonymousMode: boolean; - fileSize: number = 0; - writer: any = null; - progressCallback: (progress: number) => void; completeCallback: () => void; - errorCallback: (msg: string) => void; - abortCallback: () => void; toastCallback: () => void; constructor( path: string, id: string, - anonymousMode: boolean, - fileSize: number, - progressCallback: (progress: number) => void, completeCallback: () => void, - errorCallback: (msg: string) => void, - abortCallback: () => void, toastCallback: () => void, ) { - this.aborter = new AbortController(); this.path = path; this.id = id; - this.anonymousMode = anonymousMode; - this.fileSize = fileSize; - this.progressCallback = progressCallback; this.completeCallback = completeCallback; - this.errorCallback = errorCallback; - this.abortCallback = abortCallback; this.toastCallback = toastCallback; } - abort(): void { - this.aborter.abort(); - this.abortCallback(); - if (this.writer) { - this.writer.abort(); - } - } - send(): void { - let isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - if (isSafari) { - this.toastCallback(); - this.downloadByBrowser(); - } else if (!this.fileSize) { - this.downloadByBrowser(); - } else { - this.download({ - url: this.path, - chunkSize: 1024 * 1024 * 1024 * 1.5, - }); - } - } - - async getRangeContent(url: string, start: number, end: number) { - const info = this.getRequestInfo(start, end); - const response = await fetch(url, info); - if (response.ok && response.body) { - if (!this.filename) { - this.filename = this.getFilename(response); - } - if (!this.writer) { - this.writer = streamSaver.createWriteStream(this.filename).getWriter(); - } - const reader = response.body.getReader(); - let done, value; - while (!done) { - ({ value, done } = await reader.read()); - if (done) { - break; - } - await this.writer.write(value); - } - } else { - throw new Error(`Unexpected response status code (${response.status}).`); - } - } - - getRequestInfo(start: number, end: number) { - const info: RequestInit = { - signal: this.aborter.signal, - headers: { range: `bytes=${start}-${end}` }, - }; - if (this.anonymousMode) { - info.headers = { ...info.headers, "X-Anonymous": "1" }; - } - return info; - } - - getFilename(response: Response) { - const rspHeader = response.headers.get("Content-Disposition"); - if (rspHeader) { - let rspHeaderDecoded = decodeURIComponent(rspHeader); - return rspHeaderDecoded.split('"')[1]; - } - return "download"; - } - - async download({ url, chunkSize }: any) { - const numberOfChunks = Math.ceil(this.fileSize / chunkSize); - this.progressCallback(0); - try { - for (let i = 0; i < numberOfChunks; i++) { - let start = i * chunkSize; - let end = - i + 1 === numberOfChunks - ? this.fileSize - 1 - : (i + 1) * chunkSize - 1; - await this.getRangeContent(url, start, end); - let percentComplete = Math.round(((i + 1) / numberOfChunks) * 100); - this.progressCallback(percentComplete); - } - this.writer.close(); - this.completeCallback(); - removeTrace(this.id); - } catch (e: any) { - this.errorCallback(e.message); - } - } - - downloadByBrowser() { + this.toastCallback(); const link = document.createElement("a"); link.href = this.path; document.body.appendChild(link); diff --git a/portal-ui/yarn.lock b/portal-ui/yarn.lock index 9a83941e9..721536fc1 100644 --- a/portal-ui/yarn.lock +++ b/portal-ui/yarn.lock @@ -2782,11 +2782,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/streamsaver@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/streamsaver/-/streamsaver-2.0.1.tgz#fa5e5b891d1b282be3078c232a30ee004b8e0be0" - integrity sha512-I49NtT8w6syBI3Zg3ixCyygTHoTVMY0z2TMRcTgccdIsVd2MwlKk7ITLHLsJtgchUHcOd7QEARG9h0ifcA6l2Q== - "@types/styled-components@^5.1.25": version "5.1.26" resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.26.tgz#5627e6812ee96d755028a98dae61d28e57c233af" @@ -11115,11 +11110,6 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -streamsaver@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/streamsaver/-/streamsaver-2.0.6.tgz#869d2347dd70191e0ac888d52296956a8cba2ed9" - integrity sha512-LK4e7TfCV8HzuM0PKXuVUfKyCB1FtT9L0EGxsFk5Up8njj0bXK8pJM9+Wq2Nya7/jslmCQwRK39LFm55h7NBTw== - strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"