modio/util/upload/
multipart.rs

1use std::future::{Future, IntoFuture};
2use std::marker::PhantomData;
3use std::pin::Pin;
4use std::task::{ready, Context, Poll};
5
6use bytes::Bytes;
7use futures_util::TryStream;
8
9use crate::request::files::multipart::{
10    AddMultipartUploadFile, ContentRange, CreateMultipartUploadSession,
11};
12use crate::types::files::multipart::{UploadId, UploadPart, UploadSession};
13use crate::types::files::File;
14use crate::types::id::{GameId, ModId};
15use crate::util::{DataError, DataFromRequest, DataFuture};
16use crate::Client;
17
18use super::Error;
19
20pub struct MultipartUploader<'a, State> {
21    state: State,
22    phantom: PhantomData<fn(&'a State) -> State>,
23}
24
25impl<'a> MultipartUploader<'a, ()> {
26    pub(crate) const fn init(
27        http: &'a Client,
28        game_id: GameId,
29        mod_id: ModId,
30        filename: &'a str,
31    ) -> MultipartUploader<'a, Init<'a>> {
32        MultipartUploader {
33            state: Init {
34                http,
35                game_id,
36                mod_id,
37                create_session: http.create_multipart_upload_session(game_id, mod_id, filename),
38            },
39            phantom: PhantomData,
40        }
41    }
42
43    pub(crate) const fn started(
44        http: &'a Client,
45        game_id: GameId,
46        mod_id: ModId,
47        upload_id: UploadId,
48    ) -> MultipartUploader<'a, Started<'a>> {
49        MultipartUploader {
50            state: Started {
51                http,
52                game_id,
53                mod_id,
54                upload_id,
55            },
56            phantom: PhantomData,
57        }
58    }
59}
60
61pub struct Init<'a> {
62    http: &'a Client,
63    game_id: GameId,
64    mod_id: ModId,
65    create_session: CreateMultipartUploadSession<'a>,
66}
67
68impl<'a> MultipartUploader<'a, Init<'a>> {
69    /// An optional nonce to provide to prevent duplicate upload sessions
70    /// from being created concurrently.
71    ///
72    /// Maximum 64 characters (Recommended: SHA-256)
73    pub const fn nonce(mut self, nonce: &'a str) -> Self {
74        self.state.create_session = self.state.create_session.nonce(nonce);
75        self
76    }
77}
78
79impl<'a> IntoFuture for MultipartUploader<'a, Init<'a>> {
80    type Output = <MultipartUploaderInitFuture<'a> as Future>::Output;
81    type IntoFuture = MultipartUploaderInitFuture<'a>;
82
83    fn into_future(self) -> Self::IntoFuture {
84        let Init {
85            http,
86            game_id,
87            mod_id,
88            create_session,
89        } = self.state;
90
91        MultipartUploaderInitFuture {
92            future: create_session.data(),
93            state: Some((http, game_id, mod_id)),
94        }
95    }
96}
97
98pin_project_lite::pin_project! {
99    pub struct MultipartUploaderInitFuture<'a> {
100        #[pin]
101        future: DataFuture<UploadSession>,
102        state: Option<(&'a Client, GameId, ModId)>,
103    }
104}
105
106impl<'a> Future for MultipartUploaderInitFuture<'a> {
107    type Output = Result<MultipartUploader<'a, Started<'a>>, Error>;
108
109    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
110        let mut this = self.project();
111
112        let result = match ready!(this.future.as_mut().poll(cx)) {
113            Ok(session) => {
114                let (http, game_id, mod_id) = this.state.take().expect("Polled after completion");
115                Ok(MultipartUploader {
116                    state: Started {
117                        http,
118                        game_id,
119                        mod_id,
120                        upload_id: session.id,
121                    },
122                    phantom: PhantomData,
123                })
124            }
125            Err(err) => Err(match err {
126                DataError::Request(err) => Error::request(err),
127                DataError::Body(err) => Error::body(err),
128            }),
129        };
130
131        Poll::Ready(result)
132    }
133}
134
135pub struct Started<'a> {
136    http: &'a Client,
137    game_id: GameId,
138    mod_id: ModId,
139    upload_id: UploadId,
140}
141
142impl<'a> MultipartUploader<'a, Started<'a>> {
143    pub const fn id(&self) -> UploadId {
144        self.state.upload_id
145    }
146
147    /// Get the uploaded parts of the current session.
148    pub async fn get_parts(&self) -> Result<Vec<UploadPart>, Error> {
149        let Started {
150            http,
151            game_id,
152            mod_id,
153            upload_id,
154        } = self.state;
155
156        match http
157            .get_multipart_upload_parts(game_id, mod_id, upload_id)
158            .data()
159            .await
160        {
161            Ok(parts) => Ok(parts.data),
162            Err(err) => Err(match err {
163                DataError::Request(err) => Error::request(err),
164                DataError::Body(err) => Error::body(err),
165            }),
166        }
167    }
168
169    /// Add a new part to the upload session.
170    pub async fn add_part<S>(&self, range: ContentRange, stream: S) -> Result<UploadPart, Error>
171    where
172        S: TryStream + Send + 'static,
173        S::Ok: Into<Bytes>,
174        S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
175    {
176        let Started {
177            http,
178            game_id,
179            mod_id,
180            upload_id,
181        } = self.state;
182
183        match http
184            .add_multipart_upload_part(game_id, mod_id, upload_id, range, stream)
185            .data()
186            .await
187        {
188            Ok(part) => Ok(part),
189            Err(err) => Err(match err {
190                DataError::Request(err) => Error::request(err),
191                DataError::Body(err) => Error::body(err),
192            }),
193        }
194    }
195
196    /// Complete the active upload session after uploading all parts with
197    /// [`MultipartUploader::add_part`].
198    pub async fn complete(self) -> Result<MultipartUploader<'a, Completed<'a>>, Error> {
199        let Started {
200            http,
201            game_id,
202            mod_id,
203            upload_id,
204        } = self.state;
205
206        let upload_id = match http
207            .complete_multipart_upload_session(game_id, mod_id, upload_id)
208            .data()
209            .await
210        {
211            Ok(UploadSession { id, .. }) => id,
212            Err(err) => {
213                return Err(match err {
214                    DataError::Request(err) => Error::request(err),
215                    DataError::Body(err) => Error::body(err),
216                })
217            }
218        };
219
220        let add_file = http.add_multipart_upload_file(game_id, mod_id, upload_id);
221        Ok(MultipartUploader {
222            state: Completed { add_file },
223            phantom: PhantomData,
224        })
225    }
226
227    /// Terminate the upload session.
228    pub async fn abort(self) -> Result<(), Error> {
229        let Started {
230            http,
231            game_id,
232            mod_id,
233            upload_id,
234        } = self.state;
235
236        if let Err(err) = http
237            .delete_multipart_upload_session(game_id, mod_id, upload_id)
238            .await
239        {
240            return Err(Error::request(err));
241        }
242
243        Ok(())
244    }
245}
246
247pub struct Completed<'a> {
248    add_file: AddMultipartUploadFile<'a>,
249}
250
251impl<'a> MultipartUploader<'a, Completed<'a>> {
252    pub const fn active(mut self, active: bool) -> Self {
253        self.state.add_file = self.state.add_file.active(active);
254        self
255    }
256
257    pub const fn changelog(mut self, changelog: &'a str) -> Self {
258        self.state.add_file = self.state.add_file.changelog(changelog);
259        self
260    }
261
262    pub const fn filehash(mut self, filehash: &'a str) -> Self {
263        self.state.add_file = self.state.add_file.filehash(filehash);
264        self
265    }
266
267    pub const fn metadata_blob(mut self, metadata: &'a str) -> Self {
268        self.state.add_file = self.state.add_file.metadata_blob(metadata);
269        self
270    }
271
272    pub const fn version(mut self, version: &'a str) -> Self {
273        self.state.add_file = self.state.add_file.version(version);
274        self
275    }
276}
277
278impl IntoFuture for MultipartUploader<'_, Completed<'_>> {
279    type Output = <MultipartUploaderCompleteFuture as Future>::Output;
280    type IntoFuture = MultipartUploaderCompleteFuture;
281
282    fn into_future(self) -> Self::IntoFuture {
283        let Completed { add_file } = self.state;
284
285        MultipartUploaderCompleteFuture {
286            future: add_file.data(),
287        }
288    }
289}
290
291pin_project_lite::pin_project! {
292    pub struct MultipartUploaderCompleteFuture {
293        #[pin]
294        future: DataFuture<File>,
295    }
296}
297
298impl Future for MultipartUploaderCompleteFuture {
299    type Output = Result<File, Error>;
300
301    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
302        let mut this = self.project();
303
304        let result = match ready!(this.future.as_mut().poll(cx)) {
305            Ok(file) => Ok(file),
306            Err(err) => Err(match err {
307                DataError::Request(err) => Error::request(err),
308                DataError::Body(err) => Error::body(err),
309            }),
310        };
311
312        Poll::Ready(result)
313    }
314}