loadsmith_install/ops/
install.rs1use std::{
2 fmt::Debug,
3 fs::{self},
4 path::Path,
5};
6
7use camino::Utf8PathBuf;
8use loadsmith_core::{Checksum, InstalledFile, InstalledPackage, PackageRef};
9use tracing::{debug, trace};
10use walkdir::WalkDir;
11
12use crate::{
13 InstallRule, InstallRuleset,
14 error::{Error, Result},
15};
16
17pub fn install(
18 package: PackageRef,
19 ruleset: InstallRuleset,
20 source: impl AsRef<Path>,
21 profile: impl AsRef<Path>,
22 no_links: bool,
23 checksum: Option<Checksum>,
24) -> Result<(InstalledPackage, Vec<Utf8PathBuf>)> {
25 let source = source.as_ref();
26 let profile = profile.as_ref();
27
28 let mut installed_files = Vec::new();
29 let mut overriden_files = Vec::new();
30
31 let walkdir = WalkDir::new(source).follow_links(false).into_iter();
32
33 for entry in walkdir {
34 let entry = entry?;
35
36 if entry.file_type().is_dir() {
37 continue; }
39
40 let relative_path = entry
41 .path()
42 .strip_prefix(source)
43 .expect("entry path should be relative to source");
44
45 let relative_path = Utf8PathBuf::try_from(relative_path.to_path_buf())?;
46
47 let Some((mapped, rule)) = ruleset.map_file_and_return_rule(&relative_path, &package)
48 else {
49 debug!(%relative_path, "files left unmapped by ruleset, skipping");
50 continue;
51 };
52
53 trace!(
54 from = ?relative_path,
55 to = ?mapped,
56 "mapped file"
57 );
58
59 let (installed, overwrote) =
60 install_file(entry.path(), &profile.join(&mapped), mapped, rule, no_links)?;
61
62 if let Some(installed) = installed {
63 installed_files.push(installed);
64 }
65
66 if overwrote {
67 overriden_files.push(relative_path);
68 }
69 }
70
71 Ok((
72 InstalledPackage::now(package, installed_files, checksum),
73 overriden_files,
74 ))
75}
76
77fn install_file(
78 source: &Path,
79 target: &Path,
80 mapped_relative: Utf8PathBuf,
81 rule: &InstallRule,
82 no_links: bool,
83) -> Result<(Option<InstalledFile>, bool)> {
84 let link = !no_links && rule.use_links();
85 let mut overwrote = false;
86
87 if target.exists() {
88 match rule.conflict_strategy() {
89 ConflictStrategy::Overwrite => {
90 trace!(%mapped_relative, "overwriting existing file");
91 overwrote = true;
92 if link {
94 fs::remove_file(&target)?;
95 }
96 }
97 ConflictStrategy::Skip => {
98 trace!(%mapped_relative, "skipping existing file");
99 return Ok((None, false));
100 }
101 ConflictStrategy::Error => {
102 return Err(Error::FileAlreadyExists(target.into()));
103 }
104 }
105 }
106
107 loadsmith_util::create_parent_dirs(&target)?;
108
109 if link {
110 trace!(%mapped_relative, "link file");
111 fs::hard_link(source, &target)?;
112 } else {
113 trace!(%mapped_relative, "copy file");
114 fs::copy(source, &target)?;
115 }
116
117 Ok((Some(InstalledFile::new(mapped_relative, link)), overwrote))
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum ConflictStrategy {
122 Overwrite,
123 Skip,
124 Error,
125}