deepl_rustls/endpoint/
document.rs1use super::{Pollable, Result};
2use crate::{impl_requester, Formality, Lang};
3use serde::{Deserialize, Serialize};
4use std::{
5 future::IntoFuture,
6 path::{Path, PathBuf},
7};
8use tokio::io::AsyncWriteExt;
9use tokio_stream::StreamExt;
10
11#[derive(Serialize, Deserialize)]
13pub struct UploadDocumentResp {
14 pub document_id: String,
17 pub document_key: String,
21}
22
23#[derive(Deserialize, Debug)]
25pub struct DocumentStatusResp {
26 pub document_id: String,
29 pub status: DocumentTranslateStatus,
32 pub seconds_remaining: Option<u64>,
35 pub billed_characters: Option<u64>,
37 pub error_message: Option<String>,
40}
41
42#[derive(Debug, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "lowercase")]
45pub enum DocumentTranslateStatus {
46 Queued,
48 Translating,
50 Done,
52 Error,
54}
55
56impl DocumentTranslateStatus {
57 pub fn is_done(&self) -> bool {
58 self == &Self::Done
59 }
60}
61
62impl_requester! {
63 UploadDocumentRequester {
64 @required{
65 file_path: PathBuf,
66 target_lang: Lang,
67 };
68 @optional{
69 source_lang: Lang,
70 filename: String,
71 formality: Formality,
72 glossary_id: String,
73 };
74 } -> Result<UploadDocumentResp, Error>;
75}
76
77impl<'a> UploadDocumentRequester<'a> {
78 fn to_multipart_form(&self) -> reqwest::multipart::Form {
79 let Self {
80 source_lang,
81 target_lang,
82 formality,
83 glossary_id,
84 ..
85 } = self;
86
87 let mut form = reqwest::multipart::Form::new();
88
89 if let Some(lang) = source_lang {
91 form = form.text("source_lang", lang.to_string());
92 }
93
94 form = form.text("target_lang", target_lang.to_string());
96
97 if let Some(formal) = formality {
99 form = form.text("formality", formal.to_string());
100 }
101
102 if let Some(id) = glossary_id {
104 form = form.text("glossary_id", id.to_string());
105 }
106
107 form
108 }
109
110 fn send(&self) -> Pollable<'a, Result<UploadDocumentResp>> {
111 let mut form = self.to_multipart_form();
112 let client = self.client.clone();
113 let filename = self.filename.clone();
114 let file_path = self.file_path.clone();
115
116 let fut = async move {
117 let file = tokio::fs::read(&file_path).await.map_err(|err| {
119 Error::ReadFileError(file_path.to_str().unwrap().to_string(), err)
120 })?;
121
122 let mut part = reqwest::multipart::Part::bytes(file);
123 if let Some(filename) = filename {
124 part = part.file_name(filename.to_string());
125 form = form.text("filename", filename);
126 } else {
127 part = part.file_name(file_path.file_name().expect(
128 "No extension found for this file, and no filename given, cannot make request",
129 ).to_str().expect("not a valid UTF-8 filepath!").to_string());
130 }
131
132 form = form.part("file", part);
133
134 let res = client
135 .post(client.get_endpoint("document"))
136 .multipart(form)
137 .send()
138 .await
139 .map_err(|err| Error::RequestFail(format!("fail to upload file: {err}")))?;
140
141 if !res.status().is_success() {
142 return super::extract_deepl_error(res).await;
143 }
144
145 let res: UploadDocumentResp = res.json().await.map_err(|err| {
146 Error::InvalidResponse(format!("fail to decode response body: {err}"))
147 })?;
148 Ok(res)
149 };
150
151 Box::pin(fut)
152 }
153}
154
155impl<'a> IntoFuture for UploadDocumentRequester<'a> {
156 type Output = Result<UploadDocumentResp>;
157 type IntoFuture = Pollable<'a, Self::Output>;
158
159 fn into_future(self) -> Self::IntoFuture {
160 self.send()
161 }
162}
163
164impl<'a> IntoFuture for &mut UploadDocumentRequester<'a> {
165 type Output = Result<UploadDocumentResp>;
166 type IntoFuture = Pollable<'a, Self::Output>;
167
168 fn into_future(self) -> Self::IntoFuture {
169 self.send()
170 }
171}
172
173impl DeepLApi {
174 pub fn upload_document(
199 &self,
200 fp: impl Into<std::path::PathBuf>,
201 target_lang: Lang,
202 ) -> UploadDocumentRequester {
203 UploadDocumentRequester::new(self, fp.into(), target_lang)
204 }
205
206 async fn open_file_to_write(p: &Path) -> Result<tokio::fs::File> {
207 let open_result = tokio::fs::OpenOptions::new()
208 .append(true)
209 .create_new(true)
210 .open(p)
211 .await;
212
213 if let Ok(file) = open_result {
214 return Ok(file);
215 }
216
217 let err = open_result.unwrap_err();
218 if err.kind() != std::io::ErrorKind::AlreadyExists {
219 return Err(Error::WriteFileError(format!(
220 "Fail to open file {p:?}: {err}"
221 )));
222 }
223
224 tokio::fs::remove_file(p).await.map_err(|err| {
225 Error::WriteFileError(format!(
226 "There was already a file there and it is not deletable: {err}"
227 ))
228 })?;
229 dbg!("Detect exist, removed");
230
231 let open_result = tokio::fs::OpenOptions::new()
232 .append(true)
233 .create_new(true)
234 .open(p)
235 .await;
236
237 if let Err(err) = open_result {
238 return Err(Error::WriteFileError(format!(
239 "Fail to open file for download document, even after retry: {err}"
240 )));
241 }
242
243 Ok(open_result.unwrap())
244 }
245
246 pub async fn check_document_status(
248 &self,
249 ident: &UploadDocumentResp,
250 ) -> Result<DocumentStatusResp> {
251 let form = [("document_key", ident.document_key.as_str())];
252 let url = self.get_endpoint(&format!("document/{}", ident.document_id));
253 let res = self
254 .post(url)
255 .form(&form)
256 .send()
257 .await
258 .map_err(|err| Error::RequestFail(err.to_string()))?;
259
260 if !res.status().is_success() {
261 return super::extract_deepl_error(res).await;
262 }
263
264 let status: DocumentStatusResp = res
265 .json()
266 .await
267 .map_err(|err| Error::InvalidResponse(format!("response is not JSON: {err}")))?;
268
269 Ok(status)
270 }
271
272 pub async fn download_document<O: AsRef<Path>>(
277 &self,
278 ident: &UploadDocumentResp,
279 output: O,
280 ) -> Result<PathBuf> {
281 let url = self.get_endpoint(&format!("document/{}/result", ident.document_id));
282 let form = [("document_key", ident.document_key.as_str())];
283 let res = self
284 .post(url)
285 .form(&form)
286 .send()
287 .await
288 .map_err(|err| Error::RequestFail(err.to_string()))?;
289
290 if res.status() == reqwest::StatusCode::NOT_FOUND {
291 return Err(Error::NonExistDocument);
292 }
293
294 if res.status() == reqwest::StatusCode::SERVICE_UNAVAILABLE {
295 return Err(Error::TranslationNotDone);
296 }
297
298 if !res.status().is_success() {
299 return super::extract_deepl_error(res).await;
300 }
301
302 let mut file = Self::open_file_to_write(output.as_ref()).await?;
303
304 let mut stream = res.bytes_stream();
305
306 #[inline]
307 fn mapper<E: std::error::Error>(s: &'static str) -> Box<dyn FnOnce(E) -> Error> {
308 Box::new(move |err: E| Error::WriteFileError(format!("{s}: {err}")))
309 }
310
311 while let Some(chunk) = stream.next().await {
312 let chunk = chunk.map_err(mapper("fail to download part of the document"))?;
313 file.write_all(&chunk)
314 .await
315 .map_err(mapper("fail to write downloaded part into file"))?;
316 file.sync_all()
317 .await
318 .map_err(mapper("fail to sync file content"))?;
319 }
320
321 Ok(output.as_ref().to_path_buf())
322 }
323}
324
325#[tokio::test]
326async fn test_upload_document() {
327 let key = std::env::var("DEEPL_API_KEY").unwrap();
328 let api = DeepLApi::with(&key).new();
329
330 let raw_text = "Hello World";
331
332 tokio::fs::write("./test.txt", &raw_text).await.unwrap();
333
334 let test_file = PathBuf::from("./test.txt");
335 let response = api.upload_document(&test_file, Lang::DE).await.unwrap();
336 let mut status = api.check_document_status(&response).await.unwrap();
337
338 loop {
340 if status.status.is_done() {
341 break;
342 }
343 if let Some(msg) = status.error_message {
344 println!("{}", msg);
345 break;
346 }
347 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
348 status = api.check_document_status(&response).await.unwrap();
349 dbg!(&status);
350 }
351
352 let path = api
353 .download_document(&response, "test_translated.txt")
354 .await
355 .unwrap();
356
357 let content = tokio::fs::read_to_string(path).await.unwrap();
358 let expect = "Hallo Welt";
359 assert_eq!(content, expect);
360}
361
362#[tokio::test]
363async fn test_upload_docx() {
364 use docx_rs::{read_docx, DocumentChild, Docx, Paragraph, ParagraphChild, Run, RunChild};
365
366 let key = std::env::var("DEEPL_API_KEY").unwrap();
367 let api = DeepLApi::with(&key).new();
368
369 let test_file = PathBuf::from("./example.docx");
370 let file = std::fs::File::create(&test_file).expect("fail to create test asserts");
371 Docx::new()
372 .add_paragraph(
373 Paragraph::new()
374 .add_run(Run::new().add_text("To be, or not to be, that is the question")),
375 )
376 .build()
377 .pack(file)
378 .expect("fail to write test asserts");
379
380 let response = api.upload_document(&test_file, Lang::DE).await.unwrap();
381 let mut status = api.check_document_status(&response).await.unwrap();
382
383 loop {
385 if status.status.is_done() {
386 break;
387 }
388 if let Some(msg) = status.error_message {
389 println!("{}", msg);
390 break;
391 }
392 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
393 status = api.check_document_status(&response).await.unwrap();
394 dbg!(&status);
395 }
396
397 let path = api
398 .download_document(&response, "translated.docx")
399 .await
400 .unwrap();
401 let get = tokio::fs::read(&path).await.unwrap();
402 let doc = read_docx(&get).expect("can not open downloaded document");
403 let text = doc
405 .document
406 .children
407 .iter()
408 .filter_map(|child| {
409 if let DocumentChild::Paragraph(paragraph) = child {
410 let text = paragraph
411 .children
412 .iter()
413 .filter_map(|pchild| {
414 if let ParagraphChild::Run(run) = pchild {
415 let text = run
416 .children
417 .iter()
418 .filter_map(|rchild| {
419 if let RunChild::Text(text) = rchild {
420 Some(text.text.to_string())
421 } else {
422 None
423 }
424 })
425 .collect::<String>();
426
427 Some(text)
428 } else {
429 None
430 }
431 })
432 .collect::<String>();
433 Some(text)
434 } else {
435 None
436 }
437 })
438 .collect::<String>();
439
440 assert_eq!(text, "Sein oder nicht sein, das ist hier die Frage");
441}