Skip to main content

conduit_cli/core/engine/
downloader.rs

1use 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}