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#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct CreateUploadRequest {
15 pub bytes: u64,
18 pub filename: String,
21 pub mime_type: String,
24 pub purpose: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
30 pub expires_after: Option<FileExpirationPolicy>,
31}
32
33impl CreateUploadRequest {
34 pub fn builder() -> CreateUploadRequestBuilder {
37 CreateUploadRequestBuilder::default()
38 }
39}
40
41#[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 pub fn bytes(mut self, bytes: u64) -> Self {
57 self.bytes = Some(bytes);
58 self
59 }
60
61 pub fn filename(mut self, filename: impl Into<String>) -> Self {
64 self.filename = Some(filename.into());
65 self
66 }
67
68 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 pub fn purpose(mut self, purpose: impl Into<String>) -> Self {
78 self.purpose = Some(purpose.into());
79 self
80 }
81
82 pub fn expires_after(mut self, expires_after: FileExpirationPolicy) -> Self {
85 self.expires_after = Some(expires_after);
86 self
87 }
88
89 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#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
115#[non_exhaustive]
116pub struct Upload {
117 pub id: String,
120 pub object: String,
123 pub bytes: u64,
126 pub created_at: u64,
129 pub expires_at: u64,
132 pub filename: String,
135 pub purpose: String,
138 pub status: UploadStatus,
141 #[serde(default)]
144 pub file: Option<FileObject>,
145 #[serde(flatten)]
148 pub extra: BTreeMap<String, Value>,
149 #[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 pub fn request_id(&self) -> Option<&RequestId> {
164 self.request_id.as_ref()
165 }
166}
167
168#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
171#[serde(rename_all = "snake_case")]
172#[non_exhaustive]
173pub enum UploadStatus {
174 Pending,
177 Completed,
180 Cancelled,
183 Expired,
186 #[serde(other)]
189 Unknown,
190}
191
192#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
195#[non_exhaustive]
196pub struct UploadPart {
197 pub id: String,
200 pub object: String,
203 pub created_at: u64,
206 pub upload_id: String,
209 #[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 pub fn request_id(&self) -> Option<&RequestId> {
224 self.request_id.as_ref()
225 }
226}
227
228#[derive(Clone, Debug, PartialEq, Eq)]
231#[non_exhaustive]
232pub struct UploadPartData {
233 pub filename: String,
236 pub content_type: String,
239 content: Bytes,
240}
241
242impl UploadPartData {
243 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 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 pub fn bytes(&self) -> Bytes {
270 self.content.clone()
271 }
272}
273
274#[derive(Clone, Debug, PartialEq, Eq)]
277#[non_exhaustive]
278pub struct CreateUploadPartRequest {
279 pub data: UploadPartData,
282}
283
284impl CreateUploadPartRequest {
285 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#[derive(Clone, Debug, Default)]
319#[non_exhaustive]
320pub struct CreateUploadPartRequestBuilder {
321 data: Option<UploadPartData>,
322}
323
324impl CreateUploadPartRequestBuilder {
325 pub fn data(mut self, data: UploadPartData) -> Self {
328 self.data = Some(data);
329 self
330 }
331
332 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#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
345#[non_exhaustive]
346pub struct CompleteUploadRequest {
347 pub part_ids: Vec<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
353 pub md5: Option<String>,
354}
355
356impl CompleteUploadRequest {
357 pub fn builder() -> CompleteUploadRequestBuilder {
360 CompleteUploadRequestBuilder::default()
361 }
362}
363
364#[derive(Clone, Debug, Default)]
367#[non_exhaustive]
368pub struct CompleteUploadRequestBuilder {
369 part_ids: Vec<String>,
370 md5: Option<String>,
371}
372
373impl CompleteUploadRequestBuilder {
374 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 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 pub fn md5(mut self, md5: impl Into<String>) -> Self {
391 self.md5 = Some(md5.into());
392 self
393 }
394
395 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}