openfare_lib/common/fs/
archive.rs

1use 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
42/// Extract and return archive file extension from given path.
43fn 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
141/// Extract .tar.gz archives.
142///
143/// Note that .tgz archives are the same as .tar.gz archives.
144pub 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        // Create temporary workspace directory with unique name.
166        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
195/// Returns the top level directory name from within the given archive.
196///
197/// This function advances the archive's position counter.
198/// The archive can not be unpacked after this operation, it is therefore dropped.
199fn 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}