11import axios from 'axios' ;
2+ import crypto from 'node:crypto' ;
23import fs from 'node:fs' ;
34import path from 'node:path' ;
45import FormData from 'form-data' ;
@@ -18,6 +19,7 @@ export interface UploadOptions {
1819 credentials : Credentials ;
1920 contentType : ContentType ;
2021 showProgress ?: boolean ;
22+ checksum ?: string ;
2123}
2224
2325export interface UploadResult {
@@ -40,7 +42,6 @@ export default class Upload {
4042 const totalSize = fileStats . size ;
4143 const sizeMB = ( totalSize / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
4244
43- // Create progress tracker
4445 const progressTracker = progress ( {
4546 length : totalSize ,
4647 time : 100 , // Emit progress every 100ms
@@ -49,7 +50,6 @@ export default class Upload {
4950 let lastPercent = 0 ;
5051
5152 if ( showProgress ) {
52- // Draw initial progress bar
5353 this . drawProgressBar ( fileName , sizeMB , 0 ) ;
5454
5555 progressTracker . on ( 'progress' , ( prog ) => {
@@ -61,7 +61,6 @@ export default class Upload {
6161 } ) ;
6262 }
6363
64- // Create file stream and pipe through progress tracker
6564 const fileStream = fs . createReadStream ( filePath ) ;
6665 const trackedStream = fileStream . pipe ( progressTracker ) ;
6766
@@ -72,6 +71,10 @@ export default class Upload {
7271 knownLength : totalSize ,
7372 } ) ;
7473
74+ if ( options . checksum ) {
75+ formData . append ( 'checksum' , options . checksum ) ;
76+ }
77+
7578 try {
7679 const response = await axios . post ( url , formData , {
7780 headers : {
@@ -87,6 +90,10 @@ export default class Upload {
8790 maxRedirects : 0 , // Recommended for stream uploads to avoid buffering
8891 } ) ;
8992
93+ // Check for version update notification
94+ const latestVersion = response . headers ?. [ 'x-testingbotctl-version' ] ;
95+ utils . checkForUpdate ( latestVersion ) ;
96+
9097 const result = response . data ;
9198 if ( result . id ) {
9299 if ( showProgress ) {
@@ -151,4 +158,19 @@ export default class Upload {
151158 throw new TestingBotError ( `File not found or not readable: ${ filePath } ` ) ;
152159 }
153160 }
161+
162+ /**
163+ * Calculate MD5 checksum of a file, returning base64-encoded result
164+ * This matches ActiveStorage's checksum format
165+ */
166+ public async calculateChecksum ( filePath : string ) : Promise < string > {
167+ return new Promise ( ( resolve , reject ) => {
168+ const hash = crypto . createHash ( 'md5' ) ;
169+ const stream = fs . createReadStream ( filePath ) ;
170+
171+ stream . on ( 'data' , ( chunk ) => hash . update ( chunk ) ) ;
172+ stream . on ( 'end' , ( ) => resolve ( hash . digest ( 'base64' ) ) ) ;
173+ stream . on ( 'error' , ( err ) => reject ( err ) ) ;
174+ } ) ;
175+ }
154176}
0 commit comments