use crate::cache::CacheConfig;
use crate::{HttpClient, HttpClientConfig};
use futures::stream::TryStreamExt;
use signer_core::SignerRemoteResource;
use std::path::PathBuf;
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;
pub struct RemoteResourceService;
impl RemoteResourceService {
pub fn find_cache(
cache_config: &CacheConfig,
resource: &SignerRemoteResource,
) -> Result<Option<PathBuf>, crate::DaemonError> {
let cache_path = cache_config
.cache_dir
.join(&resource.pubkey)
.join(&resource.hash);
if cache_path.exists() {
Ok(Some(cache_path))
} else {
Ok(None)
}
}
pub async fn download(
cache_config: &CacheConfig,
resource: &SignerRemoteResource,
) -> Result<PathBuf, crate::DaemonError> {
if let Some(cache_path) = Self::find_cache(cache_config, resource)? {
return Ok(cache_path);
}
for remote in &resource.remotes {
let config = HttpClientConfig::new_no_auth(remote.clone());
let client = HttpClient::new(config);
let result = client
.get_raw(&format!(
"/api/storage/item/{}/{}",
&resource.pubkey, &resource.hash
))
.await;
if let Ok(res) = result {
let cache_dir = cache_config.cache_dir.join(&resource.pubkey);
fs::create_dir_all(&cache_dir).await?;
let cache_path = cache_dir.join(&resource.hash);
let mut file = File::create(&cache_path).await?;
let mut response_stream = res.bytes_stream();
while let Some(chunk) = response_stream.try_next().await? {
file.write_all(&chunk).await?;
}
return Ok(cache_path);
}
}
Err(crate::DaemonError::Signer(crate::SignerError::Msg(
"在所有远端都找不到或无法下载该文件".to_string(),
)))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{SignerDaemonStorage, SignerRemote};
use signer_core::SignerUser;
use tempfile::tempdir;
async fn setup() -> crate::DaemonResult<(SignerUser, String)> {
let user = SignerUser::generete("test_user_for_resource")?;
let remote_addr = "http://localhost:8080".to_string();
let remote = SignerRemote::new(&remote_addr);
remote.ping(&user).await?;
Ok((user, remote_addr))
}
#[tokio::test]
async fn test_download_and_cache_hit() -> crate::DaemonResult<()> {
let (user, remote_addr) = setup().await?;
let temp_dir = tempdir()?;
let cache_config = CacheConfig::new(temp_dir.path().to_path_buf());
let storage = SignerDaemonStorage::new(remote_addr.clone(), user.clone());
let source_path = PathBuf::from("../../Cargo.toml");
let item = storage.upload(source_path.clone()).await?;
let resource = SignerRemoteResource {
pubkey: item.pubkey.clone(),
hash: item.hash.clone(),
remotes: vec![remote_addr.clone()],
};
let start_time = std::time::Instant::now();
let downloaded_path = RemoteResourceService::download(&cache_config, &resource).await?;
let first_download_duration = start_time.elapsed();
let source_content = tokio::fs::read(source_path).await?;
let downloaded_content = tokio::fs::read(&downloaded_path).await?;
assert_eq!(source_content, downloaded_content);
let start_time = std::time::Instant::now();
let cached_path = RemoteResourceService::download(&cache_config, &resource).await?;
let second_download_duration = start_time.elapsed();
assert_eq!(downloaded_path, cached_path);
assert!(
second_download_duration < first_download_duration,
"缓存命中应该比首次下载快"
);
Ok(())
}
#[tokio::test]
async fn test_download_with_one_invalid_remote() -> crate::DaemonResult<()> {
let (user, remote_addr) = setup().await?;
let temp_dir = tempdir()?;
let cache_config = CacheConfig::new(temp_dir.path().to_path_buf());
let storage = SignerDaemonStorage::new(remote_addr.clone(), user.clone());
let source_path = PathBuf::from("../../Cargo.toml");
let item = storage.upload(source_path).await?;
let invalid_remote = "http://localhost:12345".to_string();
let resource = SignerRemoteResource {
pubkey: item.pubkey,
hash: item.hash,
remotes: vec![invalid_remote, remote_addr],
};
let downloaded_path = RemoteResourceService::download(&cache_config, &resource).await?;
assert!(downloaded_path.exists());
Ok(())
}
#[tokio::test]
async fn test_download_with_all_invalid_remotes() -> crate::DaemonResult<()> {
let (_, _remote_addr) = setup().await?;
let temp_dir = tempdir()?;
let cache_config = CacheConfig::new(temp_dir.path().to_path_buf());
let resource = SignerRemoteResource {
pubkey: "any_pubkey".to_string(),
hash: "any_hash".to_string(),
remotes: vec![
"http://localhost:12345".to_string(),
"http://localhost:54321".to_string(),
],
};
let result = RemoteResourceService::download(&cache_config, &resource).await;
assert!(result.is_err());
Ok(())
}
}