conduit_cli/core/engine/
downloader.rs1use std::io::{Error, ErrorKind};
2use std::sync::Arc;
3
4use crate::core::engine::store::Store;
5use crate::core::domain::source::Hash;
6use crate::errors::ConduitResult;
7use crate::core::schemas::lock::HashKind;
8use crate::errors::ConduitError;
9use futures_util::StreamExt;
10use reqwest::Client;
11use reqwest::redirect::Policy;
12use tokio::fs;
13use tokio::io::AsyncWriteExt;
14
15pub struct Downloader {
16 client: Client,
17 store: Arc<Store>,
18}
19
20impl Downloader {
21 pub fn new(store: Arc<Store>) -> Self {
22 Self {
23 client: Client::builder()
24 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0")
25 .redirect(Policy::limited(10))
26 .build()
27 .unwrap(),
28 store,
29 }
30 }
31
32 pub async fn download_to_store(
33 &self,
34 url: &str,
35 expected_hash: Option<&Hash>,
36 ) -> ConduitResult<(String, HashKind)> {
37 if let Some(hash) = expected_hash {
38 let (val, kind) = if let Some(h) = &hash.sha512 {
39 (h, HashKind::Sha512)
40 } else if let Some(h) = &hash.sha256 {
41 (h, HashKind::Sha256)
42 } else if let Some(h) = &hash.sha1 {
43 (h, HashKind::Sha1)
44 } else {
45 (&String::new(), HashKind::Sha1)
46 };
47
48 if !val.is_empty() {
49 let path = self.store.object_path(val, kind);
50 if path.exists() {
51 return Ok((val.clone(), kind));
52 }
53 }
54 }
55
56 let response = self.client.get(url).send().await?;
57
58 if !response.status().is_success() {
59 return Err(ConduitError::Network(
60 response.error_for_status().unwrap_err(),
61 ));
62 }
63 let temp_path = std::env::temp_dir().join(format!("conduit-{}", uuid::Uuid::new_v4()));
64
65 {
66 let mut file = fs::File::create(&temp_path).await?;
67 let mut stream = response.bytes_stream();
68 while let Some(item) = stream.next().await {
69 let chunk = item?;
70 file.write_all(&chunk).await?;
71 }
72 file.flush().await?;
73 }
74
75 let kind = if let Some(h) = expected_hash {
76 if h.sha512.is_some() {
77 HashKind::Sha512
78 } else if h.sha256.is_some() {
79 HashKind::Sha256
80 } else {
81 HashKind::Sha1
82 }
83 } else {
84 HashKind::Sha1
85 };
86
87 let actual_hash = self.store.calculate_hash(&temp_path, kind).await?;
88
89 if let Some(hash) = expected_hash {
90 let expected_val = match kind {
91 HashKind::Sha512 => hash.sha512.as_ref(),
92 HashKind::Sha256 => hash.sha256.as_ref(),
93 HashKind::Sha1 => hash.sha1.as_ref(),
94 };
95
96 if let Some(ev) = expected_val
97 && *ev != actual_hash
98 {
99 let _ = fs::remove_file(&temp_path).await;
100 return Err(ConduitError::HashMismatch {
101 expected: ev.clone(),
102 actual: actual_hash,
103 });
104 }
105 }
106
107 self.store.add_file(&temp_path, &actual_hash, kind).await?;
108 let _ = fs::remove_file(&temp_path).await;
109
110 Ok((actual_hash, kind))
111 }
112
113 pub fn download_to_store_by_hash(
114 &self,
115 hash: &str,
116 kind: HashKind,
117 ) -> ConduitResult<(String, HashKind)> {
118 if hash.is_empty() {
119 return Err(ConduitError::Io(Error::new(
120 ErrorKind::InvalidInput,
121 "empty hash",
122 )));
123 }
124
125 let path = self.store.object_path(hash, kind);
126 if path.exists() {
127 Ok((hash.to_string(), kind))
128 } else {
129 Err(ConduitError::Io(Error::new(
130 ErrorKind::NotFound,
131 format!("hash {hash} not found"),
132 )))
133 }
134 }
135}