1use crate::types::AnthropicError;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use chrono::{DateTime, Utc};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct FileObject {
9 pub id: String,
11
12 #[serde(rename = "type")]
14 pub object_type: String,
15
16 pub filename: String,
18
19 pub size_bytes: u64,
21
22 pub content_type: String,
24
25 pub purpose: FilePurpose,
27
28 pub created_at: DateTime<Utc>,
30
31 pub expires_at: Option<DateTime<Utc>>,
33
34 pub status: FileStatus,
36
37 #[serde(default)]
39 pub metadata: HashMap<String, String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44#[serde(rename_all = "snake_case")]
45pub enum FilePurpose {
46 BatchInput,
48
49 BatchOutput,
51
52 Vision,
54
55 Document,
57
58 Upload,
60}
61
62impl FilePurpose {
63 pub fn all() -> Vec<FilePurpose> {
65 vec![
66 FilePurpose::BatchInput,
67 FilePurpose::BatchOutput,
68 FilePurpose::Vision,
69 FilePurpose::Document,
70 FilePurpose::Upload,
71 ]
72 }
73
74 pub fn supports_mime_type(&self, mime_type: &str) -> bool {
76 match self {
77 FilePurpose::BatchInput => {
78 mime_type == "application/json" || mime_type == "text/plain"
79 }
80 FilePurpose::BatchOutput => {
81 mime_type == "application/json" || mime_type == "text/plain"
82 }
83 FilePurpose::Vision => {
84 mime_type.starts_with("image/")
85 }
86 FilePurpose::Document => {
87 mime_type == "application/pdf"
88 || mime_type == "text/plain"
89 || mime_type == "application/msword"
90 || mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
91 }
92 FilePurpose::Upload => true, }
94 }
95}
96
97#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
99#[serde(rename_all = "snake_case")]
100pub enum FileStatus {
101 Processing,
103
104 Processed,
106
107 Error,
109
110 Deleted,
112}
113
114impl FileStatus {
115 pub fn is_ready(&self) -> bool {
117 *self == FileStatus::Processed
118 }
119
120 pub fn has_error(&self) -> bool {
122 *self == FileStatus::Error
123 }
124
125 pub fn is_deleted(&self) -> bool {
127 *self == FileStatus::Deleted
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct FileUploadParams {
134 pub content: Vec<u8>,
136
137 pub filename: String,
139
140 pub content_type: String,
142
143 pub purpose: FilePurpose,
145
146 pub metadata: HashMap<String, String>,
148}
149
150impl FileUploadParams {
151 pub fn new(
153 content: Vec<u8>,
154 filename: impl Into<String>,
155 content_type: impl Into<String>,
156 purpose: FilePurpose,
157 ) -> Self {
158 Self {
159 content,
160 filename: filename.into(),
161 content_type: content_type.into(),
162 purpose,
163 metadata: HashMap::new(),
164 }
165 }
166
167 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
169 self.metadata = metadata;
170 self
171 }
172
173 pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
175 self.metadata.insert(key.into(), value.into());
176 self
177 }
178
179 pub fn validate(&self) -> Result<(), AnthropicError> {
181 const MAX_SIZE: u64 = 100 * 1024 * 1024;
183 if self.content.len() as u64 > MAX_SIZE {
184 return Err(AnthropicError::Other(format!(
185 "File size {} bytes exceeds maximum of {} bytes",
186 self.content.len(),
187 MAX_SIZE
188 )));
189 }
190
191 if self.filename.is_empty() {
193 return Err(AnthropicError::Other(
194 "Filename cannot be empty".to_string()
195 ));
196 }
197
198 if !self.purpose.supports_mime_type(&self.content_type) {
200 return Err(AnthropicError::Other(format!(
201 "MIME type '{}' not supported for purpose '{:?}'",
202 self.content_type, self.purpose
203 )));
204 }
205
206 Ok(())
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, Default)]
212pub struct FileListParams {
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub purpose: Option<FilePurpose>,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub after: Option<String>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub limit: Option<u32>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub order: Option<FileOrder>,
228}
229
230impl FileListParams {
231 pub fn new() -> Self {
233 Self::default()
234 }
235
236 pub fn purpose(mut self, purpose: FilePurpose) -> Self {
238 self.purpose = Some(purpose);
239 self
240 }
241
242 pub fn after(mut self, after: impl Into<String>) -> Self {
244 self.after = Some(after.into());
245 self
246 }
247
248 pub fn limit(mut self, limit: u32) -> Self {
250 self.limit = Some(limit.clamp(1, 100));
251 self
252 }
253
254 pub fn order(mut self, order: FileOrder) -> Self {
256 self.order = Some(order);
257 self
258 }
259}
260
261#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
263#[serde(rename_all = "snake_case")]
264pub enum FileOrder {
265 NewestFirst,
267
268 OldestFirst,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct FileList {
275 pub data: Vec<FileObject>,
277
278 pub has_more: bool,
280
281 pub first_id: Option<String>,
283
284 pub last_id: Option<String>,
286}
287
288#[derive(Debug, Clone)]
290pub struct UploadProgress {
291 pub bytes_uploaded: u64,
293
294 pub total_bytes: u64,
296
297 pub percentage: f64,
299
300 pub speed_bps: Option<f64>,
302
303 pub eta_seconds: Option<f64>,
305}
306
307impl UploadProgress {
308 pub fn new(bytes_uploaded: u64, total_bytes: u64) -> Self {
310 let percentage = if total_bytes > 0 {
311 (bytes_uploaded as f64 / total_bytes as f64) * 100.0
312 } else {
313 0.0
314 };
315
316 Self {
317 bytes_uploaded,
318 total_bytes,
319 percentage,
320 speed_bps: None,
321 eta_seconds: None,
322 }
323 }
324
325 pub fn with_speed(mut self, speed_bps: f64) -> Self {
327 self.speed_bps = Some(speed_bps);
328
329 if speed_bps > 0.0 {
331 let remaining_bytes = self.total_bytes - self.bytes_uploaded;
332 self.eta_seconds = Some(remaining_bytes as f64 / speed_bps);
333 }
334
335 self
336 }
337
338 pub fn is_complete(&self) -> bool {
340 self.bytes_uploaded >= self.total_bytes
341 }
342
343 pub fn percentage_string(&self) -> String {
345 format!("{:.1}%", self.percentage)
346 }
347
348 pub fn size_string(&self) -> String {
350 format!("{} / {}",
351 format_bytes(self.bytes_uploaded),
352 format_bytes(self.total_bytes)
353 )
354 }
355
356 pub fn speed_string(&self) -> Option<String> {
358 self.speed_bps.map(|speed| format!("{}/s", format_bytes(speed as u64)))
359 }
360
361 pub fn eta_string(&self) -> Option<String> {
363 self.eta_seconds.map(|eta| {
364 if eta < 60.0 {
365 format!("{:.0}s", eta)
366 } else if eta < 3600.0 {
367 format!("{:.0}m {:.0}s", eta / 60.0, eta % 60.0)
368 } else {
369 format!("{:.0}h {:.0}m", eta / 3600.0, (eta % 3600.0) / 60.0)
370 }
371 })
372 }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct StorageInfo {
378 pub quota_bytes: u64,
380
381 pub used_bytes: u64,
383
384 pub available_bytes: u64,
386
387 pub file_count: u32,
389
390 pub usage_by_purpose: HashMap<String, u64>,
392}
393
394impl StorageInfo {
395 pub fn usage_percentage(&self) -> f64 {
397 if self.quota_bytes > 0 {
398 (self.used_bytes as f64 / self.quota_bytes as f64) * 100.0
399 } else {
400 0.0
401 }
402 }
403
404 pub fn is_nearly_full(&self) -> bool {
406 self.usage_percentage() > 90.0
407 }
408
409 pub fn is_full(&self) -> bool {
411 self.used_bytes >= self.quota_bytes
412 }
413
414 pub fn quota_string(&self) -> String {
416 format_bytes(self.quota_bytes)
417 }
418
419 pub fn usage_string(&self) -> String {
421 format!("{} / {} ({:.1}%)",
422 format_bytes(self.used_bytes),
423 format_bytes(self.quota_bytes),
424 self.usage_percentage()
425 )
426 }
427}
428
429fn format_bytes(bytes: u64) -> String {
431 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
432
433 if bytes == 0 {
434 return "0 B".to_string();
435 }
436
437 let mut size = bytes as f64;
438 let mut unit_index = 0;
439
440 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
441 size /= 1024.0;
442 unit_index += 1;
443 }
444
445 if unit_index == 0 {
446 format!("{} {}", size as u64, UNITS[unit_index])
447 } else {
448 format!("{:.1} {}", size, UNITS[unit_index])
449 }
450}
451
452#[derive(Debug, Clone)]
454pub struct FileDownload {
455 pub content: Vec<u8>,
457
458 pub content_type: String,
460
461 pub filename: String,
463
464 pub size: u64,
466}
467
468impl FileDownload {
469 pub async fn save_to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), std::io::Error> {
471 tokio::fs::write(path, &self.content).await
472 }
473
474 pub fn as_string(&self) -> Result<String, std::string::FromUtf8Error> {
476 String::from_utf8(self.content.clone())
477 }
478
479 pub fn as_json<T>(&self) -> Result<T, serde_json::Error>
481 where
482 T: for<'de> Deserialize<'de>,
483 {
484 serde_json::from_slice(&self.content)
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn test_file_purpose_mime_type_support() {
494 assert!(FilePurpose::Vision.supports_mime_type("image/jpeg"));
495 assert!(FilePurpose::Vision.supports_mime_type("image/png"));
496 assert!(!FilePurpose::Vision.supports_mime_type("application/pdf"));
497
498 assert!(FilePurpose::Document.supports_mime_type("application/pdf"));
499 assert!(FilePurpose::Document.supports_mime_type("text/plain"));
500 assert!(!FilePurpose::Document.supports_mime_type("image/jpeg"));
501
502 assert!(FilePurpose::BatchInput.supports_mime_type("application/json"));
503 assert!(FilePurpose::BatchInput.supports_mime_type("text/plain"));
504 assert!(!FilePurpose::BatchInput.supports_mime_type("image/jpeg"));
505 }
506
507 #[test]
508 fn test_upload_params_validation() {
509 let params = FileUploadParams::new(
511 b"test content".to_vec(),
512 "test.txt",
513 "text/plain",
514 FilePurpose::Document,
515 );
516 assert!(params.validate().is_ok());
517
518 let params = FileUploadParams::new(
520 b"test content".to_vec(),
521 "test.txt",
522 "image/jpeg",
523 FilePurpose::BatchInput,
524 );
525 assert!(params.validate().is_err());
526
527 let params = FileUploadParams::new(
529 b"test content".to_vec(),
530 "",
531 "text/plain",
532 FilePurpose::Document,
533 );
534 assert!(params.validate().is_err());
535 }
536
537 #[test]
538 fn test_upload_progress() {
539 let progress = UploadProgress::new(512, 1024);
540 assert_eq!(progress.percentage, 50.0);
541 assert!(!progress.is_complete());
542
543 let progress = UploadProgress::new(1024, 1024);
544 assert_eq!(progress.percentage, 100.0);
545 assert!(progress.is_complete());
546
547 let progress = UploadProgress::new(512, 1024).with_speed(1024.0);
548 assert!(progress.speed_bps.is_some());
549 assert!(progress.eta_seconds.is_some());
550 }
551
552 #[test]
553 fn test_storage_info() {
554 let storage = StorageInfo {
555 quota_bytes: 1000,
556 used_bytes: 910, available_bytes: 90,
558 file_count: 10,
559 usage_by_purpose: HashMap::new(),
560 };
561
562 assert_eq!(storage.usage_percentage(), 91.0);
563 assert!(storage.is_nearly_full());
564 assert!(!storage.is_full());
565
566 let storage = StorageInfo {
567 quota_bytes: 1000,
568 used_bytes: 1000,
569 available_bytes: 0,
570 file_count: 10,
571 usage_by_purpose: HashMap::new(),
572 };
573
574 assert!(storage.is_full());
575 }
576
577 #[test]
578 fn test_format_bytes() {
579 assert_eq!(format_bytes(0), "0 B");
580 assert_eq!(format_bytes(512), "512 B");
581 assert_eq!(format_bytes(1024), "1.0 KB");
582 assert_eq!(format_bytes(1536), "1.5 KB");
583 assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
584 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
585 }
586
587 #[test]
588 fn test_file_status() {
589 assert!(FileStatus::Processed.is_ready());
590 assert!(!FileStatus::Processing.is_ready());
591 assert!(FileStatus::Error.has_error());
592 assert!(FileStatus::Deleted.is_deleted());
593 }
594}