1- import request from 'request' ;
21import fs from 'fs-extra' ;
2+ import https from 'https' ;
3+ import http from 'http' ;
4+ import { URL } from 'url' ;
35/**
46 * Promise based download file method
7+ * Uses native Node.js http/https modules to preserve S3 pre-signed URLs exactly as-is
58 * See: https://ourcodeworld.com/articles/read/228/how-to-download-a-webfile-with-electron-save-it-and-show-download-progress
69 */
710
@@ -24,63 +27,105 @@ export const downloadFile = (
2427 let total_bytes = 0 ;
2528 let error : Error | null = null ;
2629
27- const req = request ( {
28- method : 'GET' ,
29- uri : url ,
30- } ) ;
30+ try {
31+ // Parse URL to get components, but use the original URL string for the request
32+ // to preserve the exact path and query string (important for S3 signatures)
33+ const urlObj = new URL ( url ) ;
34+ const isHttps = urlObj . protocol === 'https:' ;
35+ const client = isHttps ? https : http ;
3136
32- const out = fs . createWriteStream ( localPath ) ;
33- req . pipe ( out ) ;
37+ // Use the original URL string to preserve exact path encoding
38+ // This is critical for S3 pre-signed URLs where the signature depends on the exact URL
39+ const options = {
40+ hostname : urlObj . hostname ,
41+ port : urlObj . port || ( isHttps ? 443 : 80 ) ,
42+ path : urlObj . pathname + urlObj . search , // Preserve path and query exactly
43+ method : 'GET' ,
44+ headers : {
45+ 'User-Agent' : 'Audio-Project-Manager' ,
46+ } ,
47+ } ;
3448
35- req . on ( 'response' , ( data : { headers : { 'content-length' : string } } ) => {
36- total_bytes = parseInt ( data . headers [ 'content-length' ] || '' ) ;
37- if ( isNaN ( total_bytes ) ) {
38- error = new Error ( 'Invalid content-length' ) as any ;
39- }
40- if ( token ) {
41- downloadMap . set ( token , {
42- received : 0 ,
43- total : total_bytes ,
44- error : error ,
49+ const out = fs . createWriteStream ( localPath ) ;
50+ const req = client . request ( options , ( res ) => {
51+ if ( res . statusCode && res . statusCode >= 400 ) {
52+ error = new Error (
53+ `HTTP ${ res . statusCode } : ${ res . statusMessage } `
54+ ) as any ;
55+ res . resume ( ) ; // Consume response to free up memory
56+ out . destroy ( ) ;
57+ if ( key ) {
58+ const status = downloadMap . get ( key ) ;
59+ downloadMap . set ( key , { ...status , error } ) ;
60+ }
61+ reject ( error ) ;
62+ return ;
63+ }
64+
65+ total_bytes = parseInt ( res . headers [ 'content-length' ] || '0' , 10 ) ;
66+ if ( token ) {
67+ downloadMap . set ( token , {
68+ received : 0 ,
69+ total : total_bytes ,
70+ error : error ,
71+ } ) ;
72+ key = token ;
73+ }
74+
75+ res . on ( 'data' , ( chunk : Buffer ) => {
76+ received_bytes += chunk . length ;
77+ if ( key ) {
78+ const status = downloadMap . get ( key ) ;
79+ downloadMap . set ( key , { ...status , received : received_bytes } ) ;
80+ }
81+ } ) ;
82+
83+ res . on ( 'end' , ( ) => {
84+ out . end ( ) ;
4585 } ) ;
46- key = token ;
47- }
48- } ) ;
4986
50- req . on ( 'data' , ( chunk : { length : number } ) => {
51- received_bytes += chunk . length ;
52- if ( key ) {
53- const status = downloadMap . get ( key ) ;
54- downloadMap . set ( key , { ...status , received : received_bytes } ) ;
55- }
56- } ) ;
87+ res . pipe ( out ) ;
88+ } ) ;
89+
90+ req . on ( 'error' , ( err : Error ) => {
91+ error = err ;
92+ out . destroy ( ) ;
93+ if ( key ) {
94+ const status = downloadMap . get ( key ) ;
95+ downloadMap . set ( key , { ...status , error } ) ;
96+ }
97+ reject ( error ) ;
98+ } ) ;
99+
100+ out . on ( 'finish' , ( ) => {
101+ let err = error ;
102+ if ( key ) {
103+ err = downloadMap . get ( key ) . error ;
104+ }
105+ if ( err ) {
106+ fs . unlink ( localPath ) . catch ( ( ) => {
107+ // Ignore unlink errors
108+ } ) ;
109+ reject ( err ) ;
110+ } else {
111+ resolve ( void 0 ) ;
112+ }
113+ } ) ;
57114
58- req . on ( 'error' , ( err : Error ) => {
59- const error = err ;
60- if ( key ) {
61- const status = downloadMap . get ( key ) ;
62- downloadMap . set ( key , { ...status , error } ) ;
63- }
64- } ) ;
115+ out . on ( 'error' , ( err : Error ) => {
116+ error = err ;
117+ req . destroy ( ) ;
118+ if ( key ) {
119+ const status = downloadMap . get ( key ) ;
120+ downloadMap . set ( key , { ...status , error } ) ;
121+ }
122+ reject ( error ) ;
123+ } ) ;
65124
66- out . on ( 'finish' , ( ) => {
67- let err = error ;
68- if ( key ) {
69- err = downloadMap . get ( key ) . error ;
70- }
71- if ( err ) {
72- fs . unlink ( localPath ) ;
73- reject ( err ) ;
74- } else {
75- resolve ( void 0 ) ;
76- }
77- } ) ;
78- out . on ( 'error' , ( err : Error ) => {
79- const error = err ;
80- if ( key ) {
81- const status = downloadMap . get ( key ) ;
82- downloadMap . set ( key , { ...status , error } ) ;
83- }
84- } ) ;
125+ req . end ( ) ;
126+ } catch ( err ) {
127+ const error = err as Error ;
128+ reject ( error ) ;
129+ }
85130 } ) ;
86131} ;
0 commit comments