Skip to main content

zulip_api/client/
tus.rs

1use std::io::Read;
2
3use crate::{upload_file, Error, Result};
4
5use super::{Client, Endpoint};
6
7impl Client {
8    /// Gets the base64-encoded filename for [TUS resumable uploads](https://tus.io).
9    pub fn encode_filename(&self, file_path: &str) -> Result<String> {
10        use base64::{Base64, Encoding};
11
12        let path = std::path::Path::new(file_path);
13
14        path.file_name()
15            .and_then(|s| s.to_str())
16            .map(|s| Base64::encode_string(s.as_bytes()))
17            .ok_or(Error::io_filename("missing file name"))
18    }
19
20    /// Creates a [TUS resumable upload](https://tus.io) link.
21    ///
22    /// Returns the resumable file URL path for further TUS requests.
23    pub async fn tus_create(&self, file_name: &str, file_len: usize) -> Result<String> {
24        let file_md = self
25            .encode_filename(file_name)
26            .map(|b64| format!("filename {b64},is_confidential"))?;
27
28        self.http
29            .post(self.endpoint(Endpoint::ResumableUploadFile))
30            .header("Authorization", self.auth_header())
31            .header("Content-Length", "0")
32            .header("Upload-Length", format!("{file_len}"))
33            .header("Tus-Resumable", "1.0.0")
34            .header("Upload-Metadata", file_md)
35            .send()
36            .await
37            .map_err(Error::from)
38            .and_then(|r| {
39                r.headers()
40                    .get("Location")
41                    .cloned()
42                    .ok_or(Error::io_other("missing Location header"))
43            })
44            .and_then(|v| v.to_str().map(|s| s.to_string()).map_err(Error::from))
45    }
46
47    /// Uploads a file using [TUS resumable upload](https://tus.io).
48    ///
49    /// Returns the resumable file URL path for further TUS requests.
50    pub async fn tus_upload(
51        &self,
52        file_url: &str,
53        file_path: &str,
54    ) -> Result<upload_file::Response> {
55        let file = std::fs::File::open(file_path)?;
56        let file_len = file
57            .metadata()
58            .map_err(Error::from)
59            .map(|m| m.len() as usize)?;
60
61        let mut file_buf = std::io::BufReader::new(file);
62
63        let mut body = Vec::with_capacity(file_len);
64        file_buf.read_to_end(&mut body)?;
65
66        let res = self
67            .http
68            .patch(file_url)
69            .header("Authorization", self.auth_header())
70            .header("Content-Type", "application/offset+octet-stream")
71            .header("Upload-Offset", "0")
72            .header("Tus-Resumable", "1.0.0")
73            .body(body)
74            .send()
75            .await?;
76
77        log::debug!("File upload (TUS) response: {res:?}");
78
79        let bytes = res.bytes().await?;
80        let body = str::from_utf8(&bytes[..])?;
81
82        log::debug!("File upload (TUS) response body: {body}");
83
84        serde_json::from_str::<upload_file::Response>(body).map_err(Error::from)
85    }
86
87    /// Creates and uploads a file using the TUS resumable upload protocol.
88    pub async fn tus_create_and_upload(&self, file_path: &str) -> Result<upload_file::Response> {
89        let file = std::fs::File::open(file_path)?;
90        let path = std::path::Path::new(file_path);
91
92        let file_name = path
93            .file_name()
94            .and_then(|p| p.to_str())
95            .ok_or(Error::io_other("invalid filename for the file path"))?;
96
97        let file_md = self
98            .encode_filename(file_name)
99            .map(|b64| format!("filename {b64},is_confidential"))?;
100
101        let file_len = file
102            .metadata()
103            .map_err(Error::from)
104            .map(|m| m.len() as usize)?;
105
106        let mut file_buf = std::io::BufReader::new(file);
107
108        let mut body = Vec::with_capacity(file_len);
109        file_buf.read_to_end(&mut body)?;
110
111        let res = self
112            .http
113            .post(self.endpoint(Endpoint::ResumableUploadFile))
114            .header("Authorization", self.auth_header())
115            .header("Content-Type", "application/offset+octet-stream")
116            .header("Upload-Length", format!("{file_len}"))
117            .header("Upload-Offset", "0")
118            .header("Tus-Resumable", "1.0.0")
119            .header("Upload-Metadata", file_md)
120            .body(body)
121            .send()
122            .await?;
123
124        log::debug!("File upload (TUS) response: {res:?}");
125
126        let bytes = res.bytes().await?;
127        let body = str::from_utf8(&bytes[..])?;
128
129        log::debug!("File upload (TUS) response body: {body}");
130
131        serde_json::from_str::<upload_file::Response>(body).map_err(Error::from)
132    }
133}