claude-agent 0.2.25

Rust SDK for building AI agents with Anthropic's Claude - Direct API, no CLI dependency
Documentation
//! Files API client for managing uploaded files.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use url::form_urlencoded;

use super::messages::ErrorResponse;
use crate::{Error, Result};

const FILES_API_BETA: &str = "files-api-2025-04-14";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct File {
    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,
    #[serde(default)]
    pub downloadable: bool,
}

#[derive(Debug, Clone)]
pub struct UploadFileRequest {
    pub data: FileData,
    pub filename: Option<String>,
}

impl UploadFileRequest {
    pub fn from_bytes(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
        Self {
            data: FileData::Bytes {
                data,
                mime_type: mime_type.into(),
            },
            filename: None,
        }
    }

    pub fn from_path(path: impl Into<PathBuf>) -> Self {
        Self {
            data: FileData::Path(path.into()),
            filename: None,
        }
    }

    pub fn filename(mut self, filename: impl Into<String>) -> Self {
        self.filename = Some(filename.into());
        self
    }
}

#[derive(Debug, Clone)]
pub enum FileData {
    Bytes { data: Vec<u8>, mime_type: String },
    Path(PathBuf),
}

#[derive(Debug, Clone, Deserialize)]
pub struct FileListResponse {
    pub data: Vec<File>,
    pub has_more: bool,
    pub first_id: Option<String>,
    pub last_id: Option<String>,
}

pub struct FileDownload {
    response: reqwest::Response,
    pub content_type: String,
    pub content_length: Option<u64>,
}

impl FileDownload {
    pub fn into_response(self) -> reqwest::Response {
        self.response
    }

    pub fn bytes_stream(
        self,
    ) -> impl futures::Stream<Item = std::result::Result<bytes::Bytes, reqwest::Error>> {
        self.response.bytes_stream()
    }

    pub async fn bytes(self) -> Result<bytes::Bytes> {
        self.response.bytes().await.map_err(Error::Network)
    }
}

pub struct FilesClient<'a> {
    client: &'a super::Client,
}

impl<'a> FilesClient<'a> {
    pub fn new(client: &'a super::Client) -> Self {
        Self { client }
    }

    fn base_url(&self) -> &str {
        self.client.adapter().base_url()
    }

    fn api_version(&self) -> &str {
        &self.client.config().api_version
    }

    fn build_url(&self, path: &str) -> String {
        format!("{}/v1/files{}", self.base_url(), path)
    }

    async fn build_request(&self, method: reqwest::Method, url: &str) -> reqwest::RequestBuilder {
        if let Err(e) = self.client.adapter().ensure_fresh_credentials().await {
            tracing::debug!("Proactive credential refresh failed: {}", e);
        }

        let req = self.client.http().request(method, url);
        self.client
            .adapter()
            .apply_auth_headers(req)
            .await
            .header("anthropic-version", self.api_version())
            .header("anthropic-beta", FILES_API_BETA)
    }

    pub async fn upload(&self, request: UploadFileRequest) -> Result<File> {
        let url = self.build_url("");

        let (data, mime_type, filename) = match request.data {
            FileData::Bytes { data, mime_type } => {
                let filename = request.filename.unwrap_or_else(|| "file".to_string());
                (data, mime_type, filename)
            }
            FileData::Path(path) => {
                let filename = request
                    .filename
                    .or_else(|| path.file_name().and_then(|n| n.to_str()).map(String::from))
                    .unwrap_or_else(|| "file".to_string());

                let data = tokio::fs::read(&path).await.map_err(Error::Io)?;

                let mime_type = mime_guess::from_path(&path)
                    .first_or_octet_stream()
                    .to_string();

                (data, mime_type, filename)
            }
        };

        let part = reqwest::multipart::Part::bytes(data)
            .file_name(filename)
            .mime_str(&mime_type)
            .map_err(|e| Error::Config(e.to_string()))?;

        let form = reqwest::multipart::Form::new().part("file", part);

        let response = self
            .build_request(reqwest::Method::POST, &url)
            .await
            .multipart(form)
            .send()
            .await
            .map_err(Error::Network)?;

        self.handle_response(response).await
    }

    pub async fn get(&self, file_id: &str) -> Result<File> {
        let url = self.build_url(&format!("/{}", file_id));
        let response = self
            .build_request(reqwest::Method::GET, &url)
            .await
            .send()
            .await
            .map_err(Error::Network)?;
        self.handle_response(response).await
    }

    pub async fn download(&self, file_id: &str) -> Result<FileDownload> {
        let url = self.build_url(&format!("/{}/content", file_id));
        let response = self
            .build_request(reqwest::Method::GET, &url)
            .await
            .send()
            .await
            .map_err(Error::Network)?;

        if !response.status().is_success() {
            let status = response.status().as_u16();
            let error: ErrorResponse = response.json().await.map_err(Error::Network)?;
            return Err(error.into_error(status));
        }

        let content_type = response
            .headers()
            .get(reqwest::header::CONTENT_TYPE)
            .and_then(|v| v.to_str().ok())
            .unwrap_or("application/octet-stream")
            .to_string();

        let content_length = response
            .headers()
            .get(reqwest::header::CONTENT_LENGTH)
            .and_then(|v| v.to_str().ok())
            .and_then(|v| v.parse().ok());

        Ok(FileDownload {
            response,
            content_type,
            content_length,
        })
    }

    pub async fn download_bytes(&self, file_id: &str) -> Result<Vec<u8>> {
        let download = self.download(file_id).await?;
        let bytes = download.bytes().await?;
        Ok(bytes.to_vec())
    }

    pub async fn delete(&self, file_id: &str) -> Result<()> {
        let url = self.build_url(&format!("/{}", file_id));
        let response = self
            .build_request(reqwest::Method::DELETE, &url)
            .await
            .send()
            .await
            .map_err(Error::Network)?;
        self.handle_response::<serde_json::Value>(response).await?;
        Ok(())
    }

    pub async fn list(
        &self,
        limit: Option<u32>,
        after_id: Option<&str>,
    ) -> Result<FileListResponse> {
        let mut url = self.build_url("");

        let mut query_params: Vec<(&str, String)> = Vec::new();
        if let Some(limit) = limit {
            query_params.push(("limit", limit.to_string()));
        }
        if let Some(after_id) = after_id {
            query_params.push(("after_id", after_id.to_string()));
        }
        if !query_params.is_empty() {
            let encoded: String = form_urlencoded::Serializer::new(String::new())
                .extend_pairs(query_params.iter().map(|(k, v)| (*k, v.as_str())))
                .finish();
            url = format!("{}?{}", url, encoded);
        }

        let response = self
            .build_request(reqwest::Method::GET, &url)
            .await
            .send()
            .await
            .map_err(Error::Network)?;
        self.handle_response(response).await
    }

    pub async fn list_all(&self) -> Result<Vec<File>> {
        let mut all_files = Vec::new();
        let mut after_id: Option<String> = None;

        loop {
            let response = self.list(Some(100), after_id.as_deref()).await?;
            all_files.extend(response.data);

            if !response.has_more {
                break;
            }
            after_id = response.last_id;
        }

        Ok(all_files)
    }

    async fn handle_response<T: serde::de::DeserializeOwned>(
        &self,
        response: reqwest::Response,
    ) -> Result<T> {
        if !response.status().is_success() {
            let status = response.status().as_u16();
            let error: ErrorResponse = response.json().await.map_err(Error::Network)?;
            return Err(error.into_error(status));
        }

        response.json().await.map_err(Error::Network)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_upload_request_from_bytes() {
        let request = UploadFileRequest::from_bytes(vec![1, 2, 3], "image/png");
        assert!(request.filename.is_none());
    }

    #[test]
    fn test_upload_request_with_filename() {
        let request =
            UploadFileRequest::from_bytes(vec![1, 2, 3], "image/png").filename("test.png");
        assert_eq!(request.filename, Some("test.png".to_string()));
    }

    #[test]
    fn test_file_deserialization() {
        let json = r#"{
            "id": "file_abc123",
            "type": "file",
            "filename": "test.pdf",
            "mime_type": "application/pdf",
            "size_bytes": 1024,
            "created_at": "2025-01-01T00:00:00Z",
            "downloadable": false
        }"#;
        let file: File = serde_json::from_str(json).unwrap();
        assert_eq!(file.id, "file_abc123");
        assert_eq!(file.filename, "test.pdf");
    }

    #[test]
    fn test_file_list_response_deserialization() {
        let json = r#"{
            "data": [],
            "has_more": false,
            "first_id": null,
            "last_id": null
        }"#;
        let response: FileListResponse = serde_json::from_str(json).unwrap();
        assert!(!response.has_more);
        assert!(response.data.is_empty());
    }
}