1use async_trait::async_trait;
2use reqwest::header::{CONTENT_LENGTH, ETAG};
3use reqwest::{Client, Method};
4use std::sync::Arc;
5use std::time::Duration;
6use tokio::fs::OpenOptions;
7use tokio::time::sleep;
8
9use crate::chunk_outcome::ChunkOutcome;
10use crate::direction::Direction;
11use crate::error::{InnerErrorCode, MeowError};
12use crate::http_breakpoint::{
13 BreakpointDownload, BreakpointUpload, DefaultStyleUpload, DownloadHeadCtx,
14 StandardRangeDownload, UploadPrepareCtx,
15};
16use crate::prepare_outcome::PrepareOutcome;
17use crate::transfer_executor_trait::TransferTrait;
18use crate::transfer_task::TransferTask;
19
20use super::default_http_transfer_chunks::{
21 download_one_chunk, download_one_chunk_part_positioned, map_reqwest, upload_one_chunk,
22 upload_one_chunk_part,
23};
24
25pub(crate) fn default_breakpoint_arcs() -> (
27 Arc<dyn BreakpointUpload + Send + Sync>,
28 Arc<dyn BreakpointDownload + Send + Sync>,
29) {
30 (
31 Arc::new(DefaultStyleUpload::default()),
32 Arc::new(StandardRangeDownload::default()),
33 )
34}
35
36const DEFAULT_POOL_MAX_IDLE_PER_HOST: usize = 16;
44
45const DEFAULT_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
54
55const DEFAULT_CONNECT_TIMEOUT_CAP: Duration = Duration::from_secs(10);
64
65pub(crate) fn build_internal_client(
74 http_timeout: Duration,
75 tcp_keepalive: Duration,
76) -> Result<reqwest::Client, reqwest::Error> {
77 Client::builder()
78 .timeout(http_timeout)
79 .connect_timeout(http_timeout.min(DEFAULT_CONNECT_TIMEOUT_CAP))
82 .tcp_keepalive(tcp_keepalive)
83 .pool_max_idle_per_host(DEFAULT_POOL_MAX_IDLE_PER_HOST)
88 .pool_idle_timeout(Some(DEFAULT_POOL_IDLE_TIMEOUT))
89 .build()
90}
91
92pub struct DefaultHttpTransfer {
94 client: reqwest::Client,
96 fallback_upload: Arc<dyn BreakpointUpload + Send + Sync>,
98 fallback_download: Arc<dyn BreakpointDownload + Send + Sync>,
100}
101
102impl DefaultHttpTransfer {
103 pub fn new() -> Self {
114 Self::with_http_timeouts(Duration::from_secs(5), Duration::from_secs(30))
115 }
116
117 pub fn with_http_timeouts(http_timeout: Duration, tcp_keepalive: Duration) -> Self {
137 let client = match build_internal_client(http_timeout, tcp_keepalive) {
140 Ok(c) => c,
141 Err(e) => {
142 crate::meow_warn_log!(
143 "http_client",
144 "with_http_timeouts build failed, fallback to Client::new(): {}",
145 crate::log::redact_secrets(&e.to_string())
146 );
147 Client::new()
148 }
149 };
150 Self {
151 client,
152 fallback_upload: Arc::new(DefaultStyleUpload::default()),
153 fallback_download: Arc::new(StandardRangeDownload::default()),
154 }
155 }
156
157 pub fn try_with_http_timeouts(
178 http_timeout: Duration,
179 tcp_keepalive: Duration,
180 ) -> Result<Self, MeowError> {
181 let client = build_internal_client(http_timeout, tcp_keepalive)
182 .map_err(|e| {
183 MeowError::from_source(
184 InnerErrorCode::HttpClientBuildFailed,
185 format!(
186 "build reqwest client failed (timeout={:?}, keepalive={:?})",
187 http_timeout, tcp_keepalive
188 ),
189 e,
190 )
191 })?;
192 Ok(Self {
193 client,
194 fallback_upload: Arc::new(DefaultStyleUpload::default()),
195 fallback_download: Arc::new(StandardRangeDownload::default()),
196 })
197 }
198
199 pub fn with_client(client: reqwest::Client) -> Self {
211 Self {
212 client,
213 fallback_upload: Arc::new(DefaultStyleUpload::default()),
214 fallback_download: Arc::new(StandardRangeDownload::default()),
215 }
216 }
217
218 pub fn with_fallbacks(
236 client: reqwest::Client,
237 upload: Arc<dyn BreakpointUpload + Send + Sync>,
238 download: Arc<dyn BreakpointDownload + Send + Sync>,
239 ) -> Self {
240 Self {
241 client,
242 fallback_upload: upload,
243 fallback_download: download,
244 }
245 }
246
247 fn client_for(&self, task: &TransferTask) -> reqwest::Client {
249 task.http_client_ref()
250 .cloned()
251 .unwrap_or_else(|| self.client.clone())
252 }
253
254 fn upload_arc(&self, task: &TransferTask) -> Arc<dyn BreakpointUpload + Send + Sync> {
256 match task.breakpoint_upload() {
257 Some(a) => a.clone(),
258 None => self.fallback_upload.clone(),
259 }
260 }
261
262 fn download_arc(&self, task: &TransferTask) -> Arc<dyn BreakpointDownload + Send + Sync> {
264 match task.breakpoint_download() {
265 Some(a) => a.clone(),
266 None => self.fallback_download.clone(),
267 }
268 }
269}
270
271impl Default for DefaultHttpTransfer {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277async fn upload_prepare(
278 client: &reqwest::Client,
279 task: &TransferTask,
280 upload: Arc<dyn BreakpointUpload + Send + Sync>,
281 local_offset: u64,
282) -> Result<PrepareOutcome, MeowError> {
283 let max_retries = task.max_upload_prepare_retries();
284 let mut attempt: u32 = 0;
285 loop {
286 crate::meow_flow_log!(
287 "upload_prepare",
288 "start: file={} local_offset={} total={} attempt={} max_retries={}",
289 task.file_name(),
290 local_offset,
291 task.total_size(),
292 attempt,
293 max_retries
294 );
295 match upload_prepare_once(client, task, upload.clone(), local_offset).await {
296 Ok(outcome) => {
297 if attempt > 0 {
298 crate::meow_key_log!(
299 "upload_prepare",
300 "prepare retry recovered: file={} attempts_used={}",
301 task.file_name(),
302 attempt
303 );
304 }
305 return Ok(outcome);
306 }
307 Err(err) => {
308 let retryable = crate::inner::exec_impl::retry::is_transport_retryable(&err);
309 let reached_limit = attempt >= max_retries;
310 if !retryable || reached_limit {
311 crate::log::emit_lazy(|| {
312 let mut log = crate::log::Log::error(
313 "upload_prepare",
314 format!(
315 "prepare give up: file={} attempt={} max_retries={} retryable={} err={}",
316 task.file_name(),
317 attempt,
318 max_retries,
319 retryable,
320 crate::log::redact_secrets(&err.to_string())
321 ),
322 )
323 .with_key(task.file_name())
324 .with_offset(local_offset)
325 .with_attempt(attempt)
326 .with_max_retries(max_retries);
327 if let Some(s) = err.http_status() {
328 log = log.with_http_status(s);
329 }
330 log
331 });
332 return Err(err);
333 }
334 let delay_ms = crate::inner::exec_impl::retry::calc_backoff_with_jitter_ms(attempt);
335 crate::meow_warn_log!(
336 "upload_prepare",
337 "prepare retry scheduled: file={} next_attempt={} delay_ms={} err={}",
338 task.file_name(),
339 attempt + 1,
340 delay_ms,
341 crate::log::redact_secrets(&err.to_string())
342 );
343 sleep(Duration::from_millis(delay_ms)).await;
344 attempt += 1;
345 }
346 }
347 }
348}
349
350async fn upload_prepare_once(
351 client: &reqwest::Client,
352 task: &TransferTask,
353 upload: Arc<dyn BreakpointUpload + Send + Sync>,
354 local_offset: u64,
355) -> Result<PrepareOutcome, MeowError> {
356 let info = upload
357 .prepare(UploadPrepareCtx {
358 client,
359 task,
360 local_offset,
361 })
362 .await?;
363 crate::meow_key_log!(
364 "upload_prepare",
365 "prepare protocol completed: file={} local_offset={}",
366 task.file_name(),
367 local_offset
368 );
369 if info.completed_file_id.is_some() {
370 let total = task.total_size();
371 crate::meow_key_log!(
372 "upload_prepare",
373 "server indicates upload already complete: file={} total={}",
374 task.file_name(),
375 total
376 );
377 return Ok(PrepareOutcome {
378 next_offset: total,
379 total_size: total,
380 });
381 }
382 let server_off = info.next_byte.unwrap_or(0);
383 let next = local_offset.max(server_off).min(task.total_size());
384 crate::meow_flow_log!(
385 "upload_prepare",
386 "prepared: server_next={} local_offset={} final_next={}",
387 server_off,
388 local_offset,
389 next
390 );
391 Ok(PrepareOutcome {
392 next_offset: next,
393 total_size: task.total_size(),
394 })
395}
396
397fn download_is_parallel(
400 task: &TransferTask,
401 download: &Arc<dyn BreakpointDownload + Send + Sync>,
402) -> bool {
403 task.direction() == Direction::Download
404 && task.max_parts_in_flight() > 1
405 && download.supports_parallel_parts()
406}
407
408fn download_identity(url: &str) -> String {
412 let mut h: u64 = 0xcbf29ce484222325;
413 for b in url.as_bytes() {
414 h ^= *b as u64;
415 h = h.wrapping_mul(0x00000100000001B3);
416 }
417 format!("{h:016x}")
418}
419
420async fn download_prepare_finish(
426 task: &TransferTask,
427 download: &Arc<dyn BreakpointDownload + Send + Sync>,
428 start: u64,
429 total: u64,
430) -> Result<PrepareOutcome, MeowError> {
431 let path = task.file_path();
432 if download_is_parallel(task, download) {
433 if total == 0 {
434 return Ok(PrepareOutcome {
436 next_offset: 0,
437 total_size: 0,
438 });
439 }
440 if let Some(parent) = path.parent() {
441 if !parent.as_os_str().is_empty() {
442 tokio::fs::create_dir_all(parent).await.map_err(|e| {
443 MeowError::from_io(
444 format!("create download dir failed: {}", parent.display()),
445 e,
446 )
447 })?;
448 }
449 }
450 let identity = download_identity(&download.range_url(task));
456 let progress = crate::dflt::download_progress::DownloadProgress::load_or_create(
457 path,
458 total,
459 task.chunk_size(),
460 task.max_parts_in_flight(),
461 &identity,
462 )
463 .map_err(|e| MeowError::from_io("load .rcdl sidecar failed".to_string(), e))?;
464 let watermark = progress.contiguous_watermark();
465
466 let file = OpenOptions::new()
470 .write(true)
471 .create(true)
472 .truncate(false)
473 .open(path)
474 .await
475 .map_err(|e| {
476 MeowError::from_io(format!("open for presize failed: {}", path.display()), e)
477 })?;
478 file.set_len(total)
479 .await
480 .map_err(|e| MeowError::from_io("presize set_len failed".to_string(), e))?;
481 file.sync_all()
482 .await
483 .map_err(|e| MeowError::from_io("presize sync failed".to_string(), e))?;
484 drop(file);
485
486 if let Ok(mut slot) = task.download_progress().try_lock() {
487 *slot = Some(progress);
488 } else {
489 return Err(MeowError::from_code_str(
492 InnerErrorCode::InvalidTaskState,
493 "download progress slot unexpectedly locked during prepare",
494 ));
495 }
496 crate::meow_key_log!(
497 "download_prepare",
498 "prepared concurrent download: resume_watermark={} remote_total={}",
499 watermark,
500 total
501 );
502 return Ok(PrepareOutcome {
503 next_offset: watermark,
504 total_size: total,
505 });
506 }
507
508 if crate::dflt::download_progress::DownloadProgress::sidecar_exists(path) {
511 return Err(MeowError::from_code_str(
512 InnerErrorCode::InvalidTaskState,
513 "found an in-progress parallel download sidecar (.rcdl); resume with \
514 max_parts_in_flight > 1, or delete the sidecar and the partial file",
515 ));
516 }
517
518 if start > total {
520 crate::log::emit_lazy(|| {
521 crate::log::Log::error(
522 "download_prepare",
523 format!(
524 "invalid local length larger than remote: local={} remote={}",
525 start, total
526 ),
527 )
528 .with_key(task.file_name())
529 .with_offset(start)
530 });
531 return Err(MeowError::from_code_str(
532 InnerErrorCode::InvalidRange,
533 "local file larger than remote total size",
534 ));
535 }
536 if start >= total {
537 crate::meow_key_log!(
538 "download_prepare",
539 "already complete by local length: local={} remote={}",
540 start,
541 total
542 );
543 return Ok(PrepareOutcome {
544 next_offset: total,
545 total_size: total,
546 });
547 }
548 crate::meow_key_log!(
549 "download_prepare",
550 "prepared resume offset: start={} remote_total={}",
551 start,
552 total
553 );
554 Ok(PrepareOutcome {
555 next_offset: start,
556 total_size: total,
557 })
558}
559
560async fn download_prepare(
562 client: &reqwest::Client,
563 task: &TransferTask,
564 download: Arc<dyn BreakpointDownload + Send + Sync>,
565 _local_offset: u64,
566) -> Result<PrepareOutcome, MeowError> {
567 crate::meow_flow_log!(
568 "download_prepare",
569 "start: file={} path={}",
570 task.file_name(),
571 task.file_path().display()
572 );
573 let path = task.file_path();
574 let local_len = match tokio::fs::metadata(path).await {
575 Ok(meta) => meta.len(),
576 Err(e) if e.kind() == std::io::ErrorKind::NotFound => 0u64,
579 Err(e) => {
580 crate::log::emit_lazy(|| {
581 crate::log::Log::error(
582 "download_prepare",
583 format!("stat failed: path={} err={}", path.display(), e),
584 )
585 .with_key(task.file_name())
586 });
587 return Err(MeowError::from_io(
588 format!("download_prepare stat failed: {}", path.display()),
589 e,
590 ));
591 }
592 };
593
594 let start = local_len;
596
597 if let Some(hinted) = download.total_size_hint(task) {
603 crate::meow_key_log!(
604 "download_prepare",
605 "resolved total from total_size_hint: start={} remote_total={}",
606 start,
607 hinted
608 );
609 return download_prepare_finish(task, &download, start, hinted).await;
610 }
611 if task.total_size() > 0 {
612 let hinted = task.total_size();
614 crate::meow_key_log!(
615 "download_prepare",
616 "resolved total from with_total_size: start={} remote_total={}",
617 start,
618 hinted
619 );
620 return download_prepare_finish(task, &download, start, hinted).await;
621 }
622
623 let head_url = download.head_url(task);
624 let mut head_headers = task.headers().clone();
625 download
626 .merge_head_headers(DownloadHeadCtx {
627 task,
628 base: &mut head_headers,
629 })
630 .map_err(|e| {
631 crate::log::emit_lazy(|| {
632 crate::log::Log::warn(
633 "head",
634 format!("merge_head_headers failed: err={}", crate::log::redact_secrets(&e.to_string())),
635 )
636 .with_key(task.file_name())
637 .with_url(head_url.as_str())
638 });
639 e
640 })?;
641 let head_resp = client
642 .request(Method::HEAD, &head_url)
643 .headers(head_headers)
644 .send()
645 .await
646 .map_err(|e| {
647 crate::log::emit_lazy(|| {
648 crate::log::Log::error(
649 "head",
650 format!("HEAD send failed: err={}", crate::log::redact_secrets(&e.to_string())),
651 )
652 .with_key(task.file_name())
653 .with_url(head_url.as_str())
654 });
655 map_reqwest(e)
656 })?;
657 if !head_resp.status().is_success() {
658 let head_status = head_resp.status();
659 crate::log::emit_lazy(|| {
660 crate::log::Log::error(
661 "head",
662 format!("head failed: status={}", head_status),
663 )
664 .with_key(task.file_name())
665 .with_http_status(head_status.as_u16())
666 .with_url(head_url.as_str())
667 });
668 return Err(MeowError::from_code(
669 InnerErrorCode::ResponseStatusError,
670 format!("download_prepare HEAD failed: {}", head_resp.status()),
671 )
672 .with_http_status(head_resp.status().as_u16()));
673 }
674 let head_content_length = head_resp
675 .headers()
676 .get(CONTENT_LENGTH)
677 .and_then(|v| v.to_str().ok())
678 .unwrap_or("<missing>");
679 let head_etag = head_resp
680 .headers()
681 .get(ETAG)
682 .and_then(|v| v.to_str().ok())
683 .unwrap_or("<missing>");
684 crate::meow_flow_log!(
685 "download_prepare",
686 "head metadata: url={} content_length={} etag={}",
687 crate::log::sanitize_url(&head_url),
688 head_content_length,
689 head_etag
690 );
691 let total = download
692 .total_size_from_head(head_resp.headers())
693 .map_err(|e| {
694 crate::log::emit_lazy(|| {
695 crate::log::Log::error(
696 "head",
697 format!("total_size_from_head parse failed: err={}", crate::log::redact_secrets(&e.to_string())),
698 )
699 .with_key(task.file_name())
700 .with_url(head_url.as_str())
701 });
702 e
703 })?;
704 download_prepare_finish(task, &download, start, total).await
706}
707
708#[async_trait]
709impl TransferTrait for DefaultHttpTransfer {
710 async fn prepare(
712 &self,
713 task: &TransferTask,
714 local_offset: u64,
715 ) -> Result<PrepareOutcome, MeowError> {
716 let client = self.client_for(task);
717 match task.direction() {
718 Direction::Upload => {
719 upload_prepare(&client, task, self.upload_arc(task), local_offset).await
720 }
721 Direction::Download => {
722 download_prepare(&client, task, self.download_arc(task), local_offset).await
723 }
724 }
725 }
726
727 async fn transfer_chunk(
729 &self,
730 task: &TransferTask,
731 offset: u64,
732 chunk_size: u64,
733 remote_total_size: u64,
734 ) -> Result<ChunkOutcome, MeowError> {
735 let client = self.client_for(task);
736 match task.direction() {
737 Direction::Upload => {
738 upload_one_chunk(&client, task, self.upload_arc(task), offset, chunk_size).await
739 }
740 Direction::Download => {
741 download_one_chunk(
742 &client,
743 task,
744 self.download_arc(task),
745 offset,
746 chunk_size,
747 remote_total_size,
748 )
749 .await
750 }
751 }
752 }
753
754 async fn cancel(&self, task: &TransferTask) -> Result<(), MeowError> {
756 if task.direction() != Direction::Upload {
757 return Ok(());
758 }
759 let client = self.client_for(task);
760 self.upload_arc(task).abort_upload(&client, task).await
761 }
762
763 fn supports_parallel_parts(&self, task: &TransferTask) -> bool {
766 match task.direction() {
767 Direction::Upload => self.upload_arc(task).supports_parallel_parts(),
768 Direction::Download => self.download_arc(task).supports_parallel_parts(),
769 }
770 }
771
772 async fn transfer_chunk_part(
775 &self,
776 task: &TransferTask,
777 offset: u64,
778 chunk_size: u64,
779 remote_total_size: u64,
780 ) -> Result<ChunkOutcome, MeowError> {
781 let client = self.client_for(task);
782 match task.direction() {
783 Direction::Upload => {
784 upload_one_chunk_part(&client, task, self.upload_arc(task), offset, chunk_size).await
785 }
786 Direction::Download => {
787 {
791 let guard = task.download_progress().lock().await;
792 if let Some(p) = guard.as_ref() {
793 if p.is_done(offset) {
794 return Ok(ChunkOutcome {
795 next_offset: (offset + chunk_size).min(remote_total_size),
796 total_size: remote_total_size,
797 done: (offset + chunk_size) >= remote_total_size,
798 completion_payload: None,
799 });
800 }
801 }
802 }
803 let outcome = download_one_chunk_part_positioned(
804 &client,
805 task,
806 self.download_arc(task),
807 offset,
808 chunk_size,
809 remote_total_size,
810 )
811 .await?;
812 {
814 let mut guard = task.download_progress().lock().await;
815 if let Some(p) = guard.as_mut() {
816 p.mark_done_and_persist(offset).map_err(|e| {
817 MeowError::from_io("persist .rcdl progress failed".to_string(), e)
818 })?;
819 }
820 }
821 Ok(outcome)
822 }
823 }
824 }
825
826 async fn complete(&self, task: &TransferTask) -> Result<Option<String>, MeowError> {
833 match task.direction() {
834 Direction::Upload => {
835 let client = self.client_for(task);
836 self.upload_arc(task).complete_upload(&client, task).await
837 }
838 Direction::Download => {
839 let progress = {
842 let mut guard = task.download_progress().lock().await;
843 guard.take()
844 };
845 if let Some(p) = progress {
846 let expected = p.total();
847 let actual = tokio::fs::metadata(task.file_path())
848 .await
849 .map(|m| m.len())
850 .map_err(|e| {
851 MeowError::from_io("stat completed download failed".to_string(), e)
852 })?;
853 if actual != expected {
854 return Err(MeowError::from_code(
855 InnerErrorCode::InvalidRange,
856 format!(
857 "download length mismatch on complete: expected {expected}, got {actual}"
858 ),
859 ));
860 }
861 if !p.all_done() {
862 return Err(MeowError::from_code_str(
863 InnerErrorCode::InvalidRange,
864 "download complete called before all parts recorded done",
865 ));
866 }
867 if let Err(e) = p.delete() {
870 crate::meow_warn_log!("download_complete", "sidecar delete failed: {}", e);
871 }
872 }
873 Ok(None)
874 }
875 }
876 }
877}