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(
47 package: PackageRef,
48 ruleset: InstallRuleset,
49 source: impl AsRef<Path>,
50 profile: impl AsRef<Path>,
51 no_links: bool,
52 checksum: Option<Checksum>,
53) -> Result<(InstalledPackage, Vec<Utf8PathBuf>)> {
54 let source = source.as_ref();
55 let profile = profile.as_ref();
56
57 let mut installed_files = Vec::new();
58 let mut overriden_files = Vec::new();
59
60 let walkdir = WalkDir::new(source).follow_links(false).into_iter();
61
62 for entry in walkdir {
63 let entry = entry?;
64
65 if entry.file_type().is_dir() {
66 continue; }
68
69 let relative_path = entry
70 .path()
71 .strip_prefix(source)
72 .expect("entry path should be relative to source");
73
74 let relative_path = Utf8PathBuf::try_from(relative_path.to_path_buf())?;
75
76 let Some((mapped, rule)) = ruleset.map_file_and_return_rule(&relative_path, &package)
77 else {
78 debug!(%relative_path, "files left unmapped by ruleset, skipping");
79 continue;
80 };
81
82 trace!(
83 from = ?relative_path,
84 to = ?mapped,
85 "mapped file"
86 );
87
88 let (installed, overwrote) =
89 install_file(entry.path(), &profile.join(&mapped), mapped, rule, no_links)?;
90
91 if let Some(installed) = installed {
92 installed_files.push(installed);
93 }
94
95 if overwrote {
96 overriden_files.push(relative_path);
97 }
98 }
99
100 Ok((
101 InstalledPackage::now(package, installed_files, checksum),
102 overriden_files,
103 ))
104}
105
106fn install_file(
107 source: &Path,
108 target: &Path,
109 mapped_relative: Utf8PathBuf,
110 rule: &InstallRule,
111 no_links: bool,
112) -> Result<(Option<InstalledFile>, bool)> {
113 let link = !no_links && rule.use_links();
114 let mut overwrote = false;
115
116 if target.exists() {
117 match rule.conflict_strategy() {
118 ConflictStrategy::Overwrite => {
119 trace!(%mapped_relative, "overwriting existing file");
120 overwrote = true;
121 if link {
123 fs::remove_file(&target)?;
124 }
125 }
126 ConflictStrategy::Skip => {
127 trace!(%mapped_relative, "skipping existing file");
128 return Ok((None, false));
129 }
130 ConflictStrategy::Error => {
131 return Err(Error::FileAlreadyExists(target.into()));
132 }
133 }
134 }
135
136 loadsmith_util::create_parent_dirs(&target)?;
137
138 if link {
139 trace!(%mapped_relative, "link file");
140 fs::hard_link(source, &target)?;
141 } else {
142 trace!(%mapped_relative, "copy file");
143 fs::copy(source, &target)?;
144 }
145
146 Ok((Some(InstalledFile::new(mapped_relative, link)), overwrote))
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum ConflictStrategy {
161 Overwrite,
163 Skip,
165 Error,
167}