@@ -32,6 +32,8 @@ pub struct CopyOp<
3232 force : bool ,
3333 #[ builder( default = false ) ]
3434 follow_symlinks : bool ,
35+ #[ builder( default = false ) ]
36+ hard_link : bool ,
3537 #[ builder( skip) ]
3638 _marker1 : PhantomData < & ' a I1 > ,
3739 #[ builder( skip) ]
5254 ///
5355 /// Returns the underlying I/O errors that occurred.
5456 pub fn run ( self ) -> Result < ( ) , Error > {
55- let copy = compat:: copy_impl ( self . follow_symlinks ) ;
57+ let copy = compat:: copy_impl ( self . follow_symlinks , self . hard_link ) ;
5658 let result = schedule_copies ( self , & copy) ;
5759 copy. finish ( ) . and ( result)
5860 }
@@ -73,6 +75,7 @@ fn schedule_copies<
7375 files,
7476 force,
7577 follow_symlinks,
78+ hard_link,
7679 _marker1 : _,
7780 _marker2 : _,
7881 } : CopyOp < ' a , ' b , I1 , I2 , F > ,
@@ -119,8 +122,20 @@ fn schedule_copies<
119122 Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => ( ) ,
120123 r => r. map_io_err ( || format ! ( "Failed to remove existing file: {to:?}" ) ) ?,
121124 }
122- std:: os:: unix:: fs:: symlink ( link, & to)
123- . map_io_err ( || format ! ( "Failed to create symlink: {to:?}" ) ) ?;
125+ if hard_link {
126+ fs:: hard_link ( & link, & to)
127+ . map_io_err ( || format ! ( "Failed to create hard link: {to:?} -> {link:?}" ) ) ?;
128+ } else {
129+ std:: os:: unix:: fs:: symlink ( & link, & to)
130+ . map_io_err ( || format ! ( "Failed to create symlink: {to:?} -> {link:?}" ) ) ?;
131+ }
132+ } else if hard_link {
133+ match fs:: remove_file ( & to) {
134+ Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => ( ) ,
135+ r => r. map_io_err ( || format ! ( "Failed to remove existing file: {to:?}" ) ) ?,
136+ }
137+ fs:: hard_link ( & from, & to)
138+ . map_io_err ( || format ! ( "Failed to create hard link: {to:?} -> {from:?}" ) ) ?;
124139 } else {
125140 fs:: copy ( & from, & to) . map_io_err ( || format ! ( "Failed to copy file: {from:?}" ) ) ?;
126141 }
@@ -156,8 +171,8 @@ mod compat {
156171 use crossbeam_channel:: { Receiver , Sender } ;
157172 use rustix:: {
158173 fs:: {
159- AtFlags , CWD , FileType , Mode , OFlags , RawDir , StatxFlags , copy_file_range, mkdirat ,
160- openat, readlinkat, statx, symlinkat,
174+ AtFlags , CWD , FileType , Mode , OFlags , RawDir , StatxFlags , copy_file_range, linkat ,
175+ mkdirat , openat, readlinkat, statx, symlinkat,
161176 } ,
162177 io:: Errno ,
163178 thread:: { UnshareFlags , unshare} ,
@@ -174,12 +189,17 @@ mod compat {
174189
175190 pub fn copy_impl < ' a , ' b > (
176191 follow_symlinks : bool ,
192+ hard_link : bool ,
177193 ) -> impl DirectoryOp < ( Cow < ' a , Path > , Cow < ' b , Path > ) > {
178194 let scheduling = LazyCell :: new ( move || {
179195 let ( tx, rx) = crossbeam_channel:: unbounded ( ) ;
180196 (
181197 tx,
182- thread:: spawn ( move || root_worker_thread ( rx, follow_symlinks) ) ,
198+ if hard_link {
199+ thread:: spawn ( move || root_worker_thread :: < true > ( rx, follow_symlinks) )
200+ } else {
201+ thread:: spawn ( move || root_worker_thread :: < false > ( rx, follow_symlinks) )
202+ } ,
183203 )
184204 } ) ;
185205
@@ -221,7 +241,10 @@ mod compat {
221241 }
222242
223243 #[ cfg_attr( feature = "tracing" , tracing:: instrument( level = "trace" , skip( tasks) ) ) ]
224- fn root_worker_thread ( tasks : Receiver < TreeNode > , follow_symlinks : bool ) -> Result < ( ) , Error > {
244+ fn root_worker_thread < const HARD_LINK : bool > (
245+ tasks : Receiver < TreeNode > ,
246+ follow_symlinks : bool ,
247+ ) -> Result < ( ) , Error > {
225248 unshare_files ( ) ?;
226249
227250 let mut available_parallelism = thread:: available_parallelism ( )
@@ -265,13 +288,19 @@ mod compat {
265288 available_parallelism -= 1 ;
266289 threads. push ( scope. spawn ( {
267290 let tasks = tasks. clone ( ) ;
268- move || worker_thread ( tasks, root_to_inode, follow_symlinks)
291+ move || {
292+ worker_thread :: < HARD_LINK > (
293+ tasks,
294+ root_to_inode,
295+ follow_symlinks,
296+ )
297+ }
269298 } ) ) ;
270299 }
271300 } ;
272301 maybe_spawn ( ) ;
273302
274- copy_dir (
303+ copy_dir :: < HARD_LINK > (
275304 node,
276305 root_to_inode,
277306 follow_symlinks,
@@ -290,7 +319,7 @@ mod compat {
290319 }
291320
292321 #[ cfg_attr( feature = "tracing" , tracing:: instrument( level = "trace" , skip( tasks) ) ) ]
293- fn worker_thread (
322+ fn worker_thread < const HARD_LINK : bool > (
294323 tasks : Receiver < TreeNode > ,
295324 root_to_inode : u64 ,
296325 follow_symlinks : bool ,
@@ -300,7 +329,7 @@ mod compat {
300329 let mut buf = [ MaybeUninit :: < u8 > :: uninit ( ) ; 8192 ] ;
301330 let symlink_buf_cache = Cell :: new ( Vec :: new ( ) ) ;
302331 for node in tasks {
303- copy_dir (
332+ copy_dir :: < HARD_LINK > (
304333 node,
305334 root_to_inode,
306335 follow_symlinks,
@@ -339,7 +368,7 @@ mod compat {
339368 feature = "tracing" ,
340369 tracing:: instrument( level = "info" , skip( messages, buf, symlink_buf_cache, maybe_spawn) )
341370 ) ]
342- fn copy_dir (
371+ fn copy_dir < const HARD_LINK : bool > (
343372 TreeNode { from, to, messages } : TreeNode ,
344373 root_to_inode : u64 ,
345374 follow_symlinks : bool ,
@@ -402,6 +431,20 @@ mod compat {
402431 messages : messages. clone ( ) ,
403432 } )
404433 . map_err ( |_| Error :: Internal ) ?;
434+ } else if HARD_LINK {
435+ let name = file. file_name ( ) ;
436+ let flags = if follow_symlinks {
437+ AtFlags :: SYMLINK_FOLLOW
438+ } else {
439+ AtFlags :: empty ( )
440+ } ;
441+ linkat ( & from_dir, name, & to_dir, name, flags) . map_io_err ( || {
442+ format ! (
443+ "Failed to create symlink: {:?} -> {:?}" ,
444+ join_cstr_paths( & to, name) ,
445+ join_cstr_paths( & from, name) ,
446+ )
447+ } ) ?;
405448 } else {
406449 copy_one_file (
407450 & from_dir,
@@ -614,8 +657,8 @@ mod compat {
614657
615658 symlinkat ( & from_symlink, & to_dir, file_name) . map_io_err ( || {
616659 format ! (
617- "Failed to create symlink: {:?}" ,
618- join_cstr_paths( to_path, file_name)
660+ "Failed to create symlink: {:?} -> {from_symlink:?} " ,
661+ join_cstr_paths( to_path, file_name) ,
619662 )
620663 } ) ?;
621664
@@ -652,18 +695,23 @@ mod compat {
652695
653696 struct Impl {
654697 follow_symlinks : bool ,
698+ hard_link : bool ,
655699 }
656700
657701 pub fn copy_impl < ' a , ' b > (
658702 follow_symlinks : bool ,
703+ hard_link : bool ,
659704 ) -> impl DirectoryOp < ( Cow < ' a , Path > , Cow < ' b , Path > ) > {
660- Impl { follow_symlinks }
705+ Impl {
706+ follow_symlinks,
707+ hard_link,
708+ }
661709 }
662710
663711 impl DirectoryOp < ( Cow < ' _ , Path > , Cow < ' _ , Path > ) > for Impl {
664712 #[ cfg_attr( feature = "tracing" , tracing:: instrument( level = "trace" , skip( self ) ) ) ]
665713 fn run ( & self , ( from, to) : ( Cow < Path > , Cow < Path > ) ) -> Result < ( ) , Error > {
666- copy_dir ( & from, to, self . follow_symlinks , None )
714+ copy_dir ( & from, to, self . follow_symlinks , self . hard_link , None )
667715 . map_io_err ( || format ! ( "Failed to copy directory: {from:?}" ) )
668716 }
669717
@@ -678,6 +726,7 @@ mod compat {
678726 from : P ,
679727 to : Q ,
680728 follow_symlinks : bool ,
729+ hard_link : bool ,
681730 root_to_inode : Option < u64 > ,
682731 ) -> Result < ( ) , io:: Error > {
683732 let to = to. as_ref ( ) ;
@@ -713,17 +762,29 @@ mod compat {
713762 } ;
714763
715764 if file_type. is_dir ( ) {
716- copy_dir ( dir_entry. path ( ) , to, follow_symlinks, root_to_inode) ?;
765+ copy_dir (
766+ dir_entry. path ( ) ,
767+ to,
768+ follow_symlinks,
769+ hard_link,
770+ root_to_inode,
771+ ) ?;
717772 } else if file_type. is_symlink ( ) {
718773 let from = fs:: read_link ( dir_entry. path ( ) ) ?;
719- #[ cfg( unix) ]
720- std:: os:: unix:: fs:: symlink ( from, to) ?;
721- #[ cfg( windows) ]
722- if fs:: metadata ( & from) ?. file_type ( ) . is_dir ( ) {
723- std:: os:: windows:: fs:: symlink_dir ( from, to) ?;
774+ if hard_link {
775+ fs:: hard_link ( dir_entry. path ( ) , to) ?;
724776 } else {
725- std:: os:: windows:: fs:: symlink_file ( from, to) ?;
777+ #[ cfg( unix) ]
778+ std:: os:: unix:: fs:: symlink ( from, to) ?;
779+ #[ cfg( windows) ]
780+ if fs:: metadata ( & from) ?. file_type ( ) . is_dir ( ) {
781+ std:: os:: windows:: fs:: symlink_dir ( from, to) ?;
782+ } else {
783+ std:: os:: windows:: fs:: symlink_file ( from, to) ?;
784+ }
726785 }
786+ } else if hard_link {
787+ fs:: hard_link ( dir_entry. path ( ) , to) ?;
727788 } else {
728789 fs:: copy ( dir_entry. path ( ) , to) ?;
729790 }
0 commit comments