@@ -33,7 +33,7 @@ use headers::HeaderMap;
3333use std:: path;
3434use std:: sync:: OnceLock ;
3535use std:: { net:: SocketAddr , path:: PathBuf } ;
36- use tokio:: io:: AsyncSeekExt ;
36+ use tokio:: io:: { AsyncSeekExt , AsyncWriteExt } ;
3737
3838use std:: { collections:: HashMap , sync:: Arc } ;
3939use std:: pin:: Pin ;
@@ -558,6 +558,7 @@ async fn ax_post_file(
558558 let mut path: String = "" . to_string ( ) ;
559559 let mut file0_filename: String = "" . to_string ( ) ;
560560 let mut upload_result: Option < ( StatusCode , Vec < u8 > ) > = None ;
561+ let mut buffered_file: Option < tempfile:: NamedTempFile > = None ;
561562
562563 while let Some ( field) = multipart. next_field ( ) . await . unwrap ( ) {
563564 let name = field. name ( ) . unwrap ( ) . to_string ( ) ;
@@ -580,7 +581,75 @@ async fn ax_post_file(
580581 Some ( fname) => {
581582 file0_filename = fname. to_string ( ) ;
582583
583- // At this point we have path and filename, so we can verify permissions and start streaming
584+ if path. is_empty ( ) {
585+ // BUFFERED PATH: file0 arrived before path, buffer to temp file
586+ debug_log ! ( "file0 arrived before path, buffering to temp file" ) ;
587+ let tmp = match tempfile:: NamedTempFile :: new ( ) {
588+ Ok ( f) => f,
589+ Err ( e) => {
590+ eprintln ! ( "Failed to create temp file: {:?}" , e) ;
591+ upload_result = Some ( (
592+ StatusCode :: INTERNAL_SERVER_ERROR ,
593+ "Failed to create temp file" . to_string ( ) . into_bytes ( ) ,
594+ ) ) ;
595+ break ;
596+ }
597+ } ;
598+ let tmp_path = tmp. path ( ) . to_path_buf ( ) ;
599+ let mut async_file = match tokio:: fs:: File :: create ( & tmp_path) . await {
600+ Ok ( f) => f,
601+ Err ( e) => {
602+ eprintln ! ( "Failed to open temp file for writing: {:?}" , e) ;
603+ upload_result = Some ( (
604+ StatusCode :: INTERNAL_SERVER_ERROR ,
605+ "Failed to open temp file" . to_string ( ) . into_bytes ( ) ,
606+ ) ) ;
607+ break ;
608+ }
609+ } ;
610+ let mut field_stream = FieldStream :: new ( field) ;
611+ let mut buf = [ 0u8 ; 64 * 1024 ] ;
612+ loop {
613+ use tokio:: io:: AsyncReadExt ;
614+ let n = match field_stream. read ( & mut buf) . await {
615+ Ok ( 0 ) => break ,
616+ Ok ( n) => n,
617+ Err ( e) => {
618+ eprintln ! ( "Error reading file0 field: {:?}" , e) ;
619+ upload_result = Some ( (
620+ StatusCode :: BAD_REQUEST ,
621+ "Error reading file data" . to_string ( ) . into_bytes ( ) ,
622+ ) ) ;
623+ break ;
624+ }
625+ } ;
626+ if let Err ( e) = async_file. write_all ( & buf[ ..n] ) . await {
627+ eprintln ! ( "Error writing temp file: {:?}" , e) ;
628+ upload_result = Some ( (
629+ StatusCode :: INTERNAL_SERVER_ERROR ,
630+ "Error writing temp file" . to_string ( ) . into_bytes ( ) ,
631+ ) ) ;
632+ break ;
633+ }
634+ }
635+ if upload_result. is_some ( ) {
636+ break ;
637+ }
638+ if let Err ( e) = async_file. flush ( ) . await {
639+ eprintln ! ( "Error flushing temp file: {:?}" , e) ;
640+ upload_result = Some ( (
641+ StatusCode :: INTERNAL_SERVER_ERROR ,
642+ "Error flushing temp file" . to_string ( ) . into_bytes ( ) ,
643+ ) ) ;
644+ break ;
645+ }
646+ drop ( async_file) ;
647+ buffered_file = Some ( tmp) ;
648+ // Continue loop to find path field
649+ continue ;
650+ }
651+
652+ // FAST PATH: path already set, stream directly
584653 // if path ends on /, remove it
585654 if path. ends_with ( "/" ) {
586655 debug_log ! ( "Removing trailing /, workaround" ) ;
@@ -695,6 +764,106 @@ async fn ax_post_file(
695764 }
696765 }
697766
767+ // Handle buffered file0 case (file0 arrived before path)
768+ if upload_result. is_none ( ) {
769+ if let Some ( tmp) = buffered_file {
770+ if path. is_empty ( ) {
771+ return (
772+ StatusCode :: BAD_REQUEST ,
773+ b"Missing path field in upload" . to_vec ( ) ,
774+ ) ;
775+ }
776+
777+ if path. ends_with ( "/" ) {
778+ debug_log ! ( "Removing trailing /, workaround" ) ;
779+ path. pop ( ) ;
780+ }
781+
782+ let full_path = format ! ( "{}/{}" , path, file0_filename) ;
783+
784+ match verify_upload_permissions ( & owner, & path) {
785+ Ok ( _) => ( ) ,
786+ Err ( e) => {
787+ return ( StatusCode :: FORBIDDEN , e. to_string ( ) . into_bytes ( ) ) ;
788+ }
789+ }
790+
791+ let hdr_content_type = headers. get ( "Content-Type-Upstream" ) ;
792+ let semaphore = get_or_create_semaphore ( & state. file_locks , & full_path) . await ;
793+
794+ let _permit = match semaphore. try_acquire ( ) {
795+ Ok ( permit) => permit,
796+ Err ( _) => {
797+ return (
798+ StatusCode :: CONFLICT ,
799+ "Upload already in progress" . to_string ( ) . into_bytes ( ) ,
800+ ) ;
801+ }
802+ } ;
803+
804+ let content_type: String = match hdr_content_type {
805+ Some ( content_type) => content_type. to_str ( ) . unwrap ( ) . to_string ( ) ,
806+ None => {
807+ let heuristic_ctype = heuristic_filetype ( file0_filename. clone ( ) ) ;
808+ debug_log ! (
809+ "Content-Type not found, using heuristics: {}" ,
810+ heuristic_ctype
811+ ) ;
812+ heuristic_ctype
813+ }
814+ } ;
815+
816+ debug_log ! ( "Starting buffered upload for {}" , full_path) ;
817+ let mut async_file = match tokio:: fs:: File :: open ( tmp. path ( ) ) . await {
818+ Ok ( f) => f,
819+ Err ( e) => {
820+ eprintln ! ( "Failed to reopen temp file: {:?}" , e) ;
821+ return (
822+ StatusCode :: INTERNAL_SERVER_ERROR ,
823+ "Failed to reopen temp file" . to_string ( ) . into_bytes ( ) ,
824+ ) ;
825+ }
826+ } ;
827+
828+ let driver_name = get_driver_type ( ) ;
829+ let driver = init_driver ( & driver_name) ;
830+
831+ let ( result, file_size) = driver
832+ . write_file_streaming (
833+ full_path. clone ( ) ,
834+ & mut async_file,
835+ content_type. to_string ( ) ,
836+ Some ( owner. clone ( ) ) ,
837+ )
838+ . await ;
839+
840+ // tmp (NamedTempFile) is dropped here, auto-deleting the temp file
841+
842+ if result. is_empty ( ) {
843+ return ( StatusCode :: CONFLICT , Vec :: new ( ) ) ;
844+ }
845+
846+ let status = StatusCode :: OK ;
847+ let client_ip = client_ip_from_headers ( & headers, remote_addr) ;
848+ let timestamp = std:: time:: SystemTime :: now ( ) ;
849+ let human_time = chrono:: DateTime :: < chrono:: Utc > :: from ( timestamp) ;
850+ let request_target = original_uri. to_string ( ) ;
851+ println ! (
852+ "{} {} {} {} {} {} {} {} buggyclient" ,
853+ client_ip,
854+ status. as_u16( ) ,
855+ file_size,
856+ human_time,
857+ Method :: POST ,
858+ request_target,
859+ full_path,
860+ owner
861+ ) ;
862+
863+ return ( status, Vec :: new ( ) ) ;
864+ }
865+ }
866+
698867 // Return the upload result if we processed the file
699868 if let Some ( result) = upload_result {
700869 return result;
0 commit comments