Skip to main content

xai_rust/api/
files.rs

1//! Files API for file management.
2
3use crate::client::XaiClient;
4use crate::models::file::{
5    DeleteFileResponse, FileDownloadRequest, FileListResponse, FileObject, FilePurpose,
6    FileUploadChunksRequest, FileUploadChunksResponse, FileUploadInitializeRequest,
7    FileUploadInitializeResponse,
8};
9use crate::{Error, Result};
10
11/// Files API for uploading and managing files.
12#[derive(Debug, Clone)]
13pub struct FilesApi {
14    client: XaiClient,
15}
16
17impl FilesApi {
18    pub(crate) fn new(client: XaiClient) -> Self {
19        Self { client }
20    }
21
22    /// List all uploaded files.
23    ///
24    /// # Example
25    ///
26    /// ```rust,no_run
27    /// use xai_rust::XaiClient;
28    ///
29    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30    /// let client = XaiClient::from_env()?;
31    ///
32    /// let files = client.files().list().await?;
33    /// for file in files.data {
34    ///     println!("{}: {}", file.id, file.filename);
35    /// }
36    /// # Ok(())
37    /// # }
38    /// ```
39    pub async fn list(&self) -> Result<FileListResponse> {
40        let url = format!("{}/files", self.client.base_url());
41
42        let response = self.client.send(self.client.http().get(&url)).await?;
43
44        if !response.status().is_success() {
45            return Err(Error::from_response(response).await);
46        }
47
48        Ok(response.json().await?)
49    }
50
51    /// Get a file by ID.
52    pub async fn get(&self, file_id: &str) -> Result<FileObject> {
53        let id = XaiClient::encode_path(file_id);
54        let url = format!("{}/files/{}", self.client.base_url(), id);
55
56        let response = self.client.send(self.client.http().get(&url)).await?;
57
58        if !response.status().is_success() {
59            return Err(Error::from_response(response).await);
60        }
61
62        Ok(response.json().await?)
63    }
64
65    /// Get the content of a file.
66    pub async fn content(&self, file_id: &str) -> Result<Vec<u8>> {
67        let id = XaiClient::encode_path(file_id);
68        let url = format!("{}/files/{}/content", self.client.base_url(), id);
69
70        let response = self.client.send(self.client.http().get(&url)).await?;
71
72        if !response.status().is_success() {
73            return Err(Error::from_response(response).await);
74        }
75
76        Ok(response.bytes().await?.to_vec())
77    }
78
79    /// Delete a file.
80    pub async fn delete(&self, file_id: &str) -> Result<DeleteFileResponse> {
81        let id = XaiClient::encode_path(file_id);
82        let url = format!("{}/files/{}", self.client.base_url(), id);
83
84        let response = self.client.send(self.client.http().delete(&url)).await?;
85
86        if !response.status().is_success() {
87            return Err(Error::from_response(response).await);
88        }
89
90        Ok(response.json().await?)
91    }
92
93    /// Upload a file.
94    ///
95    /// # Example
96    ///
97    /// ```rust,no_run
98    /// use xai_rust::{XaiClient, FilePurpose};
99    ///
100    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
101    /// let client = XaiClient::from_env()?;
102    ///
103    /// let data = std::fs::read("document.pdf")?;
104    /// let file = client.files()
105    ///     .upload("document.pdf", data)
106    ///     .purpose(FilePurpose::Assistants)
107    ///     .send()
108    ///     .await?;
109    ///
110    /// println!("Uploaded: {}", file.id);
111    /// # Ok(())
112    /// # }
113    /// ```
114    pub fn upload(&self, filename: impl Into<String>, data: Vec<u8>) -> UploadFileBuilder {
115        UploadFileBuilder::new(self.client.clone(), filename.into(), data)
116    }
117
118    /// Download file contents by explicit file download endpoint.
119    pub async fn download(&self, request: FileDownloadRequest) -> Result<Vec<u8>> {
120        let url = format!("{}/files:download", self.client.base_url());
121
122        let response = self
123            .client
124            .send(self.client.http().post(&url).json(&request))
125            .await?;
126
127        if !response.status().is_success() {
128            return Err(Error::from_response(response).await);
129        }
130
131        Ok(response.bytes().await?.to_vec())
132    }
133
134    /// Initialize a multi-part upload session.
135    pub async fn initialize_upload(
136        &self,
137        request: FileUploadInitializeRequest,
138    ) -> Result<FileUploadInitializeResponse> {
139        let url = format!("{}/files:initialize", self.client.base_url());
140
141        let response = self
142            .client
143            .send(self.client.http().post(&url).json(&request))
144            .await?;
145
146        if !response.status().is_success() {
147            return Err(Error::from_response(response).await);
148        }
149
150        Ok(response.json().await?)
151    }
152
153    /// Upload an individual file chunk.
154    pub async fn upload_chunks(
155        &self,
156        request: FileUploadChunksRequest,
157    ) -> Result<FileUploadChunksResponse> {
158        let url = format!("{}/files:uploadChunks", self.client.base_url());
159
160        let response = self
161            .client
162            .send(self.client.http().post(&url).json(&request))
163            .await?;
164
165        if !response.status().is_success() {
166            return Err(Error::from_response(response).await);
167        }
168
169        Ok(response.json().await?)
170    }
171}
172
173/// Builder for file upload requests.
174#[derive(Debug)]
175pub struct UploadFileBuilder {
176    client: XaiClient,
177    filename: String,
178    data: Vec<u8>,
179    purpose: FilePurpose,
180}
181
182impl UploadFileBuilder {
183    fn new(client: XaiClient, filename: String, data: Vec<u8>) -> Self {
184        Self {
185            client,
186            filename,
187            data,
188            purpose: FilePurpose::default(),
189        }
190    }
191
192    /// Set the file purpose.
193    pub fn purpose(mut self, purpose: FilePurpose) -> Self {
194        self.purpose = purpose;
195        self
196    }
197
198    /// Send the upload request.
199    pub async fn send(self) -> Result<FileObject> {
200        let url = format!("{}/files", self.client.base_url());
201
202        // Guess the mime type
203        let mime_type = mime_guess::from_path(&self.filename)
204            .first_or_octet_stream()
205            .to_string();
206
207        let file_part = reqwest::multipart::Part::bytes(self.data)
208            .file_name(self.filename)
209            .mime_str(&mime_type)
210            .map_err(|e| Error::InvalidRequest(e.to_string()))?;
211
212        let form = reqwest::multipart::Form::new()
213            .text("purpose", self.purpose.to_string())
214            .part("file", file_part);
215
216        let response = self
217            .client
218            .send(self.client.http().post(&url).multipart(form))
219            .await?;
220
221        if !response.status().is_success() {
222            return Err(Error::from_response(response).await);
223        }
224
225        Ok(response.json().await?)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use serde_json::json;
233    use wiremock::matchers::{method, path};
234    use wiremock::{Mock, MockServer, ResponseTemplate};
235
236    #[tokio::test]
237    async fn get_content_and_delete_encode_file_id() {
238        let server = MockServer::start().await;
239
240        Mock::given(method("GET"))
241            .and(path("/files/file%2Fsync"))
242            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
243                "id": "file/sync",
244                "filename": "notes.txt",
245                "bytes": 11,
246                "created_at": 1700000000,
247                "purpose": "assistants",
248                "object": "file"
249            })))
250            .mount(&server)
251            .await;
252
253        Mock::given(method("GET"))
254            .and(path("/files/file%2Fsync/content"))
255            .respond_with(ResponseTemplate::new(200).set_body_bytes(b"hello world".to_vec()))
256            .mount(&server)
257            .await;
258
259        Mock::given(method("DELETE"))
260            .and(path("/files/file%2Fsync"))
261            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
262                "id": "file/sync",
263                "deleted": true,
264                "object": "file"
265            })))
266            .mount(&server)
267            .await;
268
269        let client = XaiClient::builder()
270            .api_key("test-key")
271            .base_url(server.uri())
272            .build()
273            .unwrap();
274
275        let file = client.files().get("file/sync").await.unwrap();
276        assert_eq!(file.id, "file/sync");
277
278        let bytes = client.files().content("file/sync").await.unwrap();
279        assert_eq!(bytes, b"hello world".to_vec());
280
281        let deleted = client.files().delete("file/sync").await.unwrap();
282        assert!(deleted.deleted);
283    }
284
285    #[tokio::test]
286    async fn upload_forwards_purpose_value() {
287        let server = MockServer::start().await;
288
289        Mock::given(method("POST"))
290            .and(path("/files"))
291            .respond_with(move |req: &wiremock::Request| {
292                let body = String::from_utf8_lossy(&req.body);
293                assert!(body.contains("name=\"purpose\""));
294                assert!(body.contains("batch"));
295                ResponseTemplate::new(200).set_body_json(json!({
296                    "id": "file_sync_uploaded",
297                    "filename": "upload.txt",
298                    "bytes": 11,
299                    "created_at": 1700000000,
300                    "purpose": "batch",
301                    "object": "file"
302                }))
303            })
304            .mount(&server)
305            .await;
306
307        let client = XaiClient::builder()
308            .api_key("test-key")
309            .base_url(server.uri())
310            .build()
311            .unwrap();
312
313        let uploaded = client
314            .files()
315            .upload("upload.txt", b"hello world".to_vec())
316            .purpose(FilePurpose::Batch)
317            .send()
318            .await
319            .unwrap();
320
321        assert_eq!(uploaded.id, "file_sync_uploaded");
322        assert_eq!(uploaded.purpose, "batch");
323    }
324
325    #[tokio::test]
326    async fn download_posts_request_payload_and_returns_bytes() {
327        let server = MockServer::start().await;
328
329        Mock::given(method("POST"))
330            .and(path("/files:download"))
331            .respond_with(move |req: &wiremock::Request| {
332                let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
333                assert_eq!(body["file_id"], "file-1");
334                ResponseTemplate::new(200).set_body_bytes(b"downloaded-bytes".to_vec())
335            })
336            .mount(&server)
337            .await;
338
339        let client = XaiClient::builder()
340            .api_key("test-key")
341            .base_url(server.uri())
342            .build()
343            .unwrap();
344
345        let bytes = client
346            .files()
347            .download(FileDownloadRequest {
348                file_id: "file-1".to_string(),
349            })
350            .await
351            .unwrap();
352        assert_eq!(bytes, b"downloaded-bytes".to_vec());
353    }
354
355    #[tokio::test]
356    async fn initialize_upload_forwards_payload() {
357        let server = MockServer::start().await;
358
359        Mock::given(method("POST"))
360            .and(path("/files:initialize"))
361            .respond_with(move |req: &wiremock::Request| {
362                let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
363                assert_eq!(body["filename"], "video.mp4");
364                assert_eq!(body["purpose"], "assistants");
365                ResponseTemplate::new(200).set_body_json(json!({
366                    "upload_id": "u-123",
367                    "object": "upload_session"
368                }))
369            })
370            .mount(&server)
371            .await;
372
373        let client = XaiClient::builder()
374            .api_key("test-key")
375            .base_url(server.uri())
376            .build()
377            .unwrap();
378
379        let response = client
380            .files()
381            .initialize_upload(FileUploadInitializeRequest {
382                filename: "video.mp4".to_string(),
383                purpose: FilePurpose::Assistants,
384                total_size_bytes: None,
385            })
386            .await
387            .unwrap();
388        assert_eq!(response.upload_id, "u-123");
389    }
390
391    #[tokio::test]
392    async fn upload_chunks_forwards_payload() {
393        let server = MockServer::start().await;
394
395        Mock::given(method("POST"))
396            .and(path("/files:uploadChunks"))
397            .respond_with(move |req: &wiremock::Request| {
398                let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
399                assert_eq!(body["upload_id"], "u-123");
400                assert_eq!(body["part"], 1);
401                assert_eq!(body["chunk"], "chunk-data");
402                ResponseTemplate::new(200).set_body_json(json!({
403                    "upload_id": "u-123",
404                    "complete": false
405                }))
406            })
407            .mount(&server)
408            .await;
409
410        let client = XaiClient::builder()
411            .api_key("test-key")
412            .base_url(server.uri())
413            .build()
414            .unwrap();
415
416        let response = client
417            .files()
418            .upload_chunks(FileUploadChunksRequest {
419                upload_id: "u-123".to_string(),
420                part: 1,
421                chunk: "chunk-data".to_string(),
422                checksum: None,
423            })
424            .await
425            .unwrap();
426        assert_eq!(response.upload_id, "u-123");
427    }
428}