1use std::path::Path;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use crate::client::{Backend, ClientInner};
8use crate::error::{Error, Result};
9use crate::upload;
10#[cfg(test)]
11use crate::upload::CHUNK_SIZE;
12use rust_genai_types::enums::FileState;
13use rust_genai_types::files::{
14 DownloadFileConfig, File, ListFilesConfig, ListFilesResponse, UploadFileConfig,
15};
16use serde_json::Value;
17
18#[derive(Clone)]
19pub struct Files {
20 pub(crate) inner: Arc<ClientInner>,
21}
22
23impl Files {
24 pub(crate) const fn new(inner: Arc<ClientInner>) -> Self {
25 Self { inner }
26 }
27
28 pub async fn upload(&self, data: Vec<u8>, mime_type: impl Into<String>) -> Result<File> {
33 let config = UploadFileConfig {
34 mime_type: Some(mime_type.into()),
35 ..UploadFileConfig::default()
36 };
37 self.upload_with_config(data, config).await
38 }
39
40 pub async fn upload_with_config(
45 &self,
46 data: Vec<u8>,
47 config: UploadFileConfig,
48 ) -> Result<File> {
49 ensure_gemini_backend(&self.inner)?;
50
51 let mime_type = config
52 .mime_type
53 .clone()
54 .ok_or_else(|| Error::InvalidConfig {
55 message: "mime_type is required when uploading raw bytes".into(),
56 })?;
57 let size_bytes = data.len() as u64;
58 let file = build_upload_file(config, size_bytes, &mime_type);
59 let upload_url = self
60 .start_resumable_upload(file, size_bytes, &mime_type, None)
61 .await?;
62 self.upload_bytes(&upload_url, &data).await
63 }
64
65 pub async fn upload_from_path(&self, path: impl AsRef<Path>) -> Result<File> {
70 self.upload_from_path_with_config(path, UploadFileConfig::default())
71 .await
72 }
73
74 pub async fn upload_from_path_with_config(
79 &self,
80 path: impl AsRef<Path>,
81 mut config: UploadFileConfig,
82 ) -> Result<File> {
83 ensure_gemini_backend(&self.inner)?;
84
85 let path = path.as_ref();
86 let metadata = tokio::fs::metadata(path).await?;
87 if !metadata.is_file() {
88 return Err(Error::InvalidConfig {
89 message: format!("{} is not a valid file path", path.display()),
90 });
91 }
92
93 let size_bytes = metadata.len();
94 let mime_type = config.mime_type.take().unwrap_or_else(|| {
95 mime_guess::from_path(path)
96 .first_or_octet_stream()
97 .essence_str()
98 .to_string()
99 });
100
101 let file_name = path.file_name().and_then(|name| name.to_str());
102 let file = build_upload_file(config, size_bytes, &mime_type);
103 let upload_url = self
104 .start_resumable_upload(file, size_bytes, &mime_type, file_name)
105 .await?;
106 let mut file_handle = tokio::fs::File::open(path).await?;
107 self.upload_reader(&upload_url, &mut file_handle, size_bytes)
108 .await
109 }
110
111 pub async fn download(&self, name_or_uri: impl AsRef<str>) -> Result<Vec<u8>> {
116 ensure_gemini_backend(&self.inner)?;
117
118 let file_name = normalize_file_name(name_or_uri.as_ref())?;
119 let url = build_file_download_url(&self.inner, &file_name);
120 let request = self.inner.http.get(url);
121 let response = self.inner.send(request).await?;
122 if !response.status().is_success() {
123 return Err(Error::ApiError {
124 status: response.status().as_u16(),
125 message: response.text().await.unwrap_or_default(),
126 });
127 }
128 let bytes = response.bytes().await?;
129 Ok(bytes.to_vec())
130 }
131
132 #[allow(unused_variables)]
133 pub async fn download_with_config(
138 &self,
139 name_or_uri: impl AsRef<str>,
140 _config: DownloadFileConfig,
141 ) -> Result<Vec<u8>> {
142 self.download(name_or_uri).await
143 }
144
145 pub async fn list(&self) -> Result<ListFilesResponse> {
150 self.list_with_config(ListFilesConfig::default()).await
151 }
152
153 pub async fn list_with_config(&self, config: ListFilesConfig) -> Result<ListFilesResponse> {
158 ensure_gemini_backend(&self.inner)?;
159 let url = build_files_list_url(&self.inner, &config)?;
160 let request = self.inner.http.get(url);
161 let response = self.inner.send(request).await?;
162 if !response.status().is_success() {
163 return Err(Error::ApiError {
164 status: response.status().as_u16(),
165 message: response.text().await.unwrap_or_default(),
166 });
167 }
168 Ok(response.json::<ListFilesResponse>().await?)
169 }
170
171 pub async fn all(&self) -> Result<Vec<File>> {
176 self.all_with_config(ListFilesConfig::default()).await
177 }
178
179 pub async fn all_with_config(&self, mut config: ListFilesConfig) -> Result<Vec<File>> {
184 let mut files = Vec::new();
185 loop {
186 let response = self.list_with_config(config.clone()).await?;
187 if let Some(items) = response.files {
188 files.extend(items);
189 }
190 match response.next_page_token {
191 Some(token) if !token.is_empty() => {
192 config.page_token = Some(token);
193 }
194 _ => break,
195 }
196 }
197 Ok(files)
198 }
199
200 pub async fn get(&self, name_or_uri: impl AsRef<str>) -> Result<File> {
205 ensure_gemini_backend(&self.inner)?;
206
207 let file_name = normalize_file_name(name_or_uri.as_ref())?;
208 let url = build_file_url(&self.inner, &file_name);
209 let request = self.inner.http.get(url);
210 let response = self.inner.send(request).await?;
211 if !response.status().is_success() {
212 return Err(Error::ApiError {
213 status: response.status().as_u16(),
214 message: response.text().await.unwrap_or_default(),
215 });
216 }
217 Ok(response.json::<File>().await?)
218 }
219
220 pub async fn delete(&self, name_or_uri: impl AsRef<str>) -> Result<()> {
225 ensure_gemini_backend(&self.inner)?;
226
227 let file_name = normalize_file_name(name_or_uri.as_ref())?;
228 let url = build_file_url(&self.inner, &file_name);
229 let request = self.inner.http.delete(url);
230 let response = self.inner.send(request).await?;
231 if !response.status().is_success() {
232 return Err(Error::ApiError {
233 status: response.status().as_u16(),
234 message: response.text().await.unwrap_or_default(),
235 });
236 }
237 Ok(())
238 }
239
240 pub async fn wait_for_active(
245 &self,
246 name_or_uri: impl AsRef<str>,
247 config: WaitForFileConfig,
248 ) -> Result<File> {
249 ensure_gemini_backend(&self.inner)?;
250
251 let start = Instant::now();
252 loop {
253 let file = self.get(name_or_uri.as_ref()).await?;
254 match file.state {
255 Some(FileState::Active) => return Ok(file),
256 Some(FileState::Failed) => {
257 return Err(Error::ApiError {
258 status: 500,
259 message: "File processing failed".into(),
260 })
261 }
262 _ => {}
263 }
264
265 if let Some(timeout) = config.timeout {
266 if start.elapsed() >= timeout {
267 return Err(Error::Timeout {
268 message: "Timed out waiting for file to become ACTIVE".into(),
269 });
270 }
271 }
272
273 tokio::time::sleep(config.poll_interval).await;
274 }
275 }
276
277 async fn start_resumable_upload(
278 &self,
279 file: File,
280 size_bytes: u64,
281 mime_type: &str,
282 file_name: Option<&str>,
283 ) -> Result<String> {
284 let url = build_files_upload_url(&self.inner);
285 let mut request = self
286 .inner
287 .http
288 .post(url)
289 .header("X-Goog-Upload-Protocol", "resumable")
290 .header("X-Goog-Upload-Command", "start")
291 .header(
292 "X-Goog-Upload-Header-Content-Length",
293 size_bytes.to_string(),
294 )
295 .header("X-Goog-Upload-Header-Content-Type", mime_type);
296
297 if let Some(file_name) = file_name {
298 request = request.header("X-Goog-Upload-File-Name", file_name);
299 }
300
301 let body = serde_json::json!({ "file": file });
302 let request = request.json(&body);
303 let response = self.inner.send(request).await?;
304 if !response.status().is_success() {
305 return Err(Error::ApiError {
306 status: response.status().as_u16(),
307 message: response.text().await.unwrap_or_default(),
308 });
309 }
310
311 let upload_url = response
312 .headers()
313 .get("x-goog-upload-url")
314 .and_then(|value| value.to_str().ok())
315 .ok_or_else(|| Error::Parse {
316 message: "Missing x-goog-upload-url header".into(),
317 })?;
318
319 Ok(upload_url.to_string())
320 }
321
322 async fn upload_bytes(&self, upload_url: &str, data: &[u8]) -> Result<File> {
323 let validate_status = |status: &str| {
324 if status != "active" {
325 return Err(Error::Parse {
326 message: format!("Unexpected upload status: {status}"),
327 });
328 }
329 Ok(())
330 };
331
332 upload::upload_bytes_with(
333 data,
334 |chunk, offset, finalize| self.send_upload_chunk(upload_url, chunk, offset, finalize),
335 validate_status,
336 "Upload finished without final response",
337 )
338 .await
339 }
340
341 async fn upload_reader(
342 &self,
343 upload_url: &str,
344 reader: &mut tokio::fs::File,
345 total_size: u64,
346 ) -> Result<File> {
347 let validate_status = |status: &str| {
348 if status != "active" {
349 return Err(Error::Parse {
350 message: format!("Unexpected upload status: {status}"),
351 });
352 }
353 Ok(())
354 };
355
356 upload::upload_reader_with(
357 reader,
358 total_size,
359 |chunk, offset, finalize| self.send_upload_chunk(upload_url, chunk, offset, finalize),
360 validate_status,
361 "Upload finished without final response",
362 )
363 .await
364 }
365
366 async fn send_upload_chunk(
367 &self,
368 upload_url: &str,
369 chunk: Vec<u8>,
370 offset: u64,
371 finalize: bool,
372 ) -> Result<(String, Option<File>)> {
373 let command = if finalize {
374 "upload, finalize"
375 } else {
376 "upload"
377 };
378 let chunk_len = chunk.len();
379 let response = self
380 .inner
381 .http
382 .post(upload_url)
383 .header("X-Goog-Upload-Command", command)
384 .header("X-Goog-Upload-Offset", offset.to_string())
385 .header("Content-Length", chunk_len.to_string())
386 .body(chunk)
387 .send()
388 .await?;
389
390 if !response.status().is_success() {
391 return Err(Error::ApiError {
392 status: response.status().as_u16(),
393 message: response.text().await.unwrap_or_default(),
394 });
395 }
396
397 let upload_status = response
398 .headers()
399 .get("x-goog-upload-status")
400 .and_then(|value| value.to_str().ok())
401 .ok_or_else(|| Error::Parse {
402 message: "Missing x-goog-upload-status header".into(),
403 })?
404 .to_string();
405
406 let body = response.bytes().await?;
407 if body.is_empty() {
408 return Ok((upload_status, None));
409 }
410
411 let value: Value = serde_json::from_slice(&body)?;
412 let file_value = value.get("file").cloned().unwrap_or(value);
413 let file: File = serde_json::from_value(file_value)?;
414
415 Ok((upload_status, Some(file)))
416 }
417}
418
419#[derive(Debug, Clone)]
420pub struct WaitForFileConfig {
421 pub poll_interval: Duration,
422 pub timeout: Option<Duration>,
423}
424
425impl Default for WaitForFileConfig {
426 fn default() -> Self {
427 Self {
428 poll_interval: Duration::from_secs(2),
429 timeout: Some(Duration::from_secs(300)),
430 }
431 }
432}
433
434#[cfg(test)]
435fn finalize_upload(status: &str, file: Option<File>) -> Result<File> {
436 upload::finalize_upload(status, file)
437}
438
439fn ensure_gemini_backend(inner: &ClientInner) -> Result<()> {
440 if inner.config.backend == Backend::VertexAi {
441 return Err(Error::InvalidConfig {
442 message: "Files API is only supported in Gemini API".into(),
443 });
444 }
445 Ok(())
446}
447
448fn build_upload_file(config: UploadFileConfig, size_bytes: u64, mime_type: &str) -> File {
449 let mut file = File::default();
450 if let Some(name) = config.name {
451 file.name = Some(normalize_upload_name(&name));
452 }
453 file.display_name = config.display_name;
454 file.mime_type = Some(mime_type.to_string());
455 file.size_bytes = Some(size_bytes.to_string());
456 file
457}
458
459fn normalize_upload_name(name: &str) -> String {
460 if name.starts_with("files/") {
461 name.to_string()
462 } else {
463 format!("files/{name}")
464 }
465}
466
467fn normalize_file_name(value: &str) -> Result<String> {
468 if value.starts_with("http://") || value.starts_with("https://") {
469 let marker = "files/";
470 let start = value.find(marker).ok_or_else(|| Error::InvalidConfig {
471 message: format!("Could not find 'files/' in URI: {value}"),
472 })?;
473 let suffix = &value[start + marker.len()..];
474 let name: String = suffix
475 .chars()
476 .take_while(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '-')
477 .collect();
478 if name.is_empty() {
479 return Err(Error::InvalidConfig {
480 message: format!("Could not extract file name from URI: {value}"),
481 });
482 }
483 Ok(name)
484 } else if value.starts_with("files/") {
485 Ok(value.trim_start_matches("files/").to_string())
486 } else {
487 Ok(value.to_string())
488 }
489}
490
491fn build_files_upload_url(inner: &ClientInner) -> String {
492 let base = &inner.api_client.base_url;
493 let version = &inner.api_client.api_version;
494 format!("{base}upload/{version}/files")
495}
496
497fn build_files_list_url(inner: &ClientInner, config: &ListFilesConfig) -> Result<String> {
498 let base = &inner.api_client.base_url;
499 let version = &inner.api_client.api_version;
500 let url = format!("{base}{version}/files");
501 add_list_query_params(&url, config)
502}
503
504fn build_file_url(inner: &ClientInner, name: &str) -> String {
505 let base = &inner.api_client.base_url;
506 let version = &inner.api_client.api_version;
507 format!("{base}{version}/files/{name}")
508}
509
510fn build_file_download_url(inner: &ClientInner, name: &str) -> String {
511 let base = &inner.api_client.base_url;
512 let version = &inner.api_client.api_version;
513 format!("{base}{version}/files/{name}:download?alt=media")
514}
515
516fn add_list_query_params(url: &str, config: &ListFilesConfig) -> Result<String> {
517 let mut url = reqwest::Url::parse(url).map_err(|err| Error::InvalidConfig {
518 message: err.to_string(),
519 })?;
520 {
521 let mut pairs = url.query_pairs_mut();
522 if let Some(page_size) = config.page_size {
523 pairs.append_pair("pageSize", &page_size.to_string());
524 }
525 if let Some(page_token) = &config.page_token {
526 pairs.append_pair("pageToken", page_token);
527 }
528 }
529 Ok(url.to_string())
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::client::Client;
536 use crate::test_support::test_client_inner;
537 use serde_json::json;
538 use wiremock::matchers::{method, path};
539 use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};
540
541 #[test]
542 fn test_normalize_file_name() {
543 assert_eq!(normalize_file_name("files/abc-123").unwrap(), "abc-123");
544 assert_eq!(normalize_file_name("abc-123").unwrap(), "abc-123");
545 assert_eq!(
546 normalize_file_name("https://example.com/files/abc-123?foo=bar").unwrap(),
547 "abc-123"
548 );
549 }
550
551 #[test]
552 fn test_build_urls() {
553 let client = Client::new("test-key").unwrap();
554 let files = client.files();
555 let url = build_files_upload_url(&files.inner);
556 assert_eq!(
557 url,
558 "https://generativelanguage.googleapis.com/upload/v1beta/files"
559 );
560 }
561
562 #[test]
563 fn test_normalize_upload_and_list_params() {
564 assert_eq!(normalize_upload_name("files/abc"), "files/abc");
565 assert_eq!(normalize_upload_name("abc"), "files/abc");
566 assert!(normalize_file_name("https://example.com/no-files").is_err());
567 assert!(normalize_file_name("https://example.com/files/?x").is_err());
568
569 let url = add_list_query_params(
570 "https://example.com/files",
571 &ListFilesConfig {
572 page_size: Some(3),
573 page_token: Some("t".to_string()),
574 },
575 )
576 .unwrap();
577 assert!(url.contains("pageSize=3"));
578 assert!(url.contains("pageToken=t"));
579 }
580
581 #[test]
582 fn test_build_upload_file_and_finalize_errors() {
583 let file = build_upload_file(
584 UploadFileConfig {
585 name: Some("abc".to_string()),
586 display_name: Some("d".to_string()),
587 ..Default::default()
588 },
589 5,
590 "text/plain",
591 );
592 assert_eq!(file.name.as_deref(), Some("files/abc"));
593 assert_eq!(file.size_bytes.as_deref(), Some("5"));
594
595 let err = finalize_upload("active", None).unwrap_err();
596 assert!(matches!(err, Error::Parse { .. }));
597 let err = finalize_upload("final", None).unwrap_err();
598 assert!(matches!(err, Error::Parse { .. }));
599 }
600
601 #[test]
602 fn test_ensure_gemini_backend_error() {
603 let vertex = test_client_inner(Backend::VertexAi);
604 let err = ensure_gemini_backend(&vertex).unwrap_err();
605 assert!(matches!(err, Error::InvalidConfig { .. }));
606 }
607
608 #[tokio::test]
609 async fn test_start_resumable_upload_and_send_chunk_errors() {
610 let server = MockServer::start().await;
611 Mock::given(method("POST"))
612 .and(path("/upload/v1beta/files"))
613 .respond_with(ResponseTemplate::new(200))
614 .mount(&server)
615 .await;
616
617 let client = Client::builder()
618 .api_key("test-key")
619 .base_url(server.uri())
620 .build()
621 .unwrap();
622 let files = client.files();
623 let file = build_upload_file(UploadFileConfig::default(), 1, "text/plain");
624 let err = files
625 .start_resumable_upload(file, 1, "text/plain", None)
626 .await
627 .unwrap_err();
628 assert!(matches!(err, Error::Parse { .. }));
629
630 Mock::given(method("POST"))
631 .and(path("/upload-chunk"))
632 .respond_with(ResponseTemplate::new(200))
633 .mount(&server)
634 .await;
635 let err = files
636 .send_upload_chunk(
637 &format!("{}/upload-chunk", server.uri()),
638 Vec::new(),
639 0,
640 true,
641 )
642 .await
643 .unwrap_err();
644 assert!(matches!(err, Error::Parse { .. }));
645
646 Mock::given(method("POST"))
647 .and(path("/upload-fail"))
648 .respond_with(ResponseTemplate::new(400).set_body_string("bad"))
649 .mount(&server)
650 .await;
651 let err = files
652 .send_upload_chunk(
653 &format!("{}/upload-fail", server.uri()),
654 Vec::new(),
655 0,
656 true,
657 )
658 .await
659 .unwrap_err();
660 assert!(matches!(err, Error::ApiError { .. }));
661 }
662
663 #[tokio::test]
664 async fn test_files_upload_errors() {
665 let client = Client::new("test-key").unwrap();
666 let files = client.files();
667
668 let err = files
669 .upload_with_config(vec![1, 2, 3], UploadFileConfig::default())
670 .await
671 .unwrap_err();
672 assert!(matches!(err, Error::InvalidConfig { .. }));
673
674 let temp_dir = std::env::temp_dir().join("rust_genai_files_test_dir");
675 let _ = tokio::fs::create_dir_all(&temp_dir).await;
676 let err = files
677 .upload_from_path_with_config(&temp_dir, UploadFileConfig::default())
678 .await
679 .unwrap_err();
680 assert!(matches!(err, Error::InvalidConfig { .. }));
681 let _ = tokio::fs::remove_dir_all(&temp_dir).await;
682 }
683
684 #[tokio::test]
685 async fn test_start_resumable_upload_error_response() {
686 let server = MockServer::start().await;
687 Mock::given(method("POST"))
688 .and(path("/upload/v1beta/files"))
689 .respond_with(ResponseTemplate::new(500).set_body_string("boom"))
690 .mount(&server)
691 .await;
692
693 let client = Client::builder()
694 .api_key("test-key")
695 .base_url(server.uri())
696 .build()
697 .unwrap();
698 let files = client.files();
699 let file = build_upload_file(UploadFileConfig::default(), 1, "text/plain");
700 let err = files
701 .start_resumable_upload(file, 1, "text/plain", None)
702 .await
703 .unwrap_err();
704 assert!(matches!(err, Error::ApiError { .. }));
705 }
706
707 #[tokio::test]
708 async fn test_upload_bytes_empty_and_status_errors() {
709 let server = MockServer::start().await;
710
711 Mock::given(method("POST"))
712 .and(path("/upload-empty"))
713 .respond_with(
714 ResponseTemplate::new(200)
715 .insert_header("x-goog-upload-status", "final")
716 .set_body_json(json!({
717 "file": {"name": "files/empty", "state": "ACTIVE"}
718 })),
719 )
720 .mount(&server)
721 .await;
722
723 Mock::given(method("POST"))
724 .and(path("/upload-bad"))
725 .respond_with(
726 ResponseTemplate::new(200).insert_header("x-goog-upload-status", "paused"),
727 )
728 .mount(&server)
729 .await;
730
731 let client = Client::builder()
732 .api_key("test-key")
733 .base_url(server.uri())
734 .build()
735 .unwrap();
736 let files = client.files();
737
738 let file = files
739 .upload_bytes(&format!("{}/upload-empty", server.uri()), &[])
740 .await
741 .unwrap();
742 assert_eq!(file.name.as_deref(), Some("files/empty"));
743
744 let data = vec![0u8; CHUNK_SIZE + 1];
745 let err = files
746 .upload_bytes(&format!("{}/upload-bad", server.uri()), &data)
747 .await
748 .unwrap_err();
749 assert!(matches!(err, Error::Parse { .. }));
750 }
751
752 #[tokio::test]
753 async fn test_upload_reader_empty_file() {
754 let server = MockServer::start().await;
755 Mock::given(method("POST"))
756 .and(path("/upload-empty-file"))
757 .respond_with(
758 ResponseTemplate::new(200)
759 .insert_header("x-goog-upload-status", "final")
760 .set_body_json(json!({
761 "file": {"name": "files/empty-file", "state": "ACTIVE"}
762 })),
763 )
764 .mount(&server)
765 .await;
766
767 let client = Client::builder()
768 .api_key("test-key")
769 .base_url(server.uri())
770 .build()
771 .unwrap();
772 let files = client.files();
773 let temp_path = std::env::temp_dir().join("rust_genai_empty_upload_file");
774 let _ = tokio::fs::write(&temp_path, &[]).await;
775 let mut handle = tokio::fs::File::open(&temp_path).await.unwrap();
776
777 let file = files
778 .upload_reader(
779 &format!("{}/upload-empty-file", server.uri()),
780 &mut handle,
781 0,
782 )
783 .await
784 .unwrap();
785 assert_eq!(file.name.as_deref(), Some("files/empty-file"));
786 let _ = tokio::fs::remove_file(&temp_path).await;
787 }
788
789 #[tokio::test]
790 async fn test_upload_bytes_and_reader_active_then_final() {
791 #[derive(Clone)]
792 struct UploadResponder;
793
794 impl Respond for UploadResponder {
795 fn respond(&self, request: &Request) -> ResponseTemplate {
796 let finalize = request
797 .headers
798 .get("x-goog-upload-command")
799 .and_then(|value| value.to_str().ok())
800 .is_some_and(|value| value.contains("finalize"));
801 if finalize {
802 ResponseTemplate::new(200)
803 .insert_header("x-goog-upload-status", "final")
804 .set_body_json(json!({
805 "file": {"name": "files/final", "state": "ACTIVE"}
806 }))
807 } else {
808 ResponseTemplate::new(200).insert_header("x-goog-upload-status", "active")
809 }
810 }
811 }
812
813 let server = MockServer::start().await;
814 Mock::given(method("POST"))
815 .and(path("/upload-active"))
816 .respond_with(UploadResponder)
817 .mount(&server)
818 .await;
819
820 let client = Client::builder()
821 .api_key("test-key")
822 .base_url(server.uri())
823 .build()
824 .unwrap();
825 let files = client.files();
826 let data = vec![0u8; CHUNK_SIZE + 1];
827 let file = files
828 .upload_bytes(&format!("{}/upload-active", server.uri()), &data)
829 .await
830 .unwrap();
831 assert_eq!(file.name.as_deref(), Some("files/final"));
832
833 Mock::given(method("POST"))
834 .and(path("/upload-reader"))
835 .respond_with(UploadResponder)
836 .mount(&server)
837 .await;
838 let temp_path = std::env::temp_dir().join("rust_genai_reader_active");
839 let _ = tokio::fs::write(&temp_path, vec![0u8; CHUNK_SIZE + 1]).await;
840 let mut handle = tokio::fs::File::open(&temp_path).await.unwrap();
841 let file = files
842 .upload_reader(
843 &format!("{}/upload-reader", server.uri()),
844 &mut handle,
845 (CHUNK_SIZE + 1) as u64,
846 )
847 .await
848 .unwrap();
849 assert_eq!(file.name.as_deref(), Some("files/final"));
850 let _ = tokio::fs::remove_file(&temp_path).await;
851 }
852
853 #[tokio::test]
854 async fn test_upload_with_config_and_mime_guess() {
855 let server = MockServer::start().await;
856 Mock::given(method("POST"))
857 .and(path("/upload/v1beta/files"))
858 .respond_with(
859 ResponseTemplate::new(200)
860 .insert_header("x-goog-upload-url", format!("{}/upload-ok", server.uri())),
861 )
862 .mount(&server)
863 .await;
864 Mock::given(method("POST"))
865 .and(path("/upload-ok"))
866 .respond_with(
867 ResponseTemplate::new(200)
868 .insert_header("x-goog-upload-status", "final")
869 .set_body_json(json!({
870 "file": {"name": "files/ok", "state": "ACTIVE"}
871 })),
872 )
873 .mount(&server)
874 .await;
875
876 let client = Client::builder()
877 .api_key("test-key")
878 .base_url(server.uri())
879 .build()
880 .unwrap();
881 let files = client.files();
882 let file = files.upload(vec![1, 2, 3], "text/plain").await.unwrap();
883 assert_eq!(file.name.as_deref(), Some("files/ok"));
884
885 let temp_path = std::env::temp_dir().join("rust_genai_upload_guess.txt");
886 let _ = tokio::fs::write(&temp_path, b"hello").await;
887 let file = files
888 .upload_from_path_with_config(&temp_path, UploadFileConfig::default())
889 .await
890 .unwrap();
891 assert_eq!(file.name.as_deref(), Some("files/ok"));
892 let _ = tokio::fs::remove_file(&temp_path).await;
893 }
894
895 #[tokio::test]
896 async fn test_wait_for_active_timeout_after_sleep() {
897 let server = MockServer::start().await;
898 Mock::given(method("GET"))
899 .and(path("/v1beta/files/slow"))
900 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
901 "name": "files/slow",
902 "state": "PROCESSING"
903 })))
904 .mount(&server)
905 .await;
906
907 let client = Client::builder()
908 .api_key("test-key")
909 .base_url(server.uri())
910 .build()
911 .unwrap();
912 let files = client.files();
913 let err = files
914 .wait_for_active(
915 "slow",
916 WaitForFileConfig {
917 poll_interval: Duration::from_millis(1),
918 timeout: Some(Duration::from_millis(2)),
919 },
920 )
921 .await
922 .unwrap_err();
923 assert!(matches!(err, Error::Timeout { .. }));
924 }
925
926 #[test]
927 fn test_add_list_query_params_invalid_url() {
928 let err = add_list_query_params("http://[::1", &ListFilesConfig::default()).unwrap_err();
929 assert!(matches!(err, Error::InvalidConfig { .. }));
930 }
931}