7
7
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
8
8
use anyhow:: { bail, Context , Result } ;
9
9
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
10
+ use camino:: { Utf8Path , Utf8PathBuf } ;
11
+ #[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
10
12
use openat_ext:: OpenatDirExt ;
11
13
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
12
14
use openssl:: hash:: { Hasher , MessageDigest } ;
15
+ use rustix:: fd:: BorrowedFd ;
13
16
use serde:: { Deserialize , Serialize } ;
14
17
#[ allow( unused_imports) ]
15
18
use std:: collections:: { BTreeMap , HashMap , HashSet } ;
16
19
use std:: fmt:: Display ;
17
20
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
18
21
use 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 ;
21
24
22
25
/// The prefix we apply to our temporary files.
23
26
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
@@ -231,7 +234,7 @@ impl FileTree {
231
234
}
232
235
}
233
236
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
235
238
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
236
239
fn cleanup_tmp ( dir : & openat:: Dir ) -> Result < ( ) > {
237
240
for entry in dir. list_dir ( "." ) ? {
@@ -245,8 +248,13 @@ fn cleanup_tmp(dir: &openat::Dir) -> Result<()> {
245
248
246
249
match dir. get_file_type ( & entry) ? {
247
250
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
+ }
250
258
}
251
259
openat:: SimpleType :: File => {
252
260
if name. starts_with ( TMP_PREFIX ) {
@@ -272,7 +280,6 @@ pub(crate) struct ApplyUpdateOptions {
272
280
// Let's just fork off a helper process for now.
273
281
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
274
282
pub ( crate ) fn syncfs ( d : & openat:: Dir ) -> Result < ( ) > {
275
- use rustix:: fd:: BorrowedFd ;
276
283
use rustix:: fs:: { Mode , OFlags } ;
277
284
let d = unsafe { BorrowedFd :: borrow_raw ( d. as_raw_fd ( ) ) } ;
278
285
let oflags = OFlags :: RDONLY | OFlags :: CLOEXEC | OFlags :: DIRECTORY ;
@@ -281,13 +288,43 @@ pub(crate) fn syncfs(d: &openat::Dir) -> Result<()> {
281
288
}
282
289
283
290
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
284
- fn tmpname_for_path < P : AsRef < Path > > ( path : P ) -> std :: path :: PathBuf {
291
+ fn tmpname_for_path < P : AsRef < Utf8Path > > ( path : P ) -> Utf8PathBuf {
285
292
let path = path. as_ref ( ) ;
286
- let mut buf = path. file_name ( ) . expect ( "filename" ) . to_os_string ( ) ;
287
- buf. push ( TMP_PREFIX ) ;
293
+ let mut buf = path. file_name ( ) . expect ( "filename" ) . to_string ( ) ;
294
+ buf. insert_str ( 0 , TMP_PREFIX ) ;
288
295
path. with_file_name ( buf)
289
296
}
290
297
298
+ /// Copy from src to dst at root dir
299
+ #[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
300
+ fn copy_dir ( root : & openat:: Dir , src : & str , dst : & str ) -> Result < ( ) > {
301
+ let rootfd = unsafe { BorrowedFd :: borrow_raw ( root. as_raw_fd ( ) ) } ;
302
+ let r = unsafe {
303
+ Command :: new ( "cp" )
304
+ . args ( [ "-a" ] )
305
+ . arg ( src)
306
+ . arg ( dst)
307
+ . pre_exec ( move || rustix:: process:: fchdir ( rootfd) . map_err ( Into :: into) )
308
+ . status ( ) ?
309
+ } ;
310
+ if !r. success ( ) {
311
+ anyhow:: bail!( "Failed to copy {src} to {dst}" ) ;
312
+ }
313
+ log:: debug!( "Copy {src} to {dst}" ) ;
314
+ Ok ( ( ) )
315
+ }
316
+
317
+ /// Get first sub dir and tmp sub dir for the path
318
+ /// "fedora/foo/bar" -> ("fedora", ".btmp.fedora")
319
+ /// "foo" -> ("foo", ".btmp.foo")
320
+ #[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
321
+ fn get_subdir ( path : & Utf8Path ) -> Result < ( String , String ) > {
322
+ let buf = path. iter ( ) . next ( ) . unwrap ( ) . to_owned ( ) ;
323
+ let mut buf_tmp = buf. clone ( ) ;
324
+ buf_tmp. insert_str ( 0 , TMP_PREFIX ) ;
325
+ Ok ( ( buf, buf_tmp) )
326
+ }
327
+
291
328
/// Given two directories, apply a diff generated from srcdir to destdir
292
329
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
293
330
pub ( crate ) fn apply_diff (
@@ -302,41 +339,101 @@ pub(crate) fn apply_diff(
302
339
let opts = opts. unwrap_or ( & default_opts) ;
303
340
cleanup_tmp ( destdir) . context ( "cleaning up temporary files" ) ?;
304
341
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 ) ?;
342
+ let mut updates = HashMap :: new ( ) ;
343
+ // Handle removals in temp dir
344
+ if !opts. skip_removals {
345
+ for pathstr in diff. removals . iter ( ) {
346
+ let path = Utf8Path :: new ( pathstr) ;
347
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
348
+ let path_tmp;
349
+ if subdir != * pathstr {
350
+ // need to copy to temp subdir and remember
351
+ if !destdir. exists ( & subdir_tmp) ? {
352
+ copy_dir ( destdir, & subdir, & subdir_tmp) ?;
353
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
354
+ }
355
+ path_tmp = Utf8Path :: new ( & subdir_tmp) . join ( path. strip_prefix ( & subdir) ?) ;
356
+ } else {
357
+ // remove the file directly that is not in dir
358
+ path_tmp = path. to_path_buf ( ) ;
359
+ }
360
+ destdir
361
+ . remove_file ( path_tmp. as_std_path ( ) )
362
+ . with_context ( || format ! ( "removing {:?}" , path_tmp) ) ?;
310
363
}
311
- let destp = tmpname_for_path ( path) ;
312
- srcdir
313
- . copy_file_at ( path, destdir, destp. as_path ( ) )
314
- . with_context ( || format ! ( "writing {}" , & pathstr) ) ?;
315
364
}
316
- // Ensure all of the new files are written persistently to disk
317
- if !opts. skip_sync {
318
- syncfs ( destdir) ?;
365
+ // Write changed files to temp dir and exchange finally
366
+ for pathstr in diff. changes . iter ( ) {
367
+ let path = Utf8Path :: new ( pathstr) ;
368
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
369
+ let path_tmp;
370
+ if subdir != * pathstr {
371
+ if !destdir. exists ( & subdir_tmp) ? {
372
+ // need to copy to temp subdir and remember
373
+ copy_dir ( destdir, & subdir, & subdir_tmp) ?;
374
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
375
+ }
376
+ path_tmp = Utf8Path :: new ( & subdir_tmp) . join ( path. strip_prefix ( & subdir) ?) ;
377
+ destdir
378
+ . remove_file_optional ( path_tmp. as_std_path ( ) )
379
+ . with_context ( || format ! ( "removing {path_tmp}" ) ) ?;
380
+ } else {
381
+ // file that is not in dir, copy file to foo.tmp
382
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
383
+ path_tmp = Utf8PathBuf :: from ( & subdir_tmp) ;
384
+ }
385
+ srcdir
386
+ . copy_file_at ( path. as_std_path ( ) , destdir, path_tmp. as_std_path ( ) )
387
+ . with_context ( || format ! ( "copying {:?} to {:?}" , path, path_tmp) ) ?;
319
388
}
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}" ) ) ?;
389
+ // Write new files to temp dir if exists, else write to tmp
390
+ for pathstr in diff. additions . iter ( ) {
391
+ let path = Utf8Path :: new ( pathstr) ;
392
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
393
+ let path_tmp;
394
+ if destdir. exists ( & subdir_tmp) ? {
395
+ path_tmp = Utf8Path :: new ( & subdir_tmp) . join ( path. strip_prefix ( & subdir) ?) ;
396
+ } else {
397
+ path_tmp = tmpname_for_path ( path) ;
398
+ updates. insert ( pathstr. to_owned ( ) , path_tmp. to_string ( ) ) ;
399
+ }
400
+ // ensure new additions dir exists
401
+ if let Some ( parent) = path_tmp. parent ( ) . filter ( |& v| !v. as_os_str ( ) . is_empty ( ) ) {
402
+ destdir. ensure_dir_all ( parent. as_std_path ( ) , DEFAULT_FILE_MODE ) ?;
403
+ }
404
+ srcdir
405
+ . copy_file_at ( path. as_std_path ( ) , destdir, path_tmp. as_std_path ( ) )
406
+ . with_context ( || format ! ( "copying {:?} to {:?}" , path, path_tmp) ) ?;
326
407
}
327
- if !opts. skip_removals {
328
- for path in diff. removals . iter ( ) {
408
+
409
+ // do local exchange
410
+ for ( src, tmp) in updates. iter ( ) {
411
+ log:: trace!( "doing local exchange/rename for {} and {}" , tmp, src) ;
412
+ if destdir. exists ( src) ? {
413
+ destdir
414
+ . local_exchange ( tmp, src)
415
+ . with_context ( || format ! ( "exchange for {} and {}" , tmp, src) ) ?;
416
+ } else {
329
417
destdir
330
- . remove_file_optional ( path )
331
- . with_context ( || format ! ( "removing {path}" ) ) ?;
418
+ . local_rename ( tmp , src )
419
+ . with_context ( || format ! ( "rename for {} and {}" , tmp , src ) ) ?;
332
420
}
333
421
}
422
+ // Ensure all of the updates & changes are written persistently to disk
423
+ if !opts. skip_sync {
424
+ syncfs ( destdir) ?;
425
+ }
426
+
427
+ // finally remove the temp dir
428
+ for ( _, tmp) in updates. iter ( ) {
429
+ log:: trace!( "cleanup: {}" , tmp) ;
430
+ destdir. remove_all ( tmp) . context ( "clean up temp" ) ?;
431
+ }
334
432
// A second full filesystem sync to narrow any races rather than
335
433
// waiting for writeback to kick in.
336
434
if !opts. skip_sync {
337
435
syncfs ( destdir) ?;
338
436
}
339
-
340
437
Ok ( ( ) )
341
438
}
342
439
@@ -345,6 +442,7 @@ mod tests {
345
442
use super :: * ;
346
443
use std:: fs;
347
444
use std:: io:: Write ;
445
+ use std:: path:: Path ;
348
446
349
447
fn run_diff ( a : & openat:: Dir , b : & openat:: Dir ) -> Result < FileTreeDiff > {
350
448
let ta = FileTree :: new_from_dir ( a) ?;
@@ -508,4 +606,127 @@ mod tests {
508
606
assert ! ( !a. join( relp) . join( "shim.x64" ) . exists( ) ) ;
509
607
Ok ( ( ) )
510
608
}
609
+ #[ test]
610
+ fn test_get_subdir ( ) -> Result < ( ) > {
611
+ // test path
612
+ let path = Utf8Path :: new ( "foo/subdir/bar" ) ;
613
+ let ( tp, tp_tmp) = get_subdir ( path) ?;
614
+ assert_eq ! ( tp. eq( "foo" ) , tp_tmp. eq( ".btmp.foo" ) ) ;
615
+ // test file
616
+ let path = Utf8Path :: new ( "foo" ) ;
617
+ let ( tp, tp_tmp) = get_subdir ( path) ?;
618
+ assert_eq ! ( tp. eq( "foo" ) , tp_tmp. eq( ".btmp.foo" ) ) ;
619
+ Ok ( ( ) )
620
+ }
621
+ #[ test]
622
+ fn test_cleanup_tmp ( ) -> Result < ( ) > {
623
+ let tmpd = tempfile:: tempdir ( ) ?;
624
+ let p = tmpd. path ( ) ;
625
+ let pa = p. join ( "a/.btmp.a" ) ;
626
+ let pb = p. join ( ".btmp.b/b" ) ;
627
+ std:: fs:: create_dir_all ( & pa) ?;
628
+ std:: fs:: create_dir_all ( & pb) ?;
629
+ let dp = openat:: Dir :: open ( p) ?;
630
+ {
631
+ let mut buf = dp. write_file ( "a/foo" , 0o644 ) ?;
632
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
633
+ let mut buf = dp. write_file ( "a/.btmp.foo" , 0o644 ) ?;
634
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
635
+ let mut buf = dp. write_file ( ".btmp.b/foo" , 0o644 ) ?;
636
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
637
+ }
638
+ assert ! ( dp. exists( "a/.btmp.a" ) ?) ;
639
+ assert ! ( dp. exists( "a/foo" ) ?) ;
640
+ assert ! ( dp. exists( "a/.btmp.foo" ) ?) ;
641
+ assert ! ( dp. exists( "a/.btmp.a" ) ?) ;
642
+ assert ! ( dp. exists( ".btmp.b/b" ) ?) ;
643
+ assert ! ( dp. exists( ".btmp.b/foo" ) ?) ;
644
+ cleanup_tmp ( & dp) ?;
645
+ assert ! ( !dp. exists( "a/.btmp.a" ) ?) ;
646
+ assert ! ( dp. exists( "a/foo" ) ?) ;
647
+ assert ! ( !dp. exists( "a/.btmp.foo" ) ?) ;
648
+ assert ! ( !dp. exists( ".btmp.b" ) ?) ;
649
+ Ok ( ( ) )
650
+ }
651
+ #[ test]
652
+ fn test_apply_with_file ( ) -> Result < ( ) > {
653
+ let tmpd = tempfile:: tempdir ( ) ?;
654
+ let p = tmpd. path ( ) ;
655
+ let pa = p. join ( "a" ) ;
656
+ let pb = p. join ( "b" ) ;
657
+ std:: fs:: create_dir ( & pa) ?;
658
+ std:: fs:: create_dir ( & pb) ?;
659
+ let a = openat:: Dir :: open ( & pa) ?;
660
+ let b = openat:: Dir :: open ( & pb) ?;
661
+ a. create_dir ( "foo" , 0o755 ) ?;
662
+ a. create_dir ( "bar" , 0o755 ) ?;
663
+ let foo = Path :: new ( "foo/bar" ) ;
664
+ let bar = Path :: new ( "bar/foo" ) ;
665
+ let testfile = "testfile" ;
666
+ {
667
+ let mut buf = a. write_file ( foo, 0o644 ) ?;
668
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
669
+ let mut buf = a. write_file ( bar, 0o644 ) ?;
670
+ buf. write_all ( "barcontents" . as_bytes ( ) ) ?;
671
+ let mut buf = a. write_file ( testfile, 0o644 ) ?;
672
+ buf. write_all ( "testfilecontents" . as_bytes ( ) ) ?;
673
+ }
674
+
675
+ let diff = run_diff ( & a, & b) ?;
676
+ assert_eq ! ( diff. count( ) , 3 ) ;
677
+ b. create_dir ( "foo" , 0o755 ) ?;
678
+ {
679
+ let mut buf = b. write_file ( foo, 0o644 ) ?;
680
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
681
+ }
682
+ let b_btime_foo = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
683
+
684
+ {
685
+ let diff = run_diff ( & b, & a) ?;
686
+ assert_eq ! ( diff. count( ) , 2 ) ;
687
+ apply_diff ( & a, & b, & diff, None ) . context ( "test additional files" ) ?;
688
+ assert_eq ! (
689
+ String :: from_utf8( std:: fs:: read( pb. join( testfile) ) ?) ?,
690
+ "testfilecontents"
691
+ ) ;
692
+ assert_eq ! (
693
+ String :: from_utf8( std:: fs:: read( pb. join( bar) ) ?) ?,
694
+ "barcontents"
695
+ ) ;
696
+ // creation time is not changed for unchanged file
697
+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
698
+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
699
+ }
700
+ {
701
+ fs:: write ( pa. join ( testfile) , "newtestfile" ) ?;
702
+ fs:: write ( pa. join ( bar) , "newbar" ) ?;
703
+ let diff = run_diff ( & b, & a) ?;
704
+ assert_eq ! ( diff. count( ) , 2 ) ;
705
+ apply_diff ( & a, & b, & diff, None ) . context ( "test changed files" ) ?;
706
+ assert_eq ! (
707
+ String :: from_utf8( std:: fs:: read( pb. join( testfile) ) ?) ?,
708
+ "newtestfile"
709
+ ) ;
710
+ assert_eq ! ( String :: from_utf8( std:: fs:: read( pb. join( bar) ) ?) ?, "newbar" ) ;
711
+ // creation time is not changed for unchanged file
712
+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
713
+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
714
+ }
715
+ {
716
+ a. remove_file ( testfile) ?;
717
+ a. remove_file ( bar) ?;
718
+ let diff = run_diff ( & b, & a) ?;
719
+ assert_eq ! ( diff. count( ) , 2 ) ;
720
+ apply_diff ( & a, & b, & diff, None ) . context ( "test removed files" ) ?;
721
+ assert_eq ! ( b. exists( testfile) ?, false ) ;
722
+ assert_eq ! ( b. exists( bar) ?, false ) ;
723
+ let diff = run_diff ( & b, & a) ?;
724
+ assert_eq ! ( diff. count( ) , 0 ) ;
725
+ // creation time is not changed for unchanged file
726
+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
727
+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
728
+ }
729
+
730
+ Ok ( ( ) )
731
+ }
511
732
}
0 commit comments