Skip to main content

openai_oxide/resources/
files.rs

1// Files resource — client.files().create() / list() / retrieve() / delete() / content()
2
3use 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
8/// Access file endpoints.
9///
10/// API reference: <https://platform.openai.com/docs/api-reference/files>
11pub 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    /// Upload a file from a filesystem path.
21    ///
22    /// Reads the file asynchronously and infers the filename from the path.
23    ///
24    /// ```ignore
25    /// let file = client.files().create_from_path("data.jsonl", FilePurpose::FineTune).await?;
26    /// ```
27    #[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    /// Upload a file.
47    ///
48    /// `POST /files`
49    #[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(&params.purpose)?);
57
58        self.client.post_multipart("/files", form).await
59    }
60
61    /// List files.
62    ///
63    /// `GET /files`
64    pub async fn list(&self) -> Result<FileList, OpenAIError> {
65        self.client.get("/files").await
66    }
67
68    /// List files with pagination parameters.
69    ///
70    /// `GET /files`
71    pub async fn list_page(&self, params: FileListParams) -> Result<FileList, OpenAIError> {
72        self.client
73            .get_with_query("/files", &params.to_query())
74            .await
75    }
76
77    /// Auto-paginate through all files.
78    ///
79    /// Returns a [`Paginator`] stream that yields individual [`FileObject`] items,
80    /// automatically fetching subsequent pages.
81    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", &params.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    /// Retrieve a file by ID.
106    ///
107    /// `GET /files/{file_id}`
108    pub async fn retrieve(&self, file_id: &str) -> Result<FileObject, OpenAIError> {
109        self.client.get(&format!("/files/{file_id}")).await
110    }
111
112    /// Delete a file.
113    ///
114    /// `DELETE /files/{file_id}`
115    pub async fn delete(&self, file_id: &str) -> Result<FileDeleted, OpenAIError> {
116        self.client.delete(&format!("/files/{file_id}")).await
117    }
118
119    /// Retrieve file content as bytes.
120    ///
121    /// `GET /files/{file_id}/content`
122    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        // Page 1: has_more=true
291        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        // Page 2: has_more=false
311        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}