Skip to main content

dxm_resources/
lib.rs

1//! A crate for installing third-party resources for FXServer.
2
3use std::{error::Error, io::Write, path::Path};
4
5use fs_extra::dir::{CopyOptions, move_dir};
6use reqwest::blocking::Client;
7use tempfile::{NamedTempFile, TempDir};
8use zip::{ZipArchive, read::root_dir_common_filter};
9
10mod github;
11
12const ROOT_GITIGNORE: &str = "\
13*
14";
15
16pub fn resolve_download_url<S>(client: &Client, url: S) -> Result<String, Box<dyn Error>>
17where
18    S: Into<String>,
19{
20    let url = url.into();
21
22    if let Some(github_url) = github::resolve_download_url(client, &url)? {
23        Ok(github_url)
24    } else {
25        Ok(url)
26    }
27}
28
29/// Downloads and installs the given archive URL to the given directory path.
30/// Returns the archive download URL.
31pub fn install<S, P, N>(
32    client: &Client,
33    download_url: S,
34    path: P,
35    nested_path: N,
36) -> Result<(), Box<dyn Error>>
37where
38    S: AsRef<str>,
39    P: AsRef<Path>,
40    N: AsRef<Path>,
41{
42    let download_url = download_url.as_ref();
43    let path = path.as_ref();
44    let nested_path = nested_path.as_ref();
45
46    fs_err::create_dir_all(path)?;
47
48    let mut file = NamedTempFile::with_suffix("dxm-resource-archive")?;
49    let bytes = client
50        .get(download_url)
51        .send()?
52        .error_for_status()?
53        .bytes()?;
54    file.write_all(&bytes)?;
55
56    log::debug!("extracting resource into {}", nested_path.display());
57    let dir = TempDir::with_suffix("dxm-resource")?;
58    ZipArchive::new(file.reopen()?)?.extract_unwrapped_root_dir(&dir, root_dir_common_filter)?;
59
60    let copy_options = CopyOptions::new().content_only(true);
61    let mut original_dir: Option<TempDir> = None;
62
63    if is_dir_with_files(path)? {
64        let tempdir = TempDir::with_suffix("dxm-resource-original")?;
65        move_dir(path, &tempdir, &copy_options)?;
66        original_dir = Some(tempdir);
67    }
68
69    let result = move_dir(dir.path().join(nested_path), path, &copy_options);
70    if result.is_err()
71        && let Some(original_dir) = original_dir
72    {
73        move_dir(original_dir, path, &copy_options)?;
74    }
75    result?;
76
77    fs_err::write(path.join(".gitignore"), ROOT_GITIGNORE)?;
78
79    Ok(())
80}
81
82fn is_dir_with_files<P>(path: P) -> std::io::Result<bool>
83where
84    P: AsRef<Path>,
85{
86    let path = path.as_ref();
87
88    if !path.is_dir() {
89        return Ok(false);
90    };
91
92    for entry in fs_err::read_dir(path)? {
93        if entry?.file_type()?.is_file() {
94            return Ok(true);
95        }
96    }
97
98    Ok(false)
99}