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" ) ) ]
@@ -272,20 +275,41 @@ pub(crate) struct ApplyUpdateOptions {
272
275
// Let's just fork off a helper process for now.
273
276
#[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
274
277
pub ( crate ) fn syncfs ( d : & openat:: Dir ) -> Result < ( ) > {
275
- use rustix:: fd:: BorrowedFd ;
276
278
use rustix:: fs:: { Mode , OFlags } ;
277
279
let d = unsafe { BorrowedFd :: borrow_raw ( d. as_raw_fd ( ) ) } ;
278
280
let oflags = OFlags :: RDONLY | OFlags :: CLOEXEC | OFlags :: DIRECTORY ;
279
281
let d = rustix:: fs:: openat ( d, "." , oflags, Mode :: empty ( ) ) ?;
280
282
rustix:: fs:: syncfs ( d) . map_err ( Into :: into)
281
283
}
282
284
285
+ /// Copy from src to dst at root dir
286
+ #[ cfg( any( target_arch = "x86_64" , target_arch = "aarch64" ) ) ]
287
+ fn copy_dir ( root : & openat:: Dir , src : & str , dst : & str ) -> Result < ( ) > {
288
+ let rootfd = unsafe { BorrowedFd :: borrow_raw ( root. as_raw_fd ( ) ) } ;
289
+ let r = unsafe {
290
+ Command :: new ( "cp" )
291
+ . args ( [ "-a" ] )
292
+ . arg ( src)
293
+ . arg ( dst)
294
+ . pre_exec ( move || rustix:: process:: fchdir ( rootfd) . map_err ( Into :: into) )
295
+ . status ( ) ?
296
+ } ;
297
+ if !r. success ( ) {
298
+ anyhow:: bail!( "Failed to copy {src} to {dst}" ) ;
299
+ }
300
+ log:: debug!( "Copy {src} to {dst}" ) ;
301
+ Ok ( ( ) )
302
+ }
303
+
304
+ /// Get first sub dir and tmp sub dir for the path
305
+ /// "fedora/foo/bar" -> ("fedora", "fedora.tmp")
306
+ /// "foo" -> ("foo", "foo.tmp")
283
307
#[ 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)
308
+ fn get_subdir ( path : & Utf8Path ) -> Result < ( String , String ) > {
309
+ let buf = path. iter ( ) . next ( ) . unwrap ( ) . to_owned ( ) ;
310
+ let mut buf_tmp = buf . clone ( ) ;
311
+ buf_tmp . push_str ( ".tmp" ) ;
312
+ Ok ( ( buf, buf_tmp ) )
289
313
}
290
314
291
315
/// Given two directories, apply a diff generated from srcdir to destdir
@@ -302,34 +326,88 @@ pub(crate) fn apply_diff(
302
326
let opts = opts. unwrap_or ( & default_opts) ;
303
327
cleanup_tmp ( destdir) . context ( "cleaning up temporary files" ) ?;
304
328
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 ) ?;
329
+ let mut updates = HashMap :: new ( ) ;
330
+ // Handle removals in temp dir
331
+ if !opts. skip_removals {
332
+ for pathstr in diff. removals . iter ( ) {
333
+ let path = Utf8Path :: new ( pathstr) ;
334
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
335
+ let path_tmp;
336
+ if subdir != * pathstr {
337
+ // need to copy to temp subdir and remember
338
+ if !destdir. exists ( & subdir_tmp) ? {
339
+ copy_dir ( destdir, & subdir, & subdir_tmp) ?;
340
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
341
+ }
342
+ path_tmp = Utf8Path :: new ( & subdir_tmp) . join ( path. strip_prefix ( & subdir) ?) ;
343
+ } else {
344
+ // remove the file directly that is not in dir
345
+ path_tmp = path. to_path_buf ( ) ;
346
+ }
347
+ destdir
348
+ . remove_file ( path_tmp. as_std_path ( ) )
349
+ . with_context ( || format ! ( "removing {:?}" , path_tmp) ) ?;
310
350
}
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
351
}
316
352
// Ensure all of the new files are written persistently to disk
317
353
if !opts. skip_sync {
318
354
syncfs ( destdir) ?;
319
355
}
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 ( ) {
356
+
357
+ // Write changed files to temp dir and rename finally
358
+ for pathstr in diff. changes . iter ( ) {
359
+ let path = Utf8Path :: new ( pathstr) ;
360
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
361
+ let path_tmp;
362
+ if subdir != * pathstr {
363
+ if !destdir. exists ( & subdir_tmp) ? {
364
+ // need to copy to temp subdir and remember
365
+ copy_dir ( destdir, & subdir, & subdir_tmp) ?;
366
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
367
+ }
368
+ path_tmp = Utf8Path :: new ( & subdir_tmp) . join ( path. strip_prefix ( & subdir) ?) ;
329
369
destdir
330
- . remove_file_optional ( path)
331
- . with_context ( || format ! ( "removing {path}" ) ) ?;
370
+ . remove_file_optional ( path_tmp. as_std_path ( ) )
371
+ . with_context ( || format ! ( "removing {path_tmp}" ) ) ?;
372
+ } else {
373
+ // file that is not in dir, copy file to foo.tmp
374
+ updates. insert ( subdir. clone ( ) , subdir_tmp. clone ( ) ) ;
375
+ path_tmp = Utf8PathBuf :: from ( & subdir_tmp) ;
332
376
}
377
+ srcdir
378
+ . copy_file_at ( path. as_std_path ( ) , destdir, path_tmp. as_std_path ( ) )
379
+ . with_context ( || format ! ( "copying {:?} to {:?}" , path, path_tmp) ) ?;
380
+ }
381
+ // Write new files to temp dir if exists, else write directly in dest
382
+ for pathstr in diff. additions . iter ( ) {
383
+ let path = Utf8Path :: new ( pathstr) ;
384
+ let ( subdir, subdir_tmp) = get_subdir ( path) ?;
385
+ let path_tmp;
386
+ if destdir. exists ( & subdir_tmp) ? {
387
+ path_tmp = Utf8Path :: new ( & subdir_tmp) . join ( path. strip_prefix ( & subdir) ?) ;
388
+ } else {
389
+ path_tmp = path. to_path_buf ( ) ;
390
+ }
391
+ // ensure new additions dir exists
392
+ if let Some ( parent) = path_tmp. parent ( ) . filter ( |& v| !v. as_os_str ( ) . is_empty ( ) ) {
393
+ destdir. ensure_dir_all ( parent. as_std_path ( ) , DEFAULT_FILE_MODE ) ?;
394
+ }
395
+ srcdir
396
+ . copy_file_at ( path. as_std_path ( ) , destdir, path_tmp. as_std_path ( ) )
397
+ . with_context ( || format ! ( "copying {:?} to {:?}" , path, path_tmp) ) ?;
398
+ }
399
+
400
+ // do local exchange
401
+ for ( src, tmp) in updates. iter ( ) {
402
+ log:: trace!( "doing local exchange for {} and {}" , tmp, src) ;
403
+ destdir
404
+ . local_exchange ( tmp, src)
405
+ . with_context ( || format ! ( "exchange for {} and {}" , tmp, src) ) ?;
406
+ }
407
+ // finally remove the temp dir
408
+ for ( _, tmp) in updates. iter ( ) {
409
+ log:: trace!( "cleanup: {}" , tmp) ;
410
+ destdir. remove_all ( tmp) . context ( "clean up temp" ) ?;
333
411
}
334
412
// A second full filesystem sync to narrow any races rather than
335
413
// waiting for writeback to kick in.
@@ -345,6 +423,7 @@ mod tests {
345
423
use super :: * ;
346
424
use std:: fs;
347
425
use std:: io:: Write ;
426
+ use std:: path:: Path ;
348
427
349
428
fn run_diff ( a : & openat:: Dir , b : & openat:: Dir ) -> Result < FileTreeDiff > {
350
429
let ta = FileTree :: new_from_dir ( a) ?;
@@ -508,4 +587,91 @@ mod tests {
508
587
assert ! ( !a. join( relp) . join( "shim.x64" ) . exists( ) ) ;
509
588
Ok ( ( ) )
510
589
}
590
+ #[ test]
591
+ fn test_get_subdir ( ) -> Result < ( ) > {
592
+ // test path
593
+ let path = Utf8Path :: new ( "foo/subdir/bar" ) ;
594
+ let ( tp, tp_tmp) = get_subdir ( path) ?;
595
+ assert_eq ! ( tp. eq( "foo" ) , tp_tmp. eq( "foo.tmp" ) ) ;
596
+ // test file
597
+ let path = Utf8Path :: new ( "foo" ) ;
598
+ let ( tp, tp_tmp) = get_subdir ( path) ?;
599
+ assert_eq ! ( tp. eq( "foo" ) , tp_tmp. eq( "foo.tmp" ) ) ;
600
+ Ok ( ( ) )
601
+ }
602
+ #[ test]
603
+ fn test_apply_with_file ( ) -> Result < ( ) > {
604
+ let tmpd = tempfile:: tempdir ( ) ?;
605
+ let p = tmpd. path ( ) ;
606
+ let pa = p. join ( "a" ) ;
607
+ let pb = p. join ( "b" ) ;
608
+ std:: fs:: create_dir ( & pa) ?;
609
+ std:: fs:: create_dir ( & pb) ?;
610
+ let a = openat:: Dir :: open ( & pa) ?;
611
+ let b = openat:: Dir :: open ( & pb) ?;
612
+ a. create_dir ( "foo" , 0o755 ) ?;
613
+ a. create_dir ( "bar" , 0o755 ) ?;
614
+ let foo = Path :: new ( "foo/bar" ) ;
615
+ let bar = Path :: new ( "bar/foo" ) ;
616
+ let testfile = "testfile" ;
617
+ {
618
+ let mut buf = a. write_file ( foo, 0o644 ) ?;
619
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
620
+ let mut buf = a. write_file ( bar, 0o644 ) ?;
621
+ buf. write_all ( "barcontents" . as_bytes ( ) ) ?;
622
+ let mut buf = a. write_file ( testfile, 0o644 ) ?;
623
+ buf. write_all ( "testfilecontents" . as_bytes ( ) ) ?;
624
+ }
625
+
626
+ let diff = run_diff ( & a, & b) ?;
627
+ assert_eq ! ( diff. count( ) , 3 ) ;
628
+ b. create_dir ( "foo" , 0o755 ) ?;
629
+ {
630
+ let mut buf = b. write_file ( foo, 0o644 ) ?;
631
+ buf. write_all ( "foocontents" . as_bytes ( ) ) ?;
632
+ }
633
+ let b_btime_foo = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
634
+
635
+ {
636
+ let diff = run_diff ( & b, & a) ?;
637
+ assert_eq ! ( diff. count( ) , 2 ) ;
638
+ apply_diff ( & a, & b, & diff, None ) . context ( "test additional files" ) ?;
639
+ assert_eq ! (
640
+ String :: from_utf8( std:: fs:: read( pb. join( testfile) ) ?) ?,
641
+ "testfilecontents"
642
+ ) ;
643
+ assert_eq ! (
644
+ String :: from_utf8( std:: fs:: read( pb. join( bar) ) ?) ?,
645
+ "barcontents"
646
+ ) ;
647
+ // creation time is not changed for unchanged file
648
+ let b_btime_foo_new = fs:: metadata ( pb. join ( foo) ) ?. created ( ) ?;
649
+ assert_eq ! ( b_btime_foo_new, b_btime_foo) ;
650
+ }
651
+ {
652
+ fs:: write ( pa. join ( testfile) , "newtestfile" ) ?;
653
+ fs:: write ( pa. join ( bar) , "newbar" ) ?;
654
+ let diff = run_diff ( & b, & a) ?;
655
+ assert_eq ! ( diff. count( ) , 2 ) ;
656
+ apply_diff ( & a, & b, & diff, None ) . context ( "test changed files" ) ?;
657
+ assert_eq ! (
658
+ String :: from_utf8( std:: fs:: read( pb. join( testfile) ) ?) ?,
659
+ "newtestfile"
660
+ ) ;
661
+ assert_eq ! ( String :: from_utf8( std:: fs:: read( pb. join( bar) ) ?) ?, "newbar" ) ;
662
+ }
663
+ {
664
+ a. remove_file ( testfile) ?;
665
+ a. remove_file ( bar) ?;
666
+ let diff = run_diff ( & b, & a) ?;
667
+ assert_eq ! ( diff. count( ) , 2 ) ;
668
+ apply_diff ( & a, & b, & diff, None ) . context ( "test removed file" ) ?;
669
+ assert_eq ! ( b. exists( testfile) ?, false ) ;
670
+ assert_eq ! ( b. exists( bar) ?, false ) ;
671
+ let diff = run_diff ( & b, & a) ?;
672
+ assert_eq ! ( diff. count( ) , 0 ) ;
673
+ }
674
+
675
+ Ok ( ( ) )
676
+ }
511
677
}
0 commit comments