Skip to main content

loadsmith_install/ops/
install.rs

1use 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; // we create the necessary dirs when creating files instead
38        }
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                // fs::copy already overwrites the file, no need to remove it first
93                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}