use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
use bytes::Bytes;
use objectstore_types::metadata::Metadata;
use objectstore_types::resumable::{
CompleteUploadResponse, CreateSessionResponse, HEADER_UPLOAD_LENGTH, HEADER_UPLOAD_OFFSET,
UploadOffset,
};
use reqwest::{Method, Response, StatusCode};
use serde::Serialize;
pub use objectstore_types::resumable::{SessionToken, UploadProgress};
use crate::response::ResponseExt as _;
use crate::{Compression, Error, ExpirationPolicy, ObjectKey, Session};
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum UploadType {
Resumable,
}
#[derive(Serialize)]
struct UploadTypeQuery {
upload_type: UploadType,
}
#[derive(Serialize)]
struct SessionQuery<'a> {
session: &'a SessionToken,
}
#[derive(Clone, Debug)]
pub struct ResumableUpload {
session: Session,
key: ObjectKey,
token: SessionToken,
}
impl Session {
pub fn create_upload(&self, object_length: u64) -> CreateResumableUploadBuilder {
let metadata = Metadata {
expiration_policy: self.scope.usecase().expiration_policy(),
..Default::default()
};
CreateResumableUploadBuilder {
session: self.clone(),
total_length: object_length,
key: None,
metadata,
}
}
pub fn resume_upload(&self, key: impl Into<ObjectKey>, token: SessionToken) -> ResumableUpload {
ResumableUpload {
session: self.clone(),
key: key.into(),
token,
}
}
}
impl ResumableUpload {
pub fn key(&self) -> &str {
&self.key
}
pub fn token(&self) -> &SessionToken {
&self.token
}
pub fn progress(&self) -> UploadProgressBuilder {
UploadProgressBuilder {
upload: self.clone(),
}
}
pub fn put(&self, offset: u64, chunk: impl Into<Bytes>) -> PutChunkBuilder {
PutChunkBuilder {
upload: self.clone(),
offset,
chunk: chunk.into(),
}
}
pub fn cancel(&self) -> CancelUploadBuilder {
CancelUploadBuilder {
upload: self.clone(),
}
}
fn request(&self, method: Method) -> crate::Result<reqwest::RequestBuilder> {
Ok(self
.session
.request(method, &self.key)?
.query(&SessionQuery {
session: &self.token,
}))
}
}
#[derive(Debug)]
pub struct CreateResumableUploadBuilder {
session: Session,
total_length: u64,
key: Option<ObjectKey>,
metadata: Metadata,
}
impl CreateResumableUploadBuilder {
pub fn key(mut self, key: impl Into<ObjectKey>) -> Self {
self.key = Some(key.into()).filter(|key| !key.is_empty());
self
}
pub fn content_type(mut self, content_type: impl Into<Cow<'static, str>>) -> Self {
self.metadata.content_type = content_type.into();
self
}
pub fn expiration_policy(mut self, expiration_policy: ExpirationPolicy) -> Self {
self.metadata.expiration_policy = expiration_policy;
self
}
pub fn compression(mut self, compression: impl Into<Option<Compression>>) -> Self {
self.metadata.compression = compression.into();
self
}
pub fn origin(mut self, origin: impl Into<String>) -> Self {
self.metadata.origin = Some(origin.into());
self
}
pub fn filename(mut self, filename: impl Into<String>) -> Self {
self.metadata.filename = Some(filename.into());
self
}
pub fn set_metadata(mut self, metadata: impl Into<BTreeMap<String, String>>) -> Self {
self.metadata.custom = metadata.into();
self
}
pub fn append_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.custom.insert(key.into(), value.into());
self
}
pub async fn send(self) -> crate::Result<Option<ResumableUpload>> {
let method = if self.key.is_some() {
Method::PUT
} else {
Method::POST
};
let request = self
.session
.request(method, self.key.as_deref().unwrap_or_default())?
.query(&UploadTypeQuery {
upload_type: UploadType::Resumable,
})
.headers(self.metadata.to_headers("")?)
.header(HEADER_UPLOAD_LENGTH, self.total_length.to_string());
let response = request.send().await?;
match response.status() {
StatusCode::OK => {}
StatusCode::NOT_IMPLEMENTED => {
response.drain_body().await;
return Ok(None);
}
status => {
let response = response.error_for_status_and_drain().await?;
response.drain_body().await;
return Err(Error::MalformedResponse(format!(
"unexpected HTTP status {status} while creating a resumable upload"
)));
}
}
let response: CreateSessionResponse = response.json().await?;
Ok(Some(
self.session.resume_upload(response.key, response.session),
))
}
}
#[derive(Debug)]
pub struct UploadProgressBuilder {
upload: ResumableUpload,
}
impl UploadProgressBuilder {
pub async fn send(self) -> crate::Result<UploadProgress> {
let response = self
.upload
.request(Method::PUT)?
.header(HEADER_UPLOAD_OFFSET, "*")
.send()
.await?;
parse_progress_response(response).await
}
}
pub struct PutChunkBuilder {
upload: ResumableUpload,
offset: u64,
chunk: Bytes,
}
impl fmt::Debug for PutChunkBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PutChunkBuilder")
.field("upload", &self.upload)
.field("offset", &self.offset)
.field("content_length", &self.chunk.len())
.finish()
}
}
impl PutChunkBuilder {
pub async fn send(self) -> crate::Result<UploadProgress> {
let content_length = self.chunk.len();
let response = self
.upload
.request(Method::PUT)?
.header(HEADER_UPLOAD_OFFSET, self.offset.to_string())
.header(reqwest::header::CONTENT_LENGTH, content_length)
.body(self.chunk)
.send()
.await?;
parse_progress_response(response).await
}
}
#[derive(Debug)]
pub struct CancelUploadBuilder {
upload: ResumableUpload,
}
impl CancelUploadBuilder {
pub async fn send(self) -> crate::Result<()> {
let response = self.upload.request(Method::DELETE)?.send().await?;
match response.status() {
StatusCode::NO_CONTENT => {
response.drain_body().await;
Ok(())
}
StatusCode::NOT_FOUND | StatusCode::GONE => {
response.drain_body().await;
Err(Error::ResumableUploadUnavailable)
}
status => {
let response = response.error_for_status_and_drain().await?;
response.drain_body().await;
Err(Error::MalformedResponse(format!(
"unexpected HTTP status {status} while canceling a resumable upload"
)))
}
}
}
}
async fn parse_progress_response(response: Response) -> crate::Result<UploadProgress> {
match response.status() {
StatusCode::NO_CONTENT | StatusCode::CONFLICT => {
let offset = parse_offset(&response);
response.drain_body().await;
let offset = offset.ok_or_else(|| {
crate::Error::MalformedResponse(
"resumable upload response has no valid Upload-Offset header".into(),
)
})?;
Ok(UploadProgress::Incomplete { offset })
}
StatusCode::CREATED => {
let _: CompleteUploadResponse = response.json().await?;
Ok(UploadProgress::Complete)
}
StatusCode::NOT_FOUND | StatusCode::GONE => {
response.drain_body().await;
Err(Error::ResumableUploadUnavailable)
}
status => {
let response = response.error_for_status_and_drain().await?;
response.drain_body().await;
Err(Error::MalformedResponse(format!(
"unexpected HTTP status {status} while continuing a resumable upload"
)))
}
}
}
fn parse_offset(response: &Response) -> Option<u64> {
let value = response
.headers()
.get(HEADER_UPLOAD_OFFSET)?
.to_str()
.ok()?;
match value.parse().ok()? {
UploadOffset::At(offset) => Some(offset),
UploadOffset::Unknown => None,
}
}