1use std::fmt;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7pub use nextcloud_dav::DESTINATION_HEADER;
8use nextcloud_sdk_core::{
9 ApiPath, AuthProvider, Error, Headers, HttpMethod, Request, Response, StatusError, Transport,
10};
11
12use crate::path::{
13 abort_path, assembly_path, chunk_path, destination_header_value, upload_collection_path,
14};
15use crate::session::UploadSession;
16
17#[cfg(test)]
18mod tests;
19
20pub const TOTAL_LENGTH_HEADER: &str = "OC-Total-Length";
22
23pub const MTIME_HEADER: &str = "X-OC-Mtime";
25
26static UPLOAD_COUNTER: AtomicU64 = AtomicU64::new(1);
27
28fn generate_upload_id() -> String {
30 let now = SystemTime::now()
31 .duration_since(UNIX_EPOCH)
32 .unwrap_or_default()
33 .as_millis();
34 let count = UPLOAD_COUNTER.fetch_add(1, Ordering::Relaxed);
35 format!("chunked-upload-{now}-{count}")
36}
37
38#[derive(Clone, Copy, Debug)]
46pub struct UploadClient<'a, T: Transport, A: AuthProvider> {
47 transport: &'a T,
48 auth: &'a A,
49}
50
51impl<'a, T: Transport, A: AuthProvider> UploadClient<'a, T, A> {
52 pub const fn new(transport: &'a T, auth: &'a A) -> Self {
54 Self { transport, auth }
55 }
56
57 pub const fn transport(&self) -> &'a T {
59 self.transport
60 }
61
62 pub const fn auth(&self) -> &'a A {
64 self.auth
65 }
66
67 pub async fn start(
71 &self,
72 user_id: &str,
73 destination_path: &str,
74 total_size: u64,
75 ) -> Result<UploadSession, Error> {
76 let upload_id = generate_upload_id();
77 self.start_with_id(user_id, destination_path, total_size, &upload_id)
78 .await
79 }
80
81 pub async fn start_with_id(
85 &self,
86 user_id: &str,
87 destination_path: &str,
88 total_size: u64,
89 upload_id: &str,
90 ) -> Result<UploadSession, Error> {
91 tracing::info!(
92 user_id,
93 destination_path,
94 total_size,
95 upload_id,
96 "Starting chunked upload session"
97 );
98 let destination = destination_header_value(user_id, destination_path)?;
99 let path = upload_collection_path(user_id, upload_id)?;
100
101 let mut headers = self.authenticated_headers();
102 headers.set(DESTINATION_HEADER, &destination);
103
104 let request = Request::new(HttpMethod::Mkcol, path.clone()).with_headers(headers);
105 let response = self.transport.execute(request).await?;
106
107 ensure_success(HttpMethod::Mkcol, &path, &response)?;
108
109 Ok(UploadSession::new(
110 user_id,
111 upload_id,
112 destination,
113 total_size,
114 ))
115 }
116
117 pub async fn upload_chunk(
126 &self,
127 session: &UploadSession,
128 chunk_index: u32,
129 chunk_data: Vec<u8>,
130 ) -> Result<(), Error> {
131 let path = chunk_path(session.user_id(), session.upload_id(), chunk_index)?;
132 tracing::debug!(
133 upload_id = session.upload_id(),
134 chunk_index,
135 bytes = chunk_data.len(),
136 "Uploading file chunk"
137 );
138
139 let mut headers = self.authenticated_headers();
140 headers.set(DESTINATION_HEADER, session.destination_path());
141 headers.set(TOTAL_LENGTH_HEADER, session.total_size().to_string());
142
143 let request = Request::new(HttpMethod::Put, path.clone())
144 .with_headers(headers)
145 .body_bytes(chunk_data);
146
147 let response = self.transport.execute(request).await?;
148 ensure_success(HttpMethod::Put, &path, &response)?;
149
150 session.mark_chunk_uploaded(chunk_index);
151 Ok(())
152 }
153
154 pub async fn finish(&self, session: &UploadSession, mtime: Option<u64>) -> Result<(), Error> {
161 let path = assembly_path(session.user_id(), session.upload_id())?;
162 tracing::info!(
163 upload_id = session.upload_id(),
164 destination = session.destination_path(),
165 "Finalizing chunked upload assembly"
166 );
167
168 let mut headers = self.authenticated_headers();
169 headers.set(DESTINATION_HEADER, session.destination_path());
170 headers.set(TOTAL_LENGTH_HEADER, session.total_size().to_string());
171 if let Some(mtime) = mtime {
172 headers.set(MTIME_HEADER, mtime.to_string());
173 }
174
175 let request = Request::new(HttpMethod::Move, path.clone()).with_headers(headers);
176 let response = self.transport.execute(request).await?;
177
178 ensure_success(HttpMethod::Move, &path, &response)
179 }
180
181 pub async fn abort(&self, session: &UploadSession) -> Result<(), Error> {
185 let path = abort_path(session.user_id(), session.upload_id())?;
186 tracing::warn!(
187 upload_id = session.upload_id(),
188 "Aborting chunked upload session"
189 );
190
191 let headers = self.authenticated_headers();
192 let request = Request::new(HttpMethod::Delete, path.clone()).with_headers(headers);
193 let response = self.transport.execute(request).await?;
194
195 ensure_success(HttpMethod::Delete, &path, &response)
196 }
197
198 fn authenticated_headers(&self) -> Headers {
199 let mut headers = Headers::new();
200 self.auth.authenticate(&mut headers);
201 headers
202 }
203}
204
205fn ensure_success(method: HttpMethod, path: &ApiPath, response: &Response) -> Result<(), Error> {
206 if !response.status().is_success() {
207 tracing::error!(
208 method = ?method,
209 path = %path.as_str(),
210 status = %response.status(),
211 "Chunked upload operation failed"
212 );
213 return Err(Error::Status(StatusError::new(
214 method.as_str(),
215 path.clone(),
216 response.status(),
217 response.body(),
218 )));
219 }
220 tracing::trace!(method = ?method, path = %path.as_str(), "Chunked upload request succeeded");
221 Ok(())
222}
223
224impl<T: Transport, A: AuthProvider> fmt::Display for UploadClient<'_, T, A> {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 f.write_str("UploadClient")
227 }
228}