use super::InsertObjectOptional;
use crate::{
objects::{Metadata, Object},
types::{BucketName, ObjectName},
Error,
};
use std::io;
#[cfg(feature = "async-multipart")]
mod async_mp;
#[cfg(feature = "async-multipart")]
pub use async_mp::*;
const MULTI_PART_SEPARATOR: &[u8] = b"--tame_gcs\n";
const MULTI_PART_SUFFIX: &[u8] = b"\n--tame_gcs--";
const MULTI_PART_CT: &[u8] = b"content-type: application/json; charset=utf-8\n\n";
enum MultipartPart {
Prefix,
Body,
Suffix,
End,
}
impl MultipartPart {
fn next(&mut self) {
match self {
MultipartPart::Prefix => *self = MultipartPart::Body,
MultipartPart::Body => *self = MultipartPart::Suffix,
MultipartPart::Suffix => *self = MultipartPart::End,
MultipartPart::End => unreachable!(),
}
}
}
struct MultipartCursor {
position: usize,
part: MultipartPart,
}
pub struct Multipart<B> {
body: B,
prefix: bytes::Bytes,
body_len: u64,
total_len: u64,
cursor: MultipartCursor,
}
impl<B> Multipart<B> {
#[cfg(feature = "async-multipart")]
pin_utils::unsafe_pinned!(body: B);
pub fn wrap(body: B, body_length: u64, metadata: &Metadata) -> Result<Self, Error> {
use bytes::BufMut;
const CT_HN: &[u8] = b"content-type: ";
let serialized_metadata = serde_json::to_vec(metadata)?;
let content_type = metadata
.content_type
.as_deref()
.unwrap_or("application/octet-stream")
.as_bytes();
let metadata = &serialized_metadata[..];
let prefix_len = MULTI_PART_SEPARATOR.len()
+ MULTI_PART_CT.len()
+ metadata.len()
+ 1
+ MULTI_PART_SEPARATOR.len()
+ CT_HN.len()
+ content_type.len()
+ 2;
let prefix = {
let mut prefix = bytes::BytesMut::with_capacity(prefix_len);
prefix.put_slice(MULTI_PART_SEPARATOR);
prefix.put_slice(MULTI_PART_CT);
prefix.put_slice(metadata);
prefix.put_slice(b"\n");
prefix.put_slice(MULTI_PART_SEPARATOR);
prefix.put_slice(CT_HN);
prefix.put_slice(content_type);
prefix.put_slice(b"\n\n");
prefix.freeze()
};
let total_len = prefix_len as u64 + body_length + MULTI_PART_SUFFIX.len() as u64;
Ok(Self {
body,
prefix,
body_len: body_length,
total_len,
cursor: MultipartCursor {
position: 0,
part: MultipartPart::Prefix,
},
})
}
pub fn total_len(&self) -> u64 {
self.total_len
}
}
impl<B> io::Read for Multipart<B>
where
B: io::Read,
{
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
use std::cmp::min;
let mut total_copied = 0;
while total_copied < buffer.len() {
let buf = &mut buffer[total_copied..];
let (copied, len) = match self.cursor.part {
MultipartPart::Prefix => {
let to_copy = min(buf.len(), self.prefix.len() - self.cursor.position);
buf[..to_copy].copy_from_slice(
&self.prefix[self.cursor.position..self.cursor.position + to_copy],
);
(to_copy, self.prefix.len())
}
MultipartPart::Body => {
let copied = self.body.read(buf)?;
(copied, self.body_len as usize)
}
MultipartPart::Suffix => {
let to_copy = min(buf.len(), MULTI_PART_SUFFIX.len() - self.cursor.position);
buf[..to_copy].copy_from_slice(
&MULTI_PART_SUFFIX[self.cursor.position..self.cursor.position + to_copy],
);
(to_copy, MULTI_PART_SUFFIX.len())
}
MultipartPart::End => return Ok(total_copied),
};
self.cursor.position += copied;
total_copied += copied;
if self.cursor.position == len {
self.cursor.part.next();
self.cursor.position = 0;
}
}
Ok(total_copied)
}
}
impl Object {
pub fn insert_multipart<B>(
&self,
bucket: &BucketName<'_>,
content: B,
length: u64,
metadata: &Metadata,
optional: Option<InsertObjectOptional<'_>>,
) -> Result<http::Request<Multipart<B>>, Error> {
match metadata.name {
Some(ref name) => ObjectName::try_from(name.as_ref())?,
None => {
return Err(Error::InvalidLength {
len: 0,
min: 1,
max: 1024,
})
}
};
let mut uri = format!(
"https://{}/upload/storage/v1/b/{}/o?uploadType=multipart",
self.authority.as_str(),
percent_encoding::percent_encode(bucket.as_ref(), crate::util::PATH_ENCODE_SET,),
);
let query = optional.unwrap_or_default();
let multipart = Multipart::wrap(content, length, metadata)?;
let req_builder = http::Request::builder()
.header(
http::header::CONTENT_TYPE,
http::header::HeaderValue::from_static("multipart/related; boundary=tame_gcs"),
)
.header(http::header::CONTENT_LENGTH, multipart.total_len());
let query_params = serde_urlencoded::to_string(query)?;
if !query_params.is_empty() {
uri.push('&');
uri.push_str(&query_params);
}
Ok(req_builder.method("POST").uri(uri).body(multipart)?)
}
}