anthropic_sdk/files/
mod.rs

1use std::io::{Read, BufReader};
2use std::path::Path;
3use std::fs::File as StdFile;
4use std::fmt;
5use bytes::Bytes;
6use mime::Mime;
7use sha2::{Sha256, Digest};
8use base64::{Engine as _, engine::general_purpose};
9
10/// Represents a file that can be uploaded to the Anthropic API
11#[derive(Debug, Clone)]
12pub struct File {
13    /// File name
14    pub name: String,
15    /// MIME type
16    pub mime_type: Mime,
17    /// File data
18    pub data: FileData,
19    /// File size in bytes
20    pub size: u64,
21    /// Optional file hash for integrity verification
22    pub hash: Option<String>,
23}
24
25/// Different sources of file data
26#[derive(Debug, Clone)]
27pub enum FileData {
28    /// In-memory bytes
29    Bytes(Bytes),
30    /// Base64 encoded data
31    Base64(String),
32    /// File path for lazy loading
33    Path(std::path::PathBuf),
34    /// Temporary file
35    TempFile(std::path::PathBuf),
36}
37
38/// File validation constraints
39#[derive(Debug, Clone)]
40pub struct FileConstraints {
41    /// Maximum file size in bytes (default: 10MB)
42    pub max_size: u64,
43    /// Allowed MIME types (None = allow all)
44    pub allowed_types: Option<Vec<Mime>>,
45    /// Require hash verification
46    pub require_hash: bool,
47}
48
49impl Default for FileConstraints {
50    fn default() -> Self {
51        Self {
52            max_size: 10 * 1024 * 1024, // 10MB
53            allowed_types: None,
54            require_hash: false,
55        }
56    }
57}
58
59/// Errors that can occur during file operations
60#[derive(Debug, thiserror::Error)]
61pub enum FileError {
62    #[error("File not found: {path}")]
63    NotFound { path: String },
64    
65    #[error("File too large: {size} bytes (max: {max_size} bytes)")]
66    TooLarge { size: u64, max_size: u64 },
67    
68    #[error("Invalid MIME type: {mime_type} (allowed: {allowed:?})")]
69    InvalidMimeType { mime_type: String, allowed: Vec<String> },
70    
71    #[error("IO error: {0}")]
72    Io(#[from] std::io::Error),
73    
74    #[error("Invalid base64 data: {0}")]
75    InvalidBase64(#[from] base64::DecodeError),
76    
77    #[error("MIME detection failed")]
78    MimeDetectionFailed,
79    
80    #[error("Hash verification failed")]
81    HashVerificationFailed,
82    
83    #[error("Invalid file data")]
84    InvalidData,
85}
86
87impl File {
88    /// Create a new file from bytes
89    pub fn from_bytes(
90        name: impl Into<String>,
91        bytes: impl Into<Bytes>,
92        mime_type: Option<Mime>,
93    ) -> Result<Self, FileError> {
94        let name = name.into();
95        let bytes = bytes.into();
96        let size = bytes.len() as u64;
97        
98        let mime_type = match mime_type {
99            Some(mime) => mime,
100            None => detect_mime_type(&name, Some(&bytes))?,
101        };
102
103        Ok(Self {
104            name,
105            mime_type,
106            data: FileData::Bytes(bytes),
107            size,
108            hash: None,
109        })
110    }
111
112    /// Create a new file from a file path
113    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, FileError> {
114        let path = path.as_ref();
115        
116        if !path.exists() {
117            return Err(FileError::NotFound {
118                path: path.display().to_string(),
119            });
120        }
121        
122        let metadata = std::fs::metadata(path)?;
123        let size = metadata.len();
124        let name = path.file_name()
125            .and_then(|n| n.to_str())
126            .unwrap_or("file")
127            .to_string();
128            
129        let mime_type = detect_mime_type(&name, None)?;
130
131        Ok(Self {
132            name,
133            mime_type,
134            data: FileData::Path(path.to_path_buf()),
135            size,
136            hash: None,
137        })
138    }
139
140    /// Create a new file from base64 data
141    pub fn from_base64(
142        name: impl Into<String>,
143        base64_data: impl Into<String>,
144        mime_type: Option<Mime>,
145    ) -> Result<Self, FileError> {
146        let name = name.into();
147        let base64_data = base64_data.into();
148        
149        // Decode to get size
150        let decoded = general_purpose::STANDARD.decode(&base64_data)?;
151        let size = decoded.len() as u64;
152        
153        let mime_type = match mime_type {
154            Some(mime) => mime,
155            None => detect_mime_type(&name, Some(&decoded))?,
156        };
157
158        Ok(Self {
159            name,
160            mime_type,
161            data: FileData::Base64(base64_data),
162            size,
163            hash: None,
164        })
165    }
166
167    /// Create a file from a standard library File
168    pub fn from_std_file(
169        std_file: StdFile,
170        name: impl Into<String>,
171        mime_type: Option<Mime>,
172    ) -> Result<Self, FileError> {
173        let name = name.into();
174        let metadata = std_file.metadata()?;
175        let size = metadata.len();
176        
177        // Read file contents
178        let mut reader = BufReader::new(std_file);
179        let mut buffer = Vec::new();
180        reader.read_to_end(&mut buffer)?;
181        
182        let mime_type = match mime_type {
183            Some(mime) => mime,
184            None => detect_mime_type(&name, Some(&buffer))?,
185        };
186
187        Ok(Self {
188            name,
189            mime_type,
190            data: FileData::Bytes(Bytes::from(buffer)),
191            size,
192            hash: None,
193        })
194    }
195
196    /// Validate the file against constraints
197    pub fn validate(&self, constraints: &FileConstraints) -> Result<(), FileError> {
198        // Check size
199        if self.size > constraints.max_size {
200            return Err(FileError::TooLarge {
201                size: self.size,
202                max_size: constraints.max_size,
203            });
204        }
205
206        // Check MIME type
207        if let Some(allowed_types) = &constraints.allowed_types {
208            if !allowed_types.iter().any(|mime| mime == &self.mime_type) {
209                return Err(FileError::InvalidMimeType {
210                    mime_type: self.mime_type.to_string(),
211                    allowed: allowed_types.iter().map(|m| m.to_string()).collect(),
212                });
213            }
214        }
215
216        Ok(())
217    }
218
219    /// Get the file data as bytes
220    pub async fn to_bytes(&self) -> Result<Bytes, FileError> {
221        match &self.data {
222            FileData::Bytes(bytes) => Ok(bytes.clone()),
223            FileData::Base64(base64_data) => {
224                let decoded = general_purpose::STANDARD.decode(base64_data)?;
225                Ok(Bytes::from(decoded))
226            },
227            FileData::Path(path) => {
228                let bytes = tokio::fs::read(path).await?;
229                Ok(Bytes::from(bytes))
230            },
231            FileData::TempFile(path) => {
232                let bytes = tokio::fs::read(path).await?;
233                Ok(Bytes::from(bytes))
234            },
235        }
236    }
237
238    /// Get the file data as base64 string
239    pub async fn to_base64(&self) -> Result<String, FileError> {
240        match &self.data {
241            FileData::Base64(base64_data) => Ok(base64_data.clone()),
242            _ => {
243                let bytes = self.to_bytes().await?;
244                Ok(general_purpose::STANDARD.encode(&bytes))
245            }
246        }
247    }
248
249    /// Calculate and set the file hash
250    pub async fn calculate_hash(&mut self) -> Result<String, FileError> {
251        let bytes = self.to_bytes().await?;
252        let mut hasher = Sha256::new();
253        hasher.update(&bytes);
254        let hash = format!("{:x}", hasher.finalize());
255        self.hash = Some(hash.clone());
256        Ok(hash)
257    }
258
259    /// Verify the file hash
260    pub async fn verify_hash(&self, expected_hash: &str) -> Result<bool, FileError> {
261        let bytes = self.to_bytes().await?;
262        let mut hasher = Sha256::new();
263        hasher.update(&bytes);
264        let actual_hash = format!("{:x}", hasher.finalize());
265        Ok(actual_hash == expected_hash)
266    }
267
268    /// Check if this is an image file
269    pub fn is_image(&self) -> bool {
270        self.mime_type.type_() == mime::IMAGE
271    }
272
273    /// Check if this is a text file
274    pub fn is_text(&self) -> bool {
275        self.mime_type.type_() == mime::TEXT
276    }
277
278    /// Check if this is an application file (e.g., PDF, document)
279    pub fn is_application(&self) -> bool {
280        self.mime_type.type_() == mime::APPLICATION
281    }
282}
283
284/// Utility function to create a File from various sources (like TypeScript SDK's toFile)
285pub async fn to_file(
286    source: FileSource,
287    name: Option<String>,
288    mime_type: Option<Mime>,
289) -> Result<File, FileError> {
290    match source {
291        FileSource::Bytes(bytes) => {
292            let name = name.unwrap_or_else(|| "file".to_string());
293            File::from_bytes(name, bytes, mime_type)
294        },
295        FileSource::Base64(base64_data) => {
296            let name = name.unwrap_or_else(|| "file".to_string());
297            File::from_base64(name, base64_data, mime_type)
298        },
299        FileSource::Path(path) => File::from_path(path),
300        FileSource::StdFile(std_file, file_name) => {
301            let name = name.or(file_name).unwrap_or_else(|| "file".to_string());
302            File::from_std_file(std_file, name, mime_type)
303        },
304    }
305}
306
307/// Different sources for creating files
308pub enum FileSource {
309    /// Raw bytes
310    Bytes(Bytes),
311    /// Base64 encoded string
312    Base64(String),
313    /// File system path
314    Path(std::path::PathBuf),
315    /// Standard library File with optional name
316    StdFile(StdFile, Option<String>),
317}
318
319/// Detect MIME type from filename and optional file data
320fn detect_mime_type(filename: &str, data: Option<&[u8]>) -> Result<Mime, FileError> {
321    // First try to detect from file extension
322    if let Some(extension) = Path::new(filename).extension() {
323        if let Some(ext_str) = extension.to_str() {
324            let mime_type = match ext_str.to_lowercase().as_str() {
325                // Images
326                "jpg" | "jpeg" => mime::IMAGE_JPEG,
327                "png" => mime::IMAGE_PNG,
328                "gif" => mime::IMAGE_GIF,
329                "webp" => "image/webp".parse().unwrap(),
330                "svg" => mime::IMAGE_SVG,
331                "bmp" => "image/bmp".parse().unwrap(),
332                
333                // Documents
334                "pdf" => "application/pdf".parse().unwrap(),
335                "doc" => "application/msword".parse().unwrap(),
336                "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document".parse().unwrap(),
337                "txt" => mime::TEXT_PLAIN,
338                "md" => "text/markdown".parse().unwrap(),
339                "rtf" => "application/rtf".parse().unwrap(),
340                
341                // Spreadsheets
342                "xls" => "application/vnd.ms-excel".parse().unwrap(),
343                "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet".parse().unwrap(),
344                
345                // Presentations
346                "ppt" => "application/vnd.ms-powerpoint".parse().unwrap(),
347                "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation".parse().unwrap(),
348                
349                // Audio
350                "mp3" => "audio/mpeg".parse().unwrap(),
351                "wav" => "audio/wav".parse().unwrap(),
352                "ogg" => "audio/ogg".parse().unwrap(),
353                
354                // Video
355                "mp4" => "video/mp4".parse().unwrap(),
356                "avi" => "video/x-msvideo".parse().unwrap(),
357                "mov" => "video/quicktime".parse().unwrap(),
358                
359                // Archives
360                "zip" => "application/zip".parse().unwrap(),
361                "tar" => "application/x-tar".parse().unwrap(),
362                "gz" => "application/gzip".parse().unwrap(),
363                
364                // JSON/XML
365                "json" => mime::APPLICATION_JSON,
366                "xml" => mime::TEXT_XML,
367                
368                _ => mime::APPLICATION_OCTET_STREAM,
369            };
370            return Ok(mime_type);
371        }
372    }
373
374    // If no extension, try magic bytes detection
375    if let Some(bytes) = data {
376        if bytes.len() >= 4 {
377            let magic = &bytes[0..4];
378            
379            // PNG magic bytes
380            if magic == [0x89, 0x50, 0x4E, 0x47] {
381                return Ok(mime::IMAGE_PNG);
382            }
383            
384            // JPEG magic bytes
385            if magic[0..2] == [0xFF, 0xD8] {
386                return Ok(mime::IMAGE_JPEG);
387            }
388            
389            // PDF magic bytes
390            if magic == [0x25, 0x50, 0x44, 0x46] {
391                return Ok("application/pdf".parse().unwrap());
392            }
393            
394            // GIF magic bytes
395            if magic[0..3] == [0x47, 0x49, 0x46] {
396                return Ok(mime::IMAGE_GIF);
397            }
398        }
399    }
400
401    // Default fallback
402    Ok(mime::APPLICATION_OCTET_STREAM)
403}
404
405/// File upload builder for complex scenarios
406#[derive(Debug)]
407pub struct FileBuilder {
408    name: Option<String>,
409    mime_type: Option<Mime>,
410    constraints: FileConstraints,
411    calculate_hash: bool,
412}
413
414impl FileBuilder {
415    /// Create a new file builder
416    pub fn new() -> Self {
417        Self {
418            name: None,
419            mime_type: None,
420            constraints: FileConstraints::default(),
421            calculate_hash: false,
422        }
423    }
424
425    /// Set the file name
426    pub fn name(mut self, name: impl Into<String>) -> Self {
427        self.name = Some(name.into());
428        self
429    }
430
431    /// Set the MIME type
432    pub fn mime_type(mut self, mime_type: Mime) -> Self {
433        self.mime_type = Some(mime_type);
434        self
435    }
436
437    /// Set file constraints
438    pub fn constraints(mut self, constraints: FileConstraints) -> Self {
439        self.constraints = constraints;
440        self
441    }
442
443    /// Enable hash calculation
444    pub fn with_hash(mut self) -> Self {
445        self.calculate_hash = true;
446        self
447    }
448
449    /// Build file from source
450    pub async fn build(self, source: FileSource) -> Result<File, FileError> {
451        let mut file = to_file(source, self.name, self.mime_type).await?;
452        
453        // Validate constraints
454        file.validate(&self.constraints)?;
455        
456        // Calculate hash if requested
457        if self.calculate_hash {
458            file.calculate_hash().await?;
459        }
460        
461        Ok(file)
462    }
463}
464
465impl Default for FileBuilder {
466    fn default() -> Self {
467        Self::new()
468    }
469}
470
471impl fmt::Display for File {
472    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473        write!(
474            f,
475            "File {{ name: {}, type: {}, size: {} bytes }}",
476            self.name, self.mime_type, self.size
477        )
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    #[test] 
486    fn test_file_from_bytes() {
487        let data = b"Hello, world!";
488        let file = File::from_bytes("test.txt", Bytes::from_static(data), None).unwrap();
489        
490        assert_eq!(file.name, "test.txt");
491        assert_eq!(file.size, 13);
492        assert_eq!(file.mime_type, mime::TEXT_PLAIN);
493    }
494
495    #[test]
496    fn test_mime_detection() {
497        assert_eq!(detect_mime_type("test.jpg", None).unwrap(), mime::IMAGE_JPEG);
498        assert_eq!(detect_mime_type("test.png", None).unwrap(), mime::IMAGE_PNG);
499        assert_eq!(detect_mime_type("test.txt", None).unwrap(), mime::TEXT_PLAIN);
500        assert_eq!(detect_mime_type("test.json", None).unwrap(), mime::APPLICATION_JSON);
501    }
502
503    #[test]
504    fn test_file_validation() {
505        let data = b"Hello, world!";
506        let file = File::from_bytes("test.txt", Bytes::from_static(data), None).unwrap();
507        
508        let constraints = FileConstraints {
509            max_size: 10,
510            allowed_types: None,
511            require_hash: false,
512        };
513        
514        // Should fail size validation
515        assert!(file.validate(&constraints).is_err());
516    }
517
518    #[test]
519    fn test_file_type_checks() {
520        let image_file = File::from_bytes("test.jpg", Bytes::new(), Some(mime::IMAGE_JPEG)).unwrap();
521        let text_file = File::from_bytes("test.txt", Bytes::new(), Some(mime::TEXT_PLAIN)).unwrap();
522        
523        assert!(image_file.is_image());
524        assert!(!image_file.is_text());
525        
526        assert!(text_file.is_text());
527        assert!(!text_file.is_image());
528    }
529}
530