1use 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#[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 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 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 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 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 pub fn upload(&self, filename: impl Into<String>, data: Vec<u8>) -> UploadFileBuilder {
115 UploadFileBuilder::new(self.client.clone(), filename.into(), data)
116 }
117
118 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 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 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#[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 pub fn purpose(mut self, purpose: FilePurpose) -> Self {
194 self.purpose = purpose;
195 self
196 }
197
198 pub async fn send(self) -> Result<FileObject> {
200 let url = format!("{}/files", self.client.base_url());
201
202 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}