modio/util/download/mod.rs
1//! Download interface for mod files.
2
3use std::fmt;
4use std::marker::PhantomData;
5use std::path::Path;
6
7use bytes::Bytes;
8use futures_util::StreamExt;
9use http::HeaderMap;
10use http_body_util::{BodyDataStream, BodyExt};
11use tokio::fs::File;
12use tokio::io::{AsyncWriteExt, BufWriter};
13
14mod action;
15mod error;
16mod info;
17
18pub use action::{DownloadAction, ResolvePolicy};
19pub use error::{Error, ErrorKind};
20pub use info::Info;
21
22use crate::client::service::{Body, Response};
23use crate::request::body::Body as RequestBody;
24use crate::Client;
25
26/// Extention trait for downloading mod files.
27pub trait Download: private::Sealed {
28 /// Returns [`Downloader`] for saving to file or retrieving the data chunked as `Bytes`.
29 ///
30 /// The download fails with [`modio::util::download::Error`] as source if a primary file, a
31 /// specific file or a specific version is not found.
32 ///
33 /// [`Downloader`]: crate::util::download::Downloader
34 /// [`modio::util::download::Error`]: crate::util::download::Error
35 ///
36 /// # Example
37 ///
38 /// ```no_run
39 /// use modio::types::id::Id;
40 /// use modio::util::download::{Download, DownloadAction, ResolvePolicy};
41 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
42 /// # let modio = modio::Client::builder("user-or-game-api-key".to_owned()).build()?;
43 ///
44 /// // Download the primary file of a mod.
45 /// let action = DownloadAction::Primary {
46 /// game_id: Id::new(5),
47 /// mod_id: Id::new(19),
48 /// };
49 /// modio.download(action).save_to_file("mod.zip").await?;
50 ///
51 /// // Download the specific file of a mod.
52 /// let action = DownloadAction::File {
53 /// game_id: Id::new(5),
54 /// mod_id: Id::new(19),
55 /// file_id: Id::new(101),
56 /// };
57 /// modio.download(action).save_to_file("mod.zip").await?;
58 ///
59 /// // Download the specific version of a mod.
60 /// // if multiple files are found then the latest file is downloaded.
61 /// // Set policy to `ResolvePolicy::Fail` to return with
62 /// // `modio::download::Error::MultipleFilesFound` as source error.
63 /// let action = DownloadAction::Version {
64 /// game_id: Id::new(5),
65 /// mod_id: Id::new(19),
66 /// version: "0.1".to_string(),
67 /// policy: ResolvePolicy::Latest,
68 /// };
69 /// let mut chunked = modio.download(action).chunked().await?;
70 ///
71 /// while let Some(chunk) = chunked.data().await {
72 /// println!("Bytes: {:?}", chunk?);
73 /// }
74 /// # Ok(())
75 /// # }
76 /// ```
77 fn download<A>(&self, action: A) -> Downloader<'_, Init<'_>>
78 where
79 DownloadAction: From<A>;
80}
81
82impl Download for Client {
83 fn download<A>(&self, action: A) -> Downloader<'_, Init<'_>>
84 where
85 DownloadAction: From<A>,
86 {
87 Downloader::<Init<'_>>::new(self, action.into())
88 }
89}
90
91/// A `Downloader` can be used to stream a mod file or save the file to a local file.
92/// Constructed with [`Download::download`].
93pub struct Downloader<'a, State> {
94 state: State,
95 phantom: PhantomData<fn(&'a State) -> State>,
96}
97
98impl<T> Downloader<'_, T> {
99 pub(crate) fn new(http: &Client, action: DownloadAction) -> Downloader<'_, Init<'_>> {
100 Downloader {
101 state: Init { http, action },
102 phantom: PhantomData,
103 }
104 }
105}
106
107/// Downloader state where the caller must choose how the file is downloaded.
108#[doc(hidden)]
109pub struct Init<'a> {
110 http: &'a Client,
111 action: DownloadAction,
112}
113
114impl<'a> Downloader<'a, Init<'a>> {
115 /// Retrieve the mod file in chunks of `Bytes`.
116 ///
117 /// # Example
118 /// ```no_run
119 /// # use modio::types::id::Id;
120 /// # use modio::util::Download;
121 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
122 /// # let modio = modio::Client::builder("api-key".to_owned()).build()?;
123 /// let action = modio::util::download::DownloadAction::Primary {
124 /// game_id: Id::new(5),
125 /// mod_id: Id::new(19),
126 /// };
127 ///
128 /// let mut chunked = modio.download(action).chunked().await?;
129 /// while let Some(bytes) = chunked.data().await {
130 /// println!("Bytes: {:?}", bytes);
131 /// }
132 /// # Ok(())
133 /// # }
134 /// ```
135 pub async fn chunked(self) -> Result<Downloader<'a, Chunked>, Error> {
136 let Init { http, action } = self.state;
137 let info = info::download_info(http, action).await?;
138
139 let req = http::Request::get(info.download_url.as_str())
140 .body(RequestBody::empty())
141 .map_err(Error::request)?;
142
143 let response = http.raw_request(req).await.map_err(Error::request)?;
144
145 Ok(Downloader {
146 state: Chunked::new(info, response),
147 phantom: PhantomData,
148 })
149 }
150
151 /// Save the mod file to a local file.
152 ///
153 /// # Example
154 /// ```no_run
155 /// # use modio::types::id::Id;
156 /// # use modio::util::Download;
157 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
158 /// # let modio = modio::Client::builder("api-key".to_owned()).build()?;
159 /// let action = modio::util::download::DownloadAction::Primary {
160 /// game_id: Id::new(5),
161 /// mod_id: Id::new(19),
162 /// };
163 ///
164 /// modio.download(action).save_to_file("mod.zip").await?;
165 /// # Ok(())
166 /// # }
167 /// ```
168 pub async fn save_to_file(self, path: impl AsRef<Path>) -> Result<(), Error> {
169 let mut chunked = self.chunked().await?;
170
171 let out = File::create(path)
172 .await
173 .map_err(|err| Error::new(ErrorKind::Io).with(err))?;
174 let mut out = BufWriter::with_capacity(512 * 512, out);
175
176 while let Some(chunk) = chunked.data().await {
177 out.write_all(&chunk?)
178 .await
179 .map_err(|err| Error::new(ErrorKind::Io).with(err))?;
180 }
181 Ok(())
182 }
183}
184
185/// Downloader state where the caller
186#[doc(hidden)]
187pub struct Chunked {
188 info: Info,
189 headers: HeaderMap,
190 body: BodyDataStream<Body>,
191}
192
193impl Chunked {
194 fn new(info: Info, response: Response) -> Self {
195 let (parts, body) = response.into_parts();
196 let headers = parts.headers;
197 let body = body.into_data_stream();
198
199 Self {
200 info,
201 headers,
202 body,
203 }
204 }
205}
206
207impl Downloader<'_, Chunked> {
208 pub fn info(&self) -> &Info {
209 &self.state.info
210 }
211
212 pub fn headers(&self) -> &HeaderMap {
213 &self.state.headers
214 }
215
216 pub async fn data(&mut self) -> Option<Result<Bytes, Error>> {
217 let chunk = self.state.body.next().await;
218 chunk.map(|c| c.map_err(Error::body))
219 }
220}
221
222impl<'a> fmt::Debug for Downloader<'a, Init<'a>> {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 f.debug_struct("Downloader")
225 .field("action", &self.state.action)
226 .finish_non_exhaustive()
227 }
228}
229
230impl fmt::Debug for Downloader<'_, Chunked> {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 f.debug_struct("Downloader")
233 .field("info", &self.state.info)
234 .finish_non_exhaustive()
235 }
236}
237
238mod private {
239 use crate::client::Client;
240
241 pub trait Sealed {}
242
243 impl Sealed for Client {}
244}