use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
pub use nextcloud_dav::DESTINATION_HEADER;
use nextcloud_sdk_core::{
ApiPath, AuthProvider, Error, Headers, HttpMethod, Request, Response, StatusError, Transport,
};
use crate::path::{
abort_path, assembly_path, chunk_path, destination_header_value, upload_collection_path,
};
use crate::session::UploadSession;
#[cfg(test)]
mod tests;
pub const TOTAL_LENGTH_HEADER: &str = "OC-Total-Length";
pub const MTIME_HEADER: &str = "X-OC-Mtime";
static UPLOAD_COUNTER: AtomicU64 = AtomicU64::new(1);
fn generate_upload_id() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let count = UPLOAD_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("chunked-upload-{now}-{count}")
}
#[derive(Clone, Copy, Debug)]
pub struct UploadClient<'a, T: Transport, A: AuthProvider> {
transport: &'a T,
auth: &'a A,
}
impl<'a, T: Transport, A: AuthProvider> UploadClient<'a, T, A> {
pub const fn new(transport: &'a T, auth: &'a A) -> Self {
Self { transport, auth }
}
pub const fn transport(&self) -> &'a T {
self.transport
}
pub const fn auth(&self) -> &'a A {
self.auth
}
pub async fn start(
&self,
user_id: &str,
destination_path: &str,
total_size: u64,
) -> Result<UploadSession, Error> {
let upload_id = generate_upload_id();
self.start_with_id(user_id, destination_path, total_size, &upload_id)
.await
}
pub async fn start_with_id(
&self,
user_id: &str,
destination_path: &str,
total_size: u64,
upload_id: &str,
) -> Result<UploadSession, Error> {
tracing::info!(
user_id,
destination_path,
total_size,
upload_id,
"Starting chunked upload session"
);
let destination = destination_header_value(user_id, destination_path)?;
let path = upload_collection_path(user_id, upload_id)?;
let mut headers = self.authenticated_headers();
headers.set(DESTINATION_HEADER, &destination);
let request = Request::new(HttpMethod::Mkcol, path.clone()).with_headers(headers);
let response = self.transport.execute(request).await?;
ensure_success(HttpMethod::Mkcol, &path, &response)?;
Ok(UploadSession::new(
user_id,
upload_id,
destination,
total_size,
))
}
pub async fn upload_chunk(
&self,
session: &UploadSession,
chunk_index: u32,
chunk_data: Vec<u8>,
) -> Result<(), Error> {
let path = chunk_path(session.user_id(), session.upload_id(), chunk_index)?;
tracing::debug!(
upload_id = session.upload_id(),
chunk_index,
bytes = chunk_data.len(),
"Uploading file chunk"
);
let mut headers = self.authenticated_headers();
headers.set(DESTINATION_HEADER, session.destination_path());
headers.set(TOTAL_LENGTH_HEADER, session.total_size().to_string());
let request = Request::new(HttpMethod::Put, path.clone())
.with_headers(headers)
.body_bytes(chunk_data);
let response = self.transport.execute(request).await?;
ensure_success(HttpMethod::Put, &path, &response)?;
session.mark_chunk_uploaded(chunk_index);
Ok(())
}
pub async fn finish(&self, session: &UploadSession, mtime: Option<u64>) -> Result<(), Error> {
let path = assembly_path(session.user_id(), session.upload_id())?;
tracing::info!(
upload_id = session.upload_id(),
destination = session.destination_path(),
"Finalizing chunked upload assembly"
);
let mut headers = self.authenticated_headers();
headers.set(DESTINATION_HEADER, session.destination_path());
headers.set(TOTAL_LENGTH_HEADER, session.total_size().to_string());
if let Some(mtime) = mtime {
headers.set(MTIME_HEADER, mtime.to_string());
}
let request = Request::new(HttpMethod::Move, path.clone()).with_headers(headers);
let response = self.transport.execute(request).await?;
ensure_success(HttpMethod::Move, &path, &response)
}
pub async fn abort(&self, session: &UploadSession) -> Result<(), Error> {
let path = abort_path(session.user_id(), session.upload_id())?;
tracing::warn!(
upload_id = session.upload_id(),
"Aborting chunked upload session"
);
let headers = self.authenticated_headers();
let request = Request::new(HttpMethod::Delete, path.clone()).with_headers(headers);
let response = self.transport.execute(request).await?;
ensure_success(HttpMethod::Delete, &path, &response)
}
fn authenticated_headers(&self) -> Headers {
let mut headers = Headers::new();
self.auth.authenticate(&mut headers);
headers
}
}
fn ensure_success(method: HttpMethod, path: &ApiPath, response: &Response) -> Result<(), Error> {
if !response.status().is_success() {
tracing::error!(
method = ?method,
path = %path.as_str(),
status = %response.status(),
"Chunked upload operation failed"
);
return Err(Error::Status(StatusError::new(
method.as_str(),
path.clone(),
response.status(),
response.body(),
)));
}
tracing::trace!(method = ?method, path = %path.as_str(), "Chunked upload request succeeded");
Ok(())
}
impl<T: Transport, A: AuthProvider> fmt::Display for UploadClient<'_, T, A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("UploadClient")
}
}