1use crate::client::OpenAI;
4use crate::error::{OpenAIError, enum_to_string};
5use crate::pagination::{Page, Paginator};
6use crate::types::file::{FileDeleted, FileList, FileListParams, FileObject, FileUploadParams};
7
8pub struct Files<'a> {
12 client: &'a OpenAI,
13}
14
15impl<'a> Files<'a> {
16 pub(crate) fn new(client: &'a OpenAI) -> Self {
17 Self { client }
18 }
19
20 #[cfg(not(target_arch = "wasm32"))]
28 pub async fn create_from_path(
29 &self,
30 path: impl AsRef<std::path::Path>,
31 purpose: crate::types::file::FilePurpose,
32 ) -> Result<FileObject, OpenAIError> {
33 let path = path.as_ref();
34 let filename = path
35 .file_name()
36 .and_then(|n| n.to_str())
37 .unwrap_or("upload")
38 .to_string();
39 let data = tokio::fs::read(path).await.map_err(|e| {
40 OpenAIError::InvalidArgument(format!("failed to read {}: {e}", path.display()))
41 })?;
42 self.create(FileUploadParams::new(data, filename, purpose))
43 .await
44 }
45
46 #[cfg(not(target_arch = "wasm32"))]
50 pub async fn create(&self, params: FileUploadParams) -> Result<FileObject, OpenAIError> {
51 let form = reqwest::multipart::Form::new()
52 .part(
53 "file",
54 reqwest::multipart::Part::bytes(params.file).file_name(params.filename),
55 )
56 .text("purpose", enum_to_string(¶ms.purpose)?);
57
58 self.client.post_multipart("/files", form).await
59 }
60
61 pub async fn list(&self) -> Result<FileList, OpenAIError> {
65 self.client.get("/files").await
66 }
67
68 pub async fn list_page(&self, params: FileListParams) -> Result<FileList, OpenAIError> {
72 self.client
73 .get_with_query("/files", ¶ms.to_query())
74 .await
75 }
76
77 pub fn list_auto(&self, params: FileListParams) -> Paginator<FileObject> {
82 let client = self.client.clone();
83 let base_params = params;
84 Paginator::new(move |cursor| {
85 let client = client.clone();
86 let mut params = base_params.clone();
87 if cursor.is_some() {
88 params.after = cursor;
89 }
90 async move {
91 let list: FileList = client.get_with_query("/files", ¶ms.to_query()).await?;
92 let after_cursor = list
93 .last_id
94 .clone()
95 .or_else(|| list.data.last().map(|f| f.id.clone()));
96 Ok(Page {
97 has_more: list.has_more.unwrap_or(false),
98 after_cursor,
99 data: list.data,
100 })
101 }
102 })
103 }
104
105 pub async fn retrieve(&self, file_id: &str) -> Result<FileObject, OpenAIError> {
109 self.client.get(&format!("/files/{file_id}")).await
110 }
111
112 pub async fn delete(&self, file_id: &str) -> Result<FileDeleted, OpenAIError> {
116 self.client.delete(&format!("/files/{file_id}")).await
117 }
118
119 pub async fn content(&self, file_id: &str) -> Result<bytes::Bytes, OpenAIError> {
123 self.client
124 .get_raw(&format!("/files/{file_id}/content"))
125 .await
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use crate::OpenAI;
132 use crate::config::ClientConfig;
133 use crate::types::file::{FileListParams, FileUploadParams};
134
135 #[tokio::test]
136 async fn test_files_create() {
137 let mut server = mockito::Server::new_async().await;
138 let mock = server
139 .mock("POST", "/files")
140 .match_header("authorization", "Bearer sk-test")
141 .with_status(200)
142 .with_header("content-type", "application/json")
143 .with_body(
144 r#"{
145 "id": "file-abc123",
146 "object": "file",
147 "bytes": 120000,
148 "created_at": 1677610602,
149 "filename": "data.jsonl",
150 "purpose": "fine-tune",
151 "status": "uploaded"
152 }"#,
153 )
154 .create_async()
155 .await;
156
157 let client = OpenAI::with_config(ClientConfig::new("sk-test").base_url(server.url()));
158 let params = FileUploadParams::new(
159 b"test data".to_vec(),
160 "data.jsonl",
161 crate::types::file::FilePurpose::FineTune,
162 );
163
164 let response = client.files().create(params).await.unwrap();
165 assert_eq!(response.id, "file-abc123");
166 assert_eq!(response.purpose, crate::types::file::FilePurpose::FineTune);
167 mock.assert_async().await;
168 }
169
170 #[tokio::test]
171 async fn test_files_list() {
172 let mut server = mockito::Server::new_async().await;
173 let mock = server
174 .mock("GET", "/files")
175 .with_status(200)
176 .with_header("content-type", "application/json")
177 .with_body(
178 r#"{
179 "object": "list",
180 "data": [{
181 "id": "file-abc123",
182 "object": "file",
183 "bytes": 120000,
184 "created_at": 1677610602,
185 "filename": "data.jsonl",
186 "purpose": "fine-tune",
187 "status": "processed"
188 }]
189 }"#,
190 )
191 .create_async()
192 .await;
193
194 let client = OpenAI::with_config(ClientConfig::new("sk-test").base_url(server.url()));
195 let response = client.files().list().await.unwrap();
196 assert_eq!(response.data.len(), 1);
197 mock.assert_async().await;
198 }
199
200 #[tokio::test]
201 async fn test_files_retrieve() {
202 let mut server = mockito::Server::new_async().await;
203 let mock = server
204 .mock("GET", "/files/file-abc123")
205 .with_status(200)
206 .with_header("content-type", "application/json")
207 .with_body(
208 r#"{
209 "id": "file-abc123",
210 "object": "file",
211 "bytes": 120000,
212 "created_at": 1677610602,
213 "filename": "data.jsonl",
214 "purpose": "fine-tune",
215 "status": "processed"
216 }"#,
217 )
218 .create_async()
219 .await;
220
221 let client = OpenAI::with_config(ClientConfig::new("sk-test").base_url(server.url()));
222 let file = client.files().retrieve("file-abc123").await.unwrap();
223 assert_eq!(file.id, "file-abc123");
224 mock.assert_async().await;
225 }
226
227 #[tokio::test]
228 async fn test_files_delete() {
229 let mut server = mockito::Server::new_async().await;
230 let mock = server
231 .mock("DELETE", "/files/file-abc123")
232 .with_status(200)
233 .with_header("content-type", "application/json")
234 .with_body(r#"{"id": "file-abc123", "object": "file", "deleted": true}"#)
235 .create_async()
236 .await;
237
238 let client = OpenAI::with_config(ClientConfig::new("sk-test").base_url(server.url()));
239 let resp = client.files().delete("file-abc123").await.unwrap();
240 assert!(resp.deleted);
241 mock.assert_async().await;
242 }
243
244 #[tokio::test]
245 async fn test_files_list_page_with_params() {
246 let mut server = mockito::Server::new_async().await;
247 let mock = server
248 .mock("GET", "/files")
249 .match_query(mockito::Matcher::AllOf(vec![
250 mockito::Matcher::UrlEncoded("limit".into(), "2".into()),
251 mockito::Matcher::UrlEncoded("after".into(), "file-cursor".into()),
252 ]))
253 .with_status(200)
254 .with_header("content-type", "application/json")
255 .with_body(
256 r#"{
257 "object": "list",
258 "data": [{
259 "id": "file-page2",
260 "object": "file",
261 "bytes": 100,
262 "created_at": 1677610602,
263 "filename": "test.jsonl",
264 "purpose": "fine-tune",
265 "status": "processed"
266 }],
267 "has_more": false,
268 "first_id": "file-page2",
269 "last_id": "file-page2"
270 }"#,
271 )
272 .create_async()
273 .await;
274
275 let client = OpenAI::with_config(ClientConfig::new("sk-test").base_url(server.url()));
276 let params = FileListParams::new().limit(2).after("file-cursor");
277 let response = client.files().list_page(params).await.unwrap();
278 assert_eq!(response.data.len(), 1);
279 assert_eq!(response.has_more, Some(false));
280 assert_eq!(response.last_id.as_deref(), Some("file-page2"));
281 mock.assert_async().await;
282 }
283
284 #[tokio::test]
285 async fn test_files_list_auto_multi_page() {
286 use futures_util::StreamExt;
287
288 let mut server = mockito::Server::new_async().await;
289
290 let _mock_p1 = server
292 .mock("GET", "/files")
293 .match_query(mockito::Matcher::Missing)
294 .with_status(200)
295 .with_header("content-type", "application/json")
296 .with_body(
297 r#"{
298 "object": "list",
299 "data": [
300 {"id": "file-1", "object": "file", "bytes": 100, "created_at": 1, "filename": "a.jsonl", "purpose": "fine-tune", "status": "processed"},
301 {"id": "file-2", "object": "file", "bytes": 200, "created_at": 2, "filename": "b.jsonl", "purpose": "fine-tune", "status": "processed"}
302 ],
303 "has_more": true,
304 "last_id": "file-2"
305 }"#,
306 )
307 .create_async()
308 .await;
309
310 let _mock_p2 = server
312 .mock("GET", "/files")
313 .match_query(mockito::Matcher::AllOf(vec![
314 mockito::Matcher::UrlEncoded("after".into(), "file-2".into()),
315 ]))
316 .with_status(200)
317 .with_header("content-type", "application/json")
318 .with_body(
319 r#"{
320 "object": "list",
321 "data": [
322 {"id": "file-3", "object": "file", "bytes": 300, "created_at": 3, "filename": "c.jsonl", "purpose": "fine-tune", "status": "processed"}
323 ],
324 "has_more": false,
325 "last_id": "file-3"
326 }"#,
327 )
328 .create_async()
329 .await;
330
331 let client = OpenAI::with_config(ClientConfig::new("sk-test").base_url(server.url()));
332 let stream = client.files().list_auto(FileListParams::new());
333 let files: Vec<_> = stream
334 .collect::<Vec<_>>()
335 .await
336 .into_iter()
337 .map(|r| r.unwrap())
338 .collect();
339
340 assert_eq!(files.len(), 3);
341 assert_eq!(files[0].id, "file-1");
342 assert_eq!(files[1].id, "file-2");
343 assert_eq!(files[2].id, "file-3");
344 }
345
346 #[tokio::test]
347 async fn test_files_content() {
348 let mut server = mockito::Server::new_async().await;
349 let content_bytes = b"line1\nline2\nline3";
350 let mock = server
351 .mock("GET", "/files/file-abc123/content")
352 .with_status(200)
353 .with_header("content-type", "application/octet-stream")
354 .with_body(content_bytes)
355 .create_async()
356 .await;
357
358 let client = OpenAI::with_config(ClientConfig::new("sk-test").base_url(server.url()));
359 let response = client.files().content("file-abc123").await.unwrap();
360 assert_eq!(response.as_ref(), content_bytes);
361 mock.assert_async().await;
362 }
363}