@@ -7,6 +7,7 @@ namespace ImperatorToCK3.CommonUtils;
77using System ;
88using Polly ;
99using System . IO ;
10+ using System . Linq ;
1011using System . Text ;
1112
1213public static class FileHelper {
@@ -71,6 +72,52 @@ public static void DeleteWithRetries(string filePath) {
7172 }
7273 }
7374
75+ public static void DeleteDirectoryWithRetries ( string directoryPath ) {
76+ if ( string . IsNullOrEmpty ( directoryPath ) || ! Directory . Exists ( directoryPath ) ) {
77+ return ;
78+ }
79+
80+ const int maxAttempts = 10 ;
81+ int currentAttempt = 0 ;
82+
83+ var policy = Policy
84+ . Handle < IOException > ( IsFilesSharingViolation )
85+ . Or < UnauthorizedAccessException > ( )
86+ . WaitAndRetry ( maxAttempts ,
87+ sleepDurationProvider : _ => TimeSpan . FromSeconds ( 1 ) ,
88+ onRetry : ( _ , _ , _ ) => {
89+ currentAttempt ++ ;
90+ Logger . Warn ( $ "Attempt { currentAttempt } to delete directory \" { directoryPath } \" failed.") ;
91+ Logger . Warn ( CloseProgramsHint ) ;
92+ } ) ;
93+
94+ try {
95+ policy . Execute ( ( ) => {
96+ ResetAttributesRecursively ( directoryPath ) ;
97+ Directory . Delete ( directoryPath , recursive : true ) ;
98+ } ) ;
99+ } catch ( IOException ex ) when ( IsFilesSharingViolation ( ex ) ) {
100+ Logger . Debug ( ex . ToString ( ) ) ;
101+ throw new UserErrorException ( $ "Failed to delete directory \" { directoryPath } \" . { CloseProgramsHint } ") ;
102+ } catch ( UnauthorizedAccessException ex ) {
103+ Logger . Debug ( ex . ToString ( ) ) ;
104+ throw new UserErrorException ( $ "Failed to delete directory \" { directoryPath } \" : { ex . Message } ") ;
105+ }
106+ }
107+
108+ private static void ResetAttributesRecursively ( string directoryPath ) {
109+ File . SetAttributes ( directoryPath , FileAttributes . Normal ) ;
110+
111+ foreach ( var filePath in Directory . EnumerateFiles ( directoryPath , "*" , SearchOption . AllDirectories ) ) {
112+ File . SetAttributes ( filePath , FileAttributes . Normal ) ;
113+ }
114+
115+ foreach ( var subdirectoryPath in Directory . EnumerateDirectories ( directoryPath , "*" , SearchOption . AllDirectories )
116+ . OrderByDescending ( path => path . Length ) ) {
117+ File . SetAttributes ( subdirectoryPath , FileAttributes . Normal ) ;
118+ }
119+ }
120+
74121 // Ensures that the given directory path exists. If a file exists with the
75122 // same name as the desired directory it will be removed first. The method
76123 // retries the creation when a sharing violation occurs, much like the
@@ -134,4 +181,28 @@ public static void MoveWithRetries(string sourceFilePath, string destFilePath) {
134181 throw new UserErrorException ( $ "Failed to move \" { sourceFilePath } \" to \" { destFilePath } \" . { CloseProgramsHint } ") ;
135182 }
136183 }
184+
185+ /// <summary>
186+ /// Makes an existing regular file writable by the current user.
187+ /// On Windows this clears the read-only attribute flag; on macOS and Linux
188+ /// it adds the user-write bit directly via the POSIX file mode so that
189+ /// the change takes effect even when the Win32-style attribute mapping
190+ /// does not round-trip correctly on the host OS.
191+ /// Does nothing when the file does not exist or when the path refers to a
192+ /// directory rather than a regular file.
193+ /// </summary>
194+ public static void EnsureFileIsWritable ( string filePath ) {
195+ if ( ! File . Exists ( filePath ) ) {
196+ return ;
197+ }
198+
199+ if ( OperatingSystem . IsWindows ( ) ) {
200+ File . SetAttributes ( filePath , FileAttributes . Normal ) ;
201+ } else {
202+ var mode = File . GetUnixFileMode ( filePath ) ;
203+ if ( ! mode . HasFlag ( UnixFileMode . UserWrite ) ) {
204+ File . SetUnixFileMode ( filePath , mode | UnixFileMode . UserWrite ) ;
205+ }
206+ }
207+ }
137208}
0 commit comments