Skip to main content

signer_storage/
lib.rs

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// --- Moved from signer-daemon/src/cache.rs ---
10/// 缓存配置
11#[derive(Debug, Clone)]
12pub struct CacheConfig {
13    /// 缓存目录
14    pub cache_dir: PathBuf,
15}
16
17impl CacheConfig {
18    /// 创建一个新的缓存配置
19    pub fn new(cache_dir: PathBuf) -> Self {
20        Self { cache_dir }
21    }
22}
23
24// --- New Error Type ---
25#[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
39// --- Moved from signer-daemon/src/remote_resource_service.rs ---
40
41/// 远端资源服务
42///
43/// 提供处理远端资源下载和缓存的静态方法
44pub struct RemoteResourceService;
45
46impl RemoteResourceService {
47    /// 从本地缓存中查询文件
48    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    /// 从远端下载文件到缓存
64    pub async fn download(
65        cache_config: &CacheConfig,
66        resource: &SignerRemoteResource,
67    ) -> Result<PathBuf, StorageError> {
68        // 检查缓存是否存在
69        if let Some(cache_path) = Self::find_cache(cache_config, resource)? {
70            return Ok(cache_path);
71        }
72
73        // 遍历远端服务查找并下载
74        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            // 如果请求失败,则继续尝试下一个 remote
100        }
101
102        Err(StorageError::Msg(
103            "在所有远端都找不到或无法下载该文件".to_string(),
104        ))
105    }
106}
107
108// --- Tests ---
109#[cfg(test)]
110mod test {
111    use super::*;
112    // Note: This test now has a dev-dependency on signer-daemon
113    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        // This part is problematic as it requires a running daemon.
128        // For now, I will keep it, but this highlights that these are integration tests.
129        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] // Ignoring tests as they require a live daemon and are more for integration.
136    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        // 1. 上传一个文件获取 hash
142        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        // 2. 首次下载
147        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        // 验证下载内容
160        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        // 3. 再次下载(应该命中缓存)
165        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}