@@ -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 ;
@@ -73,7 +82,7 @@ export class NetworkFacade {
7382 if ( rangeOptions ) {
7483 startOffsetByte = rangeOptions . parsed . start ;
7584 }
76- fileStream = await this . cryptoService . decryptStream (
85+ fileStream = this . cryptoService . decryptStream (
7786 encryptedContentStreams ,
7887 Buffer . from ( key as ArrayBuffer ) ,
7988 Buffer . from ( iv as ArrayBuffer ) ,
@@ -183,4 +192,144 @@ 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+ type Part = {
220+ PartNumber : number ;
221+ ETag : string ;
222+ } ;
223+ const fileParts : Part [ ] = [ ] ;
224+
225+ const onProgress = ( partId : number , loadedBytes : number ) => {
226+ if ( ! options ?. progressCallback ) return ;
227+ partsUploadedBytes [ partId ] = loadedBytes ;
228+ const currentTotalLoadedBytes = Object . values ( partsUploadedBytes ) . reduce ( ( a , p ) => a + p , 0 ) ;
229+ const reportedProgress = Math . round ( ( currentTotalLoadedBytes / size ) * 100 ) ;
230+ options . progressCallback ( reportedProgress ) ;
231+ } ;
232+
233+ const encryptFile : EncryptFileFunction = async ( _ , key , iv ) => {
234+ const encryptionCipher = this . cryptoService . getEncryptionTransform (
235+ Buffer . from ( key as ArrayBuffer ) ,
236+ Buffer . from ( iv as ArrayBuffer ) ,
237+ ) ;
238+ const streamInParts = this . cryptoService . encryptStreamInParts ( from , encryptionCipher , size , options . parts ) ;
239+ encryptionTransform = streamInParts . pipe ( hashStream ) ;
240+ } ;
241+
242+ const uploadFileMultipart : UploadFileMultipartFunction = async ( urls : string [ ] ) => {
243+ let partIndex = 0 ;
244+ const limitConcurrency = 6 ;
245+
246+ const uploadPart = async ( upload : UploadTask ) => {
247+ const { etag } = await this . uploadService . uploadFile ( upload . urlToUpload , upload . contentToUpload , {
248+ abortController : abortable ,
249+ progressCallback : ( loadedBytes : number ) => {
250+ onProgress ( upload . index , loadedBytes ) ;
251+ } ,
252+ } ) ;
253+
254+ fileParts . push ( {
255+ ETag : etag ,
256+ PartNumber : upload . index + 1 ,
257+ } ) ;
258+ } ;
259+
260+ const uploadQueue : QueueObject < UploadTask > = queue < UploadTask > ( function ( task , callback ) {
261+ uploadPart ( task )
262+ . then ( ( ) => {
263+ callback ( ) ;
264+ } )
265+ . catch ( ( e ) => {
266+ callback ( e ) ;
267+ } ) ;
268+ } , limitConcurrency ) ;
269+
270+ for await ( const chunk of encryptionTransform ) {
271+ const part : Buffer = chunk ;
272+
273+ if ( uploadQueue . running ( ) === limitConcurrency ) {
274+ await uploadQueue . unsaturated ( ) ;
275+ }
276+
277+ if ( abortable . signal . aborted ) {
278+ throw new Error ( 'Upload cancelled by user' ) ;
279+ }
280+
281+ let errorAlreadyThrown = false ;
282+
283+ uploadQueue
284+ . pushAsync ( {
285+ contentToUpload : part ,
286+ urlToUpload : urls [ partIndex ] ,
287+ index : partIndex ++ ,
288+ } )
289+ . catch ( ( err : Error ) => {
290+ if ( errorAlreadyThrown ) return ;
291+
292+ errorAlreadyThrown = true ;
293+ if ( err ) {
294+ uploadQueue . kill ( ) ;
295+ if ( ! abortable ?. signal . aborted ) {
296+ abortable . abort ( ) ;
297+ }
298+ }
299+ } ) ;
300+ }
301+
302+ while ( uploadQueue . running ( ) > 0 || uploadQueue . length ( ) > 0 ) {
303+ await uploadQueue . drain ( ) ;
304+ }
305+
306+ hash = hashStream . getHash ( ) ;
307+ const compareParts = ( pA : Part , pB : Part ) => pA . PartNumber - pB . PartNumber ;
308+ const sortedParts = fileParts . sort ( compareParts ) ;
309+ return {
310+ hash : hash . toString ( 'hex' ) ,
311+ parts : sortedParts ,
312+ } ;
313+ } ;
314+
315+ const uploadOperation = async ( ) => {
316+ const uploadResult = await NetworkUpload . uploadMultipartFile (
317+ this . network ,
318+ this . cryptoLib ,
319+ bucketId ,
320+ mnemonic ,
321+ size ,
322+ encryptFile ,
323+ uploadFileMultipart ,
324+ options . parts ,
325+ ) ;
326+
327+ return {
328+ fileId : uploadResult ,
329+ hash : hash ,
330+ } ;
331+ } ;
332+
333+ return [ uploadOperation ( ) , abortable ] ;
334+ }
186335}
0 commit comments