Skip to main content

linger_openai_sdk/
uploads.rs

1use crate::error::LingerError;
2use crate::files::{FileExpirationPolicy, FileObject};
3use crate::transport::HttpRequest;
4use crate::RequestId;
5use bytes::Bytes;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::BTreeMap;
9
10/// EN: Request body for `POST /v1/uploads`.
11/// 中文:`POST /v1/uploads` 的请求体。
12#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct CreateUploadRequest {
15    /// EN: Total expected upload size in bytes.
16    /// 中文:预期上传总大小,单位为字节。
17    pub bytes: u64,
18    /// EN: Final filename for the completed upload.
19    /// 中文:完成上传后的最终文件名。
20    pub filename: String,
21    /// EN: MIME type for the completed upload.
22    /// 中文:完成上传后的 MIME 类型。
23    pub mime_type: String,
24    /// EN: OpenAI file purpose.
25    /// 中文:OpenAI 文件用途。
26    pub purpose: String,
27    /// EN: Optional expiration policy for supported purposes.
28    /// 中文:受支持用途的可选过期策略。
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub expires_after: Option<FileExpirationPolicy>,
31}
32
33impl CreateUploadRequest {
34    /// EN: Starts building an upload creation request.
35    /// 中文:开始构建上传创建请求。
36    pub fn builder() -> CreateUploadRequestBuilder {
37        CreateUploadRequestBuilder::default()
38    }
39}
40
41/// EN: Builder for upload creation requests.
42/// 中文:上传创建请求的构建器。
43#[derive(Clone, Debug, Default)]
44#[non_exhaustive]
45pub struct CreateUploadRequestBuilder {
46    bytes: Option<u64>,
47    filename: Option<String>,
48    mime_type: Option<String>,
49    purpose: Option<String>,
50    expires_after: Option<FileExpirationPolicy>,
51}
52
53impl CreateUploadRequestBuilder {
54    /// EN: Sets the total expected upload size in bytes.
55    /// 中文:设置预期上传总大小,单位为字节。
56    pub fn bytes(mut self, bytes: u64) -> Self {
57        self.bytes = Some(bytes);
58        self
59    }
60
61    /// EN: Sets the final filename.
62    /// 中文:设置最终文件名。
63    pub fn filename(mut self, filename: impl Into<String>) -> Self {
64        self.filename = Some(filename.into());
65        self
66    }
67
68    /// EN: Sets the MIME type.
69    /// 中文:设置 MIME 类型。
70    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
71        self.mime_type = Some(mime_type.into());
72        self
73    }
74
75    /// EN: Sets the file purpose.
76    /// 中文:设置文件用途。
77    pub fn purpose(mut self, purpose: impl Into<String>) -> Self {
78        self.purpose = Some(purpose.into());
79        self
80    }
81
82    /// EN: Sets the optional expiration policy.
83    /// 中文:设置可选的过期策略。
84    pub fn expires_after(mut self, expires_after: FileExpirationPolicy) -> Self {
85        self.expires_after = Some(expires_after);
86        self
87    }
88
89    /// EN: Builds and validates the request.
90    /// 中文:构建并校验请求。
91    pub fn build(self) -> Result<CreateUploadRequest, LingerError> {
92        let bytes = self
93            .bytes
94            .filter(|bytes| *bytes > 0)
95            .ok_or_else(|| LingerError::invalid_config("bytes must be greater than zero"))?;
96        let filename = required_string("filename", self.filename)?;
97        let mime_type = required_string("mime_type", self.mime_type)?;
98        let purpose = required_string("purpose", self.purpose)?;
99        if let Some(expires_after) = &self.expires_after {
100            expires_after.validate_for_uploads()?;
101        }
102        Ok(CreateUploadRequest {
103            bytes,
104            filename,
105            mime_type,
106            purpose,
107            expires_after: self.expires_after,
108        })
109    }
110}
111
112/// EN: Upload object returned by the Uploads API.
113/// 中文:Uploads API 返回的上传对象。
114#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
115#[non_exhaustive]
116pub struct Upload {
117    /// EN: Upload id.
118    /// 中文:上传 ID。
119    pub id: String,
120    /// EN: API object type.
121    /// 中文:API 对象类型。
122    pub object: String,
123    /// EN: Total expected upload size in bytes.
124    /// 中文:预期上传总大小,单位为字节。
125    pub bytes: u64,
126    /// EN: Unix timestamp for creation.
127    /// 中文:创建时间的 Unix 时间戳。
128    pub created_at: u64,
129    /// EN: Unix timestamp for expiration.
130    /// 中文:过期时间的 Unix 时间戳。
131    pub expires_at: u64,
132    /// EN: Final filename.
133    /// 中文:最终文件名。
134    pub filename: String,
135    /// EN: File purpose.
136    /// 中文:文件用途。
137    pub purpose: String,
138    /// EN: Upload status.
139    /// 中文:上传状态。
140    pub status: UploadStatus,
141    /// EN: Completed file object, when returned.
142    /// 中文:完成后返回的文件对象。
143    #[serde(default)]
144    pub file: Option<FileObject>,
145    /// EN: Additional fields preserved for forward compatibility.
146    /// 中文:为前向兼容保留的额外字段。
147    #[serde(flatten)]
148    pub extra: BTreeMap<String, Value>,
149    /// EN: OpenAI request id from response headers.
150    /// 中文:响应头中的 OpenAI 请求 ID。
151    #[serde(skip)]
152    request_id: Option<RequestId>,
153}
154
155impl Upload {
156    pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
157        self.request_id = request_id;
158        self
159    }
160
161    /// EN: Returns the OpenAI request id, when present.
162    /// 中文:返回 OpenAI 请求 ID,如存在。
163    pub fn request_id(&self) -> Option<&RequestId> {
164        self.request_id.as_ref()
165    }
166}
167
168/// EN: Status of an OpenAI upload.
169/// 中文:OpenAI 上传状态。
170#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
171#[serde(rename_all = "snake_case")]
172#[non_exhaustive]
173pub enum UploadStatus {
174    /// EN: Upload accepts more parts.
175    /// 中文:上传仍接受更多分段。
176    Pending,
177    /// EN: Upload completed and produced a file.
178    /// 中文:上传已完成并生成文件。
179    Completed,
180    /// EN: Upload was cancelled.
181    /// 中文:上传已取消。
182    Cancelled,
183    /// EN: Upload expired before completion.
184    /// 中文:上传在完成前过期。
185    Expired,
186    /// EN: Unknown status retained for forward compatibility.
187    /// 中文:为前向兼容保留的未知状态。
188    #[serde(other)]
189    Unknown,
190}
191
192/// EN: Upload part object returned after adding part data.
193/// 中文:添加分段数据后返回的上传分段对象。
194#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
195#[non_exhaustive]
196pub struct UploadPart {
197    /// EN: Upload part id.
198    /// 中文:上传分段 ID。
199    pub id: String,
200    /// EN: API object type.
201    /// 中文:API 对象类型。
202    pub object: String,
203    /// EN: Unix timestamp for creation.
204    /// 中文:创建时间的 Unix 时间戳。
205    pub created_at: u64,
206    /// EN: Parent upload id.
207    /// 中文:父上传 ID。
208    pub upload_id: String,
209    /// EN: OpenAI request id from response headers.
210    /// 中文:响应头中的 OpenAI 请求 ID。
211    #[serde(skip)]
212    request_id: Option<RequestId>,
213}
214
215impl UploadPart {
216    pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
217        self.request_id = request_id;
218        self
219    }
220
221    /// EN: Returns the OpenAI request id, when present.
222    /// 中文:返回 OpenAI 请求 ID,如存在。
223    pub fn request_id(&self) -> Option<&RequestId> {
224        self.request_id.as_ref()
225    }
226}
227
228/// EN: Uploadable part bytes and multipart metadata.
229/// 中文:可上传分段字节及 multipart 元数据。
230#[derive(Clone, Debug, PartialEq, Eq)]
231#[non_exhaustive]
232pub struct UploadPartData {
233    /// EN: Filename sent in the multipart part.
234    /// 中文:multipart 分段中发送的文件名。
235    pub filename: String,
236    /// EN: Content type sent for the part.
237    /// 中文:分段发送的内容类型。
238    pub content_type: String,
239    content: Bytes,
240}
241
242impl UploadPartData {
243    /// EN: Creates upload part data from already available bytes without copying them.
244    /// 中文:通过已可用字节创建上传分段数据,不复制这些字节。
245    pub fn from_bytes(
246        filename: impl Into<String>,
247        content: impl Into<Bytes>,
248    ) -> Result<Self, LingerError> {
249        let filename = filename.into();
250        validate_header_param("filename", &filename)?;
251        Ok(Self {
252            filename,
253            content_type: "application/octet-stream".to_string(),
254            content: content.into(),
255        })
256    }
257
258    /// EN: Sets the multipart part content type.
259    /// 中文:设置 multipart 分段内容类型。
260    pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
261        let content_type = content_type.into();
262        validate_header_value("content_type", &content_type)?;
263        self.content_type = content_type;
264        Ok(self)
265    }
266
267    /// EN: Returns part bytes as a cheap `Bytes` clone.
268    /// 中文:以廉价 `Bytes` 克隆返回分段字节。
269    pub fn bytes(&self) -> Bytes {
270        self.content.clone()
271    }
272}
273
274/// EN: Request body descriptor for `POST /v1/uploads/{upload_id}/parts`.
275/// 中文:`POST /v1/uploads/{upload_id}/parts` 的请求体描述。
276#[derive(Clone, Debug, PartialEq, Eq)]
277#[non_exhaustive]
278pub struct CreateUploadPartRequest {
279    /// EN: Part data to add to the upload.
280    /// 中文:要添加到上传的分段数据。
281    pub data: UploadPartData,
282}
283
284impl CreateUploadPartRequest {
285    /// EN: Starts building an upload part request.
286    /// 中文:开始构建上传分段请求。
287    pub fn builder() -> CreateUploadPartRequestBuilder {
288        CreateUploadPartRequestBuilder::default()
289    }
290
291    pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
292        let boundary = multipart_boundary(&self.data.content);
293        request.insert_header(
294            "content-type",
295            format!("multipart/form-data; boundary={boundary}"),
296        );
297        request.set_body_stream(self.multipart_stream(boundary));
298    }
299
300    fn multipart_stream(
301        &self,
302        boundary: String,
303    ) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
304        let mut chunks = Vec::new();
305        chunks.push(Ok(Bytes::from(format!(
306            "--{boundary}\r\nContent-Disposition: form-data; name=\"data\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
307            escape_multipart_param(&self.data.filename),
308            self.data.content_type
309        ))));
310        chunks.push(Ok(self.data.content.clone()));
311        chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
312        futures_util::stream::iter(chunks)
313    }
314}
315
316/// EN: Builder for upload part requests.
317/// 中文:上传分段请求的构建器。
318#[derive(Clone, Debug, Default)]
319#[non_exhaustive]
320pub struct CreateUploadPartRequestBuilder {
321    data: Option<UploadPartData>,
322}
323
324impl CreateUploadPartRequestBuilder {
325    /// EN: Sets the upload part data.
326    /// 中文:设置上传分段数据。
327    pub fn data(mut self, data: UploadPartData) -> Self {
328        self.data = Some(data);
329        self
330    }
331
332    /// EN: Builds and validates the request.
333    /// 中文:构建并校验请求。
334    pub fn build(self) -> Result<CreateUploadPartRequest, LingerError> {
335        let data = self
336            .data
337            .ok_or_else(|| LingerError::invalid_config("data is required"))?;
338        Ok(CreateUploadPartRequest { data })
339    }
340}
341
342/// EN: Request body for `POST /v1/uploads/{upload_id}/complete`.
343/// 中文:`POST /v1/uploads/{upload_id}/complete` 的请求体。
344#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
345#[non_exhaustive]
346pub struct CompleteUploadRequest {
347    /// EN: Ordered upload part ids to combine.
348    /// 中文:要按顺序合并的上传分段 ID。
349    pub part_ids: Vec<String>,
350    /// EN: Optional MD5 checksum.
351    /// 中文:可选的 MD5 校验值。
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub md5: Option<String>,
354}
355
356impl CompleteUploadRequest {
357    /// EN: Starts building an upload completion request.
358    /// 中文:开始构建上传完成请求。
359    pub fn builder() -> CompleteUploadRequestBuilder {
360        CompleteUploadRequestBuilder::default()
361    }
362}
363
364/// EN: Builder for upload completion requests.
365/// 中文:上传完成请求的构建器。
366#[derive(Clone, Debug, Default)]
367#[non_exhaustive]
368pub struct CompleteUploadRequestBuilder {
369    part_ids: Vec<String>,
370    md5: Option<String>,
371}
372
373impl CompleteUploadRequestBuilder {
374    /// EN: Adds an upload part id in completion order.
375    /// 中文:按完成顺序添加上传分段 ID。
376    pub fn part_id(mut self, part_id: impl Into<String>) -> Self {
377        self.part_ids.push(part_id.into());
378        self
379    }
380
381    /// EN: Replaces the ordered part id list.
382    /// 中文:替换有序分段 ID 列表。
383    pub fn part_ids(mut self, part_ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
384        self.part_ids = part_ids.into_iter().map(Into::into).collect();
385        self
386    }
387
388    /// EN: Sets the optional MD5 checksum.
389    /// 中文:设置可选的 MD5 校验值。
390    pub fn md5(mut self, md5: impl Into<String>) -> Self {
391        self.md5 = Some(md5.into());
392        self
393    }
394
395    /// EN: Builds and validates the request.
396    /// 中文:构建并校验请求。
397    pub fn build(self) -> Result<CompleteUploadRequest, LingerError> {
398        if self.part_ids.is_empty() {
399            return Err(LingerError::invalid_config("part_ids is required"));
400        }
401        for part_id in &self.part_ids {
402            if part_id.trim().is_empty() {
403                return Err(LingerError::invalid_config(
404                    "part_ids must not contain empty values",
405                ));
406            }
407        }
408        if self
409            .md5
410            .as_deref()
411            .is_some_and(|value| value.trim().is_empty())
412        {
413            return Err(LingerError::invalid_config("md5 must not be empty"));
414        }
415        Ok(CompleteUploadRequest {
416            part_ids: self.part_ids,
417            md5: self.md5,
418        })
419    }
420}
421
422fn required_string(name: &str, value: Option<String>) -> Result<String, LingerError> {
423    value
424        .filter(|value| !value.trim().is_empty())
425        .ok_or_else(|| LingerError::invalid_config(format!("{name} is required")))
426}
427
428fn multipart_boundary(content: &Bytes) -> String {
429    for counter in 0.. {
430        let boundary = format!("linger-openai-sdk-upload-boundary-{counter}");
431        if !contains_bytes(content, boundary.as_bytes()) {
432            return boundary;
433        }
434    }
435    unreachable!("unbounded boundary counter")
436}
437
438fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
439    if needle.is_empty() {
440        return true;
441    }
442    haystack
443        .windows(needle.len())
444        .any(|window| window == needle)
445}
446
447fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
448    if value.trim().is_empty() {
449        return Err(LingerError::invalid_config(format!("{name} is required")));
450    }
451    validate_header_value(name, value)
452}
453
454fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
455    if value.contains('\r') || value.contains('\n') {
456        return Err(LingerError::invalid_config(format!(
457            "{name} must not contain CR or LF"
458        )));
459    }
460    Ok(())
461}
462
463fn escape_multipart_param(value: &str) -> String {
464    value.replace('\\', "\\\\").replace('"', "\\\"")
465}