@@ -18,6 +18,7 @@ import { BucketObjectItem } from "./ListObjects/types";
18
18
import { IAllowResources } from "../../../types" ;
19
19
import { encodeURLString } from "../../../../../common/utils" ;
20
20
import { removeTrace } from "../../../ObjectBrowser/transferManager" ;
21
+ import streamSaver from "streamsaver" ;
21
22
import store from "../../../../../store" ;
22
23
23
24
export const download = (
@@ -30,7 +31,8 @@ export const download = (
30
31
progressCallback : ( progress : number ) => void ,
31
32
completeCallback : ( ) => void ,
32
33
errorCallback : ( msg : string ) => void ,
33
- abortCallback : ( ) => void
34
+ abortCallback : ( ) => void ,
35
+ toastCallback : ( ) => void
34
36
) => {
35
37
const anchor = document . createElement ( "a" ) ;
36
38
document . body . appendChild ( anchor ) ;
@@ -48,75 +50,153 @@ export const download = (
48
50
if ( versionID ) {
49
51
path = path . concat ( `&version_id=${ versionID } ` ) ;
50
52
}
51
-
52
- var req = new XMLHttpRequest ( ) ;
53
- req . open ( "GET" , path , true ) ;
54
- if ( anonymousMode ) {
55
- req . setRequestHeader ( "X-Anonymous" , "1" ) ;
56
- }
57
- req . addEventListener (
58
- "progress" ,
59
- function ( evt ) {
60
- let percentComplete = Math . round ( ( evt . loaded / fileSize ) * 100 ) ;
61
-
62
- if ( progressCallback ) {
63
- progressCallback ( percentComplete ) ;
64
- }
65
- } ,
66
- false
53
+ return new DownloadHelper (
54
+ path ,
55
+ id ,
56
+ anonymousMode ,
57
+ fileSize ,
58
+ progressCallback ,
59
+ completeCallback ,
60
+ errorCallback ,
61
+ abortCallback ,
62
+ toastCallback
67
63
) ;
64
+ } ;
68
65
69
- req . responseType = "blob" ;
70
- req . onreadystatechange = ( ) => {
71
- if ( req . readyState === 4 ) {
72
- if ( req . status === 200 ) {
73
- const rspHeader = req . getResponseHeader ( "Content-Disposition" ) ;
66
+ class DownloadHelper {
67
+ aborter : AbortController ;
68
+ path : string ;
69
+ id : string ;
70
+ filename : string = "" ;
71
+ anonymousMode : boolean ;
72
+ fileSize : number = 0 ;
73
+ writer : any = null ;
74
+ progressCallback : ( progress : number ) => void ;
75
+ completeCallback : ( ) => void ;
76
+ errorCallback : ( msg : string ) => void ;
77
+ abortCallback : ( ) => void ;
78
+ toastCallback : ( ) => void ;
79
+
80
+ constructor (
81
+ path : string ,
82
+ id : string ,
83
+ anonymousMode : boolean ,
84
+ fileSize : number ,
85
+ progressCallback : ( progress : number ) => void ,
86
+ completeCallback : ( ) => void ,
87
+ errorCallback : ( msg : string ) => void ,
88
+ abortCallback : ( ) => void ,
89
+ toastCallback : ( ) => void
90
+ ) {
91
+ this . aborter = new AbortController ( ) ;
92
+ this . path = path ;
93
+ this . id = id ;
94
+ this . anonymousMode = anonymousMode ;
95
+ this . fileSize = fileSize ;
96
+ this . progressCallback = progressCallback ;
97
+ this . completeCallback = completeCallback ;
98
+ this . errorCallback = errorCallback ;
99
+ this . abortCallback = abortCallback ;
100
+ this . toastCallback = toastCallback ;
101
+ }
74
102
75
- let filename = "download" ;
76
- if ( rspHeader ) {
77
- let rspHeaderDecoded = decodeURIComponent ( rspHeader ) ;
78
- filename = rspHeaderDecoded . split ( '"' ) [ 1 ] ;
79
- }
103
+ abort ( ) : void {
104
+ this . aborter . abort ( ) ;
105
+ this . abortCallback ( ) ;
106
+ if ( this . writer ) {
107
+ this . writer . abort ( ) ;
108
+ }
109
+ }
80
110
81
- if ( completeCallback ) {
82
- completeCallback ( ) ;
83
- }
111
+ send ( ) : void {
112
+ let isSafari = / ^ ( (? ! c h r o m e | a n d r o i d ) .) * s a f a r i / i. test ( navigator . userAgent ) ;
113
+ if ( isSafari ) {
114
+ this . toastCallback ( ) ;
115
+ this . downloadSafari ( ) ;
116
+ } else {
117
+ this . download ( {
118
+ url : this . path ,
119
+ chunkSize : 1024 * 1024 * 1024 * 1.5 ,
120
+ } ) ;
121
+ }
122
+ }
84
123
85
- removeTrace ( id ) ;
86
-
87
- var link = document . createElement ( "a" ) ;
88
- link . href = window . URL . createObjectURL ( req . response ) ;
89
- link . download = filename ;
90
- document . body . appendChild ( link ) ;
91
- link . click ( ) ;
92
- document . body . removeChild ( link ) ;
93
- } else {
94
- if ( req . getResponseHeader ( "Content-Type" ) === "application/json" ) {
95
- const rspBody : { detailedMessage ?: string } = JSON . parse (
96
- req . response
97
- ) ;
98
- if ( rspBody . detailedMessage ) {
99
- errorCallback ( rspBody . detailedMessage ) ;
100
- return ;
101
- }
124
+ async getRangeContent ( url : string , start : number , end : number ) {
125
+ const info = this . getRequestInfo ( start , end ) ;
126
+ const response = await fetch ( url , info ) ;
127
+ if ( response . ok && response . body ) {
128
+ if ( ! this . filename ) {
129
+ this . filename = this . getFilename ( response ) ;
130
+ }
131
+ if ( ! this . writer ) {
132
+ this . writer = streamSaver . createWriteStream ( this . filename ) . getWriter ( ) ;
133
+ }
134
+ const reader = response . body . getReader ( ) ;
135
+ let done , value ;
136
+ while ( ! done ) {
137
+ ( { value, done } = await reader . read ( ) ) ;
138
+ if ( done ) {
139
+ break ;
102
140
}
103
- errorCallback ( `Unexpected response status code ( ${ req . status } ).` ) ;
141
+ await this . writer . write ( value ) ;
104
142
}
143
+ } else {
144
+ throw new Error ( `Unexpected response status code (${ response . status } ).` ) ;
105
145
}
106
- } ;
107
- req . onerror = ( ) => {
108
- if ( errorCallback ) {
109
- errorCallback ( "A network error occurred." ) ;
146
+ }
147
+
148
+ getRequestInfo ( start : number , end : number ) {
149
+ const info : RequestInit = {
150
+ signal : this . aborter . signal ,
151
+ headers : { range : `bytes=${ start } -${ end } ` } ,
152
+ } ;
153
+ if ( this . anonymousMode ) {
154
+ info . headers = { ...info . headers , "X-Anonymous" : "1" } ;
110
155
}
111
- } ;
112
- req . onabort = ( ) => {
113
- if ( abortCallback ) {
114
- abortCallback ( ) ;
156
+ return info ;
157
+ }
158
+
159
+ getFilename ( response : Response ) {
160
+ const rspHeader = response . headers . get ( "Content-Disposition" ) ;
161
+ if ( rspHeader ) {
162
+ let rspHeaderDecoded = decodeURIComponent ( rspHeader ) ;
163
+ return rspHeaderDecoded . split ( '"' ) [ 1 ] ;
115
164
}
116
- } ;
165
+ return "download" ;
166
+ }
117
167
118
- return req ;
119
- } ;
168
+ async download ( { url, chunkSize } : any ) {
169
+ const numberOfChunks = Math . ceil ( this . fileSize / chunkSize ) ;
170
+ this . progressCallback ( 0 ) ;
171
+ try {
172
+ for ( let i = 0 ; i < numberOfChunks ; i ++ ) {
173
+ let start = i * chunkSize ;
174
+ let end =
175
+ i + 1 === numberOfChunks
176
+ ? this . fileSize - 1
177
+ : ( i + 1 ) * chunkSize - 1 ;
178
+ await this . getRangeContent ( url , start , end ) ;
179
+ let percentComplete = Math . round ( ( ( i + 1 ) / numberOfChunks ) * 100 ) ;
180
+ this . progressCallback ( percentComplete ) ;
181
+ }
182
+ this . writer . close ( ) ;
183
+ this . completeCallback ( ) ;
184
+ removeTrace ( this . id ) ;
185
+ } catch ( e : any ) {
186
+ this . errorCallback ( e . message ) ;
187
+ }
188
+ }
189
+
190
+ downloadSafari ( ) {
191
+ const link = document . createElement ( "a" ) ;
192
+ link . href = this . path ;
193
+ document . body . appendChild ( link ) ;
194
+ link . click ( ) ;
195
+ document . body . removeChild ( link ) ;
196
+ this . completeCallback ( ) ;
197
+ removeTrace ( this . id ) ;
198
+ }
199
+ }
120
200
121
201
// Review file extension by name & returns the type of preview browser that can be used
122
202
export const extensionPreview = (
0 commit comments