signer-daemon 0.3.1

Signer daemon package.
Documentation
use crate::OpenapiConfiguration;
use crate::cache::CacheConfig;
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>, anyhow::Error> {
    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, anyhow::Error> {
    // 检查缓存是否存在
    if let Some(cache_path) = Self::find_cache(cache_config, resource)? {
      return Ok(cache_path);
    }

    // 遍历远端服务器查找并下载
    for remote in &resource.remotes {
      let config = OpenapiConfiguration::new_no_auth(remote.clone());
      let built_config = config.build();

      let result = signer_hub_kit::apis::default_api::api_signer_storage_item_pubkey_hash_get(
                &built_config,
                &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(anyhow::anyhow!("在所有远端都找不到或无法下载该文件"))
  }
}

#[cfg(test)]
mod test {
  use super::*;
  use crate::{SignerDaemonStorage, SignerRemote};
  use signer_core::SignerUser;
  use tempfile::tempdir;

  async fn setup() -> anyhow::Result<(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() -> anyhow::Result<()> {
    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() -> anyhow::Result<()> {
    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() -> anyhow::Result<()> {
    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(())
  }
}