use crate::error::LingerError;
use crate::files::{FileExpirationPolicy, FileObject};
use crate::transport::HttpRequest;
use crate::RequestId;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateUploadRequest {
pub bytes: u64,
pub filename: String,
pub mime_type: String,
pub purpose: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_after: Option<FileExpirationPolicy>,
}
impl CreateUploadRequest {
pub fn builder() -> CreateUploadRequestBuilder {
CreateUploadRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateUploadRequestBuilder {
bytes: Option<u64>,
filename: Option<String>,
mime_type: Option<String>,
purpose: Option<String>,
expires_after: Option<FileExpirationPolicy>,
}
impl CreateUploadRequestBuilder {
pub fn bytes(mut self, bytes: u64) -> Self {
self.bytes = Some(bytes);
self
}
pub fn filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
pub fn purpose(mut self, purpose: impl Into<String>) -> Self {
self.purpose = Some(purpose.into());
self
}
pub fn expires_after(mut self, expires_after: FileExpirationPolicy) -> Self {
self.expires_after = Some(expires_after);
self
}
pub fn build(self) -> Result<CreateUploadRequest, LingerError> {
let bytes = self
.bytes
.filter(|bytes| *bytes > 0)
.ok_or_else(|| LingerError::invalid_config("bytes must be greater than zero"))?;
let filename = required_string("filename", self.filename)?;
let mime_type = required_string("mime_type", self.mime_type)?;
let purpose = required_string("purpose", self.purpose)?;
if let Some(expires_after) = &self.expires_after {
expires_after.validate_for_uploads()?;
}
Ok(CreateUploadRequest {
bytes,
filename,
mime_type,
purpose,
expires_after: self.expires_after,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct Upload {
pub id: String,
pub object: String,
pub bytes: u64,
pub created_at: u64,
pub expires_at: u64,
pub filename: String,
pub purpose: String,
pub status: UploadStatus,
#[serde(default)]
pub file: Option<FileObject>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl Upload {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum UploadStatus {
Pending,
Completed,
Cancelled,
Expired,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct UploadPart {
pub id: String,
pub object: String,
pub created_at: u64,
pub upload_id: String,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl UploadPart {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct UploadPartData {
pub filename: String,
pub content_type: String,
content: Bytes,
}
impl UploadPartData {
pub fn from_bytes(
filename: impl Into<String>,
content: impl Into<Bytes>,
) -> Result<Self, LingerError> {
let filename = filename.into();
validate_header_param("filename", &filename)?;
Ok(Self {
filename,
content_type: "application/octet-stream".to_string(),
content: content.into(),
})
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
let content_type = content_type.into();
validate_header_value("content_type", &content_type)?;
self.content_type = content_type;
Ok(self)
}
pub fn bytes(&self) -> Bytes {
self.content.clone()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateUploadPartRequest {
pub data: UploadPartData,
}
impl CreateUploadPartRequest {
pub fn builder() -> CreateUploadPartRequestBuilder {
CreateUploadPartRequestBuilder::default()
}
pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
let boundary = multipart_boundary(&self.data.content);
request.insert_header(
"content-type",
format!("multipart/form-data; boundary={boundary}"),
);
request.set_body_stream(self.multipart_stream(boundary));
}
fn multipart_stream(
&self,
boundary: String,
) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
let mut chunks = Vec::new();
chunks.push(Ok(Bytes::from(format!(
"--{boundary}\r\nContent-Disposition: form-data; name=\"data\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
escape_multipart_param(&self.data.filename),
self.data.content_type
))));
chunks.push(Ok(self.data.content.clone()));
chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
futures_util::stream::iter(chunks)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateUploadPartRequestBuilder {
data: Option<UploadPartData>,
}
impl CreateUploadPartRequestBuilder {
pub fn data(mut self, data: UploadPartData) -> Self {
self.data = Some(data);
self
}
pub fn build(self) -> Result<CreateUploadPartRequest, LingerError> {
let data = self
.data
.ok_or_else(|| LingerError::invalid_config("data is required"))?;
Ok(CreateUploadPartRequest { data })
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompleteUploadRequest {
pub part_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub md5: Option<String>,
}
impl CompleteUploadRequest {
pub fn builder() -> CompleteUploadRequestBuilder {
CompleteUploadRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CompleteUploadRequestBuilder {
part_ids: Vec<String>,
md5: Option<String>,
}
impl CompleteUploadRequestBuilder {
pub fn part_id(mut self, part_id: impl Into<String>) -> Self {
self.part_ids.push(part_id.into());
self
}
pub fn part_ids(mut self, part_ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.part_ids = part_ids.into_iter().map(Into::into).collect();
self
}
pub fn md5(mut self, md5: impl Into<String>) -> Self {
self.md5 = Some(md5.into());
self
}
pub fn build(self) -> Result<CompleteUploadRequest, LingerError> {
if self.part_ids.is_empty() {
return Err(LingerError::invalid_config("part_ids is required"));
}
for part_id in &self.part_ids {
if part_id.trim().is_empty() {
return Err(LingerError::invalid_config(
"part_ids must not contain empty values",
));
}
}
if self
.md5
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
return Err(LingerError::invalid_config("md5 must not be empty"));
}
Ok(CompleteUploadRequest {
part_ids: self.part_ids,
md5: self.md5,
})
}
}
fn required_string(name: &str, value: Option<String>) -> Result<String, LingerError> {
value
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| LingerError::invalid_config(format!("{name} is required")))
}
fn multipart_boundary(content: &Bytes) -> String {
for counter in 0.. {
let boundary = format!("linger-openai-sdk-upload-boundary-{counter}");
if !contains_bytes(content, boundary.as_bytes()) {
return boundary;
}
}
unreachable!("unbounded boundary counter")
}
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() {
return true;
}
haystack
.windows(needle.len())
.any(|window| window == needle)
}
fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
if value.trim().is_empty() {
return Err(LingerError::invalid_config(format!("{name} is required")));
}
validate_header_value(name, value)
}
fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
if value.contains('\r') || value.contains('\n') {
return Err(LingerError::invalid_config(format!(
"{name} must not contain CR or LF"
)));
}
Ok(())
}
fn escape_multipart_param(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}