1use futures::stream::TryStreamExt;
2use signer_core::SignerRemoteResource;
3use signer_remote::RemoteError;
4use signer_remote::remote::{HttpClient, HttpClientConfig};
5use std::path::PathBuf;
6use tokio::fs::{self, File};
7use tokio::io::AsyncWriteExt;
8
9#[derive(Debug, Clone)]
12pub struct CacheConfig {
13 pub cache_dir: PathBuf,
15}
16
17impl CacheConfig {
18 pub fn new(cache_dir: PathBuf) -> Self {
20 Self { cache_dir }
21 }
22}
23
24#[derive(Debug, thiserror::Error)]
26pub enum StorageError {
27 #[error("I/O error: {0}")]
28 Io(#[from] std::io::Error),
29 #[error("Reqwest error: {0}")]
30 Reqwest(#[from] reqwest::Error),
31 #[error("Signer remote error: {0}")]
32 SignerRemote(#[from] RemoteError),
33 #[error("Serde json error: {0}")]
34 SerdeJson(#[from] serde_json::Error),
35 #[error("Custom error: {0}")]
36 Msg(String),
37}
38
39pub struct RemoteResourceService;
45
46impl RemoteResourceService {
47 pub fn find_cache(
49 cache_config: &CacheConfig,
50 resource: &SignerRemoteResource,
51 ) -> Result<Option<PathBuf>, StorageError> {
52 let cache_path = cache_config
53 .cache_dir
54 .join(&resource.pubkey)
55 .join(&resource.hash);
56 if cache_path.exists() {
57 Ok(Some(cache_path))
58 } else {
59 Ok(None)
60 }
61 }
62
63 pub async fn download(
65 cache_config: &CacheConfig,
66 resource: &SignerRemoteResource,
67 ) -> Result<PathBuf, StorageError> {
68 if let Some(cache_path) = Self::find_cache(cache_config, resource)? {
70 return Ok(cache_path);
71 }
72
73 for remote in &resource.remotes {
75 let config = HttpClientConfig::new_no_auth(remote.clone());
76 let client = HttpClient::new(config);
77
78 let result = client
79 .get_raw(&format!(
80 "/api/storage/item/{}/{}",
81 &resource.pubkey, &resource.hash
82 ))
83 .await;
84
85 if let Ok(res) = result {
86 let cache_dir = cache_config.cache_dir.join(&resource.pubkey);
87 fs::create_dir_all(&cache_dir).await?;
88 let cache_path = cache_dir.join(&resource.hash);
89
90 let mut file = File::create(&cache_path).await?;
91 let mut response_stream = res.bytes_stream();
92
93 while let Some(chunk) = response_stream.try_next().await? {
94 file.write_all(&chunk).await?;
95 }
96
97 return Ok(cache_path);
98 }
99 }
101
102 Err(StorageError::Msg(
103 "在所有远端都找不到或无法下载该文件".to_string(),
104 ))
105 }
106}
107
108#[cfg(test)]
110mod test {
111 use super::*;
112 use signer_core::{SignerKeys, SignerUser};
114 use signer_remote::{SignerDaemonStorage, SignerRemote};
115 use tempfile::tempdir;
116
117 type TestResult<T> = Result<T, Box<dyn std::error::Error>>;
118
119 async fn setup() -> TestResult<(SignerKeys, SignerUser, String)> {
120 let keys = SignerKeys::generate()?;
121 let user = SignerUser {
122 pub_key: keys.pub_key.clone(),
123 username: "tester".to_string(),
124 ..Default::default()
125 };
126 let remote_addr = "http://localhost:8080".to_string();
127 let remote = SignerRemote::new(&remote_addr);
130 remote.ping(&keys, &user).await?;
131 Ok((keys, user, remote_addr))
132 }
133
134 #[tokio::test]
135 #[ignore] async fn test_download_and_cache_hit() -> TestResult<()> {
137 let (keys, user, remote_addr) = setup().await?;
138 let temp_dir = tempdir()?;
139 let cache_config = CacheConfig::new(temp_dir.path().to_path_buf());
140
141 let storage = SignerDaemonStorage::new(remote_addr.clone(), keys, user.clone());
143 let source_path = PathBuf::from("../../Cargo.toml");
144 let item = storage.upload(source_path.clone()).await?;
145
146 let resource = SignerRemoteResource {
148 pubkey: item.pubkey.clone(),
149 hash: item.hash.clone(),
150 remotes: vec![remote_addr.clone()],
151 file_name: Some(item.file_name.clone()),
152 file_size: None,
153 };
154
155 let start_time = std::time::Instant::now();
156 let downloaded_path = RemoteResourceService::download(&cache_config, &resource).await?;
157 let first_download_duration = start_time.elapsed();
158
159 let source_content = tokio::fs::read(source_path).await?;
161 let downloaded_content = tokio::fs::read(&downloaded_path).await?;
162 assert_eq!(source_content, downloaded_content);
163
164 let start_time = std::time::Instant::now();
166 let cached_path = RemoteResourceService::download(&cache_config, &resource).await?;
167 let second_download_duration = start_time.elapsed();
168
169 assert_eq!(downloaded_path, cached_path);
170 assert!(
171 second_download_duration < first_download_duration,
172 "缓存命中应该比首次下载快"
173 );
174
175 Ok(())
176 }
177
178 #[tokio::test]
179 #[ignore]
180 async fn test_download_with_one_invalid_remote() -> TestResult<()> {
181 let (keys, user, remote_addr) = setup().await?;
182 let temp_dir = tempdir()?;
183 let cache_config = CacheConfig::new(temp_dir.path().to_path_buf());
184
185 let storage = SignerDaemonStorage::new(remote_addr.clone(), keys, user.clone());
186 let source_path = PathBuf::from("../../Cargo.toml");
187 let item = storage.upload(source_path).await?;
188
189 let invalid_remote = "http://localhost:12345".to_string();
190 let resource = SignerRemoteResource {
191 pubkey: item.pubkey,
192 hash: item.hash,
193 remotes: vec![invalid_remote, remote_addr],
194 file_name: Some(item.file_name.clone()),
195 file_size: None,
196 };
197
198 let downloaded_path = RemoteResourceService::download(&cache_config, &resource).await?;
199 assert!(downloaded_path.exists());
200
201 Ok(())
202 }
203
204 #[tokio::test]
205 #[ignore]
206 async fn test_download_with_all_invalid_remotes() -> TestResult<()> {
207 let (_, _, _remote_addr) = setup().await?;
208 let temp_dir = tempdir()?;
209 let cache_config = CacheConfig::new(temp_dir.path().to_path_buf());
210
211 let resource = SignerRemoteResource {
212 pubkey: "any_pubkey".to_string(),
213 hash: "any_hash".to_string(),
214 remotes: vec![
215 "http://localhost:12345".to_string(),
216 "http://localhost:54321".to_string(),
217 ],
218 file_name: Some("any_file_name".to_string()),
219 file_size: None,
220 };
221
222 let result = RemoteResourceService::download(&cache_config, &resource).await;
223 assert!(result.is_err());
224
225 Ok(())
226 }
227}