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