Skip to main content

banished_banishedinfo/
native.rs

1//! Sending the requests on native builds (blocking; run them off the main thread in a window),
2//! and installing a version.
3
4use std::io::Read;
5use std::path::{Path, PathBuf};
6
7use ureq::ResponseExt;
8
9use crate::{Error, Mod, Sort, Version};
10
11/// A client that names the application in its user agent.
12pub struct Client {
13    agent: ureq::Agent,
14    user_agent: String,
15}
16
17impl Client {
18    /// A client for Un-Banished at `version`.
19    pub fn new(version: &str) -> Client {
20        Client {
21            agent: ureq::Agent::config_builder()
22                .http_status_as_error(false)
23                .timeout_global(Some(std::time::Duration::from_secs(600)))
24                .build()
25                .into(),
26            user_agent: crate::user_agent(version),
27        }
28    }
29
30    /// GET an address; returns the final address (after redirects) and the body.
31    fn get(
32        &self,
33        url: &str,
34        limit: u64,
35        mut progress: impl FnMut(u64, Option<u64>),
36    ) -> Result<(String, Vec<u8>), Error> {
37        let mut response = self
38            .agent
39            .get(url)
40            .header("User-Agent", &self.user_agent)
41            .call()
42            .map_err(|e| Error::Network(e.to_string()))?;
43        let status = response.status().as_u16();
44        if !(200..300).contains(&status) {
45            return Err(Error::Status(status));
46        }
47        let final_url = response.get_uri().to_string();
48        let total = response
49            .headers()
50            .get("content-length")
51            .and_then(|v| v.to_str().ok())
52            .and_then(|v| v.parse().ok());
53        let mut reader = response.body_mut().with_config().limit(limit).reader();
54        let mut data = Vec::new();
55        let mut chunk = vec![0; 64 << 10];
56        loop {
57            let n = reader
58                .read(&mut chunk)
59                .map_err(|e| Error::Network(e.to_string()))?;
60            if n == 0 {
61                break;
62            }
63            data.extend_from_slice(&chunk[..n]);
64            progress(data.len() as u64, total);
65        }
66        Ok((final_url, data))
67    }
68
69    /// A small file from the site, such as a thumbnail (at most 4 MB).
70    pub fn image(&self, url: &str) -> Result<Vec<u8>, Error> {
71        self.get(url, 4 << 20, |_, _| {}).map(|(_, b)| b)
72    }
73
74    fn page(&self, url: &str) -> Result<String, Error> {
75        let (_, body) = self.get(url, 16 << 20, |_, _| {})?;
76        String::from_utf8(body).map_err(|e| Error::Answer(e.to_string()))
77    }
78
79    /// `count` mods from `start` of a section (`""` for all), in an order.
80    pub fn list(
81        &self,
82        sort: Sort,
83        section: &str,
84        start: u32,
85        count: u32,
86    ) -> Result<Vec<Mod>, Error> {
87        let (_, body) = self.get(
88            &crate::list_url(sort, section, start, count),
89            16 << 20,
90            |_, _| {},
91        )?;
92        crate::parse_list(&body)
93    }
94
95    /// The mods the site's search finds for `text`.
96    pub fn search(&self, text: &str) -> Result<Vec<Mod>, Error> {
97        crate::parse_search(&self.page(&crate::search_url(text))?)
98    }
99
100    /// A mod's versions, newest first.
101    pub fn versions(&self, m: &Mod) -> Result<Vec<Version>, Error> {
102        let versions = crate::parse_versions(&self.page(&m.page())?, m.id);
103        if versions.is_empty() {
104            return Err(Error::Answer(format!("no downloads on {}", m.page())));
105        }
106        Ok(versions)
107    }
108
109    /// Download a version: its file name and bytes.
110    pub fn download(
111        &self,
112        version: &Version,
113        progress: impl FnMut(u64, Option<u64>),
114    ) -> Result<(String, Vec<u8>), Error> {
115        let limit = banished_nexus::install::MAX_PACKAGE * 2;
116        let (final_url, data) = self.get(&version.file, limit, progress)?;
117        Ok((crate::file_name(&final_url), data))
118    }
119
120    /// Install a version into the pack folder: the `.pkm` packages of the download, written
121    /// through a temporary file each. Returns the paths written.
122    pub fn install(
123        &self,
124        version: &Version,
125        dir: &Path,
126        progress: impl FnMut(u64, Option<u64>),
127    ) -> Result<Vec<PathBuf>, Error> {
128        let (name, data) = self.download(version, progress)?;
129        let packages = banished_nexus::install::packages(&name, &data)
130            .map_err(|e| Error::Install(e.to_string()))?;
131        banished_nexus::native::install(dir, &packages).map_err(|e| Error::Install(e.to_string()))
132    }
133}