agentik_sdk/resources/
files.rs1use crate::types::{
2 FileObject, FileUploadParams, FileListParams, FileList, FileDownload,
3 UploadProgress, StorageInfo, AnthropicError, Result,
4};
5use crate::http::HttpClient;
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8use tokio::time::sleep;
9
10#[derive(Debug, Clone)]
12pub struct FilesResource {
13 http_client: Arc<HttpClient>,
14}
15
16impl FilesResource {
17 pub fn new(http_client: Arc<HttpClient>) -> Self {
19 Self { http_client }
20 }
21
22 pub async fn upload(&self, params: FileUploadParams) -> Result<FileObject> {
33 params.validate()?;
35
36 let form = self.create_multipart_form(params)?;
38
39 let response = self
40 .http_client
41 .post("/v1/files")
42 .multipart(form)
43 .send()
44 .await?;
45
46 let file_object: FileObject = response.json().await?;
47 Ok(file_object)
48 }
49
50 pub async fn upload_with_progress<F>(
62 &self,
63 params: FileUploadParams,
64 mut progress_callback: F,
65 ) -> Result<FileObject>
66 where
67 F: FnMut(UploadProgress),
68 {
69 params.validate()?;
71
72 let total_size = params.content.len() as u64;
73 let start_time = Instant::now();
74
75 let mut uploaded = 0u64;
77 let chunk_size = (total_size / 20).max(1024); while uploaded < total_size {
80 let chunk = chunk_size.min(total_size - uploaded);
81 uploaded += chunk;
82
83 let elapsed = start_time.elapsed().as_secs_f64();
84 let speed = if elapsed > 0.0 { uploaded as f64 / elapsed } else { 0.0 };
85
86 let progress = UploadProgress::new(uploaded, total_size).with_speed(speed);
87 progress_callback(progress);
88
89 sleep(Duration::from_millis(50)).await;
91 }
92
93 self.upload(params).await
95 }
96
97 pub async fn get(&self, file_id: &str) -> Result<FileObject> {
108 let response = self
109 .http_client
110 .get(&format!("/v1/files/{}", file_id))
111 .send()
112 .await?;
113
114 let file_object: FileObject = response.json().await?;
115 Ok(file_object)
116 }
117
118 pub async fn list(&self, params: Option<FileListParams>) -> Result<FileList> {
129 let mut request = self.http_client.get("/v1/files");
130
131 if let Some(params) = params {
132 if let Some(purpose) = params.purpose {
133 request = request.query(&[("purpose", serde_json::to_string(&purpose)?)]);
134 }
135 if let Some(after) = params.after {
136 request = request.query(&[("after", after)]);
137 }
138 if let Some(limit) = params.limit {
139 request = request.query(&[("limit", limit.to_string())]);
140 }
141 if let Some(order) = params.order {
142 request = request.query(&[("order", serde_json::to_string(&order)?)]);
143 }
144 }
145
146 let response = request.send().await?;
147 let file_list: FileList = response.json().await?;
148 Ok(file_list)
149 }
150
151 pub async fn download(&self, file_id: &str) -> Result<FileDownload> {
162 let response = self
163 .http_client
164 .get(&format!("/v1/files/{}/content", file_id))
165 .send()
166 .await?;
167
168 let content_type = response
169 .headers()
170 .get("content-type")
171 .and_then(|v| v.to_str().ok())
172 .unwrap_or("application/octet-stream")
173 .to_string();
174
175 let content_disposition = response
176 .headers()
177 .get("content-disposition")
178 .and_then(|v| v.to_str().ok());
179
180 let filename = extract_filename_from_disposition(content_disposition)
181 .unwrap_or_else(|| format!("file_{}", file_id));
182
183 let content = response.bytes().await?;
184 let size = content.len() as u64;
185
186 Ok(FileDownload {
187 content: content.to_vec(),
188 content_type,
189 filename,
190 size,
191 })
192 }
193
194 pub async fn delete(&self, file_id: &str) -> Result<FileObject> {
205 let response = self
206 .http_client
207 .delete(&format!("/v1/files/{}", file_id))
208 .send()
209 .await?;
210
211 let file_object: FileObject = response.json().await?;
212 Ok(file_object)
213 }
214
215 pub async fn get_storage_info(&self) -> Result<StorageInfo> {
223 let response = self
224 .http_client
225 .get("/v1/files/storage")
226 .send()
227 .await?;
228
229 let storage_info: StorageInfo = response.json().await?;
230 Ok(storage_info)
231 }
232
233 pub async fn wait_for_processing(
246 &self,
247 file_id: &str,
248 poll_interval: Option<Duration>,
249 timeout: Option<Duration>,
250 ) -> Result<FileObject> {
251 let poll_interval = poll_interval.unwrap_or(Duration::from_secs(2));
252 let timeout = timeout.unwrap_or(Duration::from_secs(300)); let start_time = Instant::now();
255
256 loop {
257 let file = self.get(file_id).await?;
258
259 if file.status.is_ready() {
260 return Ok(file);
261 }
262
263 if file.status.has_error() {
264 return Err(AnthropicError::Other(format!(
265 "File processing failed for file {}",
266 file_id
267 )));
268 }
269
270 if start_time.elapsed() > timeout {
271 return Err(AnthropicError::Timeout);
272 }
273
274 sleep(poll_interval).await;
275 }
276 }
277
278 fn create_multipart_form(&self, params: FileUploadParams) -> Result<reqwest::multipart::Form> {
280 let mut form = reqwest::multipart::Form::new();
281
282 let file_part = reqwest::multipart::Part::bytes(params.content)
284 .file_name(params.filename.clone())
285 .mime_str(¶ms.content_type)?;
286 form = form.part("file", file_part);
287
288 form = form.text("purpose", serde_json::to_string(¶ms.purpose)?);
290
291 if !params.metadata.is_empty() {
293 form = form.text("metadata", serde_json::to_string(¶ms.metadata)?);
294 }
295
296 Ok(form)
297 }
298}
299
300impl FilesResource {
302 pub async fn upload_batch(
314 &self,
315 uploads: Vec<FileUploadParams>,
316 max_concurrent: Option<usize>,
317 ) -> Result<Vec<FileObject>> {
318 let max_concurrent = max_concurrent.unwrap_or(3);
319 let semaphore = Arc::new(tokio::sync::Semaphore::new(max_concurrent));
320
321 let tasks: Vec<_> = uploads
322 .into_iter()
323 .map(|params| {
324 let files_resource = self.clone();
325 let semaphore = semaphore.clone();
326
327 tokio::spawn(async move {
328 let _permit = semaphore.acquire().await.unwrap();
329 files_resource.upload(params).await
330 })
331 })
332 .collect();
333
334 let mut results = Vec::new();
335 for task in tasks {
336 let result = task.await.map_err(|e| AnthropicError::Other(e.to_string()))??;
337 results.push(result);
338 }
339
340 Ok(results)
341 }
342
343 pub async fn cleanup_old_files(&self, max_age: Duration) -> Result<u32> {
354 let files = self.list(None).await?;
355 let cutoff_time = chrono::Utc::now() - chrono::Duration::from_std(max_age)?;
356
357 let mut deleted_count = 0;
358
359 for file in files.data {
360 if file.created_at < cutoff_time {
361 if let Ok(_) = self.delete(&file.id).await {
362 deleted_count += 1;
363 }
364 }
365 }
366
367 Ok(deleted_count)
368 }
369
370 pub async fn get_files_by_purpose(
382 &self,
383 purpose: crate::types::FilePurpose,
384 limit: Option<u32>,
385 ) -> Result<Vec<FileObject>> {
386 let params = FileListParams::new()
387 .purpose(purpose)
388 .limit(limit.unwrap_or(50));
389
390 let file_list = self.list(Some(params)).await?;
391 Ok(file_list.data)
392 }
393}
394
395fn extract_filename_from_disposition(disposition: Option<&str>) -> Option<String> {
397 disposition.and_then(|d| {
398 if let Some(start) = d.find("filename=") {
400 let start = start + 9; let rest = &d[start..];
402
403 if rest.starts_with('"') {
404 rest[1..].split('"').next().map(|s| s.to_string())
406 } else {
407 rest.split(';').next().map(|s| s.trim().to_string())
409 }
410 } else {
411 None
412 }
413 })
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::types::FilePurpose;
420
421 #[test]
422 fn test_extract_filename_from_disposition() {
423 assert_eq!(
424 extract_filename_from_disposition(Some(r#"attachment; filename="test.txt""#)),
425 Some("test.txt".to_string())
426 );
427
428 assert_eq!(
429 extract_filename_from_disposition(Some(r#"attachment; filename=test.txt"#)),
430 Some("test.txt".to_string())
431 );
432
433 assert_eq!(
434 extract_filename_from_disposition(Some(r#"inline"#)),
435 None
436 );
437
438 assert_eq!(
439 extract_filename_from_disposition(None),
440 None
441 );
442 }
443
444 #[test]
445 fn test_upload_params_creation() {
446 let params = FileUploadParams::new(
447 b"test content".to_vec(),
448 "test.txt",
449 "text/plain",
450 FilePurpose::Document,
451 );
452
453 assert_eq!(params.filename, "test.txt");
454 assert_eq!(params.content_type, "text/plain");
455 assert_eq!(params.purpose, FilePurpose::Document);
456 assert_eq!(params.content, b"test content");
457 }
458
459 #[test]
460 fn test_file_list_params_builder() {
461 let params = FileListParams::new()
462 .purpose(FilePurpose::Vision)
463 .limit(10)
464 .after("file_123");
465
466 assert_eq!(params.purpose, Some(FilePurpose::Vision));
467 assert_eq!(params.limit, Some(10));
468 assert_eq!(params.after, Some("file_123".to_string()));
469 }
470}