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        let req = self.client.http().request(method, url);
115        self.client
116            .adapter()
117            .apply_auth_headers(req)
118            .await
119            .header("anthropic-version", self.api_version())
120            .header("anthropic-beta", FILES_API_BETA)
121    }
122
123    async fn send_with_retry(&self, req: reqwest::RequestBuilder) -> Result<reqwest::Response> {
124        let response = req.send().await.map_err(Error::Network)?;
125
126        if response.status().as_u16() == 401 {
127            self.client.refresh_credentials().await?;
128        }
129
130        Ok(response)
131    }
132
133    pub async fn upload(&self, request: UploadFileRequest) -> Result<File> {
134        let url = self.build_url("");
135
136        let (data, mime_type, filename) = match request.data {
137            FileData::Bytes { data, mime_type } => {
138                let filename = request.filename.unwrap_or_else(|| "file".to_string());
139                (data, mime_type, filename)
140            }
141            FileData::Path(path) => {
142                let filename = request
143                    .filename
144                    .or_else(|| path.file_name().and_then(|n| n.to_str()).map(String::from))
145                    .unwrap_or_else(|| "file".to_string());
146
147                let data = tokio::fs::read(&path).await.map_err(Error::Io)?;
148
149                let mime_type = mime_guess::from_path(&path)
150                    .first_or_octet_stream()
151                    .to_string();
152
153                (data, mime_type, filename)
154            }
155        };
156
157        let part = reqwest::multipart::Part::bytes(data)
158            .file_name(filename)
159            .mime_str(&mime_type)
160            .map_err(|e| Error::Config(e.to_string()))?;
161
162        let form = reqwest::multipart::Form::new().part("file", part);
163
164        let req = self
165            .build_request(reqwest::Method::POST, &url)
166            .await
167            .multipart(form);
168
169        let response = self.send_with_retry(req).await?;
170        self.handle_response(response).await
171    }
172
173    pub async fn get(&self, file_id: &str) -> Result<File> {
174        let url = self.build_url(&format!("/{}", file_id));
175        let req = self.build_request(reqwest::Method::GET, &url).await;
176        let response = self.send_with_retry(req).await?;
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 req = self.build_request(reqwest::Method::GET, &url).await;
183        let response = self.send_with_retry(req).await?;
184
185        if !response.status().is_success() {
186            let status = response.status().as_u16();
187            let error: ErrorResponse = response.json().await.map_err(Error::Network)?;
188            return Err(error.into_error(status));
189        }
190
191        let content_type = response
192            .headers()
193            .get(reqwest::header::CONTENT_TYPE)
194            .and_then(|v| v.to_str().ok())
195            .unwrap_or("application/octet-stream")
196            .to_string();
197
198        let content_length = response
199            .headers()
200            .get(reqwest::header::CONTENT_LENGTH)
201            .and_then(|v| v.to_str().ok())
202            .and_then(|v| v.parse().ok());
203
204        Ok(FileDownload {
205            response,
206            content_type,
207            content_length,
208        })
209    }
210
211    pub async fn download_bytes(&self, file_id: &str) -> Result<Vec<u8>> {
212        let download = self.download(file_id).await?;
213        let bytes = download.bytes().await?;
214        Ok(bytes.to_vec())
215    }
216
217    pub async fn delete(&self, file_id: &str) -> Result<()> {
218        let url = self.build_url(&format!("/{}", file_id));
219        let req = self.build_request(reqwest::Method::DELETE, &url).await;
220        let response = self.send_with_retry(req).await?;
221
222        if !response.status().is_success() {
223            let status = response.status().as_u16();
224            let error: ErrorResponse = response.json().await.map_err(Error::Network)?;
225            return Err(error.into_error(status));
226        }
227
228        Ok(())
229    }
230
231    pub async fn list(
232        &self,
233        limit: Option<u32>,
234        after_id: Option<&str>,
235    ) -> Result<FileListResponse> {
236        let mut url = self.build_url("");
237
238        let mut query_params: Vec<(&str, String)> = Vec::new();
239        if let Some(limit) = limit {
240            query_params.push(("limit", limit.to_string()));
241        }
242        if let Some(after_id) = after_id {
243            query_params.push(("after_id", after_id.to_string()));
244        }
245        if !query_params.is_empty() {
246            let encoded: String = form_urlencoded::Serializer::new(String::new())
247                .extend_pairs(query_params.iter().map(|(k, v)| (*k, v.as_str())))
248                .finish();
249            url = format!("{}?{}", url, encoded);
250        }
251
252        let req = self.build_request(reqwest::Method::GET, &url).await;
253        let response = self.send_with_retry(req).await?;
254        self.handle_response(response).await
255    }
256
257    pub async fn list_all(&self) -> Result<Vec<File>> {
258        let mut all_files = Vec::new();
259        let mut after_id: Option<String> = None;
260
261        loop {
262            let response = self.list(Some(100), after_id.as_deref()).await?;
263            all_files.extend(response.data);
264
265            if !response.has_more {
266                break;
267            }
268            after_id = response.last_id;
269        }
270
271        Ok(all_files)
272    }
273
274    async fn handle_response<T: serde::de::DeserializeOwned>(
275        &self,
276        response: reqwest::Response,
277    ) -> Result<T> {
278        if !response.status().is_success() {
279            let status = response.status().as_u16();
280            let error: ErrorResponse = response.json().await.map_err(Error::Network)?;
281            return Err(error.into_error(status));
282        }
283
284        response.json().await.map_err(Error::Network)
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_upload_request_from_bytes() {
294        let request = UploadFileRequest::from_bytes(vec![1, 2, 3], "image/png");
295        assert!(request.filename.is_none());
296    }
297
298    #[test]
299    fn test_upload_request_with_filename() {
300        let request =
301            UploadFileRequest::from_bytes(vec![1, 2, 3], "image/png").with_filename("test.png");
302        assert_eq!(request.filename, Some("test.png".to_string()));
303    }
304
305    #[test]
306    fn test_file_deserialization() {
307        let json = r#"{
308            "id": "file_abc123",
309            "type": "file",
310            "filename": "test.pdf",
311            "mime_type": "application/pdf",
312            "size_bytes": 1024,
313            "created_at": "2025-01-01T00:00:00Z",
314            "downloadable": false
315        }"#;
316        let file: File = serde_json::from_str(json).unwrap();
317        assert_eq!(file.id, "file_abc123");
318        assert_eq!(file.filename, "test.pdf");
319    }
320
321    #[test]
322    fn test_file_list_response_deserialization() {
323        let json = r#"{
324            "data": [],
325            "has_more": false,
326            "first_id": null,
327            "last_id": null
328        }"#;
329        let response: FileListResponse = serde_json::from_str(json).unwrap();
330        assert!(!response.has_more);
331        assert!(response.data.is_empty());
332    }
333}