1use std::io::Read;
5use std::path::{Path, PathBuf};
6
7use crate::{install::Package, Error, Request};
8
9fn agent() -> ureq::Agent {
10 ureq::Agent::config_builder()
11 .http_status_as_error(false)
12 .timeout_global(Some(std::time::Duration::from_secs(600)))
13 .build()
14 .into()
15}
16
17pub fn send(request: &Request, download: bool) -> Result<Vec<u8>, Error> {
20 let agent = agent();
21 let result = match request.method {
22 "POST" => {
23 let mut r = agent.post(&request.url);
24 for (k, v) in &request.headers {
25 r = r.header(k, v);
26 }
27 r.send(request.body.clone().unwrap_or_default())
28 }
29 _ => {
30 let mut r = agent.get(&request.url);
31 for (k, v) in &request.headers {
32 r = r.header(k, v);
33 }
34 r.call()
35 }
36 };
37 let mut response = result.map_err(|e| Error::Network(e.to_string()))?;
38 let status = response.status().as_u16();
39 let body = response
40 .body_mut()
41 .with_config()
42 .limit(16 << 20)
43 .read_to_vec()
44 .map_err(|e| Error::Network(e.to_string()))?;
45 if !(200..300).contains(&status) {
46 return Err(crate::api::status_error(status, &body, download));
47 }
48 Ok(body)
49}
50
51pub fn download(url: &str, mut progress: impl FnMut(u64, Option<u64>)) -> Result<Vec<u8>, Error> {
54 let mut response = agent()
55 .get(url)
56 .call()
57 .map_err(|e| Error::Network(e.to_string()))?;
58 let status = response.status().as_u16();
59 if !(200..300).contains(&status) {
60 return Err(Error::Status(status, "the download failed".into()));
61 }
62 let total = response
63 .headers()
64 .get("content-length")
65 .and_then(|v| v.to_str().ok())
66 .and_then(|v| v.parse().ok());
67 let mut reader = response
68 .body_mut()
69 .with_config()
70 .limit(crate::install::MAX_PACKAGE * 2)
71 .reader();
72 let mut data = Vec::with_capacity(total.unwrap_or(0).min(256 << 20) as usize);
73 let mut chunk = vec![0; 64 << 10];
74 loop {
75 let n = reader
76 .read(&mut chunk)
77 .map_err(|e| Error::Network(e.to_string()))?;
78 if n == 0 {
79 break;
80 }
81 data.extend_from_slice(&chunk[..n]);
82 progress(data.len() as u64, total);
83 }
84 Ok(data)
85}
86
87pub fn install(dir: &Path, packages: &[Package]) -> Result<Vec<PathBuf>, Error> {
91 let mut written = Vec::new();
92 for p in packages {
93 let path = dir.join(&p.name);
94 let temp = dir.join(format!(".{}.download", p.name));
95 let io = |e: std::io::Error| Error::Network(format!("could not write {}: {e}", p.name));
96 std::fs::write(&temp, &p.data).map_err(io)?;
97 std::fs::rename(&temp, &path).map_err(io)?;
98 written.push(path);
99 }
100 Ok(written)
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum Source {
106 File {
108 mod_id: u32,
110 file_id: u32,
112 file_name: String,
114 },
115 Link(crate::nxm::NxmLink),
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum Step {
122 Link,
124 Download(u64, Option<u64>),
126 Install,
128}
129
130pub fn install_from(
133 app: &crate::App,
134 key: &str,
135 source: &Source,
136 dir: &Path,
137 mut progress: impl FnMut(Step),
138) -> Result<Vec<PathBuf>, Error> {
139 progress(Step::Link);
140 let (mod_id, file_id, link) = match source {
141 Source::File {
142 mod_id, file_id, ..
143 } => (*mod_id, *file_id, None),
144 Source::Link(l) => (l.mod_id, l.file_id, Some(l)),
145 };
146 let request = crate::api::download_link(app, key, mod_id, file_id, link);
147 let links = crate::api::parse_download_links(&send(&request, true)?)?;
148 let url = &links[0];
149 let data = download(url, |done, total| progress(Step::Download(done, total)))?;
150 progress(Step::Install);
151 let file_name = match source {
153 Source::File { file_name, .. } if !file_name.is_empty() => file_name.clone(),
154 _ => url
155 .split('?')
156 .next()
157 .and_then(|u| u.rsplit('/').next())
158 .map(|n| n.replace("%20", " "))
159 .unwrap_or_default(),
160 };
161 let packages = crate::install::packages(&file_name, &data)?;
162 install(dir, &packages)
163}
164
165pub const KEY_VARIABLE: &str = "BANISHED_NEXUS_API_KEY";
167
168pub const KEY_SETTING: &str = "NexusApiKey";
170
171pub fn settings_path() -> Option<PathBuf> {
174 if let Ok(p) = std::env::var("BANISHED_SETTINGS") {
175 return (!p.is_empty()).then(|| PathBuf::from(p));
176 }
177 let base = if cfg!(target_os = "windows") {
178 std::env::var_os("APPDATA").map(PathBuf::from)
179 } else if cfg!(target_os = "macos") {
180 std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library/Application Support"))
181 } else {
182 std::env::var_os("XDG_CONFIG_HOME")
183 .map(PathBuf::from)
184 .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
185 }?;
186 Some(base.join("banished-wasm").join("settings.txt"))
187}
188
189pub fn stored_key() -> Option<String> {
192 if let Ok(k) = std::env::var(KEY_VARIABLE) {
193 let k = k.trim();
194 return (!k.is_empty()).then(|| k.to_string());
195 }
196 let text = std::fs::read_to_string(settings_path()?).ok()?;
197 key_in_settings(&text)
198}
199
200pub fn key_in_settings(text: &str) -> Option<String> {
202 text.lines()
203 .filter_map(|l| l.split_once('='))
204 .find(|(k, _)| k.trim() == KEY_SETTING)
205 .map(|(_, v)| v.trim().to_string())
206 .filter(|v| !v.is_empty())
207}
208
209#[cfg(test)]
210mod tests {
211 #[test]
212 fn reads_the_key_from_settings() {
213 let text = "VideoVSync=true\nNexusApiKey= abc123 \n";
214 assert_eq!(super::key_in_settings(text).as_deref(), Some("abc123"));
215 assert_eq!(super::key_in_settings("NexusApiKey=\n"), None);
216 }
217}