@@ -6,17 +6,26 @@ import {
66 DownloadFileFunction ,
77 EncryptFileFunction ,
88 UploadFileFunction ,
9+ UploadFileMultipartFunction ,
910} from '@internxt/sdk/dist/network' ;
1011import { Environment } from '@internxt/inxt-js' ;
1112import { randomBytes } from 'node:crypto' ;
1213import { Readable , Transform } from 'node:stream' ;
13- import { DownloadOptions , UploadOptions , UploadProgressCallback , DownloadProgressCallback } from '../../types/network.types' ;
14+ import {
15+ DownloadOptions ,
16+ UploadOptions ,
17+ UploadProgressCallback ,
18+ DownloadProgressCallback ,
19+ UploadMultipartOptions ,
20+ UploadTask ,
21+ } from '../../types/network.types' ;
1422import { CryptoService } from '../crypto.service' ;
1523import { UploadService } from './upload.service' ;
1624import { DownloadService } from './download.service' ;
1725import { ValidationService } from '../validation.service' ;
1826import { HashStream } from '../../utils/hash.utils' ;
1927import { RangeOptions } from '../../utils/network.utils' ;
28+ import { queue , QueueObject } from 'async' ;
2029
2130export class NetworkFacade {
2231 private readonly cryptoLib : Network . Crypto ;
@@ -183,4 +192,138 @@ export class NetworkFacade {
183192
184193 return [ uploadOperation ( ) , abortable ] ;
185194 }
195+
196+ /**
197+ * Performs a multi-part upload encrypting the stream content
198+ *
199+ * @param bucketId The bucket where the file will be uploaded
200+ * @param mnemonic The plain mnemonic of the user
201+ * @param size The total size of the stream content
202+ * @param from The source ReadStream to upload from
203+ * @param options The upload options
204+ * @returns A promise to execute the upload and an abort controller to cancel the upload
205+ */
206+ async uploadMultipartFromStream (
207+ bucketId : string ,
208+ mnemonic : string ,
209+ size : number ,
210+ from : Readable ,
211+ options : UploadMultipartOptions ,
212+ ) : Promise < [ Promise < { fileId : string ; hash : Buffer } > , AbortController ] > {
213+ const hashStream = new HashStream ( ) ;
214+ const abortable = options ?. abortController ?? new AbortController ( ) ;
215+ let encryptionTransform : Transform ;
216+ let hash : Buffer ;
217+
218+ const partsUploadedBytes : Record < number , number > = { } ;
219+ const fileParts : { PartNumber : number ; ETag : string } [ ] = [ ] ;
220+
221+ const onProgress = ( partId : number , loadedBytes : number ) => {
222+ if ( ! options ?. progressCallback ) return ;
223+ partsUploadedBytes [ partId ] = loadedBytes ;
224+ const currentTotalLoadedBytes = Object . values ( partsUploadedBytes ) . reduce ( ( a , p ) => a + p , 0 ) ;
225+ const reportedProgress = Math . round ( ( currentTotalLoadedBytes / size ) * 100 ) ;
226+ options . progressCallback ( reportedProgress ) ;
227+ } ;
228+
229+ const encryptFile : EncryptFileFunction = async ( _ , key , iv ) => {
230+ const encryptionCipher = this . cryptoService . getEncryptionTransform (
231+ Buffer . from ( key as ArrayBuffer ) ,
232+ Buffer . from ( iv as ArrayBuffer ) ,
233+ ) ;
234+ const streamInParts = this . cryptoService . encryptStreamInParts ( from , encryptionCipher , size , options . parts ) ;
235+ encryptionTransform = streamInParts . pipe ( hashStream ) ;
236+ } ;
237+
238+ const uploadFileMultipart : UploadFileMultipartFunction = async ( urls : string [ ] ) => {
239+ let partIndex = 0 ;
240+ const limitConcurrency = 6 ;
241+
242+ const worker = async ( upload : UploadTask ) => {
243+ const { etag } = await this . uploadService . uploadFile ( upload . urlToUpload , upload . contentToUpload , {
244+ abortController : abortable ,
245+ progressCallback : ( loadedBytes : number ) => {
246+ onProgress ( upload . index , loadedBytes ) ;
247+ } ,
248+ } ) ;
249+
250+ fileParts . push ( {
251+ ETag : etag ,
252+ PartNumber : upload . index + 1 ,
253+ } ) ;
254+ } ;
255+
256+ const uploadQueue : QueueObject < UploadTask > = queue < UploadTask > ( function ( task , callback ) {
257+ worker ( task )
258+ . then ( ( ) => {
259+ callback ( ) ;
260+ } )
261+ . catch ( ( e ) => {
262+ callback ( e ) ;
263+ } ) ;
264+ } , limitConcurrency ) ;
265+
266+ for await ( const chunk of encryptionTransform ) {
267+ const part : Buffer = chunk ;
268+
269+ if ( uploadQueue . running ( ) === limitConcurrency ) {
270+ await uploadQueue . unsaturated ( ) ;
271+ }
272+
273+ if ( abortable . signal . aborted ) {
274+ throw new Error ( 'Upload cancelled by user' ) ;
275+ }
276+
277+ let errorAlreadyThrown = false ;
278+
279+ uploadQueue
280+ . pushAsync ( {
281+ contentToUpload : part ,
282+ urlToUpload : urls [ partIndex ] ,
283+ index : partIndex ++ ,
284+ } )
285+ . catch ( ( err : Error ) => {
286+ if ( errorAlreadyThrown ) return ;
287+
288+ errorAlreadyThrown = true ;
289+ if ( err ) {
290+ uploadQueue . kill ( ) ;
291+ if ( ! abortable ?. signal . aborted ) {
292+ abortable . abort ( ) ;
293+ }
294+ }
295+ } ) ;
296+ }
297+
298+ while ( uploadQueue . running ( ) > 0 || uploadQueue . length ( ) > 0 ) {
299+ await uploadQueue . drain ( ) ;
300+ }
301+
302+ hash = hashStream . getHash ( ) ;
303+ return {
304+ hash : hash . toString ( 'hex' ) ,
305+ parts : fileParts . sort ( ( pA , pB ) => pA . PartNumber - pB . PartNumber ) ,
306+ } ;
307+ } ;
308+
309+ const uploadOperation = async ( ) => {
310+ const uploadResult = await NetworkUpload . uploadMultipartFile (
311+ this . network ,
312+ this . cryptoLib ,
313+ bucketId ,
314+ mnemonic ,
315+ size ,
316+ encryptFile ,
317+ uploadFileMultipart ,
318+ options . parts ,
319+ ) ;
320+
321+ return {
322+ fileId : uploadResult ,
323+ hash : hash ,
324+ } ;
325+ } ;
326+
327+ return [ uploadOperation ( ) , abortable ] ;
328+ }
186329}
0 commit comments