77#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
88use anyhow:: { bail, Context , Result } ;
99#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
10+ use camino:: { Utf8Path , Utf8PathBuf } ;
11+ #[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
1012use openat_ext:: OpenatDirExt ;
1113#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
1214use openssl:: hash:: { Hasher , MessageDigest } ;
15+ use rustix:: fd:: BorrowedFd ;
1316use serde:: { Deserialize , Serialize } ;
1417#[ allow( unused_imports) ]
1518use std:: collections:: { BTreeMap , HashMap , HashSet } ;
1619use std:: fmt:: Display ;
1720#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
1821use std:: os:: unix:: io:: AsRawFd ;
19- # [ cfg ( any ( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
20- use std:: path :: Path ;
22+ use std :: os :: unix :: process :: CommandExt ;
23+ use std:: process :: Command ;
2124
2225/// The prefix we apply to our temporary files.
2326#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
@@ -231,7 +234,7 @@ impl FileTree {
231234 }
232235}
233236
234- // Recursively remove all files in the directory that start with our TMP_PREFIX
237+ // Recursively remove all files/dirs in the directory that start with our TMP_PREFIX
235238#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
236239fn cleanup_tmp ( dir : & openat:: Dir ) -> Result < ( ) > {
237240 for entry in dir. list_dir ( "." ) ? {
@@ -245,8 +248,13 @@ fn cleanup_tmp(dir: &openat::Dir) -> Result<()> {
245248
246249 match dir. get_file_type ( & entry) ? {
247250 openat:: SimpleType :: Dir => {
248- let child = dir. sub_dir ( name) ?;
249- cleanup_tmp ( & child) ?;
251+ if name. starts_with ( TMP_PREFIX ) {
252+ dir. remove_all ( name) ?;
253+ continue ;
254+ } else {
255+ let child = dir. sub_dir ( name) ?;
256+ cleanup_tmp ( & child) ?;
257+ }
250258 }
251259 openat:: SimpleType :: File => {
252260 if name. starts_with ( TMP_PREFIX ) {
@@ -272,20 +280,44 @@ pub(crate) struct ApplyUpdateOptions {
272280// Let's just fork off a helper process for now.
273281#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
274282pub ( crate ) fn syncfs ( d : & openat:: Dir ) -> Result < ( ) > {
275- use rustix:: fd:: BorrowedFd ;
276283 use rustix:: fs:: { Mode , OFlags } ;
277284 let d = unsafe { BorrowedFd :: borrow_raw ( d. as_raw_fd ( ) ) } ;
278285 let oflags = OFlags :: RDONLY | OFlags :: CLOEXEC | OFlags :: DIRECTORY ;
279286 let d = rustix:: fs:: openat ( d, "." , oflags, Mode :: empty ( ) ) ?;
280287 rustix:: fs:: syncfs ( d) . map_err ( Into :: into)
281288}
282289
290+ /// Copy from src to dst at root dir
291+ #[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
292+ fn copy_dir ( root : & openat:: Dir , src : & str , dst : & str ) -> Result < ( ) > {
293+ let rootfd = unsafe { BorrowedFd :: borrow_raw ( root. as_raw_fd ( ) ) } ;
294+ let r = unsafe {
295+ Command :: new ( "cp" )
296+ . args ( [ "-a" ] )
297+ . arg ( src)
298+ . arg ( dst)
299+ . pre_exec ( move || rustix:: process:: fchdir ( rootfd) . map_err ( Into :: into) )
300+ . status ( ) ?
301+ } ;
302+ if !r. success ( ) {
303+ anyhow:: bail!( "Failed to copy {src} to {dst}" ) ;
304+ }
305+ log:: debug!( "Copy {src} to {dst}" ) ;
306+ Ok ( ( ) )
307+ }
308+
309+ /// Get first sub dir and tmp sub dir for the path
310+ /// "fedora/foo/bar" -> ("fedora", ".btmp.fedora")
311+ /// "foo" -> ("foo", ".btmp.foo")
283312#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
284- fn tmpname_for_path < P : AsRef < Path > > ( path : P ) -> std:: path:: PathBuf {
285- let path = path. as_ref ( ) ;
286- let mut buf = path. file_name ( ) . expect ( "filename" ) . to_os_string ( ) ;
287- buf. push ( TMP_PREFIX ) ;
288- path. with_file_name ( buf)
313+ fn get_first_dir ( path : & Utf8Path ) -> Result < ( & Utf8Path , String ) > {
314+ let first = path
315+ . iter ( )
316+ . next ( )
317+ . ok_or_else ( || anyhow:: anyhow!( "Invalid path: {path}" ) ) ?;
318+ let mut tmp = first. to_owned ( ) ;
319+ tmp. insert_str ( 0 , TMP_PREFIX ) ;
320+ Ok ( ( first. into ( ) , tmp) )
289321}
290322
291323/// Given two directories, apply a diff generated from srcdir to destdir
@@ -302,41 +334,83 @@ pub(crate) fn apply_diff(
302334 let opts = opts. unwrap_or ( & default_opts) ;
303335 cleanup_tmp ( destdir) . context ( "cleaning up temporary files" ) ?;
304336
305- // Write new and changed files
306- for pathstr in diff. additions . iter ( ) . chain ( diff. changes . iter ( ) ) {
307- let path = Path :: new ( pathstr) ;
308- if let Some ( parent) = path. parent ( ) {
309- destdir. ensure_dir_all ( parent, DEFAULT_FILE_MODE ) ?;
337+ let mut updates = HashMap :: new ( ) ;
338+ // Handle removals in temp dir, or remove directly if file not in dir
339+ if !opts. skip_removals {
340+ for pathstr in diff. removals . iter ( ) {
341+ let path = Utf8Path :: new ( pathstr) ;
342+ let ( first_dir, first_dir_tmp) = get_first_dir ( path) ?;
343+ let path_tmp;
344+ if first_dir != path {
345+ path_tmp = Utf8Path :: new ( & first_dir_tmp) . join ( path. strip_prefix ( & first_dir) ?) ;
346+ // copy to temp dir and remember
347+ if !destdir. exists ( & first_dir_tmp) ? {
348+ copy_dir ( destdir, first_dir. as_str ( ) , & first_dir_tmp) ?;
349+ updates. insert ( first_dir, first_dir_tmp) ;
350+ }
351+ } else {
352+ path_tmp = path. to_path_buf ( ) ;
353+ }
354+ destdir
355+ . remove_file ( path_tmp. as_std_path ( ) )
356+ . with_context ( || format ! ( "removing {:?}" , path_tmp) ) ?;
357+ }
358+ }
359+ // Write changed or new files to temp dir or temp file
360+ for pathstr in diff. changes . iter ( ) . chain ( diff. additions . iter ( ) ) {
361+ let path = Utf8Path :: new ( pathstr) ;
362+ let ( first_dir, first_dir_tmp) = get_first_dir ( path) ?;
363+ let mut path_tmp = Utf8PathBuf :: from ( & first_dir_tmp) ;
364+ if first_dir != path {
365+ if !destdir. exists ( & first_dir_tmp) ? && destdir. exists ( first_dir. as_std_path ( ) ) ? {
366+ // copy to temp dir if not exists
367+ copy_dir ( destdir, first_dir. as_str ( ) , & first_dir_tmp) ?;
368+ }
369+ path_tmp = path_tmp. join ( path. strip_prefix ( & first_dir) ?) ;
370+ // ensure new additions dir exists
371+ if let Some ( parent) = path_tmp. parent ( ) {
372+ destdir. ensure_dir_all ( parent. as_std_path ( ) , DEFAULT_FILE_MODE ) ?;
373+ }
374+ // remove changed file before copying
375+ destdir
376+ . remove_file_optional ( path_tmp. as_std_path ( ) )
377+ . with_context ( || format ! ( "removing {path_tmp} before copying" ) ) ?;
310378 }
311- let destp = tmpname_for_path ( path ) ;
379+ updates . insert ( first_dir , first_dir_tmp ) ;
312380 srcdir
313- . copy_file_at ( path, destdir, destp. as_path ( ) )
314- . with_context ( || format ! ( "writing {}" , & pathstr) ) ?;
381+ . copy_file_at ( path. as_std_path ( ) , destdir, path_tmp. as_std_path ( ) )
382+ . with_context ( || format ! ( "copying {:?} to {:?}" , path, path_tmp) ) ?;
383+ }
384+
385+ // do local exchange or rename
386+ for ( dst, tmp) in updates. iter ( ) {
387+ let dst = dst. as_std_path ( ) ;
388+ log:: trace!( "doing local exchange for {} and {:?}" , tmp, dst) ;
389+ if destdir. exists ( dst) ? {
390+ destdir
391+ . local_exchange ( tmp, dst)
392+ . with_context ( || format ! ( "exchange for {} and {:?}" , tmp, dst) ) ?;
393+ } else {
394+ destdir
395+ . local_rename ( tmp, dst)
396+ . with_context ( || format ! ( "rename for {} and {:?}" , tmp, dst) ) ?;
397+ }
315398 }
316- // Ensure all of the new files are written persistently to disk
399+ // Ensure all of the updates & changes are written persistently to disk
317400 if !opts. skip_sync {
318401 syncfs ( destdir) ?;
319402 }
320- // Now move them all into place (TODO track interruption)
321- for path in diff. additions . iter ( ) . chain ( diff. changes . iter ( ) ) {
322- let pathtmp = tmpname_for_path ( path) ;
323- destdir
324- . local_rename ( & pathtmp, path)
325- . with_context ( || format ! ( "renaming {path}" ) ) ?;
326- }
327- if !opts. skip_removals {
328- for path in diff. removals . iter ( ) {
329- destdir
330- . remove_file_optional ( path)
331- . with_context ( || format ! ( "removing {path}" ) ) ?;
332- }
403+
404+ // finally remove the temp dir
405+ for ( _, tmp) in updates. iter ( ) {
406+ log:: trace!( "cleanup: {}" , tmp) ;
407+ destdir. remove_all ( tmp) . context ( "clean up temp" ) ?;
333408 }
334409 // A second full filesystem sync to narrow any races rather than
335410 // waiting for writeback to kick in.
336411 if !opts. skip_sync {
337412 syncfs ( destdir) ?;
338413 }
339-
340414 Ok ( ( ) )
341415}
342416
@@ -345,6 +419,7 @@ mod tests {
345419 use super :: * ;
346420 use std:: fs;
347421 use std:: io:: Write ;
422+ use std:: path:: Path ;
348423
349424 fn run_diff ( a : & openat:: Dir , b : & openat:: Dir ) -> Result < FileTreeDiff > {
350425 let ta = FileTree :: new_from_dir ( a) ?;
@@ -508,4 +583,129 @@ mod tests {
508583 assert ! ( !a. join( relp) . join( "shim.x64" ) . exists( ) ) ;
509584 Ok ( ( ) )
510585 }
586+ #[ test]
587+ fn test_get_first_dir ( ) -> Result < ( ) > {
588+ // test path
589+ let path = Utf8Path :: new ( "foo/subdir/bar" ) ;
590+ let ( tp, tp_tmp) = get_first_dir ( path) ?;
591+ assert_eq ! ( tp, Utf8Path :: new( "foo" ) ) ;
592+ assert_eq ! ( tp_tmp, ".btmp.foo" ) ;
593+ // test file
594+ let path = Utf8Path :: new ( "testfile" ) ;
595+ let ( tp, tp_tmp) = get_first_dir ( path) ?;
596+ assert_eq ! ( tp, Utf8Path :: new( "testfile" ) ) ;
597+ assert_eq ! ( tp_tmp, ".btmp.testfile" ) ;
598+ Ok ( ( ) )
599+ }
600+ #[ test]
601+ fn test_cleanup_tmp ( ) -> Result < ( ) > {
602+ let tmpd = tempfile:: tempdir ( ) ?;
603+ let p = tmpd. path ( ) ;
604+ let pa = p. join ( "a/.btmp.a" ) ;
605+ let pb = p. join ( ".btmp.b/b" ) ;
606+ std:: fs:: create_dir_all ( & pa) ?;
607+ std:: fs:: create_dir_all ( & pb) ?;
608+ let dp = openat:: Dir :: open ( p) ?;
609+ {
610+ let mut buf = dp. write_file ( "a/foo" , 0o644 ) ?;
611+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
612+ let mut buf = dp. write_file ( "a/.btmp.foo" , 0o644 ) ?;
613+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
614+ let mut buf = dp. write_file ( ".btmp.b/foo" , 0o644 ) ?;
615+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
616+ }
617+ assert ! ( dp. exists( "a/.btmp.a" ) ?) ;
618+ assert ! ( dp. exists( "a/foo" ) ?) ;
619+ assert ! ( dp. exists( "a/.btmp.foo" ) ?) ;
620+ assert ! ( dp. exists( "a/.btmp.a" ) ?) ;
621+ assert ! ( dp. exists( ".btmp.b/b" ) ?) ;
622+ assert ! ( dp. exists( ".btmp.b/foo" ) ?) ;
623+ cleanup_tmp ( & dp) ?;
624+ assert ! ( !dp. exists( "a/.btmp.a" ) ?) ;
625+ assert ! ( dp. exists( "a/foo" ) ?) ;
626+ assert ! ( !dp. exists( "a/.btmp.foo" ) ?) ;
627+ assert ! ( !dp. exists( ".btmp.b" ) ?) ;
628+ Ok ( ( ) )
629+ }
630+ #[ test]
631+ fn test_apply_with_file ( ) -> Result < ( ) > {
632+ let tmpd = tempfile:: tempdir ( ) ?;
633+ let p = tmpd. path ( ) ;
634+ let pa = p. join ( "a" ) ;
635+ let pb = p. join ( "b" ) ;
636+ std:: fs:: create_dir ( & pa) ?;
637+ std:: fs:: create_dir ( & pb) ?;
638+ let a = openat:: Dir :: open ( & pa) ?;
639+ let b = openat:: Dir :: open ( & pb) ?;
640+ a. create_dir ( "foo" , 0o755 ) ?;
641+ a. create_dir ( "bar" , 0o755 ) ?;
642+ let foo = Path :: new ( "foo/bar" ) ;
643+ let bar = Path :: new ( "bar/foo" ) ;
644+ let testfile = "testfile" ;
645+ {
646+ let mut buf = a. write_file ( foo, 0o644 ) ?;
647+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
648+ let mut buf = a. write_file ( bar, 0o644 ) ?;
649+ buf. write_all ( "barcontents" . as_bytes ( ) ) ?;
650+ let mut buf = a. write_file ( testfile, 0o644 ) ?;
651+ buf. write_all ( "testfilecontents" . as_bytes ( ) ) ?;
652+ }
653+
654+ let diff = run_diff ( & a, & b) ?;
655+ assert_eq ! ( diff. count( ) , 3 ) ;
656+ b. create_dir ( "foo" , 0o755 ) ?;
657+ {
658+ let mut buf = b. write_file ( foo, 0o644 ) ?;
659+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
660+ }
661+ let b_btime_foo = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
662+
663+ {
664+ let diff = run_diff ( & b, & a) ?;
665+ assert_eq ! ( diff. count( ) , 2 ) ;
666+ apply_diff ( & a, & b, & diff, None ) . context ( "test additional files" ) ?;
667+ assert_eq ! (
668+ String :: from_utf8( std:: fs:: read( pb. join( testfile) ) ?) ?,
669+ "testfilecontents"
670+ ) ;
671+ assert_eq ! (
672+ String :: from_utf8( std:: fs:: read( pb. join( bar) ) ?) ?,
673+ "barcontents"
674+ ) ;
675+ // creation time is not changed for unchanged file
676+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
677+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
678+ }
679+ {
680+ fs:: write ( pa. join ( testfile) , "newtestfile" ) ?;
681+ fs:: write ( pa. join ( bar) , "newbar" ) ?;
682+ let diff = run_diff ( & b, & a) ?;
683+ assert_eq ! ( diff. count( ) , 2 ) ;
684+ apply_diff ( & a, & b, & diff, None ) . context ( "test changed files" ) ?;
685+ assert_eq ! (
686+ String :: from_utf8( std:: fs:: read( pb. join( testfile) ) ?) ?,
687+ "newtestfile"
688+ ) ;
689+ assert_eq ! ( String :: from_utf8( std:: fs:: read( pb. join( bar) ) ?) ?, "newbar" ) ;
690+ // creation time is not changed for unchanged file
691+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
692+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
693+ }
694+ {
695+ a. remove_file ( testfile) ?;
696+ a. remove_file ( bar) ?;
697+ let diff = run_diff ( & b, & a) ?;
698+ assert_eq ! ( diff. count( ) , 2 ) ;
699+ apply_diff ( & a, & b, & diff, None ) . context ( "test removed files" ) ?;
700+ assert_eq ! ( b. exists( testfile) ?, false ) ;
701+ assert_eq ! ( b. exists( bar) ?, false ) ;
702+ let diff = run_diff ( & b, & a) ?;
703+ assert_eq ! ( diff. count( ) , 0 ) ;
704+ // creation time is not changed for unchanged file
705+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
706+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
707+ }
708+
709+ Ok ( ( ) )
710+ }
511711}
0 commit comments