winbrew_engines\archive/
install.rs1use anyhow::Result;
2use std::fs;
3use std::path::Path;
4
5use crate::core::fs::{cleanup_path, extract_archive, replace_directory};
6
7use crate::models::install::engine::EngineInstallReceipt;
8use crate::models::install::engine::EngineKind;
9
10use crate::payload::archive_kind_for_url;
11
12pub(crate) fn install(
17 download_path: &Path,
18 install_dir: &Path,
19 installer_url: &str,
20) -> Result<EngineInstallReceipt> {
21 let stage_dir = install_dir.parent().unwrap_or(install_dir).join("staging");
22 let archive_kind = archive_kind_for_url(installer_url).unwrap_or(crate::core::ArchiveKind::Zip);
23
24 cleanup_path(&stage_dir)?;
25 fs::create_dir_all(&stage_dir)?;
26
27 extract_archive(archive_kind, download_path, &stage_dir)?;
28 replace_directory(&stage_dir, install_dir)?;
29
30 Ok(EngineInstallReceipt::new(
31 EngineKind::Zip,
32 install_dir.to_string_lossy().into_owned(),
33 None,
34 ))
35}
36
37#[cfg(test)]
38mod tests {
39 use super::install;
40 use flate2::Compression;
41 use flate2::write::GzEncoder;
42 use std::fs;
43 use std::io::{Read, Write};
44 use tar::Builder;
45 use tar::Header;
46 use tempfile::tempdir;
47 use zip::ZipWriter;
48 use zip::write::SimpleFileOptions;
49
50 fn create_zip_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
51 let file = fs::File::create(path).expect("create zip file");
52 let mut writer = ZipWriter::new(file);
53 writer
54 .start_file(file_name, SimpleFileOptions::default())
55 .expect("start zip entry");
56 writer.write_all(contents).expect("write zip contents");
57 writer.finish().expect("finish zip file");
58 }
59
60 fn create_tar_gz_archive(path: &std::path::Path, file_name: &str, contents: &[u8]) {
61 let file = fs::File::create(path).expect("create tar.gz file");
62 let encoder = GzEncoder::new(file, Compression::default());
63 let mut builder = Builder::new(encoder);
64 let mut header = Header::new_gnu();
65 header.set_size(contents.len() as u64);
66 header.set_mode(0o644);
67 header.set_cksum();
68
69 builder
70 .append_data(&mut header, file_name, contents)
71 .expect("append tar.gz entry");
72 let encoder = builder.into_inner().expect("finish tar builder");
73 encoder.finish().expect("finish tar.gz file");
74 }
75
76 fn create_zip_archive_with_tree(path: &std::path::Path, entries: &[(&str, &[u8])]) {
77 let file = fs::File::create(path).expect("create zip file");
78 let mut writer = ZipWriter::new(file);
79
80 for (file_name, contents) in entries {
81 writer
82 .start_file(file_name, SimpleFileOptions::default())
83 .expect("start zip entry");
84 writer.write_all(contents).expect("write zip contents");
85 }
86
87 writer.finish().expect("finish zip file");
88 }
89
90 #[test]
91 fn install_extracts_archive_into_install_directory() {
92 let temp_root = tempdir().expect("temp root");
93 let download_path = temp_root.path().join("download.zip");
94 let install_dir = temp_root.path().join("packages").join("Contoso.Zip");
95
96 create_zip_archive(&download_path, "bin/tool.exe", b"zip-binary");
97
98 install(
99 &download_path,
100 &install_dir,
101 "https://example.invalid/download.zip",
102 )
103 .expect("zip install");
104
105 let installed_file = install_dir.join("bin").join("tool.exe");
106 let mut contents = String::default();
107 fs::File::open(&installed_file)
108 .expect("installed file")
109 .read_to_string(&mut contents)
110 .expect("read installed file");
111
112 assert_eq!(contents, "zip-binary");
113 }
114
115 #[test]
116 fn install_preserves_nested_archive_trees_without_promoting_a_binary() {
117 let temp_root = tempdir().expect("temp root");
118 let download_path = temp_root.path().join("download.zip");
119 let install_dir = temp_root.path().join("packages").join("Contoso.Tree");
120
121 create_zip_archive_with_tree(
122 &download_path,
123 &[
124 ("README.md", b"tree readme"),
125 ("docs/notes.txt", b"notes"),
126 ("bin/tool.exe", b"tree-binary"),
127 ],
128 );
129
130 install(
131 &download_path,
132 &install_dir,
133 "https://example.invalid/download.zip",
134 )
135 .expect("tree archive install");
136
137 let readme = install_dir.join("README.md");
138 let notes = install_dir.join("docs").join("notes.txt");
139 let binary = install_dir.join("bin").join("tool.exe");
140
141 assert!(readme.exists(), "README should stay at the archive root");
142 assert!(notes.exists(), "nested docs should be preserved");
143 assert!(binary.exists(), "binary should remain in its original path");
144
145 let mut readme_contents = String::default();
146 fs::File::open(&readme)
147 .expect("readme file")
148 .read_to_string(&mut readme_contents)
149 .expect("read readme");
150
151 let mut binary_contents = String::default();
152 fs::File::open(&binary)
153 .expect("binary file")
154 .read_to_string(&mut binary_contents)
155 .expect("read binary");
156
157 assert_eq!(readme_contents, "tree readme");
158 assert_eq!(binary_contents, "tree-binary");
159 }
160
161 #[test]
162 fn install_extracts_tar_gz_archive_into_install_directory() {
163 let temp_root = tempdir().expect("temp root");
164 let download_path = temp_root.path().join("download.tar.gz");
165 let install_dir = temp_root.path().join("packages").join("Contoso.Tar");
166
167 create_tar_gz_archive(&download_path, "bin/tool.exe", b"tar-binary");
168
169 install(
170 &download_path,
171 &install_dir,
172 "https://example.invalid/download.tar.gz",
173 )
174 .expect("tar.gz install");
175
176 let installed_file = install_dir.join("bin").join("tool.exe");
177 let mut contents = String::default();
178 fs::File::open(&installed_file)
179 .expect("installed file")
180 .read_to_string(&mut contents)
181 .expect("read installed file");
182
183 assert_eq!(contents, "tar-binary");
184 }
185}