use crate::error::{Error, Result};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tracing::debug;
const FILES_API_URL: &str = "https://api.anthropic.com/v1/files";
const FILES_BETA_HEADER: &str = "files-api-2025-04-14";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetadata {
pub id: String,
#[serde(rename = "type")]
pub file_type: String,
pub filename: String,
pub mime_type: String,
pub size_bytes: u64,
pub created_at: String,
pub downloadable: bool,
}
pub struct FilesClient {
http: Client,
api_key: String,
api_version: String,
}
impl FilesClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
http: Client::new(),
api_key: api_key.into(),
api_version: "2023-06-01".to_string(),
}
}
pub async fn upload(&self, path: impl AsRef<Path>) -> Result<FileMetadata> {
let path = path.as_ref();
debug!("Uploading file: {:?}", path);
let file_bytes = tokio::fs::read(path)
.await
.map_err(|e| Error::InvalidRequest(format!("Failed to read file {:?}: {}", path, e)))?;
let filename = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| Error::InvalidRequest("Invalid filename".into()))?;
let form = reqwest::multipart::Form::new().part(
"file",
reqwest::multipart::Part::bytes(file_bytes).file_name(filename.to_string()),
);
let response = self
.http
.post(FILES_API_URL)
.header("x-api-key", &self.api_key)
.header("anthropic-version", &self.api_version)
.header("anthropic-beta", FILES_BETA_HEADER)
.multipart(form)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(Error::Api {
status: status.as_u16(),
message: error_text,
error_type: None,
});
}
let metadata: FileMetadata = response.json().await?;
Ok(metadata)
}
pub async fn list(&self) -> Result<Vec<FileMetadata>> {
debug!("Listing files");
let response = self
.http
.get(FILES_API_URL)
.header("x-api-key", &self.api_key)
.header("anthropic-version", &self.api_version)
.header("anthropic-beta", FILES_BETA_HEADER)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(Error::Api {
status: status.as_u16(),
message: error_text,
error_type: None,
});
}
#[derive(Deserialize)]
struct ListResponse {
data: Vec<FileMetadata>,
}
let list_response: ListResponse = response.json().await?;
Ok(list_response.data)
}
pub async fn get_metadata(&self, file_id: &str) -> Result<FileMetadata> {
debug!("Getting metadata for file: {}", file_id);
let url = format!("{}/{}", FILES_API_URL, file_id);
let response = self
.http
.get(&url)
.header("x-api-key", &self.api_key)
.header("anthropic-version", &self.api_version)
.header("anthropic-beta", FILES_BETA_HEADER)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(Error::Api {
status: status.as_u16(),
message: error_text,
error_type: None,
});
}
let metadata: FileMetadata = response.json().await?;
Ok(metadata)
}
pub async fn delete(&self, file_id: &str) -> Result<()> {
debug!("Deleting file: {}", file_id);
let url = format!("{}/{}", FILES_API_URL, file_id);
let response = self
.http
.delete(&url)
.header("x-api-key", &self.api_key)
.header("anthropic-version", &self.api_version)
.header("anthropic-beta", FILES_BETA_HEADER)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(Error::Api {
status: status.as_u16(),
message: error_text,
error_type: None,
});
}
Ok(())
}
pub async fn download(&self, file_id: &str) -> Result<Vec<u8>> {
debug!("Downloading file: {}", file_id);
let url = format!("{}/{}/content", FILES_API_URL, file_id);
let response = self
.http
.get(&url)
.header("x-api-key", &self.api_key)
.header("anthropic-version", &self.api_version)
.header("anthropic-beta", FILES_BETA_HEADER)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(Error::Api {
status: status.as_u16(),
message: error_text,
error_type: None,
});
}
let bytes = response.bytes().await?;
Ok(bytes.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_files_client_creation() {
let client = FilesClient::new("test-key");
assert_eq!(client.api_key, "test-key");
}
#[tokio::test]
#[ignore]
async fn test_upload_file() {
let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY required");
let client = FilesClient::new(api_key);
let test_content = b"Hello, this is a test file!";
tokio::fs::write("test_upload.txt", test_content)
.await
.unwrap();
let result = client.upload("test_upload.txt").await;
let _ = tokio::fs::remove_file("test_upload.txt").await;
match result {
Ok(metadata) => {
println!("Uploaded: {}", metadata.id);
let _ = client.delete(&metadata.id).await;
}
Err(e) => println!("Upload test skipped (expected without real API): {}", e),
}
}
}