gemini_genai_rs/files/
mod.rs

1//! Files API — upload, download, list, delete files.
2//!
3//! Feature-gated behind `files`.
4
5use serde::{Deserialize, Serialize};
6
7use crate::client::Client;
8use crate::client::http::HttpError;
9use crate::transport::auth::ServiceEndpoint;
10
11/// State of a file in the Files API.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
14pub enum FileState {
15    /// Upload received, still being processed.
16    Processing,
17    /// Ready for use in requests.
18    Active,
19    /// Terminated with an error.
20    Failed,
21}
22
23/// Source type for file registration (Vertex AI).
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct FileSource {
27    /// GCS URI of the file (e.g., `gs://bucket/path`).
28    pub file_uri: String,
29    /// MIME type of the file.
30    pub mime_type: String,
31}
32
33/// A file resource.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct File {
37    /// Resource name (e.g., `files/abc123`).
38    #[serde(default)]
39    pub name: String,
40    /// Display name.
41    #[serde(default)]
42    pub display_name: String,
43    /// MIME type.
44    #[serde(default)]
45    pub mime_type: String,
46    /// Size in bytes.
47    #[serde(default)]
48    pub size_bytes: Option<u64>,
49    /// State of the file.
50    #[serde(default)]
51    pub state: Option<FileState>,
52    /// URI for downloading the file.
53    #[serde(default)]
54    pub uri: Option<String>,
55    /// SHA256 hash of the file.
56    #[serde(default)]
57    pub sha256_hash: Option<String>,
58    /// Error details if state is Failed.
59    #[serde(default)]
60    pub error: Option<serde_json::Value>,
61}
62
63/// Configuration for file upload.
64#[derive(Debug, Clone)]
65pub struct UploadFileConfig {
66    /// Display name for the uploaded file.
67    pub display_name: Option<String>,
68    /// MIME type of the file.
69    pub mime_type: String,
70}
71
72/// Response from listFiles.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct ListFilesResponse {
76    /// List of files.
77    #[serde(default)]
78    pub files: Vec<File>,
79    /// Pagination token for the next page.
80    #[serde(default)]
81    pub next_page_token: Option<String>,
82}
83
84/// Errors from the Files API.
85#[derive(Debug, thiserror::Error)]
86pub enum FilesError {
87    #[error(transparent)]
88    /// Transport-level HTTP failure.
89    Http(#[from] HttpError),
90    #[error("Failed to parse response: {0}")]
91    /// Response body failed to parse.
92    Parse(#[from] serde_json::Error),
93    #[error("Auth error: {0}")]
94    /// Authentication/authorization failure.
95    Auth(#[from] crate::session::AuthError),
96    #[error("IO error: {0}")]
97    /// Local file I/O failure.
98    Io(#[from] std::io::Error),
99    #[error("Base64 decode error: {0}")]
100    /// Downloaded bytes were not valid base64.
101    Decode(String),
102}
103
104impl Client {
105    /// List files.
106    pub async fn list_files(&self) -> Result<ListFilesResponse, FilesError> {
107        let url = self.rest_url(ServiceEndpoint::Files);
108        let headers = self.auth_headers().await?;
109        let json = self.http_client().get_json(&url, headers).await?;
110        // Handle empty response (no files)
111        if json.is_null() {
112            return Ok(ListFilesResponse {
113                files: vec![],
114                next_page_token: None,
115            });
116        }
117        Ok(serde_json::from_value(json)?)
118    }
119
120    /// Get a file by name.
121    pub async fn get_file(&self, name: &str) -> Result<File, FilesError> {
122        let base_url = self.rest_url(ServiceEndpoint::Files);
123        let url = format!("{base_url}/{name}");
124        let headers = self.auth_headers().await?;
125        let json = self.http_client().get_json(&url, headers).await?;
126        Ok(serde_json::from_value(json)?)
127    }
128
129    /// Delete a file by name.
130    pub async fn delete_file(&self, name: &str) -> Result<(), FilesError> {
131        let base_url = self.rest_url(ServiceEndpoint::Files);
132        let url = format!("{base_url}/{name}");
133        let headers = self.auth_headers().await?;
134        self.http_client().delete(&url, headers).await?;
135        Ok(())
136    }
137
138    /// Upload a file from a byte buffer.
139    pub async fn upload_file(
140        &self,
141        data: Vec<u8>,
142        config: UploadFileConfig,
143    ) -> Result<File, FilesError> {
144        let url = self.rest_url(ServiceEndpoint::Files);
145        let headers = self.auth_headers().await?;
146
147        let mut body = serde_json::json!({
148            "file": {
149                "mimeType": config.mime_type,
150            }
151        });
152        if let Some(name) = config.display_name {
153            body["file"]["displayName"] = serde_json::Value::String(name);
154        }
155
156        // For upload, we POST metadata + inline data
157        body["file"]["inlineData"] = serde_json::json!({
158            "mimeType": config.mime_type,
159            "data": base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &data),
160        });
161
162        let json = self.http_client().post_json(&url, headers, &body).await?;
163        Ok(serde_json::from_value(json)?)
164    }
165
166    /// Download a file's content by name.
167    pub async fn download_file(&self, name: &str) -> Result<Vec<u8>, FilesError> {
168        let base_url = self.rest_url(ServiceEndpoint::Files);
169        let url = format!("{base_url}/{name}:download");
170        let headers = self.auth_headers().await?;
171        let json = self.http_client().get_json(&url, headers).await?;
172
173        // The response contains base64-encoded data
174        if let Some(data) = json.get("data").and_then(|v| v.as_str()) {
175            let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data)
176                .map_err(|e| FilesError::Decode(e.to_string()))?;
177            Ok(bytes)
178        } else {
179            // Return raw JSON as bytes if no data field
180            Ok(json.to_string().into_bytes())
181        }
182    }
183
184    /// Register external files by URI (Vertex AI only).
185    pub async fn register_files(&self, sources: Vec<FileSource>) -> Result<Vec<File>, FilesError> {
186        let url = self.rest_url(ServiceEndpoint::Files);
187        let headers = self.auth_headers().await?;
188
189        let mut files = Vec::new();
190        for source in sources {
191            let body = serde_json::json!({
192                "file": {
193                    "uri": source.file_uri,
194                    "mimeType": source.mime_type,
195                }
196            });
197            let json = self
198                .http_client()
199                .post_json(&url, headers.clone(), &body)
200                .await?;
201            files.push(serde_json::from_value(json)?);
202        }
203        Ok(files)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn parse_file() {
213        let json = serde_json::json!({
214            "name": "files/abc123",
215            "displayName": "test.txt",
216            "mimeType": "text/plain",
217            "sizeBytes": 1024,
218            "state": "ACTIVE",
219            "uri": "https://example.com/file"
220        });
221        let file: File = serde_json::from_value(json).unwrap();
222        assert_eq!(file.name, "files/abc123");
223        assert_eq!(file.display_name, "test.txt");
224        assert_eq!(file.mime_type, "text/plain");
225        assert_eq!(file.size_bytes, Some(1024));
226        assert_eq!(file.state, Some(FileState::Active));
227    }
228
229    #[test]
230    fn parse_list_files_response() {
231        let json = serde_json::json!({
232            "files": [
233                {
234                    "name": "files/a",
235                    "displayName": "a.txt",
236                    "mimeType": "text/plain"
237                },
238                {
239                    "name": "files/b",
240                    "displayName": "b.pdf",
241                    "mimeType": "application/pdf"
242                }
243            ],
244            "nextPageToken": "page2"
245        });
246        let resp: ListFilesResponse = serde_json::from_value(json).unwrap();
247        assert_eq!(resp.files.len(), 2);
248        assert_eq!(resp.next_page_token, Some("page2".to_string()));
249    }
250
251    #[test]
252    fn file_state_serialization() {
253        assert_eq!(
254            serde_json::to_value(FileState::Processing).unwrap(),
255            "PROCESSING"
256        );
257        assert_eq!(serde_json::to_value(FileState::Active).unwrap(), "ACTIVE");
258        assert_eq!(serde_json::to_value(FileState::Failed).unwrap(), "FAILED");
259    }
260
261    #[test]
262    fn file_state_deserialization() {
263        let state: FileState = serde_json::from_str("\"ACTIVE\"").unwrap();
264        assert_eq!(state, FileState::Active);
265    }
266
267    #[test]
268    fn file_source_serialization() {
269        let source = FileSource {
270            file_uri: "gs://bucket/file.txt".to_string(),
271            mime_type: "text/plain".to_string(),
272        };
273        let json = serde_json::to_value(&source).unwrap();
274        assert_eq!(json["fileUri"], "gs://bucket/file.txt");
275        assert_eq!(json["mimeType"], "text/plain");
276    }
277
278    #[test]
279    fn empty_list_response() {
280        let json = serde_json::json!({"files": []});
281        let resp: ListFilesResponse = serde_json::from_value(json).unwrap();
282        assert!(resp.files.is_empty());
283        assert!(resp.next_page_token.is_none());
284    }
285
286    #[test]
287    fn file_with_error() {
288        let json = serde_json::json!({
289            "name": "files/bad",
290            "displayName": "bad.txt",
291            "mimeType": "text/plain",
292            "state": "FAILED",
293            "error": {"code": 400, "message": "Invalid file"}
294        });
295        let file: File = serde_json::from_value(json).unwrap();
296        assert_eq!(file.state, Some(FileState::Failed));
297        assert!(file.error.is_some());
298    }
299}