Skip to main content

zai_rs/file/
upload.rs

1use std::{path::PathBuf, sync::Arc};
2
3use super::request::FilePurpose;
4use crate::client::{
5    endpoints::{ApiBase, EndpointConfig, paths},
6    http::{HttpClient, HttpClientConfig, parse_typed_response, send_multipart_request},
7};
8
9/// File upload request (multipart/form-data)
10///
11/// Sends a multipart request with fields:
12/// - purpose: `FilePurpose`
13/// - file: file content
14pub struct FileUploadRequest {
15    pub key: String,
16    url: String,
17    endpoint_config: EndpointConfig,
18    api_base: ApiBase,
19    http_config: Arc<HttpClientConfig>,
20    purpose: FilePurpose,
21    file_path: PathBuf,
22    file_name: Option<String>,
23    content_type: Option<String>,
24    _body: (),
25}
26
27impl FileUploadRequest {
28    pub fn new(key: String, purpose: FilePurpose, file_path: impl Into<PathBuf>) -> Self {
29        let endpoint_config = EndpointConfig::default();
30        let api_base = ApiBase::PaasV4;
31        let url = endpoint_config.url(&api_base, paths::FILES);
32        Self {
33            key,
34            url,
35            endpoint_config,
36            api_base,
37            http_config: Arc::new(HttpClientConfig::default()),
38            purpose,
39            file_path: file_path.into(),
40            file_name: None,
41            content_type: None,
42            _body: (),
43        }
44    }
45
46    pub fn with_file_name(mut self, name: impl Into<String>) -> Self {
47        self.file_name = Some(name.into());
48        self
49    }
50
51    pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
52        self.content_type = Some(ct.into());
53
54        self
55    }
56
57    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
58        self.api_base = ApiBase::Custom(base_url.into());
59        self.url = self.endpoint_config.url(&self.api_base, paths::FILES);
60        self
61    }
62
63    pub fn with_endpoint_config(mut self, endpoint_config: EndpointConfig) -> Self {
64        self.endpoint_config = endpoint_config;
65        self.url = self.endpoint_config.url(&self.api_base, paths::FILES);
66        self
67    }
68
69    pub fn with_http_config(mut self, config: HttpClientConfig) -> Self {
70        self.http_config = Arc::new(config);
71        self
72    }
73
74    /// Send the upload request and parse typed response (`FileObject`)
75    pub async fn send(&self) -> crate::ZaiResult<super::response::FileObject> {
76        let resp: reqwest::Response = self.post().await?;
77        parse_typed_response::<super::response::FileObject>(resp).await
78    }
79}
80
81impl HttpClient for FileUploadRequest {
82    type Body = (); // unused
83    type ApiUrl = String;
84    type ApiKey = String;
85
86    fn api_url(&self) -> &Self::ApiUrl {
87        &self.url
88    }
89    fn api_key(&self) -> &Self::ApiKey {
90        &self.key
91    }
92    fn body(&self) -> &Self::Body {
93        &self._body
94    }
95
96    // Override POST to send multipart/form-data
97
98    fn post(
99        &self,
100    ) -> impl std::future::Future<Output = crate::ZaiResult<reqwest::Response>> + Send {
101        let url = self.url.clone();
102        let key: String = self.key.clone();
103        let config = self.http_config.clone();
104        let purpose = self.purpose.clone();
105        let path = self.file_path.clone();
106        let file_name = self.file_name.clone();
107        let content_type = self.content_type.clone();
108        async move {
109            let fname = file_name
110                .or_else(|| {
111                    path.file_name()
112                        .and_then(|s| s.to_str())
113                        .map(|s| s.to_string())
114                })
115                .unwrap_or_else(|| "upload.bin".to_string());
116
117            let bytes = std::fs::read(&path)?;
118            send_multipart_request(reqwest::Method::POST, url, key, config, move || {
119                let mut part =
120                    reqwest::multipart::Part::bytes(bytes.clone()).file_name(fname.clone());
121                if let Some(ct) = content_type.as_ref() {
122                    part = part.mime_str(ct).map_err(|e| {
123                        crate::client::error::ZaiError::ApiError {
124                            code: 1200,
125                            message: format!("invalid content-type: {}", e),
126                        }
127                    })?;
128                }
129                Ok(reqwest::multipart::Form::new()
130                    .text("purpose", purpose.as_str().to_string())
131                    .part("file", part))
132            })
133            .await
134        }
135    }
136
137    fn http_config(&self) -> Arc<HttpClientConfig> {
138        self.http_config.clone()
139    }
140}