1- import axios , { AxiosProgressEvent } from 'axios' ;
1+ import axios from 'axios' ;
22import fs from 'node:fs' ;
33import path from 'node:path' ;
44import FormData from 'form-data' ;
5+ import progress from 'progress-stream' ;
56import Credentials from './models/credentials' ;
67import TestingBotError from './models/testingbot_error' ;
78import utils from './utils' ;
@@ -24,31 +25,57 @@ export interface UploadResult {
2425}
2526
2627export default class Upload {
27- private lastProgressPercent : number = 0 ;
28-
2928 public async upload ( options : UploadOptions ) : Promise < UploadResult > {
3029 const {
3130 filePath,
3231 url,
3332 credentials,
34- contentType,
3533 showProgress = false ,
3634 } = options ;
3735
3836 await this . validateFile ( filePath ) ;
3937
4038 const fileName = path . basename ( filePath ) ;
4139 const fileStats = await fs . promises . stat ( filePath ) ;
40+ const totalSize = fileStats . size ;
41+ const sizeMB = ( totalSize / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
42+
43+ // Create progress tracker
44+ const progressTracker = progress ( {
45+ length : totalSize ,
46+ time : 100 , // Emit progress every 100ms
47+ } ) ;
48+
49+ let lastPercent = 0 ;
50+
51+ if ( showProgress ) {
52+ // Draw initial progress bar
53+ this . drawProgressBar ( fileName , sizeMB , 0 ) ;
54+
55+ progressTracker . on ( 'progress' , ( prog ) => {
56+ const percent = Math . round ( prog . percentage ) ;
57+ if ( percent !== lastPercent ) {
58+ lastPercent = percent ;
59+ this . drawProgressBar ( fileName , sizeMB , percent ) ;
60+ }
61+ } ) ;
62+ }
63+
64+ // Create file stream and pipe through progress tracker
4265 const fileStream = fs . createReadStream ( filePath ) ;
66+ const trackedStream = fileStream . pipe ( progressTracker ) ;
4367
4468 const formData = new FormData ( ) ;
45- formData . append ( 'file' , fileStream ) ;
69+ formData . append ( 'file' , trackedStream , {
70+ filename : fileName ,
71+ contentType : options . contentType ,
72+ knownLength : totalSize ,
73+ } ) ;
4674
4775 try {
4876 const response = await axios . post ( url , formData , {
4977 headers : {
50- 'Content-Type' : contentType ,
51- 'Content-Disposition' : `attachment; filename=${ fileName } ` ,
78+ ...formData . getHeaders ( ) ,
5279 'User-Agent' : utils . getUserAgent ( ) ,
5380 } ,
5481 auth : {
@@ -57,25 +84,28 @@ export default class Upload {
5784 } ,
5885 maxContentLength : Infinity ,
5986 maxBodyLength : Infinity ,
60- onUploadProgress : showProgress
61- ? ( progressEvent : AxiosProgressEvent ) => {
62- this . handleProgress ( progressEvent , fileStats . size , fileName ) ;
63- }
64- : undefined ,
87+ maxRedirects : 0 , // Recommended for stream uploads to avoid buffering
6588 } ) ;
6689
6790 const result = response . data ;
6891 if ( result . id ) {
6992 if ( showProgress ) {
70- this . clearProgressLine ( ) ;
93+ this . drawProgressBar ( fileName , sizeMB , 100 ) ;
94+ console . log ( '' ) ;
7195 }
7296 return { id : result . id } ;
7397 } else {
98+ if ( showProgress ) {
99+ console . log ( ' Failed' ) ;
100+ }
74101 throw new TestingBotError (
75102 `Upload failed: ${ result . error || 'Unknown error' } ` ,
76103 ) ;
77104 }
78105 } catch ( error ) {
106+ if ( showProgress ) {
107+ console . log ( ' Failed' ) ;
108+ }
79109 if ( error instanceof TestingBotError ) {
80110 throw error ;
81111 }
@@ -98,50 +128,27 @@ export default class Upload {
98128 }
99129 }
100130
101- private async validateFile ( filePath : string ) : Promise < void > {
102- try {
103- await fs . promises . access ( filePath , fs . constants . R_OK ) ;
104- } catch {
105- throw new TestingBotError ( `File not found or not readable: ${ filePath } ` ) ;
106- }
107- }
108-
109- private handleProgress (
110- progressEvent : AxiosProgressEvent ,
111- totalSize : number ,
112- fileName : string ,
113- ) : void {
114- const loaded = progressEvent . loaded ;
115- const total = progressEvent . total || totalSize ;
116- const percent = Math . round ( ( loaded / total ) * 100 ) ;
117-
118- if ( percent !== this . lastProgressPercent ) {
119- this . lastProgressPercent = percent ;
120- this . displayProgress ( fileName , percent , loaded , total ) ;
121- }
122- }
123-
124- private displayProgress (
131+ private drawProgressBar (
125132 fileName : string ,
133+ sizeMB : string ,
126134 percent : number ,
127- loaded : number ,
128- total : number ,
129135 ) : void {
130136 const barWidth = 30 ;
131- const filledWidth = Math . round ( ( percent / 100 ) * barWidth ) ;
132- const emptyWidth = barWidth - filledWidth ;
133- const bar = '█' . repeat ( filledWidth ) + '░' . repeat ( emptyWidth ) ;
134-
135- const loadedMB = ( loaded / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
136- const totalMB = ( total / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
137+ const filled = Math . round ( ( barWidth * percent ) / 100 ) ;
138+ const empty = barWidth - filled ;
139+ const bar = '█' . repeat ( filled ) + '░' . repeat ( empty ) ;
140+ const transferred = ( ( percent / 100 ) * parseFloat ( sizeMB ) ) . toFixed ( 2 ) ;
137141
138142 process . stdout . write (
139- `\r ${ fileName } : [${ bar } ] ${ percent } % (${ loadedMB } /${ totalMB } MB)` ,
143+ `\r ${ fileName } : [${ bar } ] ${ percent } % (${ transferred } /${ sizeMB } MB)` ,
140144 ) ;
141145 }
142146
143- private clearProgressLine ( ) : void {
144- process . stdout . write ( '\r' + ' ' . repeat ( 80 ) + '\r' ) ;
145- this . lastProgressPercent = 0 ;
147+ private async validateFile ( filePath : string ) : Promise < void > {
148+ try {
149+ await fs . promises . access ( filePath , fs . constants . R_OK ) ;
150+ } catch {
151+ throw new TestingBotError ( `File not found or not readable: ${ filePath } ` ) ;
152+ }
146153 }
147154}
0 commit comments