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,20 +280,41 @@ 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 ;
279
286
let d = rustix:: fs:: openat ( d, "." , oflags, Mode :: empty ( ) ) ?;
280
287
rustix:: fs:: syncfs ( d) . map_err ( Into :: into)
281
288
}
282
289
290
+ /// Copy from src to dst at root dir
283
291
#[ 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)
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")
312
+ #[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
313
+ fn get_subdir ( path : & Utf8Path ) -> Result < ( String , String ) > {
314
+ let buf = path. iter ( ) . next ( ) . unwrap ( ) . to_owned ( ) ;
315
+ let mut buf_tmp = buf. clone ( ) ;
316
+ buf_tmp. insert_str ( 0 , TMP_PREFIX ) ;
317
+ Ok ( ( buf, buf_tmp) )
289
318
}
290
319
291
320
/// Given two directories, apply a diff generated from srcdir to destdir
@@ -302,41 +331,94 @@ pub(crate) fn apply_diff(
302
331
let opts = opts. unwrap_or ( & default_opts) ;
303
332
cleanup_tmp ( destdir) . context ( "cleaning up temporary files" ) ?;
304
333
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 ) ?;
334
+ let mut updates = HashMap :: new ( ) ;
335
+ // Handle removals in temp dir, or remove file directly if not in dir
336
+ if !opts. skip_removals {
337
+ for pathstr in diff. removals . iter ( ) {
338
+ let path = Utf8Path :: new ( pathstr) ;
339
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
340
+ let path_tmp;
341
+ if subdir != * pathstr {
342
+ // need to copy to temp subdir and remember
343
+ if !destdir. exists ( & subdir_tmp) ? {
344
+ copy_dir ( destdir, & subdir, & subdir_tmp) ?;
345
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
346
+ }
347
+ path_tmp = Utf8Path :: new ( & subdir_tmp) . join ( path. strip_prefix ( & subdir) ?) ;
348
+ } else {
349
+ path_tmp = path. to_path_buf ( ) ;
350
+ }
351
+ destdir
352
+ . remove_file ( path_tmp. as_std_path ( ) )
353
+ . with_context ( || format ! ( "removing {:?}" , path_tmp) ) ?;
310
354
}
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
355
}
316
- // Ensure all of the new files are written persistently to disk
317
- if !opts. skip_sync {
318
- syncfs ( destdir) ?;
356
+ // Write changed files to temp dir or temp file
357
+ for pathstr in diff. changes . iter ( ) {
358
+ let path = Utf8Path :: new ( pathstr) ;
359
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
360
+ let mut path_tmp = Utf8PathBuf :: from ( & subdir_tmp) ;
361
+ if subdir != * pathstr {
362
+ if !destdir. exists ( & subdir_tmp) ? {
363
+ // copy to temp subdir if not exists
364
+ copy_dir ( destdir, & subdir, & subdir_tmp) ?;
365
+ }
366
+ path_tmp = path_tmp. join ( path. strip_prefix ( & subdir) ?) ;
367
+ destdir
368
+ . remove_file ( path_tmp. as_std_path ( ) )
369
+ . with_context ( || format ! ( "removing {path_tmp} before copying" ) ) ?;
370
+ }
371
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
372
+ srcdir
373
+ . copy_file_at ( path. as_std_path ( ) , destdir, path_tmp. as_std_path ( ) )
374
+ . with_context ( || format ! ( "copying {:?} to {:?}" , path, path_tmp) ) ?;
319
375
}
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}" ) ) ?;
376
+ // Write new files to temp dir or temp file
377
+ for pathstr in diff. additions . iter ( ) {
378
+ let path = Utf8Path :: new ( pathstr) ;
379
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
380
+ let mut path_tmp = Utf8PathBuf :: from ( & subdir_tmp) ;
381
+ if subdir != * pathstr {
382
+ path_tmp = path_tmp. join ( path. strip_prefix ( & subdir) ?) ;
383
+ // ensure new additions dir exists
384
+ if let Some ( parent) = path_tmp. parent ( ) . filter ( |& v| !v. as_os_str ( ) . is_empty ( ) ) {
385
+ destdir. ensure_dir_all ( parent. as_std_path ( ) , DEFAULT_FILE_MODE ) ?;
386
+ }
387
+ }
388
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
389
+ srcdir
390
+ . copy_file_at ( path. as_std_path ( ) , destdir, path_tmp. as_std_path ( ) )
391
+ . with_context ( || format ! ( "copying {:?} to {:?}" , path, path_tmp) ) ?;
326
392
}
327
- if !opts. skip_removals {
328
- for path in diff. removals . iter ( ) {
393
+
394
+ // do local exchange / rename
395
+ for ( src, tmp) in updates. iter ( ) {
396
+ log:: trace!( "doing local exchange/rename for {} and {}" , tmp, src) ;
397
+ if destdir. exists ( src) ? {
398
+ destdir
399
+ . local_exchange ( tmp, src)
400
+ . with_context ( || format ! ( "exchange for {} and {}" , tmp, src) ) ?;
401
+ } else {
329
402
destdir
330
- . remove_file_optional ( path )
331
- . with_context ( || format ! ( "removing {path}" ) ) ?;
403
+ . local_rename ( tmp , src )
404
+ . with_context ( || format ! ( "rename for {} and {}" , tmp , src ) ) ?;
332
405
}
333
406
}
407
+ // Ensure all of the updates & changes are written persistently to disk
408
+ if !opts. skip_sync {
409
+ syncfs ( destdir) ?;
410
+ }
411
+
412
+ // finally remove the temp dir
413
+ for ( _, tmp) in updates. iter ( ) {
414
+ log:: trace!( "cleanup: {}" , tmp) ;
415
+ destdir. remove_all ( tmp) . context ( "clean up temp" ) ?;
416
+ }
334
417
// A second full filesystem sync to narrow any races rather than
335
418
// waiting for writeback to kick in.
336
419
if !opts. skip_sync {
337
420
syncfs ( destdir) ?;
338
421
}
339
-
340
422
Ok ( ( ) )
341
423
}
342
424
@@ -345,6 +427,7 @@ mod tests {
345
427
use super :: * ;
346
428
use std:: fs;
347
429
use std:: io:: Write ;
430
+ use std:: path:: Path ;
348
431
349
432
fn run_diff ( a : & openat:: Dir , b : & openat:: Dir ) -> Result < FileTreeDiff > {
350
433
let ta = FileTree :: new_from_dir ( a) ?;
@@ -508,4 +591,127 @@ mod tests {
508
591
assert ! ( !a. join( relp) . join( "shim.x64" ) . exists( ) ) ;
509
592
Ok ( ( ) )
510
593
}
594
+ #[ test]
595
+ fn test_get_subdir ( ) -> Result < ( ) > {
596
+ // test path
597
+ let path = Utf8Path :: new ( "foo/subdir/bar" ) ;
598
+ let ( tp, tp_tmp) = get_subdir ( path) ?;
599
+ assert_eq ! ( tp. eq( "foo" ) , tp_tmp. eq( ".btmp.foo" ) ) ;
600
+ // test file
601
+ let path = Utf8Path :: new ( "foo" ) ;
602
+ let ( tp, tp_tmp) = get_subdir ( path) ?;
603
+ assert_eq ! ( tp. eq( "foo" ) , tp_tmp. eq( ".btmp.foo" ) ) ;
604
+ Ok ( ( ) )
605
+ }
606
+ #[ test]
607
+ fn test_cleanup_tmp ( ) -> Result < ( ) > {
608
+ let tmpd = tempfile:: tempdir ( ) ?;
609
+ let p = tmpd. path ( ) ;
610
+ let pa = p. join ( "a/.btmp.a" ) ;
611
+ let pb = p. join ( ".btmp.b/b" ) ;
612
+ std:: fs:: create_dir_all ( & pa) ?;
613
+ std:: fs:: create_dir_all ( & pb) ?;
614
+ let dp = openat:: Dir :: open ( p) ?;
615
+ {
616
+ let mut buf = dp. write_file ( "a/foo" , 0o644 ) ?;
617
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
618
+ let mut buf = dp. write_file ( "a/.btmp.foo" , 0o644 ) ?;
619
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
620
+ let mut buf = dp. write_file ( ".btmp.b/foo" , 0o644 ) ?;
621
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
622
+ }
623
+ assert ! ( dp. exists( "a/.btmp.a" ) ?) ;
624
+ assert ! ( dp. exists( "a/foo" ) ?) ;
625
+ assert ! ( dp. exists( "a/.btmp.foo" ) ?) ;
626
+ assert ! ( dp. exists( "a/.btmp.a" ) ?) ;
627
+ assert ! ( dp. exists( ".btmp.b/b" ) ?) ;
628
+ assert ! ( dp. exists( ".btmp.b/foo" ) ?) ;
629
+ cleanup_tmp ( & dp) ?;
630
+ assert ! ( !dp. exists( "a/.btmp.a" ) ?) ;
631
+ assert ! ( dp. exists( "a/foo" ) ?) ;
632
+ assert ! ( !dp. exists( "a/.btmp.foo" ) ?) ;
633
+ assert ! ( !dp. exists( ".btmp.b" ) ?) ;
634
+ Ok ( ( ) )
635
+ }
636
+ #[ test]
637
+ fn test_apply_with_file ( ) -> Result < ( ) > {
638
+ let tmpd = tempfile:: tempdir ( ) ?;
639
+ let p = tmpd. path ( ) ;
640
+ let pa = p. join ( "a" ) ;
641
+ let pb = p. join ( "b" ) ;
642
+ std:: fs:: create_dir ( & pa) ?;
643
+ std:: fs:: create_dir ( & pb) ?;
644
+ let a = openat:: Dir :: open ( & pa) ?;
645
+ let b = openat:: Dir :: open ( & pb) ?;
646
+ a. create_dir ( "foo" , 0o755 ) ?;
647
+ a. create_dir ( "bar" , 0o755 ) ?;
648
+ let foo = Path :: new ( "foo/bar" ) ;
649
+ let bar = Path :: new ( "bar/foo" ) ;
650
+ let testfile = "testfile" ;
651
+ {
652
+ let mut buf = a. write_file ( foo, 0o644 ) ?;
653
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
654
+ let mut buf = a. write_file ( bar, 0o644 ) ?;
655
+ buf. write_all ( "barcontents" . as_bytes ( ) ) ?;
656
+ let mut buf = a. write_file ( testfile, 0o644 ) ?;
657
+ buf. write_all ( "testfilecontents" . as_bytes ( ) ) ?;
658
+ }
659
+
660
+ let diff = run_diff ( & a, & b) ?;
661
+ assert_eq ! ( diff. count( ) , 3 ) ;
662
+ b. create_dir ( "foo" , 0o755 ) ?;
663
+ {
664
+ let mut buf = b. write_file ( foo, 0o644 ) ?;
665
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
666
+ }
667
+ let b_btime_foo = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
668
+
669
+ {
670
+ let diff = run_diff ( & b, & a) ?;
671
+ assert_eq ! ( diff. count( ) , 2 ) ;
672
+ apply_diff ( & a, & b, & diff, None ) . context ( "test additional files" ) ?;
673
+ assert_eq ! (
674
+ String :: from_utf8( std:: fs:: read( pb. join( testfile) ) ?) ?,
675
+ "testfilecontents"
676
+ ) ;
677
+ assert_eq ! (
678
+ String :: from_utf8( std:: fs:: read( pb. join( bar) ) ?) ?,
679
+ "barcontents"
680
+ ) ;
681
+ // creation time is not changed for unchanged file
682
+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
683
+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
684
+ }
685
+ {
686
+ fs:: write ( pa. join ( testfile) , "newtestfile" ) ?;
687
+ fs:: write ( pa. join ( bar) , "newbar" ) ?;
688
+ let diff = run_diff ( & b, & a) ?;
689
+ assert_eq ! ( diff. count( ) , 2 ) ;
690
+ apply_diff ( & a, & b, & diff, None ) . context ( "test changed files" ) ?;
691
+ assert_eq ! (
692
+ String :: from_utf8( std:: fs:: read( pb. join( testfile) ) ?) ?,
693
+ "newtestfile"
694
+ ) ;
695
+ assert_eq ! ( String :: from_utf8( std:: fs:: read( pb. join( bar) ) ?) ?, "newbar" ) ;
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
+ a. remove_file ( testfile) ?;
702
+ a. remove_file ( bar) ?;
703
+ let diff = run_diff ( & b, & a) ?;
704
+ assert_eq ! ( diff. count( ) , 2 ) ;
705
+ apply_diff ( & a, & b, & diff, None ) . context ( "test removed files" ) ?;
706
+ assert_eq ! ( b. exists( testfile) ?, false ) ;
707
+ assert_eq ! ( b. exists( bar) ?, false ) ;
708
+ let diff = run_diff ( & b, & a) ?;
709
+ assert_eq ! ( diff. count( ) , 0 ) ;
710
+ // creation time is not changed for unchanged file
711
+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
712
+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
713
+ }
714
+
715
+ Ok ( ( ) )
716
+ }
511
717
}
0 commit comments