|
1 |
| -use std::io::Read; |
| 1 | +use std::collections::BTreeMap; |
| 2 | +use std::io::{self, BufRead, Read, Write}; |
2 | 3 | use std::path::{Path, PathBuf};
|
3 | 4 | use std::process::ExitStatus;
|
4 |
| -use std::{env, fs}; |
| 5 | +use std::{env, fs, time}; |
5 | 6 |
|
6 | 7 | use super::engine::Engine;
|
7 | 8 | use super::shared::*;
|
@@ -394,6 +395,209 @@ pub fn copy_volume_container_rust(
|
394 | 395 | Ok(())
|
395 | 396 | }
|
396 | 397 |
|
| 398 | +type FingerprintMap = BTreeMap<String, time::SystemTime>; |
| 399 | + |
| 400 | +fn parse_project_fingerprint(path: &Path) -> Result<FingerprintMap> { |
| 401 | + let epoch = time::SystemTime::UNIX_EPOCH; |
| 402 | + let file = fs::OpenOptions::new().read(true).open(path)?; |
| 403 | + let reader = io::BufReader::new(file); |
| 404 | + let mut result = BTreeMap::new(); |
| 405 | + for line in reader.lines() { |
| 406 | + let line = line?; |
| 407 | + let (timestamp, relpath) = line |
| 408 | + .split_once('\t') |
| 409 | + .ok_or_else(|| eyre::eyre!("unable to parse fingerprint line '{line}'"))?; |
| 410 | + let modified = epoch + time::Duration::from_millis(timestamp.parse::<u64>()?); |
| 411 | + result.insert(relpath.to_string(), modified); |
| 412 | + } |
| 413 | + |
| 414 | + Ok(result) |
| 415 | +} |
| 416 | + |
| 417 | +fn write_project_fingerprint(path: &Path, fingerprint: &FingerprintMap) -> Result<()> { |
| 418 | + let epoch = time::SystemTime::UNIX_EPOCH; |
| 419 | + let mut file = fs::OpenOptions::new() |
| 420 | + .write(true) |
| 421 | + .truncate(true) |
| 422 | + .create(true) |
| 423 | + .open(path)?; |
| 424 | + for (relpath, modified) in fingerprint { |
| 425 | + let timestamp = modified.duration_since(epoch)?.as_millis() as u64; |
| 426 | + writeln!(file, "{timestamp}\t{relpath}")?; |
| 427 | + } |
| 428 | + |
| 429 | + Ok(()) |
| 430 | +} |
| 431 | + |
| 432 | +fn read_dir_fingerprint( |
| 433 | + home: &Path, |
| 434 | + path: &Path, |
| 435 | + map: &mut FingerprintMap, |
| 436 | + copy_cache: bool, |
| 437 | +) -> Result<()> { |
| 438 | + let epoch = time::SystemTime::UNIX_EPOCH; |
| 439 | + for entry in fs::read_dir(path)? { |
| 440 | + let file = entry?; |
| 441 | + let file_type = file.file_type()?; |
| 442 | + // only parse known files types: 0 or 1 of these tests can pass. |
| 443 | + if file_type.is_dir() { |
| 444 | + if copy_cache || !is_cachedir(&file) { |
| 445 | + read_dir_fingerprint(home, &path.join(file.file_name()), map, copy_cache)?; |
| 446 | + } |
| 447 | + } else if file_type.is_file() || file_type.is_symlink() { |
| 448 | + // we're mounting to the same location, so this should fine |
| 449 | + // we need to round the modified date to millis. |
| 450 | + let modified = file.metadata()?.modified()?; |
| 451 | + let millis = modified.duration_since(epoch)?.as_millis() as u64; |
| 452 | + let rounded = epoch + time::Duration::from_millis(millis); |
| 453 | + let relpath = file.path().strip_prefix(home)?.as_posix()?; |
| 454 | + map.insert(relpath, rounded); |
| 455 | + } |
| 456 | + } |
| 457 | + |
| 458 | + Ok(()) |
| 459 | +} |
| 460 | + |
| 461 | +fn get_project_fingerprint(home: &Path, copy_cache: bool) -> Result<FingerprintMap> { |
| 462 | + let mut result = BTreeMap::new(); |
| 463 | + read_dir_fingerprint(home, home, &mut result, copy_cache)?; |
| 464 | + Ok(result) |
| 465 | +} |
| 466 | + |
| 467 | +fn get_fingerprint_difference<'a, 'b>( |
| 468 | + previous: &'a FingerprintMap, |
| 469 | + current: &'b FingerprintMap, |
| 470 | +) -> (Vec<&'b str>, Vec<&'a str>) { |
| 471 | + // this can be added or updated |
| 472 | + let changed: Vec<&str> = current |
| 473 | + .iter() |
| 474 | + .filter(|(ref k, ref v1)| { |
| 475 | + previous |
| 476 | + .get(&k.to_string()) |
| 477 | + .map(|ref v2| v1 != v2) |
| 478 | + .unwrap_or(true) |
| 479 | + }) |
| 480 | + .map(|(k, _)| k.as_str()) |
| 481 | + .collect(); |
| 482 | + let removed: Vec<&str> = previous |
| 483 | + .iter() |
| 484 | + .filter(|(ref k, _)| !current.contains_key(&k.to_string())) |
| 485 | + .map(|(k, _)| k.as_str()) |
| 486 | + .collect(); |
| 487 | + (changed, removed) |
| 488 | +} |
| 489 | + |
| 490 | +// copy files for a docker volume, for remote host support |
| 491 | +// provides a list of files relative to src. |
| 492 | +fn copy_volume_file_list( |
| 493 | + engine: &Engine, |
| 494 | + container: &str, |
| 495 | + src: &Path, |
| 496 | + dst: &Path, |
| 497 | + files: &[&str], |
| 498 | + verbose: bool, |
| 499 | +) -> Result<ExitStatus> { |
| 500 | + // SAFETY: safe, single-threaded execution. |
| 501 | + let tempdir = unsafe { temp::TempDir::new()? }; |
| 502 | + let temppath = tempdir.path(); |
| 503 | + for file in files { |
| 504 | + let src_path = src.join(file); |
| 505 | + let dst_path = temppath.join(file); |
| 506 | + fs::create_dir_all(dst_path.parent().expect("must have parent"))?; |
| 507 | + fs::copy(&src_path, &dst_path)?; |
| 508 | + } |
| 509 | + copy_volume_files(engine, container, temppath, dst, verbose) |
| 510 | +} |
| 511 | + |
| 512 | +// removed files from a docker volume, for remote host support |
| 513 | +// provides a list of files relative to src. |
| 514 | +fn remove_volume_file_list( |
| 515 | + engine: &Engine, |
| 516 | + container: &str, |
| 517 | + dst: &Path, |
| 518 | + files: &[&str], |
| 519 | + verbose: bool, |
| 520 | +) -> Result<ExitStatus> { |
| 521 | + const PATH: &str = "/tmp/remove_list"; |
| 522 | + let mut script = vec![]; |
| 523 | + if verbose { |
| 524 | + script.push("set -x".to_string()); |
| 525 | + } |
| 526 | + script.push(format!( |
| 527 | + "cat \"{PATH}\" | while read line; do |
| 528 | + rm -f \"${{line}}\" |
| 529 | +done |
| 530 | +
|
| 531 | +rm \"{PATH}\" |
| 532 | +" |
| 533 | + )); |
| 534 | + |
| 535 | + // SAFETY: safe, single-threaded execution. |
| 536 | + let mut tempfile = unsafe { temp::TempFile::new()? }; |
| 537 | + for file in files { |
| 538 | + writeln!(tempfile.file(), "{}", dst.join(file).as_posix()?)?; |
| 539 | + } |
| 540 | + |
| 541 | + // need to avoid having hundreds of files on the command, so |
| 542 | + // just provide a single file name. |
| 543 | + subcommand(engine, "cp") |
| 544 | + .arg(tempfile.path()) |
| 545 | + .arg(format!("{container}:{PATH}")) |
| 546 | + .run_and_get_status(verbose, true)?; |
| 547 | + |
| 548 | + subcommand(engine, "exec") |
| 549 | + .arg(container) |
| 550 | + .args(&["sh", "-c", &script.join("\n")]) |
| 551 | + .run_and_get_status(verbose, true) |
| 552 | + .map_err(Into::into) |
| 553 | +} |
| 554 | + |
| 555 | +fn copy_volume_container_project( |
| 556 | + engine: &Engine, |
| 557 | + container: &str, |
| 558 | + src: &Path, |
| 559 | + dst: &Path, |
| 560 | + volume: &VolumeId, |
| 561 | + copy_cache: bool, |
| 562 | + verbose: bool, |
| 563 | +) -> Result<()> { |
| 564 | + let copy_all = || { |
| 565 | + if copy_cache { |
| 566 | + copy_volume_files(engine, container, src, dst, verbose) |
| 567 | + } else { |
| 568 | + copy_volume_files_nocache(engine, container, src, dst, verbose) |
| 569 | + } |
| 570 | + }; |
| 571 | + match volume { |
| 572 | + VolumeId::Keep(_) => { |
| 573 | + let parent = temp::dir()?; |
| 574 | + fs::create_dir_all(&parent)?; |
| 575 | + let fingerprint = parent.join(container); |
| 576 | + let current = get_project_fingerprint(src, copy_cache)?; |
| 577 | + if fingerprint.exists() { |
| 578 | + let previous = parse_project_fingerprint(&fingerprint)?; |
| 579 | + let (changed, removed) = get_fingerprint_difference(&previous, ¤t); |
| 580 | + write_project_fingerprint(&fingerprint, ¤t)?; |
| 581 | + |
| 582 | + if !changed.is_empty() { |
| 583 | + copy_volume_file_list(engine, container, src, dst, &changed, verbose)?; |
| 584 | + } |
| 585 | + if !removed.is_empty() { |
| 586 | + remove_volume_file_list(engine, container, dst, &removed, verbose)?; |
| 587 | + } |
| 588 | + } else { |
| 589 | + write_project_fingerprint(&fingerprint, ¤t)?; |
| 590 | + copy_all()?; |
| 591 | + } |
| 592 | + } |
| 593 | + VolumeId::Discard(_) => { |
| 594 | + copy_all()?; |
| 595 | + } |
| 596 | + } |
| 597 | + |
| 598 | + Ok(()) |
| 599 | +} |
| 600 | + |
397 | 601 | fn run_and_get_status(engine: &Engine, args: &[&str], verbose: bool) -> Result<ExitStatus> {
|
398 | 602 | command(engine)
|
399 | 603 | .args(args)
|
@@ -645,7 +849,15 @@ pub(crate) fn run(
|
645 | 849 | } else {
|
646 | 850 | mount_prefix_path.join("project")
|
647 | 851 | };
|
648 |
| - copy(&dirs.host_root, &mount_root)?; |
| 852 | + copy_volume_container_project( |
| 853 | + engine, |
| 854 | + &container, |
| 855 | + &dirs.host_root, |
| 856 | + &mount_root, |
| 857 | + &volume, |
| 858 | + copy_cache, |
| 859 | + verbose, |
| 860 | + )?; |
649 | 861 |
|
650 | 862 | let mut copied = vec![
|
651 | 863 | (&dirs.xargo, mount_prefix_path.join("xargo")),
|
@@ -692,7 +904,7 @@ pub(crate) fn run(
|
692 | 904 | let mut final_args = vec![];
|
693 | 905 | let mut iter = args.iter().cloned();
|
694 | 906 | let mut has_target_dir = false;
|
695 |
| - let target_dir_string = target_dir.to_utf8()?.to_string(); |
| 907 | + let target_dir_string = target_dir.as_posix()?; |
696 | 908 | while let Some(arg) = iter.next() {
|
697 | 909 | if arg == "--target-dir" {
|
698 | 910 | has_target_dir = true;
|
|
0 commit comments