1use std::path::{Path, PathBuf};
4use std::time::Duration;
5
6use reqwest::Method;
7
8use crate::client::{Client, meta::parse_json};
9use crate::error::{Error, Result};
10use crate::http::{PreparedRequest, headers};
11use crate::retry::RetryPolicy;
12use crate::types::{File, FileDeleted, FileList};
13
14pub(crate) const TRANSFER_TIMEOUT_FLOOR: Duration = Duration::from_mins(2);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SortOrder {
21 Ascending,
23 Descending,
25}
26
27impl SortOrder {
28 fn as_str(self) -> &'static str {
29 match self {
30 Self::Ascending => "asc",
31 Self::Descending => "desc",
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct Files {
39 client: Client,
40}
41
42impl Client {
43 pub fn files(&self) -> Files {
45 Files {
46 client: self.clone(),
47 }
48 }
49}
50
51impl Files {
52 pub fn upload(&self, data: impl Into<Vec<u8>>) -> FileUpload {
54 FileUpload {
55 client: self.client.clone(),
56 source: UploadSource::Bytes(data.into()),
57 filename: None,
58 purpose: "batch".to_string(),
59 }
60 }
61
62 pub fn upload_path(&self, path: impl Into<PathBuf>) -> FileUpload {
64 FileUpload {
65 client: self.client.clone(),
66 source: UploadSource::Path(path.into()),
67 filename: None,
68 purpose: "batch".to_string(),
69 }
70 }
71
72 pub async fn retrieve(&self, file_id: &str) -> Result<File> {
74 let request = self
75 .client
76 .request(Method::GET, &format!("/v1/files/{file_id}"))?
77 .header("accept", headers::JSON_CONTENT_TYPE);
78 let response = self.client.send_once(request, RetryPolicy::NONE).await?;
79 parse_json(&response, "file")
80 }
81
82 pub fn list(&self) -> FileListRequest {
84 FileListRequest {
85 client: self.client.clone(),
86 after: None,
87 limit: None,
88 order: None,
89 purpose: None,
90 }
91 }
92
93 pub async fn content(&self, file_id: &str) -> Result<bytes::Bytes> {
95 let request = self
96 .client
97 .request(Method::GET, &format!("/v1/files/{file_id}/content"))?
98 .header("accept", headers::JSONL_CONTENT_TYPE);
99 Ok(self
100 .client
101 .send_once(request, RetryPolicy::NONE)
102 .await?
103 .body)
104 }
105
106 pub async fn delete(&self, file_id: &str) -> Result<FileDeleted> {
108 let request = self
109 .client
110 .request(Method::DELETE, &format!("/v1/files/{file_id}"))?
111 .header("accept", headers::JSON_CONTENT_TYPE);
112 let response = self.client.send_once(request, RetryPolicy::NONE).await?;
113 parse_json(&response, "file deletion")
114 }
115}
116
117#[derive(Debug, Clone)]
118enum UploadSource {
119 Bytes(Vec<u8>),
120 Path(PathBuf),
121}
122
123#[derive(Debug, Clone)]
125pub struct FileUpload {
126 client: Client,
127 source: UploadSource,
128 filename: Option<String>,
129 purpose: String,
130}
131
132impl FileUpload {
133 pub fn filename(mut self, filename: impl Into<String>) -> Self {
135 self.filename = Some(filename.into());
136 self
137 }
138
139 pub fn purpose(mut self, purpose: impl Into<String>) -> Self {
141 self.purpose = purpose.into();
142 self
143 }
144
145 pub async fn send(self) -> Result<File> {
150 let (data, default_name) = match &self.source {
151 UploadSource::Bytes(data) => (data.clone(), "upload.jsonl".to_string()),
152 UploadSource::Path(path) => {
153 let data = std::fs::read(path).map_err(|err| {
154 Error::Io(std::io::Error::new(
155 err.kind(),
156 format!("could not read {}: {err}", path.display()),
157 ))
158 })?;
159 (data, basename(path))
160 }
161 };
162 let filename = self.filename.unwrap_or(default_name);
163
164 let mut url = self.client.url("/v1/files")?;
165 url.query_pairs_mut()
166 .append_pair("purpose", &self.purpose)
167 .append_pair("filename", &filename);
168
169 let request = PreparedRequest::new(Method::POST, url)
170 .header("content-type", headers::JSONL_CONTENT_TYPE)
171 .header("accept", headers::JSON_CONTENT_TYPE)
172 .body(data);
173
174 let response = self
175 .client
176 .send_with_timeout(request, RetryPolicy::NONE, TRANSFER_TIMEOUT_FLOOR)
177 .await?;
178 parse_json(&response, "file")
179 }
180}
181
182fn basename(path: &Path) -> String {
185 path.file_name().map_or_else(
186 || "upload.jsonl".to_string(),
187 |name| name.to_string_lossy().into_owned(),
188 )
189}
190
191#[derive(Debug, Clone)]
193pub struct FileListRequest {
194 client: Client,
195 after: Option<String>,
196 limit: Option<u32>,
197 order: Option<SortOrder>,
198 purpose: Option<String>,
199}
200
201impl FileListRequest {
202 pub fn after(mut self, after: impl Into<String>) -> Self {
204 self.after = Some(after.into());
205 self
206 }
207
208 pub fn limit(mut self, limit: u32) -> Self {
210 self.limit = Some(limit);
211 self
212 }
213
214 pub fn order(mut self, order: SortOrder) -> Self {
216 self.order = Some(order);
217 self
218 }
219
220 pub fn purpose(mut self, purpose: impl Into<String>) -> Self {
222 self.purpose = Some(purpose.into());
223 self
224 }
225
226 pub async fn page(self) -> Result<FileList> {
228 let mut url = self.client.url("/v1/files")?;
229 {
230 let mut query = url.query_pairs_mut();
231 if let Some(after) = &self.after {
232 query.append_pair("after", after);
233 }
234 if let Some(limit) = self.limit {
235 query.append_pair("limit", &limit.to_string());
236 }
237 if let Some(order) = self.order {
238 query.append_pair("order", order.as_str());
239 }
240 if let Some(purpose) = &self.purpose {
241 query.append_pair("purpose", purpose);
242 }
243 }
244
245 let request =
246 PreparedRequest::new(Method::GET, url).header("accept", headers::JSON_CONTENT_TYPE);
247 let response = self.client.send_once(request, RetryPolicy::NONE).await?;
248
249 if let Ok(files) = serde_json::from_slice::<Vec<File>>(&response.body) {
251 return Ok(FileList {
252 object_kind: "list".to_string(),
253 first_id: files.first().map(|file| file.id.clone()),
254 last_id: files.last().map(|file| file.id.clone()),
255 has_more: false,
256 data: files,
257 });
258 }
259 parse_json(&response, "file list")
260 }
261
262 pub async fn send(self) -> Result<Vec<File>> {
264 Ok(self.page().await?.data)
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn a_path_contributes_only_its_basename() {
274 assert_eq!(basename(Path::new("/var/data/in.jsonl")), "in.jsonl");
275 assert_eq!(basename(Path::new("in.jsonl")), "in.jsonl");
276 assert_eq!(basename(Path::new("/")), "upload.jsonl");
277 }
278
279 #[tokio::test]
280 async fn a_missing_upload_path_fails_before_any_request() {
281 let client = Client::new("https://sie.invalid").unwrap();
282 let err = client
283 .files()
284 .upload_path("/nonexistent/sie-sdk/in.jsonl")
285 .send()
286 .await
287 .unwrap_err();
288 assert!(
289 err.to_string().contains("/nonexistent/sie-sdk/in.jsonl"),
290 "{err}"
291 );
292 }
293
294 #[test]
295 fn sort_order_renders_the_wire_tokens() {
296 assert_eq!(SortOrder::Ascending.as_str(), "asc");
297 assert_eq!(SortOrder::Descending.as_str(), "desc");
298 }
299}