|
| 1 | +use std::io::Write; |
| 2 | + |
| 3 | +use async_trait::async_trait; |
| 4 | +use bcr_ebill_core::ServiceTraitBounds; |
| 5 | +use nostr::hashes::{ |
| 6 | + Hash, |
| 7 | + sha256::{self, Hash as Sha256Hash}, |
| 8 | +}; |
| 9 | +use serde::Deserialize; |
| 10 | +use thiserror::Error; |
| 11 | + |
| 12 | +/// Generic result type |
| 13 | +pub type Result<T> = std::result::Result<T, super::Error>; |
| 14 | + |
| 15 | +/// Generic error type |
| 16 | +#[derive(Debug, Error)] |
| 17 | +pub enum Error { |
| 18 | + /// all errors originating from interacting with the web api |
| 19 | + #[error("External File Storage Web API error: {0}")] |
| 20 | + Api(#[from] reqwest::Error), |
| 21 | + /// all errors originating from invalid urls |
| 22 | + #[error("External File Storage Invalid Relay Url Error")] |
| 23 | + InvalidRelayUrl, |
| 24 | + /// all errors originating from invalid hashes |
| 25 | + #[error("External File Storage Invalid Hash")] |
| 26 | + InvalidHash, |
| 27 | + /// all errors originating from hashing |
| 28 | + #[error("External File Storage Hash Error")] |
| 29 | + Hash, |
| 30 | +} |
| 31 | + |
| 32 | +#[cfg(test)] |
| 33 | +use mockall::automock; |
| 34 | + |
| 35 | +#[cfg_attr(test, automock)] |
| 36 | +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] |
| 37 | +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] |
| 38 | +pub trait FileStorageClientApi: ServiceTraitBounds { |
| 39 | + /// Upload the given bytes, checking and returning the nostr_hash |
| 40 | + async fn upload(&self, relay_url: &str, bytes: Vec<u8>) -> Result<Sha256Hash>; |
| 41 | + /// Download the bytes with the given nostr_hash and compare if the hash matches the file |
| 42 | + async fn download(&self, relay_url: &str, nostr_hash: &str) -> Result<Vec<u8>>; |
| 43 | +} |
| 44 | + |
| 45 | +#[derive(Debug, Clone, Default)] |
| 46 | +pub struct FileStorageClient { |
| 47 | + cl: reqwest::Client, |
| 48 | +} |
| 49 | + |
| 50 | +impl ServiceTraitBounds for FileStorageClient {} |
| 51 | + |
| 52 | +#[cfg(test)] |
| 53 | +impl ServiceTraitBounds for MockFileStorageClientApi {} |
| 54 | + |
| 55 | +impl FileStorageClient { |
| 56 | + pub fn new() -> Self { |
| 57 | + Self { |
| 58 | + cl: reqwest::Client::new(), |
| 59 | + } |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] |
| 64 | +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] |
| 65 | +impl FileStorageClientApi for FileStorageClient { |
| 66 | + async fn upload(&self, relay_url: &str, bytes: Vec<u8>) -> Result<Sha256Hash> { |
| 67 | + // Calculate hash to compare with the hash we get back |
| 68 | + let mut hash_engine = sha256::HashEngine::default(); |
| 69 | + if hash_engine.write_all(&bytes).is_err() { |
| 70 | + return Err(Error::Hash.into()); |
| 71 | + } |
| 72 | + let hash = sha256::Hash::from_engine(hash_engine); |
| 73 | + |
| 74 | + // PUT {relay_url}/upload |
| 75 | + let url = reqwest::Url::parse(relay_url) |
| 76 | + .and_then(|url| url.join("upload")) |
| 77 | + .map_err(|_| Error::InvalidRelayUrl)?; |
| 78 | + // Make upload request |
| 79 | + let resp: BlobDescriptorReply = self.cl.put(url).body(bytes).send().await?.json().await?; |
| 80 | + let nostr_hash = resp.sha256; |
| 81 | + |
| 82 | + // Check hash |
| 83 | + if hash != nostr_hash { |
| 84 | + return Err(Error::InvalidHash.into()); |
| 85 | + } |
| 86 | + |
| 87 | + Ok(nostr_hash) |
| 88 | + } |
| 89 | + |
| 90 | + async fn download(&self, relay_url: &str, nostr_hash: &str) -> Result<Vec<u8>> { |
| 91 | + // GET {relay_url}/{hash} |
| 92 | + let url = reqwest::Url::parse(relay_url) |
| 93 | + .and_then(|url| url.join(nostr_hash)) |
| 94 | + .map_err(|_| Error::InvalidRelayUrl)?; |
| 95 | + // Make download request |
| 96 | + let resp: Vec<u8> = self.cl.get(url).send().await?.bytes().await?.into(); |
| 97 | + |
| 98 | + // Calculate hash to compare with the hash we sent |
| 99 | + let mut hash_engine = sha256::HashEngine::default(); |
| 100 | + if hash_engine.write_all(&resp).is_err() { |
| 101 | + return Err(Error::Hash.into()); |
| 102 | + } |
| 103 | + |
| 104 | + // Check hash |
| 105 | + let hash = sha256::Hash::from_engine(hash_engine); |
| 106 | + if hash.to_string() != nostr_hash { |
| 107 | + return Err(Error::InvalidHash.into()); |
| 108 | + } |
| 109 | + |
| 110 | + Ok(resp) |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +#[derive(Debug, Clone, Deserialize)] |
| 115 | +pub struct BlobDescriptorReply { |
| 116 | + sha256: Sha256Hash, |
| 117 | +} |
0 commit comments