@@ -779,6 +779,45 @@ static FileSystemItem CreateOnlineScenarioFileSystemItem(DtoScenarioEntry item)
779779 OnlineApiEndpointGroup = OnlineApiEndpointGroup . Scenarios ,
780780 } ;
781781
782+ static string SanitizeDownloadName ( string ? name , bool stripDirectoryComponents , bool stripExtension , string fallbackName )
783+ {
784+ var sanitizedName = name ?? string . Empty ;
785+ if ( stripDirectoryComponents )
786+ {
787+ sanitizedName = sanitizedName . Split ( [ '/' , '\\ ' ] , StringSplitOptions . RemoveEmptyEntries ) . LastOrDefault ( ) ?? string . Empty ;
788+ }
789+
790+ if ( stripExtension )
791+ {
792+ sanitizedName = Path . GetFileNameWithoutExtension ( sanitizedName ) ;
793+ }
794+
795+ sanitizedName = Path . GetInvalidFileNameChars ( ) . Aggregate ( sanitizedName , ( current , c ) => current . Replace ( c , '_' ) ) ;
796+ sanitizedName = sanitizedName
797+ . Replace ( '/' , '_' )
798+ . Replace ( '\\ ' , '_' )
799+ . Replace ( ".." , "__" , StringComparison . Ordinal ) ;
800+
801+ return string . IsNullOrWhiteSpace ( sanitizedName ) ? fallbackName : sanitizedName ;
802+ }
803+
804+ async Task ShowDownloadFailureDialogAsync ( string contentMessage )
805+ {
806+ var box = MessageBoxManager . GetMessageBoxStandard ( new MessageBoxStandardParams
807+ {
808+ ContentTitle = "Download failed" ,
809+ ContentMessage = contentMessage ,
810+ ButtonDefinitions = ButtonEnum . Ok ,
811+ Icon = Icon . Warning ,
812+ WindowStartupLocation = WindowStartupLocation . CenterOwner ,
813+ Topmost = true ,
814+ ShowInCenter = true ,
815+ SizeToContent = SizeToContent . WidthAndHeight ,
816+ MinHeight = 170 ,
817+ } ) ;
818+ _ = await box . ShowAsync ( ) ;
819+ }
820+
782821 async Task DownloadOnlineItemAsync ( FileSystemItem item )
783822 {
784823 if ( EditorContext . ObjectServiceClient == null || item . Id == null )
@@ -800,10 +839,24 @@ async Task DownloadOnlineItemAsync(FileSystemItem item)
800839 }
801840
802841 var extension = item . OnlineApiEndpointGroup == OnlineApiEndpointGroup . Scenarios ? ".SC5" : ".dat" ;
803- var safeName = Path . GetInvalidFileNameChars ( ) . Aggregate ( item . DisplayName , ( current , c ) => current . Replace ( c , '_' ) ) ;
842+ var safeName = SanitizeDownloadName ( item . DisplayName , stripDirectoryComponents : true , stripExtension : true , fallbackName : "download" ) ;
804843 var filename = Path . Combine ( EditorContext . Settings . DownloadFolder , $ "{ safeName } -{ item . Id } { extension } ") ;
805- await File . WriteAllBytesAsync ( filename , fileBytes ) ;
806- EditorContext . Logger . Info ( $ "Downloaded \" { item . DisplayName } \" to \" { filename } \" ") ;
844+ try
845+ {
846+ await using var outputStream = new FileStream ( filename , FileMode . CreateNew , FileAccess . Write , FileShare . None , 4096 , useAsync : true ) ;
847+ await outputStream . WriteAsync ( fileBytes ) ;
848+ EditorContext . Logger . Info ( $ "Downloaded \" { item . DisplayName } \" to \" { filename } \" ") ;
849+ }
850+ catch ( IOException ex )
851+ {
852+ EditorContext . Logger . Error ( $ "Failed to download \" { item . DisplayName } \" to \" { filename } \" ", ex ) ;
853+ await ShowDownloadFailureDialogAsync ( $ "Could not create:\n { filename } \n \n The destination file may already exist, be locked by another process, or the download folder may be unavailable.") ;
854+ }
855+ catch ( UnauthorizedAccessException ex )
856+ {
857+ EditorContext . Logger . Error ( $ "Failed to download \" { item . DisplayName } \" to \" { filename } \" due to insufficient permissions", ex ) ;
858+ await ShowDownloadFailureDialogAsync ( $ "You do not have permission to write to:\n { filename } \n \n Please check folder permissions or choose a different download folder.") ;
859+ }
807860 }
808861
809862 async Task DownloadOnlinePackAsync ( OnlineItemPackBrowseResult pack )
@@ -826,7 +879,7 @@ async Task DownloadOnlinePackAsync(OnlineItemPackBrowseResult pack)
826879 return ;
827880 }
828881
829- var safePackName = Path . GetInvalidFileNameChars ( ) . Aggregate ( pack . Name , ( current , c ) => current . Replace ( c , '_' ) ) ;
882+ var safePackName = SanitizeDownloadName ( pack . Name , stripDirectoryComponents : false , stripExtension : false , fallbackName : "pack" ) ;
830883 var filename = Path . Combine ( EditorContext . Settings . DownloadFolder , $ "{ safePackName } -{ pack . Id } .zip") ;
831884 try
832885 {
@@ -837,19 +890,7 @@ async Task DownloadOnlinePackAsync(OnlineItemPackBrowseResult pack)
837890 catch ( IOException ex )
838891 {
839892 EditorContext . Logger . Error ( $ "Failed to download pack \" { pack . Name } \" to \" { filename } \" ", ex ) ;
840- var box = MessageBoxManager . GetMessageBoxStandard ( new MessageBoxStandardParams
841- {
842- ContentTitle = "Download failed" ,
843- ContentMessage = $ "Could not create:\n { filename } \n \n A file may already exist or be locked by another process.",
844- ButtonDefinitions = ButtonEnum . Ok ,
845- Icon = Icon . Warning ,
846- WindowStartupLocation = WindowStartupLocation . CenterOwner ,
847- Topmost = true ,
848- ShowInCenter = true ,
849- SizeToContent = SizeToContent . WidthAndHeight ,
850- MinHeight = 170 ,
851- } ) ;
852- _ = await box . ShowAsync ( ) ;
893+ await ShowDownloadFailureDialogAsync ( $ "Could not create:\n { filename } \n \n The destination file may already exist, be locked by another process, or the download folder may be unavailable.") ;
853894 }
854895 }
855896}
0 commit comments