1use std::error::Error as StdError;
3use std::fmt;
4use std::path::Path;
5
6use bytes::Bytes;
7use futures_util::{SinkExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
8use reqwest::{Method, Response, StatusCode};
9use tokio::fs::File as AsyncFile;
10use tokio::io::BufWriter;
11use tokio_util::codec::{BytesCodec, FramedWrite};
12use tracing::debug;
13
14use crate::error::{self, Result};
15use crate::types::files::File;
16use crate::types::id::{FileId, GameId, ModId};
17use crate::types::mods::Mod;
18use crate::Modio;
19
20pub struct Downloader(Response);
23
24impl Downloader {
25 pub(crate) async fn new(modio: Modio, action: DownloadAction) -> Result<Self> {
26 Ok(Self(request_file(modio, action).await?))
27 }
28
29 pub async fn save_to_file<P: AsRef<Path>>(self, file: P) -> Result<()> {
50 let out = AsyncFile::create(file).map_err(error::decode).await?;
51 let out = BufWriter::with_capacity(512 * 512, out);
52 let out = FramedWrite::new(out, BytesCodec::new());
53 let out = SinkExt::<Bytes>::sink_map_err(out, error::decode);
54 self.stream().forward(out).await
55 }
56
57 pub async fn bytes(self) -> Result<Bytes> {
74 self.0.bytes().map_err(error::request).await
75 }
76
77 pub fn stream(self) -> impl Stream<Item = Result<Bytes>> {
99 self.0.bytes_stream().map_err(error::request)
100 }
101
102 pub fn content_length(&self) -> Option<u64> {
123 self.0.content_length()
124 }
125}
126
127async fn request_file(modio: Modio, action: DownloadAction) -> Result<Response> {
128 let url = match action {
129 DownloadAction::Primary { game_id, mod_id } => {
130 let modref = modio.mod_(game_id, mod_id);
131 let m = modref
132 .get()
133 .map_err(|e| match e.status() {
134 Some(StatusCode::NOT_FOUND) => {
135 let source = Error::ModNotFound { game_id, mod_id };
136 error::download(source)
137 }
138 _ => e,
139 })
140 .await?;
141 if let Some(file) = m.modfile {
142 file.download.binary_url
143 } else {
144 let source = Error::NoPrimaryFile { game_id, mod_id };
145 return Err(error::download(source));
146 }
147 }
148 DownloadAction::FileObj(file) => file.download.binary_url,
149 DownloadAction::File {
150 game_id,
151 mod_id,
152 file_id,
153 } => {
154 let fileref = modio.mod_(game_id, mod_id).file(file_id);
155 let file = fileref
156 .get()
157 .map_err(|e| match e.status() {
158 Some(StatusCode::NOT_FOUND) => {
159 let source = Error::FileNotFound {
160 game_id,
161 mod_id,
162 file_id,
163 };
164 error::download(source)
165 }
166 _ => e,
167 })
168 .await?;
169 file.download.binary_url
170 }
171 DownloadAction::Version {
172 game_id,
173 mod_id,
174 version,
175 policy,
176 } => {
177 use crate::files::filters::Version;
178 use crate::filter::prelude::*;
179 use ResolvePolicy::*;
180
181 let filter = Version::eq(version.clone())
182 .order_by(DateAdded::desc())
183 .limit(2);
184
185 let files = modio.mod_(game_id, mod_id).files();
186 let mut list = files
187 .search(filter)
188 .first_page()
189 .map_err(|e| match e.status() {
190 Some(StatusCode::NOT_FOUND) => {
191 let source = Error::ModNotFound { game_id, mod_id };
192 error::download(source)
193 }
194 _ => e,
195 })
196 .await?;
197
198 let (file, error) = match (list.len(), policy) {
199 (0, _) => (
200 None,
201 Some(Error::VersionNotFound {
202 game_id,
203 mod_id,
204 version,
205 }),
206 ),
207 (1, _) | (_, Latest) => (Some(list.remove(0)), None),
208 (_, Fail) => (
209 None,
210 Some(Error::MultipleFilesFound {
211 game_id,
212 mod_id,
213 version,
214 }),
215 ),
216 };
217
218 if let Some(file) = file {
219 file.download.binary_url
220 } else {
221 let source = error.expect("bug in previous match!");
222 return Err(error::download(source));
223 }
224 }
225 };
226
227 debug!("downloading file: {}", url);
228 modio
229 .inner
230 .client
231 .request(Method::GET, url)
232 .send()
233 .map_err(error::builder_or_request)
234 .await?
235 .error_for_status()
236 .map_err(error::request)
237}
238
239#[derive(Debug)]
241pub enum DownloadAction {
242 Primary { game_id: GameId, mod_id: ModId },
244 File {
246 game_id: GameId,
247 mod_id: ModId,
248 file_id: FileId,
249 },
250 FileObj(Box<File>),
252 Version {
254 game_id: GameId,
255 mod_id: ModId,
256 version: String,
257 policy: ResolvePolicy,
258 },
259}
260
261#[derive(Debug)]
263pub enum ResolvePolicy {
264 Latest,
266 Fail,
268}
269
270#[derive(Debug)]
272pub enum Error {
273 ModNotFound { game_id: GameId, mod_id: ModId },
275 NoPrimaryFile { game_id: GameId, mod_id: ModId },
277 FileNotFound {
279 game_id: GameId,
280 mod_id: ModId,
281 file_id: FileId,
282 },
283 MultipleFilesFound {
286 game_id: GameId,
287 mod_id: ModId,
288 version: String,
289 },
290 VersionNotFound {
292 game_id: GameId,
293 mod_id: ModId,
294 version: String,
295 },
296}
297
298impl StdError for Error {}
299
300impl fmt::Display for Error {
301 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
302 match self {
303 Error::ModNotFound { game_id, mod_id } => write!(
304 fmt,
305 "Mod {{id: {mod_id}, game_id: {game_id}}} not found.",
306 ),
307 Error::FileNotFound {
308 game_id,
309 mod_id,
310 file_id,
311 } => write!(
312 fmt,
313 "Mod {{id: {mod_id}, game_id: {game_id}}}: File {{ id: {file_id} }} not found.",
314 ),
315 Error::MultipleFilesFound {
316 game_id,
317 mod_id,
318 version,
319 } => write!(
320 fmt,
321 "Mod {{id: {mod_id}, game_id: {game_id}}}: Multiple files found for version '{version}'.",
322 ),
323 Error::NoPrimaryFile { game_id, mod_id } => write!(
324 fmt,
325 "Mod {{id: {mod_id}, game_id: {game_id}}} Mod has no primary file.",
326 ),
327 Error::VersionNotFound {
328 game_id,
329 mod_id,
330 version,
331 } => write!(
332 fmt,
333 "Mod {{id: {mod_id}, game_id: {game_id}}}: No file with version '{version}' found.",
334 ),
335 }
336 }
337}
338
339impl From<Mod> for DownloadAction {
341 fn from(m: Mod) -> DownloadAction {
342 if let Some(file) = m.modfile {
343 DownloadAction::from(file)
344 } else {
345 DownloadAction::Primary {
346 game_id: m.game_id,
347 mod_id: m.id,
348 }
349 }
350 }
351}
352
353impl From<File> for DownloadAction {
355 fn from(file: File) -> DownloadAction {
356 DownloadAction::FileObj(Box::new(file))
357 }
358}
359
360impl From<(GameId, ModId)> for DownloadAction {
362 fn from((game_id, mod_id): (GameId, ModId)) -> DownloadAction {
363 DownloadAction::Primary { game_id, mod_id }
364 }
365}
366
367impl From<(GameId, ModId, FileId)> for DownloadAction {
369 fn from((game_id, mod_id, file_id): (GameId, ModId, FileId)) -> DownloadAction {
370 DownloadAction::File {
371 game_id,
372 mod_id,
373 file_id,
374 }
375 }
376}
377
378impl From<(GameId, ModId, String)> for DownloadAction {
381 fn from((game_id, mod_id, version): (GameId, ModId, String)) -> DownloadAction {
382 DownloadAction::Version {
383 game_id,
384 mod_id,
385 version,
386 policy: ResolvePolicy::Latest,
387 }
388 }
389}
390
391impl<'a> From<(GameId, ModId, &'a str)> for DownloadAction {
394 fn from((game_id, mod_id, version): (GameId, ModId, &'a str)) -> DownloadAction {
395 DownloadAction::Version {
396 game_id,
397 mod_id,
398 version: version.to_string(),
399 policy: ResolvePolicy::Latest,
400 }
401 }
402}