1use std::path::Path;
11use std::time::UNIX_EPOCH;
12
13use futures::StreamExt;
14use libfw_core::metadata::{FileMeta, encode_file_meta_header};
15use libfw_core::{
16 HEADER_COMPRESS, HEADER_FILE_META, HEADER_FINAL, HEADER_OFFSET, HEADER_SESSION,
17 HEADER_SESSION_STATUS,
18};
19use reqwest::StatusCode;
20use serde::Deserialize;
21use tokio::io::{AsyncReadExt, AsyncSeekExt};
22
23use super::{NativeClient, NativeEvent, compress_chunk, session_id_for, validate_relative};
24use crate::LibfwError;
25use crate::plan::FileEntry;
26use crate::tune::{CompressLevel, LEVEL_SAMPLE_SIZE, TransferKind};
27use crate::upload::aligned_missing;
28
29fn local_meta(local: &Path, remote: &str) -> Result<FileMeta, LibfwError> {
31 let meta = std::fs::metadata(local)
32 .map_err(|e| LibfwError::Storage(format!("stat {}: {e}", local.display())))?;
33 if !meta.is_file() {
34 return Err(LibfwError::Storage(format!(
35 "{} is not a regular file",
36 local.display()
37 )));
38 }
39 let mtime = meta
40 .modified()
41 .ok()
42 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
43 .map(|d| d.as_secs())
44 .unwrap_or(0);
45 Ok(FileMeta::new(remote, meta.len(), mtime))
46}
47
48async fn read_slice(path: &Path, start: u64, len: u64) -> Result<Vec<u8>, LibfwError> {
51 let mut file = tokio::fs::File::open(path)
52 .await
53 .map_err(|e| LibfwError::Storage(format!("open {}: {e}", path.display())))?;
54 file.seek(std::io::SeekFrom::Start(start))
55 .await
56 .map_err(|e| LibfwError::Storage(format!("seek: {e}")))?;
57 let mut buf = vec![0u8; len as usize];
58 file.read_exact(&mut buf)
59 .await
60 .map_err(|e| LibfwError::Storage(format!("read {} bytes at {start}: {e}", len)))?;
61 Ok(buf)
62}
63
64impl NativeClient {
65 pub async fn upload_file(&self, local: &Path, remote: &str) -> Result<u64, LibfwError> {
72 validate_relative(remote)?;
73 let _ = self.prepare_transfer(TransferKind::Upload).await?;
77 let meta = local_meta(local, remote)?;
78 let entry = FileEntry {
79 path: remote.to_string(),
80 size: meta.size,
81 mtime: meta.mtime,
82 };
83 let session = session_id_for(&meta);
84 let compress = self.config().compress;
85
86 self.emit(NativeEvent::FileStart {
87 path: remote.to_string(),
88 size: meta.size,
89 });
90
91 let caps = self.capabilities().unwrap_or_default();
94 let sample = if compress && self.config().compress_level == CompressLevel::Auto {
95 let len = (meta.size).min(LEVEL_SAMPLE_SIZE as u64) as usize;
96 if len == 0 {
97 None
98 } else {
99 read_slice(local, 0, len as u64).await.ok()
100 }
101 } else {
102 None
103 };
104 let mbps = self.with_tune(|t| t.stats().mbps).unwrap_or(0.0);
105 let level = self.upload_level(&caps, sample.as_deref(), mbps);
106
107 let outcome = self
108 .upload_session(local, &entry, &meta, &session, compress, level)
109 .await;
110 self.finish_transfer(outcome.is_ok());
111
112 let sent = outcome?;
113 self.emit(NativeEvent::FileDone {
114 path: remote.to_string(),
115 bytes: sent,
116 });
117 Ok(sent)
118 }
119
120 async fn upload_session(
122 &self,
123 local: &Path,
124 entry: &FileEntry,
125 meta: &FileMeta,
126 session: &str,
127 compress: bool,
128 level: i32,
129 ) -> Result<u64, LibfwError> {
130 let size = meta.size;
131 let mut sent = 0u64;
132 let mut rounds = 0u32;
133 let mut first_error: Option<LibfwError> = None;
134
135 let mut received = self.probe_session(meta, session).await?;
138 let covered: u64 = received
139 .iter()
140 .map(|(s, e)| e.saturating_sub(*s))
141 .sum::<u64>()
142 .min(size);
143 if covered > 0 {
144 self.log(format!(
145 "upload `{}`: server already holds {covered} of {size} bytes; resuming",
146 entry.path
147 ));
148 }
149
150 loop {
151 if rounds > 0 {
152 received = self.probe_session(meta, session).await?;
153 }
154 let window = self
155 .with_tune(|t| t.params().upload_window.max(1))
156 .unwrap_or(self.config().upload_window.max(1));
157 let chunk_size = self
158 .with_tune(|t| t.params().chunk_size.max(1))
159 .unwrap_or(self.config().chunk_size.max(1));
160 let missing = aligned_missing(entry, chunk_size, &received);
161
162 if missing.is_empty() {
165 match self.commit(meta, session).await {
166 Ok(()) => {
167 self.tune_tick(sent, None, false);
171 return Ok(sent);
172 }
173 Err(e) => {
174 if rounds >= self.config().max_retries {
175 return Err(e);
176 }
177 rounds += 1;
178 self.log(format!(
179 "commit failed for `{}`; re-verifying server state: {e}",
180 entry.path
181 ));
182 first_error.get_or_insert(e);
183 continue;
184 }
185 }
186 }
187
188 let local_owned = local.to_path_buf();
190 let meta_owned = meta.clone();
191 let session_owned = session.to_string();
192 let mut stream = futures::stream::iter(missing.into_iter().map(|(start, end)| {
193 let local = local_owned.clone();
194 let meta = meta_owned.clone();
195 let session = session_owned.clone();
196 async move { (start, self.post_block(&local, &meta, &session, start, end, compress, level).await) }
197 }))
198 .buffer_unordered(window);
199
200 let mut round_errors = 0u32;
201 while let Some((start, result)) = stream.next().await {
202 match result {
203 Ok(sent_len) => {
204 sent = sent.saturating_add(sent_len);
205 self.emit(NativeEvent::Progress {
206 done: sent,
207 total: size,
208 });
209 self.tune_tick(sent, None, false);
210 }
211 Err(e) => {
212 round_errors += 1;
213 self.log(format!("block at {start} of `{}` failed: {e}", entry.path));
214 first_error.get_or_insert(e);
215 }
216 }
217 }
218
219 match self.commit(meta, session).await {
222 Ok(()) => {
223 self.tune_tick(sent, None, round_errors > 0);
224 return Ok(sent);
225 }
226 Err(e) => {
227 rounds += 1;
228 if rounds > self.config().max_retries {
229 let received = self.probe_session(meta, session).await?;
232 if aligned_missing(entry, chunk_size, &received).is_empty() {
233 self.commit(meta, session).await?;
234 return Ok(sent);
235 }
236 return Err(first_error.unwrap_or_else(|| {
237 LibfwError::Protocol(format!(
238 "upload of `{}` did not converge after {rounds} rounds",
239 entry.path
240 ))
241 }));
242 }
243 self.log(format!(
244 "commit failed for `{}`; re-verifying server state: {e}",
245 entry.path
246 ));
247 first_error.get_or_insert(e);
248 }
249 }
250
251 self.tune_tick(sent, None, round_errors > 0);
252 }
253 }
254
255 async fn probe_session(
257 &self,
258 meta: &FileMeta,
259 session: &str,
260 ) -> Result<Vec<(u64, u64)>, LibfwError> {
261 let url = self.file_url(&meta.path);
262 let resp = self
263 .http()
264 .post(&url)
265 .headers(self.headers(false, None))
266 .header(HEADER_OFFSET, "0")
267 .header(HEADER_FILE_META, encode_file_meta_header(meta))
268 .header(HEADER_SESSION, session)
269 .header(HEADER_SESSION_STATUS, "1")
270 .send()
271 .await
272 .map_err(|e| LibfwError::Network(e.to_string()))?;
273 let status = resp.status().as_u16();
274 if status != 200 && status != 201 {
275 return Err(LibfwError::Http { status, url });
276 }
277 let body = resp
278 .bytes()
279 .await
280 .map_err(|e| LibfwError::Network(e.to_string()))?;
281 #[derive(Deserialize)]
282 struct Ranges {
283 #[serde(default)]
284 ranges: Vec<[u64; 2]>,
285 }
286 let parsed: Ranges = serde_json::from_slice(&body)
290 .map_err(|e| LibfwError::Protocol(format!("bad session-status JSON: {e}")))?;
291 Ok(parsed
292 .ranges
293 .into_iter()
294 .map(|[s, e]| (s, e.max(s)))
295 .collect())
296 }
297
298 async fn post_block(
300 &self,
301 local: &Path,
302 meta: &FileMeta,
303 session: &str,
304 start: u64,
305 end: u64,
306 compress: bool,
307 level: i32,
308 ) -> Result<u64, LibfwError> {
309 let raw = read_slice(local, start, end - start).await?;
310 let payload = if compress {
313 compress_chunk(&raw, level)?
314 } else {
315 raw
316 };
317 let len = payload.len() as u64;
318 let url = self.file_url(&meta.path);
319 let mut attempt = 0u32;
320 loop {
321 let mut request = self
322 .http()
323 .post(&url)
324 .headers(self.headers(false, None))
325 .header(HEADER_OFFSET, start.to_string())
326 .header(HEADER_FILE_META, encode_file_meta_header(meta))
327 .header(HEADER_SESSION, session)
328 .body(payload.clone());
329 if compress {
330 request = request.header(HEADER_COMPRESS, "zrip");
331 }
332 match request.send().await {
333 Ok(resp) if resp.status() == StatusCode::CREATED => return Ok(len),
334 Ok(resp) => {
335 let status = resp.status().as_u16();
336 let err = LibfwError::Http { status, url: url.clone() };
337 if attempt >= self.config().max_retries {
338 return Err(err);
339 }
340 attempt += 1;
341 self.log(format!(
342 "retrying block {start}..{end} of `{}` (attempt {attempt}): {err}",
343 meta.path
344 ));
345 tokio::time::sleep(self.backoff(attempt)).await;
346 }
347 Err(e) => {
348 if attempt >= self.config().max_retries {
349 return Err(LibfwError::Network(e.to_string()));
350 }
351 attempt += 1;
352 self.log(format!(
353 "retrying block {start}..{end} of `{}` (attempt {attempt}): {e}",
354 meta.path
355 ));
356 tokio::time::sleep(self.backoff(attempt)).await;
357 }
358 }
359 }
360 }
361
362 async fn commit(&self, meta: &FileMeta, session: &str) -> Result<(), LibfwError> {
364 let url = self.file_url(&meta.path);
365 let resp = self
366 .http()
367 .post(&url)
368 .headers(self.headers(false, None))
369 .header(HEADER_OFFSET, meta.size.to_string())
370 .header(HEADER_FILE_META, encode_file_meta_header(meta))
371 .header(HEADER_SESSION, session)
372 .header(HEADER_FINAL, "1")
373 .send()
374 .await
375 .map_err(|e| LibfwError::Network(e.to_string()))?;
376 let status = resp.status();
377 if status == StatusCode::CREATED {
378 Ok(())
379 } else {
380 Err(LibfwError::Http {
381 status: status.as_u16(),
382 url,
383 })
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn local_meta_rejects_missing_files() {
394 let dir = tempfile::tempdir().unwrap();
395 let missing = dir.path().join("nope.bin");
396 assert!(local_meta(&missing, "nope.bin").is_err());
397 }
398
399 #[test]
400 fn local_meta_computes_a_stable_session_etag() {
401 let dir = tempfile::tempdir().unwrap();
402 let path = dir.path().join("f.bin");
403 std::fs::write(&path, b"hello").unwrap();
404 let meta = local_meta(&path, "f.bin").unwrap();
405 assert_eq!(meta.size, 5);
406 assert_eq!(meta.path, "f.bin");
407 let again = local_meta(&path, "f.bin").unwrap();
410 assert_eq!(session_id_for(&meta), session_id_for(&again));
411 assert!(!session_id_for(&meta).is_empty());
412 }
413
414 #[tokio::test]
415 async fn read_slice_reads_the_requested_window() {
416 let dir = tempfile::tempdir().unwrap();
417 let path = dir.path().join("f.bin");
418 std::fs::write(&path, b"0123456789").unwrap();
419 assert_eq!(read_slice(&path, 2, 4).await.unwrap(), b"2345");
420 assert!(read_slice(&path, 8, 4).await.is_err());
422 }
423}