11using System ;
22using System . Diagnostics ;
33using System . IO ;
4+ using System . Runtime . InteropServices ;
5+ using Serilog ;
46
57namespace Yellowcake . Helpers ;
68
79public static class JunctionManager
810{
9- public static void Create ( string junctionPoint , string targetDir , bool overwrite )
11+ public static bool IsSupported => RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ||
12+ RuntimeInformation . IsOSPlatform ( OSPlatform . Linux ) ||
13+ RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) ;
14+
15+ public static void Create ( string junctionPoint , string targetDir , bool overwrite = false )
1016 {
1117 if ( string . IsNullOrWhiteSpace ( junctionPoint ) || string . IsNullOrWhiteSpace ( targetDir ) )
1218 throw new ArgumentException ( "Junction and target paths cannot be empty." ) ;
1319
14- string normalizedTarget = Path . GetFullPath ( targetDir ) . TrimEnd ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar ) ;
15- string normalizedJunction = Path . GetFullPath ( junctionPoint ) . TrimEnd ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar ) ;
20+ var normalizedTarget = Path . GetFullPath ( targetDir ) . TrimEnd ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar ) ;
21+ var normalizedJunction = Path . GetFullPath ( junctionPoint ) . TrimEnd ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar ) ;
1622
1723 if ( ! Directory . Exists ( normalizedTarget ) )
1824 throw new DirectoryNotFoundException ( $ "Target directory does not exist: { normalizedTarget } ") ;
1925
2026 PrepareJunctionPoint ( normalizedJunction , overwrite ) ;
21-
2227 EnsureParentDirectoryExists ( normalizedJunction ) ;
2328
24- ExecuteMkLink ( normalizedJunction , normalizedTarget ) ;
29+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
30+ {
31+ CreateWindowsJunction ( normalizedJunction , normalizedTarget ) ;
32+ }
33+ else if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Linux ) || RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
34+ {
35+ CreateUnixSymlink ( normalizedJunction , normalizedTarget ) ;
36+ }
37+ else
38+ {
39+ throw new PlatformNotSupportedException ( "Symbolic links are not supported on this platform." ) ;
40+ }
41+
42+ Log . Information ( "Created junction/symlink: {Junction} -> {Target}" , normalizedJunction , normalizedTarget ) ;
2543 }
2644
2745 public static void Remove ( string junctionPoint )
2846 {
29- if ( ! Directory . Exists ( junctionPoint ) ) return ;
47+ if ( ! Directory . Exists ( junctionPoint ) && ! File . Exists ( junctionPoint ) ) return ;
3048
31- var di = new DirectoryInfo ( junctionPoint ) ;
32- if ( di . Attributes . HasFlag ( FileAttributes . ReparsePoint ) )
49+ try
3350 {
34- di . Delete ( ) ;
51+ if ( IsSymbolicLink ( junctionPoint ) )
52+ {
53+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
54+ {
55+ var di = new DirectoryInfo ( junctionPoint ) ;
56+ di . Delete ( ) ;
57+ }
58+ else
59+ {
60+ File . Delete ( junctionPoint ) ;
61+ }
62+
63+ Log . Information ( "Removed junction/symlink: {Path}" , junctionPoint ) ;
64+ }
65+ else
66+ {
67+ Directory . Delete ( junctionPoint , true ) ;
68+ Log . Information ( "Removed directory: {Path}" , junctionPoint ) ;
69+ }
3570 }
36- else
71+ catch ( Exception ex )
72+ {
73+ Log . Error ( ex , "Failed to remove junction/symlink: {Path}" , junctionPoint ) ;
74+ throw ;
75+ }
76+ }
77+
78+ public static bool IsSymbolicLink ( string path )
79+ {
80+ try
3781 {
38- Directory . Delete ( junctionPoint , true ) ;
82+ if ( Directory . Exists ( path ) )
83+ {
84+ var di = new DirectoryInfo ( path ) ;
85+ return di . Attributes . HasFlag ( FileAttributes . ReparsePoint ) ;
86+ }
87+
88+ if ( File . Exists ( path ) )
89+ {
90+ var fi = new FileInfo ( path ) ;
91+ return fi . Attributes . HasFlag ( FileAttributes . ReparsePoint ) ;
92+ }
3993 }
94+ catch ( Exception ex )
95+ {
96+ Log . Debug ( ex , "Error checking if path is symbolic link: {Path}" , path ) ;
97+ }
98+
99+ return false ;
40100 }
41101
42102 private static void PrepareJunctionPoint ( string path , bool overwrite )
43103 {
44- if ( ! Directory . Exists ( path ) ) return ;
104+ if ( ! Directory . Exists ( path ) && ! File . Exists ( path ) ) return ;
45105
46106 if ( ! overwrite )
47107 throw new InvalidOperationException ( $ "The junction point already exists: { path } ") ;
@@ -51,19 +111,20 @@ private static void PrepareJunctionPoint(string path, bool overwrite)
51111
52112 private static void EnsureParentDirectoryExists ( string path )
53113 {
54- string ? parent = Path . GetDirectoryName ( path ) ;
114+ var parent = Path . GetDirectoryName ( path ) ;
55115 if ( parent != null && ! Directory . Exists ( parent ) )
56116 {
57117 Directory . CreateDirectory ( parent ) ;
118+ Log . Debug ( "Created parent directory: {Path}" , parent ) ;
58119 }
59120 }
60121
61- private static void ExecuteMkLink ( string junction , string target )
122+ private static void CreateWindowsJunction ( string junction , string target )
62123 {
63124 var psi = new ProcessStartInfo
64125 {
65126 FileName = "cmd.exe" ,
66- Arguments = $ "/c mklink /j \" { junction } \" \" { target } \" ",
127+ Arguments = $ "/c mklink /J \" { junction } \" \" { target } \" ",
67128 WindowStyle = ProcessWindowStyle . Hidden ,
68129 CreateNoWindow = true ,
69130 UseShellExecute = false ,
@@ -74,20 +135,96 @@ private static void ExecuteMkLink(string junction, string target)
74135 try
75136 {
76137 using var process = Process . Start ( psi ) ;
77- if ( process == null ) throw new Exception ( "Failed to start the junction creation process." ) ;
138+ if ( process == null )
139+ throw new InvalidOperationException ( "Failed to start the junction creation process." ) ;
78140
79141 process . WaitForExit ( ) ;
80142
81143 if ( process . ExitCode != 0 )
82144 {
83- string error = process . StandardError . ReadToEnd ( ) ;
84- if ( string . IsNullOrWhiteSpace ( error ) ) error = process . StandardOutput . ReadToEnd ( ) ;
85- throw new Exception ( $ "mklink failed with exit code { process . ExitCode } : { error . Trim ( ) } ") ;
145+ var error = process . StandardError . ReadToEnd ( ) ;
146+ if ( string . IsNullOrWhiteSpace ( error ) )
147+ error = process . StandardOutput . ReadToEnd ( ) ;
148+
149+ throw new InvalidOperationException ( $ "mklink failed with exit code { process . ExitCode } : { error . Trim ( ) } ") ;
86150 }
87151 }
88152 catch ( System . ComponentModel . Win32Exception ex )
89153 {
90- throw new Exception ( "System error: Unable to execute mklink. Check your permissions." , ex ) ;
154+ throw new InvalidOperationException ( "Unable to execute mklink. Administrator privileges may be required." , ex ) ;
155+ }
156+ }
157+
158+ private static void CreateUnixSymlink ( string symlink , string target )
159+ {
160+ var psi = new ProcessStartInfo
161+ {
162+ FileName = "/bin/ln" ,
163+ Arguments = $ "-s \" { target } \" \" { symlink } \" ",
164+ WindowStyle = ProcessWindowStyle . Hidden ,
165+ CreateNoWindow = true ,
166+ UseShellExecute = false ,
167+ RedirectStandardError = true ,
168+ RedirectStandardOutput = true
169+ } ;
170+
171+ try
172+ {
173+ using var process = Process . Start ( psi ) ;
174+ if ( process == null )
175+ throw new InvalidOperationException ( "Failed to start the symlink creation process." ) ;
176+
177+ process . WaitForExit ( ) ;
178+
179+ if ( process . ExitCode != 0 )
180+ {
181+ var error = process . StandardError . ReadToEnd ( ) ;
182+ if ( string . IsNullOrWhiteSpace ( error ) )
183+ error = process . StandardOutput . ReadToEnd ( ) ;
184+
185+ throw new InvalidOperationException ( $ "ln failed with exit code { process . ExitCode } : { error . Trim ( ) } ") ;
186+ }
187+ }
188+ catch ( System . ComponentModel . Win32Exception ex )
189+ {
190+ throw new InvalidOperationException ( "Unable to execute ln command. Check permissions." , ex ) ;
191+ }
192+ }
193+
194+ public static string ? GetTarget ( string junctionPoint )
195+ {
196+ try
197+ {
198+ if ( ! IsSymbolicLink ( junctionPoint ) ) return null ;
199+
200+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
201+ {
202+ return new DirectoryInfo ( junctionPoint ) . LinkTarget ;
203+ }
204+ else
205+ {
206+ var psi = new ProcessStartInfo
207+ {
208+ FileName = "/bin/readlink" ,
209+ Arguments = $ "\" { junctionPoint } \" ",
210+ UseShellExecute = false ,
211+ RedirectStandardOutput = true ,
212+ CreateNoWindow = true
213+ } ;
214+
215+ using var process = Process . Start ( psi ) ;
216+ if ( process == null ) return null ;
217+
218+ var output = process . StandardOutput . ReadToEnd ( ) . Trim ( ) ;
219+ process . WaitForExit ( ) ;
220+
221+ return process . ExitCode == 0 ? output : null ;
222+ }
223+ }
224+ catch ( Exception ex )
225+ {
226+ Log . Debug ( ex , "Failed to get junction target for: {Path}" , junctionPoint ) ;
227+ return null ;
91228 }
92229 }
93230}
0 commit comments