1use crate::folder::FolderIdentifier;
2
3use super::File;
4
5#[derive(Debug, serde::Deserialize)]
9pub struct MultipartFileUploadResponse {
10 #[serde(rename = "fileids")]
12 pub file_ids: Vec<u64>,
13
14 pub metadata: Vec<File>,
16}
17
18#[derive(Debug, Default)]
23pub struct MultiFileUpload {
24 parts: Vec<reqwest::multipart::Part>,
25}
26
27impl MultiFileUpload {
28 pub fn with_stream_entry<F, S>(mut self, filename: F, length: Option<u64>, stream: S) -> Self
38 where
39 F: Into<String>,
40 S: futures_core::stream::TryStream + Send + Sync + 'static,
41 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
42 bytes::Bytes: From<S::Ok>,
43 {
44 self.add_stream_entry(filename, length, stream);
45 self
46 }
47
48 pub fn add_stream_entry<F, S>(&mut self, filename: F, length: Option<u64>, stream: S)
56 where
57 F: Into<String>,
58 S: futures_core::stream::TryStream + Send + Sync + 'static,
59 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
60 bytes::Bytes: From<S::Ok>,
61 {
62 let body = reqwest::Body::wrap_stream(stream);
63 self.add_body_entry(filename, length, body);
64 }
65
66 pub fn with_body_entry<F, B>(mut self, filename: F, length: Option<u64>, body: B) -> Self
76 where
77 F: Into<String>,
78 B: Into<reqwest::Body>,
79 {
80 self.add_body_entry(filename, length, body);
81 self
82 }
83
84 pub fn add_body_entry<F, B>(&mut self, filename: F, length: Option<u64>, body: B)
92 where
93 F: Into<String>,
94 B: Into<reqwest::Body>,
95 {
96 let part = if let Some(length) = length {
97 let mut headers = reqwest::header::HeaderMap::with_capacity(1);
98 let content_length = length.to_string();
99 headers.append(
100 reqwest::header::CONTENT_LENGTH,
101 reqwest::header::HeaderValue::from_str(&content_length)
102 .expect("content-length must be a valid number"),
103 );
104
105 reqwest::multipart::Part::stream_with_length(body, length)
106 .file_name(filename.into())
107 .headers(headers)
108 } else {
109 reqwest::multipart::Part::stream(body).file_name(filename.into())
110 };
111
112 self.parts.push(part);
113 }
114
115 fn into_form(self) -> reqwest::multipart::Form {
119 self.parts.into_iter().enumerate().fold(
120 reqwest::multipart::Form::default(),
121 |form, (index, part)| form.part(format!("f{index}"), part),
122 )
123 }
124}
125
126impl crate::Client {
127 pub async fn upload_files(
164 &self,
165 parent: impl Into<FolderIdentifier<'_>>,
166 files: MultiFileUpload,
167 ) -> crate::Result<Vec<File>> {
168 self.post_request_multipart::<MultipartFileUploadResponse, _>(
169 "uploadfile",
170 parent.into(),
171 files.into_form(),
172 )
173 .await
174 .map(|res| res.metadata)
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use crate::{file::upload::MultiFileUpload, Client, Credentials};
181 use mockito::Matcher;
182
183 #[tokio::test]
184 async fn multipart_success() {
185 let mut server = mockito::Server::new_async().await;
186 let m_upload = server
187 .mock("POST", "/uploadfile")
188 .match_query(Matcher::AllOf(vec![
189 Matcher::UrlEncoded("access_token".into(), "access-token".into()),
190 Matcher::UrlEncoded("folderid".into(), "0".into()),
191 ]))
192 .match_body(Matcher::Any)
193 .match_header("accept", "*/*")
194 .match_header("user-agent", crate::USER_AGENT)
195 .match_header(
196 "content-type",
197 Matcher::Regex("multipart/form-data; boundary=.*".to_string()),
198 )
199 .match_header("content-length", Matcher::Regex("[0-9]+".to_string()))
200 .with_status(200)
201 .with_body(
202 r#"{
203 "result": 0,
204 "metadata": [
205 {
206 "name": "big-file.bin",
207 "created": "Tue, 09 Aug 2022 13:43:17 +0000",
208 "thumb": false,
209 "modified": "Tue, 09 Aug 2022 13:43:17 +0000",
210 "isfolder": false,
211 "fileid": 15669308155,
212 "hash": 15418918230810325691,
213 "path": "/big-file.bin",
214 "category": 0,
215 "id": "f15669308155",
216 "isshared": false,
217 "ismine": true,
218 "size": 1073741824,
219 "parentfolderid": 0,
220 "contenttype": "application/octet-stream",
221 "icon": "file"
222 }
223 ],
224 "checksums": [
225 {
226 "sha1": "a91d3c45d2ff6dc99ed3d1c150f6fae91b2e10a1",
227 "sha256": "2722eb2ec44a8f5655df8ef3b7c6a1658de40d5aedcab26b3e6d043222681360"
228 }
229 ],
230 "fileids": [15669308155]
231 }"#,
232 )
233 .create();
234 let client = Client::new(server.url(), Credentials::access_token("access-token")).unwrap();
235 let file = tokio::fs::File::open("./readme.md").await.unwrap();
237 let length = std::fs::metadata("./readme.md").unwrap().len();
238 let files = MultiFileUpload::default().with_body_entry("big-file.bin", Some(length), file);
239 let result = client.upload_files(0, files).await.unwrap();
240 assert_eq!(result.len(), 1);
242 m_upload.assert();
243 }
244}