claude_agent/client/
files.rs

1//! Files API client for managing uploaded files.
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use url::form_urlencoded;
6
7use super::messages::ErrorResponse;
8use crate::{Error, Result};
9
10const FILES_BASE_URL: &str = "https://api.anthropic.com";
11const FILES_API_BETA: &str = "files-api-2025-04-14";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct File {
15    pub id: String,
16    #[serde(rename = "type")]
17    pub file_type: String,
18    pub filename: String,
19    pub mime_type: String,
20    pub size_bytes: u64,
21    pub created_at: String,
22    #[serde(default)]
23    pub downloadable: bool,
24}
25
26#[derive(Debug, Clone)]
27pub struct UploadFileRequest {
28    pub data: FileData,
29    pub filename: Option<String>,
30}
31
32impl UploadFileRequest {
33    pub fn from_bytes(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
34        Self {
35            data: FileData::Bytes {
36                data,
37                mime_type: mime_type.into(),
38            },
39            filename: None,
40        }
41    }
42
43    pub fn from_path(path: impl Into<PathBuf>) -> Self {
44        Self {
45            data: FileData::Path(path.into()),
46            filename: None,
47        }
48    }
49
50    pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
51        self.filename = Some(filename.into());
52        self
53    }
54}
55
56#[derive(Debug, Clone)]
57pub enum FileData {
58    Bytes { data: Vec<u8>, mime_type: String },
59    Path(PathBuf),
60}
61
62#[derive(Debug, Clone, Deserialize)]
63pub struct FileListResponse {
64    pub data: Vec<File>,
65    pub has_more: bool,
66    pub first_id: Option<String>,
67    pub last_id: Option<String>,
68}
69
70pub struct FileDownload {
71    response: reqwest::Response,
72    pub content_type: String,
73    pub content_length: Option<u64>,
74}
75
76impl FileDownload {
77    pub fn into_response(self) -> reqwest::Response {
78        self.response
79    }
80
81    pub fn bytes_stream(
82        self,
83    ) -> impl futures::Stream<Item = std::result::Result<bytes::Bytes, reqwest::Error>> {
84        self.response.bytes_stream()
85    }
86
87    pub async fn bytes(self) -> Result<bytes::Bytes> {
88        self.response.bytes().await.map_err(Error::Network)
89    }
90}
91
92pub struct FilesClient<'a> {
93    client: &'a super::Client,
94}
95
96impl<'a> FilesClient<'a> {
97    pub fn new(client: &'a super::Client) -> Self {
98        Self { client }
99    }
100
101    fn base_url(&self) -> String {
102        std::env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|_| FILES_BASE_URL.into())
103    }
104
105    fn api_version(&self) -> &str {
106        &self.client.config().api_version
107    }
108
109    fn build_url(&self, path: &str) -> String {
110        format!("{}/v1/files{}", self.base_url(), path)
111    }
112
113    async fn build_request(&self, method: reqwest::Method, url: &str) -> reqwest::RequestBuilder {
114        if let Err(e) = self.client.adapter().ensure_fresh_credentials().await {
115            tracing::debug!("Proactive credential refresh failed: {}", e);
116        }
117
118        let req = self.client.http().request(method, url);
119        self.client
120            .adapter()
121            .apply_auth_headers(req)
122            .await
123            .header("anthropic-version", self.api_version())
124            .header("anthropic-beta", FILES_API_BETA)
125    }
126
127    pub async fn upload(&self, request: UploadFileRequest) -> Result<File> {
128        let url = self.build_url("");
129
130        let (data, mime_type, filename) = match request.data {
131            FileData::Bytes { data, mime_type } => {
132                let filename = request.filename.unwrap_or_else(|| "file".to_string());
133                (data, mime_type, filename)
134            }
135            FileData::Path(path) => {
136                let filename = request
137                    .filename
138                    .or_else(|| path.file_name().and_then(|n| n.to_str()).map(String::from))
139                    .unwrap_or_else(|| "file".to_string());
140
141                let data = tokio::fs::read(&path).await.map_err(Error::Io)?;
142
143                let mime_type = mime_guess::from_path(&path)
144                    .first_or_octet_stream()
145                    .to_string();
146
147                (data, mime_type, filename)
148            }
149        };
150
151        let part = reqwest::multipart::Part::bytes(data)
152            .file_name(filename)
153            .mime_str(&mime_type)
154            .map_err(|e| Error::Config(e.to_string()))?;
155
156        let form = reqwest::multipart::Form::new().part("file", part);
157
158        let response = self
159            .build_request(reqwest::Method::POST, &url)
160            .await
161            .multipart(form)
162            .send()
163            .await
164            .map_err(Error::Network)?;
165
166        self.handle_response(response).await
167    }
168
169    pub async fn get(&self, file_id: &str) -> Result<File> {
170        let url = self.build_url(&format!("/{}", file_id));
171        let response = self
172            .build_request(reqwest::Method::GET, &url)
173            .await
174            .send()
175            .await
176            .map_err(Error::Network)?;
177        self.handle_response(response).await
178    }
179
180    pub async fn download(&self, file_id: &str) -> Result<FileDownload> {
181        let url = self.build_url(&format!("/{}/content", file_id));
182        let response = self
183            .build_request(reqwest::Method::GET, &url)
184            .await
185            .send()
186            .await
187            .map_err(Error::Network)?;
188
189        if !response.status().is_success() {
190            let status = response.status().as_u16();
191            let error: ErrorResponse = response.json().await.map_err(Error::Network)?;
192            return Err(error.into_error(status));
193        }
194
195        let content_type = response
196            .headers()
197            .get(reqwest::header::CONTENT_TYPE)
198            .and_then(|v| v.to_str().ok())
199            .unwrap_or("application/octet-stream")
200            .to_string();
201
202        let content_length = response
203            .headers()
204            .get(reqwest::header::CONTENT_LENGTH)
205            .and_then(|v| v.to_str().ok())
206            .and_then(|v| v.parse().ok());
207
208        Ok(FileDownload {
209            response,
210            content_type,
211            content_length,
212        })
213    }
214
215    pub async fn download_bytes(&self, file_id: &str) -> Result<Vec<u8>> {
216        let download = self.download(file_id).await?;
217        let bytes = download.bytes().await?;
218        Ok(bytes.to_vec())
219    }
220
221    pub async fn delete(&self, file_id: &str) -> Result<()> {
222        let url = self.build_url(&format!("/{}", file_id));
223        let response = self
224            .build_request(reqwest::Method::DELETE, &url)
225            .await
226            .send()
227            .await
228            .map_err(Error::Network)?;
229
230        if !response.status().is_success() {
231            let status = response.status().as_u16();
232            let error: ErrorResponse = response.json().await.map_err(Error::Network)?;
233            return Err(error.into_error(status));
234        }
235
236        Ok(())
237    }
238
239    pub async fn list(
240        &self,
241        limit: Option<u32>,
242        after_id: Option<&str>,
243    ) -> Result<FileListResponse> {
244        let mut url = self.build_url("");
245
246        let mut query_params: Vec<(&str, String)> = Vec::new();
247        if let Some(limit) = limit {
248            query_params.push(("limit", limit.to_string()));
249        }
250        if let Some(after_id) = after_id {
251            query_params.push(("after_id", after_id.to_string()));
252        }
253        if !query_params.is_empty() {
254            let encoded: String = form_urlencoded::Serializer::new(String::new())
255                .extend_pairs(query_params.iter().map(|(k, v)| (*k, v.as_str())))
256                .finish();
257            url = format!("{}?{}", url, encoded);
258        }
259
260        let response = self
261            .build_request(reqwest::Method::GET, &url)
262            .await
263            .send()
264            .await
265            .map_err(Error::Network)?;
266        self.handle_response(response).await
267    }
268
269    pub async fn list_all(&self) -> Result<Vec<File>> {
270        let mut all_files = Vec::new();
271        let mut after_id: Option<String> = None;
272
273        loop {
274            let response = self.list(Some(100), after_id.as_deref()).await?;
275            all_files.extend(response.data);
276
277            if !response.has_more {
278                break;
279            }
280            after_id = response.last_id;
281        }
282
283        Ok(all_files)
284    }
285
286    async fn handle_response<T: serde::de::DeserializeOwned>(
287        &self,
288        response: reqwest::Response,
289    ) -> Result<T> {
290        if !response.status().is_success() {
291            let status = response.status().as_u16();
292            let error: ErrorResponse = response.json().await.map_err(Error::Network)?;
293            return Err(error.into_error(status));
294        }
295
296        response.json().await.map_err(Error::Network)
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_upload_request_from_bytes() {
306        let request = UploadFileRequest::from_bytes(vec![1, 2, 3], "image/png");
307        assert!(request.filename.is_none());
308    }
309
310    #[test]
311    fn test_upload_request_with_filename() {
312        let request =
313            UploadFileRequest::from_bytes(vec![1, 2, 3], "image/png").with_filename("test.png");
314        assert_eq!(request.filename, Some("test.png".to_string()));
315    }
316
317    #[test]
318    fn test_file_deserialization() {
319        let json = r#"{
320            "id": "file_abc123",
321            "type": "file",
322            "filename": "test.pdf",
323            "mime_type": "application/pdf",
324            "size_bytes": 1024,
325            "created_at": "2025-01-01T00:00:00Z",
326            "downloadable": false
327        }"#;
328        let file: File = serde_json::from_str(json).unwrap();
329        assert_eq!(file.id, "file_abc123");
330        assert_eq!(file.filename, "test.pdf");
331    }
332
333    #[test]
334    fn test_file_list_response_deserialization() {
335        let json = r#"{
336            "data": [],
337            "has_more": false,
338            "first_id": null,
339            "last_id": null
340        }"#;
341        let response: FileListResponse = serde_json::from_str(json).unwrap();
342        assert!(!response.has_more);
343        assert!(response.data.is_empty());
344    }
345}