anthropic_sdk/types/
files_api.rs

1use crate::types::AnthropicError;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use chrono::{DateTime, Utc};
5
6/// File object from the Anthropic Files API
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct FileObject {
9    /// Unique identifier for the file
10    pub id: String,
11    
12    /// Object type (always "file")
13    #[serde(rename = "type")]
14    pub object_type: String,
15    
16    /// Original filename
17    pub filename: String,
18    
19    /// File size in bytes
20    pub size_bytes: u64,
21    
22    /// MIME type of the file
23    pub content_type: String,
24    
25    /// Purpose of the file (e.g., "batch_input", "batch_output")
26    pub purpose: FilePurpose,
27    
28    /// When the file was created
29    pub created_at: DateTime<Utc>,
30    
31    /// When the file expires (if applicable)
32    pub expires_at: Option<DateTime<Utc>>,
33    
34    /// Current status of the file
35    pub status: FileStatus,
36    
37    /// Additional metadata
38    #[serde(default)]
39    pub metadata: HashMap<String, String>,
40}
41
42/// Purpose/use case for uploaded files
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44#[serde(rename_all = "snake_case")]
45pub enum FilePurpose {
46    /// Input file for batch processing
47    BatchInput,
48    
49    /// Output file from batch processing
50    BatchOutput,
51    
52    /// File for vision/image analysis
53    Vision,
54    
55    /// Document for analysis
56    Document,
57    
58    /// General file upload
59    Upload,
60}
61
62impl FilePurpose {
63    /// Get all valid file purposes
64    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    /// Check if this purpose supports a given MIME type
75    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, // General purpose accepts most types
93        }
94    }
95}
96
97/// Status of a file in the system
98#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
99#[serde(rename_all = "snake_case")]
100pub enum FileStatus {
101    /// File is being processed
102    Processing,
103    
104    /// File is ready for use
105    Processed,
106    
107    /// File processing failed
108    Error,
109    
110    /// File has been deleted
111    Deleted,
112}
113
114impl FileStatus {
115    /// Check if the file is ready for use
116    pub fn is_ready(&self) -> bool {
117        *self == FileStatus::Processed
118    }
119    
120    /// Check if the file has an error
121    pub fn has_error(&self) -> bool {
122        *self == FileStatus::Error
123    }
124    
125    /// Check if the file is deleted
126    pub fn is_deleted(&self) -> bool {
127        *self == FileStatus::Deleted
128    }
129}
130
131/// Parameters for uploading a file
132#[derive(Debug, Clone)]
133pub struct FileUploadParams {
134    /// File content
135    pub content: Vec<u8>,
136    
137    /// Original filename
138    pub filename: String,
139    
140    /// MIME type
141    pub content_type: String,
142    
143    /// Purpose of the file
144    pub purpose: FilePurpose,
145    
146    /// Additional metadata
147    pub metadata: HashMap<String, String>,
148}
149
150impl FileUploadParams {
151    /// Create new upload parameters
152    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    /// Add metadata to the upload
168    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
169        self.metadata = metadata;
170        self
171    }
172    
173    /// Add a single metadata entry
174    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    /// Validate the upload parameters
180    pub fn validate(&self) -> Result<(), AnthropicError> {
181        // Check file size (100MB limit)
182        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        // Check filename
192        if self.filename.is_empty() {
193            return Err(AnthropicError::Other(
194                "Filename cannot be empty".to_string()
195            ));
196        }
197        
198        // Check content type vs purpose
199        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/// Parameters for listing files
211#[derive(Debug, Clone, Serialize, Deserialize, Default)]
212pub struct FileListParams {
213    /// Filter by purpose
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub purpose: Option<FilePurpose>,
216    
217    /// A cursor for use in pagination
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub after: Option<String>,
220    
221    /// Number of items to return (1-100, default 20)
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub limit: Option<u32>,
224    
225    /// Sort order (newest_first or oldest_first)
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub order: Option<FileOrder>,
228}
229
230impl FileListParams {
231    /// Create new list parameters
232    pub fn new() -> Self {
233        Self::default()
234    }
235    
236    /// Filter by purpose
237    pub fn purpose(mut self, purpose: FilePurpose) -> Self {
238        self.purpose = Some(purpose);
239        self
240    }
241    
242    /// Set pagination cursor
243    pub fn after(mut self, after: impl Into<String>) -> Self {
244        self.after = Some(after.into());
245        self
246    }
247    
248    /// Set result limit
249    pub fn limit(mut self, limit: u32) -> Self {
250        self.limit = Some(limit.clamp(1, 100));
251        self
252    }
253    
254    /// Set sort order
255    pub fn order(mut self, order: FileOrder) -> Self {
256        self.order = Some(order);
257        self
258    }
259}
260
261/// Sort order for file listings
262#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
263#[serde(rename_all = "snake_case")]
264pub enum FileOrder {
265    /// Newest files first
266    NewestFirst,
267    
268    /// Oldest files first
269    OldestFirst,
270}
271
272/// Response containing a list of files
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct FileList {
275    /// List of file objects
276    pub data: Vec<FileObject>,
277    
278    /// Whether there are more items available
279    pub has_more: bool,
280    
281    /// First ID in the current page
282    pub first_id: Option<String>,
283    
284    /// Last ID in the current page
285    pub last_id: Option<String>,
286}
287
288/// Upload progress information
289#[derive(Debug, Clone)]
290pub struct UploadProgress {
291    /// Bytes uploaded so far
292    pub bytes_uploaded: u64,
293    
294    /// Total bytes to upload
295    pub total_bytes: u64,
296    
297    /// Upload percentage (0-100)
298    pub percentage: f64,
299    
300    /// Upload speed in bytes per second
301    pub speed_bps: Option<f64>,
302    
303    /// Estimated time remaining in seconds
304    pub eta_seconds: Option<f64>,
305}
306
307impl UploadProgress {
308    /// Create new progress information
309    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    /// Update with speed calculation
326    pub fn with_speed(mut self, speed_bps: f64) -> Self {
327        self.speed_bps = Some(speed_bps);
328        
329        // Calculate ETA
330        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    /// Check if upload is complete
339    pub fn is_complete(&self) -> bool {
340        self.bytes_uploaded >= self.total_bytes
341    }
342    
343    /// Get human-readable percentage
344    pub fn percentage_string(&self) -> String {
345        format!("{:.1}%", self.percentage)
346    }
347    
348    /// Get human-readable size
349    pub fn size_string(&self) -> String {
350        format!("{} / {}", 
351            format_bytes(self.bytes_uploaded),
352            format_bytes(self.total_bytes)
353        )
354    }
355    
356    /// Get human-readable speed
357    pub fn speed_string(&self) -> Option<String> {
358        self.speed_bps.map(|speed| format!("{}/s", format_bytes(speed as u64)))
359    }
360    
361    /// Get human-readable ETA
362    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/// File storage information and quotas
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct StorageInfo {
378    /// Total storage quota in bytes
379    pub quota_bytes: u64,
380    
381    /// Used storage in bytes
382    pub used_bytes: u64,
383    
384    /// Available storage in bytes
385    pub available_bytes: u64,
386    
387    /// Number of files stored
388    pub file_count: u32,
389    
390    /// Storage by purpose
391    pub usage_by_purpose: HashMap<String, u64>,
392}
393
394impl StorageInfo {
395    /// Get usage percentage
396    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    /// Check if storage is nearly full (>90%)
405    pub fn is_nearly_full(&self) -> bool {
406        self.usage_percentage() > 90.0
407    }
408    
409    /// Check if storage is full
410    pub fn is_full(&self) -> bool {
411        self.used_bytes >= self.quota_bytes
412    }
413    
414    /// Get human-readable quota
415    pub fn quota_string(&self) -> String {
416        format_bytes(self.quota_bytes)
417    }
418    
419    /// Get human-readable usage
420    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
429/// Helper function to format bytes in human-readable format
430fn 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/// File download response
453#[derive(Debug, Clone)]
454pub struct FileDownload {
455    /// File content
456    pub content: Vec<u8>,
457    
458    /// Content type
459    pub content_type: String,
460    
461    /// Original filename
462    pub filename: String,
463    
464    /// File size
465    pub size: u64,
466}
467
468impl FileDownload {
469    /// Save content to a file
470    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    /// Get content as string (for text files)
475    pub fn as_string(&self) -> Result<String, std::string::FromUtf8Error> {
476        String::from_utf8(self.content.clone())
477    }
478    
479    /// Get content as JSON (for JSON files)
480    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        // Valid upload
510        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        // Invalid purpose/mime type combination
519        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        // Empty filename
528        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,  // 91% usage
557            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}