@@ -3,11 +3,17 @@ use std::{
33 process:: Stdio ,
44} ;
55
6+ use fs_err:: PathExt as _;
67use miette:: { Context as _, IntoDiagnostic } ;
8+ use normalize_path:: NormalizePath as _;
79use which:: which_global;
810
911use crate :: util:: PathExt as _;
1012
13+ /// Struct that keeps track of the deployment and undeployment process of multiple symlinks.
14+ ///
15+ /// We keep track of all created symlinks, as well as all symlinks where the creation or deletion failed due to insufficient permissions.
16+ /// In case of missing permissions, you can then use [`Deployer::try_run_elevated()`] to retry the operation with elevated privileges.
1117#[ derive( Default , Debug ) ]
1218pub struct Deployer {
1319 /// Symlinks that were successfully created
@@ -53,9 +59,8 @@ impl Deployer {
5359 if err. kind ( ) != std:: io:: ErrorKind :: PermissionDenied {
5460 return Err ( err) . into_diagnostic ( ) . wrap_err_with ( || {
5561 format ! (
56- "Failed to create symlink at {} -> {}" ,
57- link. abbr( ) ,
58- original. abbr( )
62+ "Failed to create symlink at {}" ,
63+ format_symlink( link. abbr( ) , original. abbr( ) )
5964 )
6065 } ) ?;
6166 }
@@ -69,7 +74,7 @@ impl Deployer {
6974 /// Remove a symlink from at the path `link` pointing to the `original` file.
7075 pub fn delete_symlink ( & mut self , path : impl AsRef < Path > ) -> miette:: Result < ( ) > {
7176 let path = path. as_ref ( ) ;
72- tracing:: trace!( "Planning to delete symlink at {}" , path. abbr( ) ) ;
77+ tracing:: trace!( "Deleting symlink at {}" , path. abbr( ) ) ;
7378 if !path. is_symlink ( ) {
7479 miette:: bail!( "Path is not a symlink: {}" , path. abbr( ) ) ;
7580 }
@@ -101,6 +106,141 @@ impl Deployer {
101106 Ok ( ( ) )
102107 }
103108
109+ /// Set up a symlink from the given `link_path` to the given `actual_path`, recursively.
110+ /// Also takes the `egg_root` dir, to ensure we can safely delete any stale symlinks on the way there.
111+ ///
112+ /// Requires all paths to be absolute, will panic otherwise.
113+ ///
114+ /// This means:
115+ /// - If `link_path` exists and is a file, abort
116+ /// - If `link_path` exists and is a symlink into the egg dir, remove the symlink and then continue.
117+ /// - If `actual_path` is a file, symlink.
118+ /// - If `actual_path` is a directory that does not exist in `link_path`, symlink it.
119+ /// - If `actual_path` is a directory that already exists in `link_path`, recurse into it and `symlink_recursive` `actual_path`s children.
120+ #[ tracing:: instrument( skip_all, fields(
121+ egg_root = egg_root. as_ref( ) . abbr( ) ,
122+ actual_path = actual_path. as_ref( ) . abbr( ) ,
123+ link_path = link_path. as_ref( ) . abbr( )
124+ ) ) ]
125+ pub fn symlink_recursive (
126+ & mut self ,
127+ egg_root : impl AsRef < Path > ,
128+ actual_path : impl AsRef < Path > ,
129+ link_path : & impl AsRef < Path > ,
130+ ) -> miette:: Result < ( ) > {
131+ fn inner (
132+ deployer : & mut Deployer ,
133+ egg_root : PathBuf ,
134+ actual_path : PathBuf ,
135+ link_path : PathBuf ,
136+ ) -> miette:: Result < ( ) > {
137+ let actual_path = actual_path. normalize ( ) ;
138+ let link_path = link_path. normalize ( ) ;
139+ let egg_root = egg_root. normalize ( ) ;
140+ link_path. assert_absolute ( "link_path" ) ;
141+ actual_path. assert_absolute ( "actual_path" ) ;
142+ actual_path. assert_starts_with ( & egg_root, "actual_path" ) ;
143+ tracing:: trace!(
144+ "symlink_recursive({}, {})" ,
145+ actual_path. abbr( ) ,
146+ link_path. abbr( )
147+ ) ;
148+
149+ let actual_path = actual_path. canonical ( ) ?;
150+
151+ if link_path. is_symlink ( ) {
152+ let link_target = link_path. fs_err_read_link ( ) . into_diagnostic ( ) ?;
153+ if link_target == actual_path {
154+ deployer. add_created_symlink ( link_path) ;
155+ return Ok ( ( ) ) ;
156+ } else if link_target. exists ( ) {
157+ miette:: bail!(
158+ "Failed to create symlink {}, as a file already exists there" ,
159+ format_symlink( link_path. abbr( ) , actual_path. abbr( ) )
160+ ) ;
161+ } else if link_target. starts_with ( & egg_root) {
162+ tracing:: info!(
163+ "Removing dead symlink {}" ,
164+ format_symlink( link_path. abbr( ) , link_target. abbr( ) )
165+ ) ;
166+ deployer. delete_symlink ( & link_path) ?;
167+ cov_mark:: hit!( remove_dead_symlink) ;
168+ // After we've removed that file, creating the symlink later will succeed!
169+ } else {
170+ miette:: bail!(
171+ "Encountered dead symlink, but it doesn't target the egg dir: {}" ,
172+ link_path. abbr( ) ,
173+ ) ;
174+ }
175+ } else if link_path. exists ( ) {
176+ tracing:: trace!( "link_path exists as non-symlink {}" , link_path. abbr( ) ) ;
177+ if link_path. is_dir ( ) && actual_path. is_dir ( ) {
178+ for entry in actual_path. fs_err_read_dir ( ) . into_diagnostic ( ) ? {
179+ let entry = entry. into_diagnostic ( ) ?;
180+ deployer. symlink_recursive (
181+ & egg_root,
182+ entry. path ( ) ,
183+ & link_path. join ( entry. file_name ( ) ) ,
184+ ) ?;
185+ }
186+ return Ok ( ( ) ) ;
187+ } else if link_path. is_dir ( ) || actual_path. is_dir ( ) {
188+ miette:: bail!(
189+ "Conflicting file or directory {} with {}" ,
190+ actual_path. abbr( ) ,
191+ link_path. abbr( )
192+ ) ;
193+ }
194+ }
195+ deployer. create_symlink ( & actual_path, & link_path) ?;
196+ tracing:: info!(
197+ "created symlink {}" ,
198+ format_symlink( link_path. abbr( ) , actual_path. abbr( ) ) ,
199+ ) ;
200+ Ok ( ( ) )
201+ }
202+ inner (
203+ self ,
204+ egg_root. as_ref ( ) . to_path_buf ( ) ,
205+ actual_path. as_ref ( ) . to_path_buf ( ) ,
206+ link_path. as_ref ( ) . to_path_buf ( ) ,
207+ )
208+ }
209+
210+ #[ tracing:: instrument( skip( actual_path, link_path) , fields(
211+ actual_path = actual_path. as_ref( ) . abbr( ) ,
212+ link_path = link_path. as_ref( ) . abbr( )
213+ ) ) ]
214+ pub fn remove_symlink_recursive (
215+ & mut self ,
216+ actual_path : impl AsRef < Path > ,
217+ link_path : & impl AsRef < Path > ,
218+ ) -> miette:: Result < ( ) > {
219+ let actual_path = actual_path. as_ref ( ) ;
220+ let link_path = link_path. as_ref ( ) ;
221+ if link_path. is_symlink ( ) && link_path. canonical ( ) ? == actual_path {
222+ tracing:: info!(
223+ "Removing symlink {}" ,
224+ format_symlink( link_path. abbr( ) , actual_path. abbr( ) )
225+ ) ;
226+ self . delete_symlink ( link_path) ?;
227+ } else if link_path. is_dir ( ) && actual_path. is_dir ( ) {
228+ for entry in actual_path. fs_err_read_dir ( ) . into_diagnostic ( ) ? {
229+ let entry = entry. into_diagnostic ( ) ?;
230+ self . remove_symlink_recursive ( entry. path ( ) , & link_path. join ( entry. file_name ( ) ) ) ?;
231+ }
232+ } else if link_path. exists ( ) {
233+ miette:: bail!(
234+ help = "Yolk will only try to remove files that are symlinks pointing into the corresponding egg." ,
235+ "Tried to remove deployment of {}, but {} doesn't link to it" ,
236+ actual_path. abbr( ) ,
237+ link_path. abbr( )
238+ ) ;
239+ }
240+ Ok ( ( ) )
241+ }
242+
243+ /// Retry running symlink creation and deletion with root priviledges.
104244 pub fn try_run_elevated ( self ) -> miette:: Result < ( ) > {
105245 if self . missing_permissions_create . is_empty ( ) && self . missing_permissions_remove . is_empty ( )
106246 {
@@ -181,9 +321,8 @@ pub fn create_symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> mie
181321 . into_diagnostic ( )
182322 . wrap_err_with ( || {
183323 format ! (
184- "Failed to create symlink at {} -> {}" ,
185- link. abbr( ) ,
186- original. abbr( )
324+ "Failed to create symlink at {}" ,
325+ format_symlink( link. abbr( ) , original. abbr( ) )
187326 )
188327 } ) ?;
189328 Ok ( ( ) )
@@ -233,3 +372,11 @@ fn try_sudo(args: &[String]) -> miette::Result<()> {
233372 }
234373 Ok ( ( ) )
235374}
375+
376+ fn format_symlink ( link_path : impl AsRef < Path > , original_path : impl AsRef < Path > ) -> String {
377+ format ! (
378+ "{} -> {}" ,
379+ link_path. as_ref( ) . display( ) ,
380+ original_path. as_ref( ) . display( )
381+ )
382+ }
0 commit comments