Skip to content

Commit 6f07f46

Browse files
improve upload progress bar
1 parent 868971e commit 6f07f46

File tree

7 files changed

+220
-178
lines changed

7 files changed

+220
-178
lines changed

package-lock.json

Lines changed: 77 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"form-data": "^4.0.5",
5656
"glob": "^13.0.0",
5757
"js-yaml": "^4.1.1",
58+
"progress-stream": "^2.0.0",
5859
"socket.io-client": "^4.8.1",
5960
"tracer": "^1.3.0"
6061
},
@@ -65,6 +66,7 @@
6566
"@types/jest": "^29.5.14",
6667
"@types/js-yaml": "^4.0.9",
6768
"@types/node": "^20.19.0",
69+
"@types/progress-stream": "^2.0.5",
6870
"babel-jest": "^29.7.0",
6971
"eslint": "^9.39.1",
7072
"eslint-config-prettier": "^10.1.8",

src/upload.ts

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import axios, { AxiosProgressEvent } from 'axios';
1+
import axios from 'axios';
22
import fs from 'node:fs';
33
import path from 'node:path';
44
import FormData from 'form-data';
5+
import progress from 'progress-stream';
56
import Credentials from './models/credentials';
67
import TestingBotError from './models/testingbot_error';
78
import utils from './utils';
@@ -24,31 +25,57 @@ export interface UploadResult {
2425
}
2526

2627
export 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

Comments
 (0)