77using System . Text ;
88using System . Text . Json ;
99using System . Text . Json . Serialization ;
10+ using System . Threading ;
1011using System . Threading . Tasks ;
1112using Appwrite . Converters ;
1213using Appwrite . Extensions ;
@@ -26,6 +27,7 @@ public class Client
2627 private string _endpoint ;
2728
2829 private static readonly int ChunkSize = 5 * 1024 * 1024 ;
30+ private static readonly int MaxConcurrentUploads = 8 ;
2931
3032 public static JsonSerializerOptions DeserializerOptions { get ; set ; } = new JsonSerializerOptions
3133 {
@@ -267,14 +269,14 @@ private HttpRequestMessage PrepareRequest(
267269 {
268270 if ( header . Key . Equals ( "content-type" , StringComparison . OrdinalIgnoreCase ) )
269271 {
270- _http . DefaultRequestHeaders . Accept . Add ( new MediaTypeWithQualityHeaderValue ( header . Value ) ) ;
272+ request . Headers . Accept . Add ( new MediaTypeWithQualityHeaderValue ( header . Value ) ) ;
271273 }
272274 else
273275 {
274- if ( _http . DefaultRequestHeaders . Contains ( header . Key ) ) {
275- _http . DefaultRequestHeaders . Remove ( header . Key ) ;
276+ if ( request . Headers . Contains ( header . Key ) ) {
277+ request . Headers . Remove ( header . Key ) ;
276278 }
277- _http . DefaultRequestHeaders . Add ( header . Key , header . Value ) ;
279+ request . Headers . Add ( header . Key , header . Value ) ;
278280 }
279281 }
280282
@@ -454,7 +456,6 @@ public async Task<T> ChunkedUpload<T>(
454456 {
455457 case "path" :
456458 var info = new FileInfo ( input . Path ) ;
457- input . Data = info . OpenRead ( ) ;
458459 size = info . Length ;
459460 break ;
460461 case "stream" :
@@ -480,6 +481,8 @@ public async Task<T> ChunkedUpload<T>(
480481 switch ( input . SourceType )
481482 {
482483 case "path" :
484+ buffer = System . IO . File . ReadAllBytes ( input . Path ) ;
485+ break ;
483486 case "stream" :
484487 var dataStream = input . Data as Stream ;
485488 if ( dataStream == null )
@@ -509,87 +512,215 @@ public async Task<T> ChunkedUpload<T>(
509512 ) ;
510513 }
511514
515+ var uploadId = string . Empty ;
516+
512517 if ( ! string . IsNullOrEmpty ( idParamName ) )
513518 {
514519 try
515520 {
516- // Make a request to check if a file already exists
517- var current = await Call < Dictionary < string , object ? > > (
518- method : "GET" ,
519- path : $ "{ path } /{ parameters [ idParamName ! ] } ",
520- new Dictionary < string , string > { { "content-type" , "application/json" } } ,
521- parameters : new Dictionary < string , object ? > ( )
522- ) ;
523- if ( current . TryGetValue ( "chunksUploaded" , out var chunksUploadedValue ) && chunksUploadedValue != null )
524- {
525- offset = Convert . ToInt64 ( chunksUploadedValue ) * ChunkSize ;
521+ // Make a request to check if a file already exists
522+ var current = await Call < Dictionary < string , object ? > > (
523+ method : "GET" ,
524+ path : $ "{ path } /{ parameters [ idParamName ! ] } ",
525+ new Dictionary < string , string > { { "content-type" , "application/json" } } ,
526+ parameters : new Dictionary < string , object ? > ( )
527+ ) ;
528+ if ( current . TryGetValue ( "chunksUploaded" , out var chunksUploadedValue ) && chunksUploadedValue != null )
529+ {
530+ offset = Convert . ToInt64 ( chunksUploadedValue ) * ChunkSize ;
531+ }
532+ result = current ;
533+ uploadId = parameters [ idParamName ! ] ? . ToString ( ) ?? string . Empty ;
526534 }
527- }
528535 catch
529536 {
530537 // ignored as it mostly means file not found
531538 }
532539 }
533540
534- while ( offset < size )
541+ var readLock = new object ( ) ;
542+
543+ async Task < byte [ ] > ReadChunkAsync ( long start , long end )
535544 {
545+ var length = ( int ) ( end - start ) ;
546+ var chunk = new byte [ length ] ;
547+
536548 switch ( input . SourceType )
537549 {
538550 case "path" :
551+ using ( var chunkStream = System . IO . File . OpenRead ( input . Path ) )
552+ {
553+ chunkStream . Seek ( start , SeekOrigin . Begin ) ;
554+ var read = 0 ;
555+ while ( read < length )
556+ {
557+ var count = await chunkStream . ReadAsync ( chunk , read , length - read ) ;
558+ if ( count == 0 )
559+ break ;
560+ read += count ;
561+ }
562+ }
563+ break ;
539564 case "stream" :
540565 var stream = input . Data as Stream ;
541566 if ( stream == null )
542567 throw new InvalidOperationException ( "Stream data is null" ) ;
543- stream . Seek ( offset , SeekOrigin . Begin ) ;
544- await stream . ReadAsync ( buffer , 0 , ChunkSize ) ;
568+ lock ( readLock )
569+ {
570+ stream . Seek ( start , SeekOrigin . Begin ) ;
571+ var read = 0 ;
572+ while ( read < length )
573+ {
574+ var count = stream . Read ( chunk , read , length - read ) ;
575+ if ( count == 0 )
576+ break ;
577+ read += count ;
578+ }
579+ }
545580 break ;
546581 case "bytes" :
547- buffer = ( ( byte [ ] ) input . Data )
548- . Skip ( ( int ) offset )
549- . Take ( ( int ) Math . Min ( size - offset , ChunkSize - 1 ) )
550- . ToArray ( ) ;
582+ Buffer . BlockCopy ( ( byte [ ] ) input . Data , ( int ) start , chunk , 0 , length ) ;
551583 break ;
552584 }
553585
554- var content = new MultipartFormDataContent {
555- { new ByteArrayContent ( buffer ) , paramName , input . Filename }
586+ return chunk ;
587+ }
588+
589+ async Task < Dictionary < string , object ? > > UploadChunkAsync ( int index , long start , long end , bool includeUploadId )
590+ {
591+ var chunkHeaders = new Dictionary < string , string > ( headers )
592+ {
593+ [ "Content-Range" ] = $ "bytes { start } -{ end - 1 } /{ size } "
556594 } ;
557595
558- parameters [ paramName ] = content ;
596+ if ( includeUploadId && ! string . IsNullOrEmpty ( uploadId ) )
597+ {
598+ chunkHeaders [ "x-appwrite-id" ] = uploadId ;
599+ }
600+
601+ var content = new MultipartFormDataContent {
602+ { new ByteArrayContent ( await ReadChunkAsync ( start , end ) ) , paramName , input . Filename }
603+ } ;
559604
560- headers [ "Content-Range" ] =
561- $ "bytes { offset } -{ Math . Min ( offset + ChunkSize - 1 , size - 1 ) } /{ size } ";
605+ var chunkParameters = new Dictionary < string , object ? > ( parameters )
606+ {
607+ [ paramName ] = content
608+ } ;
562609
563- result = await Call < Dictionary < string , object ? > > (
610+ var chunkResult = await Call < Dictionary < string , object ? > > (
564611 method : "POST" ,
565612 path ,
566- headers ,
567- parameters
613+ chunkHeaders ,
614+ chunkParameters
568615 ) ;
569616
570- offset += ChunkSize ;
617+ if ( index == 0 )
618+ {
619+ uploadId = chunkResult . ContainsKey ( "$id" )
620+ ? chunkResult [ "$id" ] ? . ToString ( ) ?? string . Empty
621+ : string . Empty ;
622+ }
623+
624+ return chunkResult ;
625+ }
626+
627+ if ( offset == 0 )
628+ {
629+ var firstChunkEnd = Math . Min ( ChunkSize , size ) ;
630+ result = await UploadChunkAsync ( 0 , 0 , firstChunkEnd , false ) ;
631+ offset = firstChunkEnd ;
571632
572- var id = result . ContainsKey ( "$id" )
573- ? result [ "$id" ] ? . ToString ( ) ?? string . Empty
574- : string . Empty ;
575633 var chunksTotal = result . TryGetValue ( "chunksTotal" , out var chunksTotalValue ) && chunksTotalValue != null
576634 ? Convert . ToInt64 ( chunksTotalValue )
577- : 0L ;
635+ : ( long ) Math . Ceiling ( size / ( double ) ChunkSize ) ;
578636 var chunksUploaded = result . TryGetValue ( "chunksUploaded" , out var chunksUploadedValue ) && chunksUploadedValue != null
579637 ? Convert . ToInt64 ( chunksUploadedValue )
580638 : 0L ;
581639
582- headers [ "x-appwrite-id" ] = id ;
583-
584640 onProgress ? . Invoke (
585641 new UploadProgress (
586- id : id ,
587- progress : Math . Min ( offset , size ) / size * 100 ,
642+ id : uploadId ,
643+ progress : Math . Min ( offset , size ) / ( double ) size * 100 ,
588644 sizeUploaded : Math . Min ( offset , size ) ,
589645 chunksTotal : chunksTotal ,
590646 chunksUploaded : chunksUploaded ) ) ;
591647 }
592648
649+ var chunks = new List < ( int Index , long Start , long End ) > ( ) ;
650+ var chunkOffset = offset ;
651+ var totalChunks = ( long ) Math . Ceiling ( size / ( double ) ChunkSize ) ;
652+ while ( chunkOffset < size )
653+ {
654+ var end = Math . Min ( chunkOffset + ChunkSize , size ) ;
655+ chunks . Add ( ( ( int ) ( chunkOffset / ChunkSize ) , chunkOffset , end ) ) ;
656+ chunkOffset = end ;
657+ }
658+
659+ if ( chunks . Count > 0 )
660+ {
661+ var nextChunk = - 1 ;
662+ var completedChunks = ( long ) ( offset / ChunkSize ) ;
663+ var uploadedBytes = offset ;
664+ var resultLock = new object ( ) ;
665+ var lastResult = result ;
666+ Dictionary < string , object ? > ? completedResult = null ;
667+
668+ bool IsUploadComplete ( Dictionary < string , object ? > chunkResult )
669+ {
670+ if ( ! chunkResult . TryGetValue ( "chunksUploaded" , out var chunksUploadedValue ) || chunksUploadedValue == null )
671+ {
672+ return false ;
673+ }
674+
675+ var chunksUploaded = Convert . ToInt64 ( chunksUploadedValue ) ;
676+ var chunksTotal = chunkResult . TryGetValue ( "chunksTotal" , out var chunksTotalValue ) && chunksTotalValue != null
677+ ? Convert . ToInt64 ( chunksTotalValue )
678+ : totalChunks ;
679+
680+ return chunksUploaded >= chunksTotal ;
681+ }
682+
683+ var workers = Enumerable . Range ( 0 , Math . Min ( MaxConcurrentUploads , chunks . Count ) )
684+ . Select ( async _ =>
685+ {
686+ while ( true )
687+ {
688+ var chunkIndex = Interlocked . Increment ( ref nextChunk ) ;
689+ if ( chunkIndex >= chunks . Count )
690+ break ;
691+
692+ var chunk = chunks [ chunkIndex ] ;
693+ var chunkResult = await UploadChunkAsync ( chunk . Index , chunk . Start , chunk . End , true ) ;
694+
695+ lock ( resultLock )
696+ {
697+ lastResult = chunkResult ;
698+ if ( IsUploadComplete ( chunkResult ) )
699+ {
700+ completedResult = chunkResult ;
701+ }
702+ }
703+
704+ var chunksUploaded = Interlocked . Increment ( ref completedChunks ) ;
705+ var sizeUploaded = Interlocked . Add ( ref uploadedBytes , chunk . End - chunk . Start ) ;
706+ var chunksTotal = chunkResult . TryGetValue ( "chunksTotal" , out var chunksTotalValue ) && chunksTotalValue != null
707+ ? Convert . ToInt64 ( chunksTotalValue )
708+ : totalChunks ;
709+
710+ onProgress ? . Invoke (
711+ new UploadProgress (
712+ id : uploadId ,
713+ progress : Math . Min ( sizeUploaded , size ) / ( double ) size * 100 ,
714+ sizeUploaded : Math . Min ( sizeUploaded , size ) ,
715+ chunksTotal : chunksTotal ,
716+ chunksUploaded : chunksUploaded ) ) ;
717+ }
718+ } ) ;
719+
720+ await Task . WhenAll ( workers ) ;
721+ result = completedResult ?? lastResult ;
722+ }
723+
593724 // Convert to non-nullable dictionary for converter
594725 var nonNullableResult = result . Where ( kvp => kvp . Value != null )
595726 . ToDictionary ( kvp => kvp . Key , kvp => kvp . Value ! ) ;
0 commit comments