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,44 @@ 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
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")
283
312
#[ 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) )
289
321
}
290
322
291
323
/// Given two directories, apply a diff generated from srcdir to destdir
@@ -302,41 +334,83 @@ pub(crate) fn apply_diff(
302
334
let opts = opts. unwrap_or ( & default_opts) ;
303
335
cleanup_tmp ( destdir) . context ( "cleaning up temporary files" ) ?;
304
336
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" ) ) ?;
310
378
}
311
- let destp = tmpname_for_path ( path ) ;
379
+ updates . insert ( first_dir , first_dir_tmp ) ;
312
380
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
+ }
315
398
}
316
- // Ensure all of the new files are written persistently to disk
399
+ // Ensure all of the updates & changes are written persistently to disk
317
400
if !opts. skip_sync {
318
401
syncfs ( destdir) ?;
319
402
}
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" ) ?;
333
408
}
334
409
// A second full filesystem sync to narrow any races rather than
335
410
// waiting for writeback to kick in.
336
411
if !opts. skip_sync {
337
412
syncfs ( destdir) ?;
338
413
}
339
-
340
414
Ok ( ( ) )
341
415
}
342
416
@@ -345,6 +419,7 @@ mod tests {
345
419
use super :: * ;
346
420
use std:: fs;
347
421
use std:: io:: Write ;
422
+ use std:: path:: Path ;
348
423
349
424
fn run_diff ( a : & openat:: Dir , b : & openat:: Dir ) -> Result < FileTreeDiff > {
350
425
let ta = FileTree :: new_from_dir ( a) ?;
@@ -508,4 +583,129 @@ mod tests {
508
583
assert ! ( !a. join( relp) . join( "shim.x64" ) . exists( ) ) ;
509
584
Ok ( ( ) )
510
585
}
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
+ }
511
711
}
0 commit comments