signer-daemon 0.3.2

Signer daemon package.
Documentation
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);
            }
            // 如果请求失败,则继续尝试下一个 remote
        }

        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());

        // 1. 上传一个文件获取 hash
        let storage = SignerDaemonStorage::new(remote_addr.clone(), user.clone());
        let source_path = PathBuf::from("../../Cargo.toml");
        let item = storage.upload(source_path.clone()).await?;

        // 2. 首次下载
        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);

        // 3. 再次下载(应该命中缓存)
        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(())
    }
}