openfare_lib/common/fs/
archive.rs1use anyhow::{format_err, Result};
2use std::convert::TryFrom;
3use std::io::Write;
4
5#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
6pub enum ArchiveType {
7 Zip,
8 TarGz,
9 Tgz,
10 Unknown,
11}
12
13impl std::convert::TryFrom<&std::path::PathBuf> for ArchiveType {
14 type Error = anyhow::Error;
15
16 fn try_from(path: &std::path::PathBuf) -> Result<Self, Self::Error> {
17 Ok(match get_file_extension(&path)?.as_str() {
18 "zip" => Self::Zip,
19 "tar.gz" => Self::TarGz,
20 "tgz" => Self::Tgz,
21 _ => Self::Unknown,
22 })
23 }
24}
25
26impl ArchiveType {
27 pub fn try_to_string(&self) -> Result<String> {
28 Ok(match self {
29 ArchiveType::Zip => "zip",
30 ArchiveType::TarGz => "tar.gz",
31 ArchiveType::Tgz => "tgz",
32 ArchiveType::Unknown => {
33 return Err(format_err!(
34 "Failed to convert unknown archive type into string."
35 ))
36 }
37 }
38 .to_string())
39 }
40}
41
42fn get_file_extension(path: &std::path::PathBuf) -> Result<String> {
44 if path
45 .to_str()
46 .ok_or(format_err!("Failed to parse URL path as str."))?
47 .ends_with(".tar.gz")
48 {
49 return Ok("tar.gz".to_string());
50 }
51
52 Ok(path
53 .extension()
54 .unwrap_or(std::ffi::OsString::from("").as_os_str())
55 .to_str()
56 .ok_or(format_err!(
57 "Failed to parse file extension unicode characters."
58 ))?
59 .to_owned())
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn test_correct_extension_extracted_for_tar_gz() -> Result<()> {
68 let result = get_file_extension(&std::path::PathBuf::from("/d3/d3-4.10.0.tar.gz"))?;
69 let expected = "tar.gz".to_string();
70 assert!(result == expected);
71 Ok(())
72 }
73}
74
75pub fn extract(
76 archive_path: &std::path::PathBuf,
77 destination_directory: &std::path::PathBuf,
78) -> Result<std::path::PathBuf> {
79 log::debug!("Extracting archive: {}", archive_path.display());
80 let archive_type = ArchiveType::try_from(archive_path)?;
81 let workspace_directory = match archive_type {
82 ArchiveType::Zip => extract_zip(&archive_path, &destination_directory)?,
83 ArchiveType::Tgz | ArchiveType::TarGz => {
84 extract_tar_gz(&archive_path, &destination_directory)?
85 }
86 ArchiveType::Unknown => {
87 return Err(format_err!(
88 "Archive extraction failed. Unsupported archive file type: {}",
89 archive_path.display()
90 ));
91 }
92 };
93 log::debug!(
94 "Archive extraction complete. Workspace directory: {}",
95 workspace_directory.display()
96 );
97 Ok(workspace_directory)
98}
99
100fn extract_zip(
101 archive_path: &std::path::PathBuf,
102 destination_directory: &std::path::PathBuf,
103) -> Result<std::path::PathBuf> {
104 let file = std::fs::File::open(&archive_path)?;
105 let mut archive = zip::ZipArchive::new(file)?;
106
107 let extracted_directory = destination_directory.join(
108 archive
109 .by_index(0)?
110 .enclosed_name()
111 .ok_or(format_err!(
112 "Archive is unexpectedly empty: {}",
113 archive_path.display()
114 ))?
115 .to_path_buf(),
116 );
117
118 for i in 0..archive.len() {
119 let mut file = archive.by_index(i)?;
120 let output_path = match file.enclosed_name() {
121 Some(path) => path.to_owned(),
122 None => continue,
123 };
124 let output_path = destination_directory.join(output_path);
125
126 if (&*file.name()).ends_with('/') {
127 std::fs::create_dir_all(&output_path)?;
128 } else {
129 if let Some(parent) = output_path.parent() {
130 if !parent.exists() {
131 std::fs::create_dir_all(&parent)?;
132 }
133 }
134 let mut output_file = std::fs::File::create(&output_path)?;
135 std::io::copy(&mut file, &mut output_file)?;
136 }
137 }
138 Ok(extracted_directory)
139}
140
141pub fn extract_tar_gz(
145 archive_path: &std::path::PathBuf,
146 destination_directory: &std::path::PathBuf,
147) -> Result<std::path::PathBuf> {
148 let top_directory_name = get_tar_top_directory_name(&archive_path)?;
149
150 let file = std::fs::File::open(archive_path)?;
151 let decoder = flate2::read::GzDecoder::new(file);
152 let mut archive = tar::Archive::new(decoder);
153 archive.unpack(&destination_directory)?;
154
155 let workspace_directory = if let Some(top_directory_name) = top_directory_name {
156 log::debug!(
157 "Found archive top level directory name: {}",
158 top_directory_name
159 );
160 let workspace_directory = destination_directory.join(top_directory_name);
161 workspace_directory
162 } else {
163 log::debug!("Archive top level directory not found. Creating stand-in.");
164
165 let uuid = uuid::Uuid::new_v4();
167 let mut encode_buffer = uuid::Uuid::encode_buffer();
168 let uuid = uuid.to_hyphenated().encode_lower(&mut encode_buffer);
169 let workspace_directory_name = "openfare-workspace-".to_string() + uuid;
170
171 let workspace_directory = destination_directory.join(workspace_directory_name);
172 std::fs::create_dir(&workspace_directory)?;
173
174 let paths = std::fs::read_dir(destination_directory)?;
175 for path in paths {
176 let file_name = path?.file_name();
177 let path = destination_directory.join(&file_name);
178 if path == workspace_directory || &path == archive_path {
179 continue;
180 }
181 std::fs::rename(&path, workspace_directory.join(&file_name))?;
182 }
183
184 workspace_directory
185 };
186
187 log::debug!(
188 "Using workspace directory: {}",
189 workspace_directory.display()
190 );
191
192 Ok(workspace_directory)
193}
194
195fn get_tar_top_directory_name(archive_path: &std::path::PathBuf) -> Result<Option<String>> {
200 let file = std::fs::File::open(archive_path)?;
201 let decoder = flate2::read::GzDecoder::new(file);
202 let mut archive = tar::Archive::new(decoder);
203
204 let first_archive_entry = archive
205 .entries()?
206 .nth(0)
207 .ok_or(format_err!("Archive empty."))??;
208 let first_archive_entry = (*first_archive_entry.path()?).to_path_buf();
209
210 let top_directory_name = first_archive_entry
211 .components()
212 .next()
213 .ok_or(format_err!("Archive empty."))?
214 .as_os_str()
215 .to_str()
216 .ok_or(format_err!("Failed to parse archive's first path."))?;
217
218 Ok(if top_directory_name == "/" {
219 None
220 } else {
221 Some(top_directory_name.to_string())
222 })
223}
224
225pub fn download(target_url: &url::Url, destination_path: &std::path::PathBuf) -> Result<()> {
226 log::debug!(
227 "Downloading archive to destination path: {}",
228 destination_path.display()
229 );
230
231 let response = reqwest::blocking::get(target_url.clone())?;
232 let mut file = std::fs::File::create(&destination_path)?;
233 let content = response.bytes()?;
234 file.write_all(&content)?;
235 file.sync_all()?;
236
237 log::debug!("Finished writing archive.");
238
239 Ok(())
240}