1use 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}