-
Notifications
You must be signed in to change notification settings - Fork 182
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
328 additions
and
177 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
use std::collections::HashMap; | ||
use std::hash::{DefaultHasher, Hash, Hasher}; | ||
|
||
use anyhow::Result; | ||
|
||
use super::metadata_service::MetadataService; | ||
|
||
/// Fake implementation of MetadataService to be used for tests only. | ||
/// It just stores uri and data in a HashMap when `upload` is called, | ||
/// and returns these data when `get` is called. | ||
#[derive(Debug, Default)] | ||
pub struct FakeMetadataService { | ||
data: HashMap<String, Vec<u8>>, | ||
} | ||
|
||
#[allow(async_fn_in_trait)] | ||
impl MetadataService for FakeMetadataService { | ||
async fn upload(&mut self, data: Vec<u8>) -> Result<String> { | ||
let mut hasher = DefaultHasher::new(); | ||
data.hash(&mut hasher); | ||
let hash = hasher.finish(); | ||
|
||
let uri = format!("ipfs://{:x}", hash); | ||
self.data.insert(uri.clone(), data); | ||
|
||
Ok(uri) | ||
} | ||
|
||
#[cfg(test)] | ||
async fn get(&self, uri: String) -> Result<Vec<u8>> { | ||
Ok(self.data.get(&uri).cloned().unwrap_or(Vec::<u8>::new())) | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
use std::io::Cursor; | ||
|
||
use anyhow::Result; | ||
#[cfg(test)] | ||
use futures::TryStreamExt; | ||
use ipfs_api_backend_hyper::{IpfsApi, TryFromUri}; | ||
|
||
use super::metadata_service::MetadataService; | ||
|
||
/// IPFS implementation of MetadataService, allowing to | ||
/// upload metadata to IPFS. | ||
pub struct IpfsMetadataService { | ||
client: ipfs_api_backend_hyper::IpfsClient, | ||
} | ||
|
||
// impl required by clippy | ||
impl std::fmt::Debug for IpfsMetadataService { | ||
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl IpfsMetadataService { | ||
/// Instanciate a new IPFS Metadata service with IPFS credentials. | ||
/// | ||
/// # Arguments | ||
/// * `client_url` - The IPFS client URL | ||
/// * `username` - The IPFS username | ||
/// * `password` - The IPFS password | ||
/// | ||
/// # Returns | ||
/// A new `IpfsMetadataService` is the IPFS client has been successfully | ||
/// instanciated or a Anyhow error if not. | ||
pub fn new(client_url: &str, username: &str, password: &str) -> Result<Self> { | ||
if client_url.is_empty() || username.is_empty() || password.is_empty() { | ||
anyhow::bail!("Invalid credentials: empty values not allowed"); | ||
} | ||
if !client_url.starts_with("http://") && !client_url.starts_with("https://") { | ||
anyhow::bail!("Invalid client URL: must start with http:// or https://"); | ||
} | ||
|
||
Ok(Self { | ||
client: ipfs_api_backend_hyper::IpfsClient::from_str(client_url)? | ||
.with_credentials(username, password), | ||
}) | ||
} | ||
} | ||
|
||
#[allow(async_fn_in_trait)] | ||
impl MetadataService for IpfsMetadataService { | ||
async fn upload(&mut self, data: Vec<u8>) -> Result<String> { | ||
let reader = Cursor::new(data); | ||
let response = self | ||
.client | ||
.add(reader) | ||
.await | ||
.map_err(|e| anyhow::anyhow!("Failed to upload to IPFS: {}", e))?; | ||
Ok(format!("ipfs://{}", response.hash)) | ||
} | ||
|
||
#[cfg(test)] | ||
async fn get(&self, uri: String) -> Result<Vec<u8>> { | ||
let res = self | ||
.client | ||
.cat(&uri.replace("ipfs://", "")) | ||
.map_ok(|chunk| chunk.to_vec()) | ||
.try_concat() | ||
.await?; | ||
Ok(res) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
use anyhow::Result; | ||
|
||
/// MetadataService trait to be implemented to upload | ||
/// some metadata on a specific storage system. | ||
#[allow(async_fn_in_trait)] | ||
pub trait MetadataService: std::marker::Send + std::marker::Sync + std::marker::Unpin { | ||
/// Upload some bytes (`data`) to the storage system, | ||
/// and get back a string URI. | ||
/// | ||
/// # Arguments | ||
/// * `data` - bytes to upload | ||
/// | ||
/// # Returns | ||
/// A string URI or a Anyhow error. | ||
async fn upload(&mut self, data: Vec<u8>) -> Result<String>; | ||
|
||
/// Read stored bytes from a URI. (for tests only) | ||
/// | ||
/// # Arguments | ||
/// * `uri` - the URI of the data to read | ||
/// | ||
/// # Returns | ||
/// the read bytes or a Anyhow error. | ||
#[cfg(test)] | ||
async fn get(&self, uri: String) -> Result<Vec<u8>>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
use std::hash::{DefaultHasher, Hash, Hasher}; | ||
|
||
use anyhow::{Context, Result}; | ||
use serde_json::json; | ||
use starknet_crypto::Felt; | ||
|
||
use super::metadata_service::MetadataService; | ||
use crate::config::metadata_config::{ResourceMetadata, WorldMetadata}; | ||
use crate::uri::Uri; | ||
|
||
/// Helper function to compute metadata hash. | ||
/// | ||
/// # Arguments | ||
/// * `data` - the data to hash. | ||
/// | ||
/// # Returns | ||
/// The hash value. | ||
fn compute_metadata_hash<T>(data: T) -> u64 | ||
where | ||
T: Hash, | ||
{ | ||
let mut hasher = DefaultHasher::new(); | ||
data.hash(&mut hasher); | ||
hasher.finish() | ||
} | ||
|
||
/// Helper function to process an optional URI. | ||
/// | ||
/// If the URI is set and refer to a local asset, this asset | ||
/// is then uploaded using the provided MetadataService. | ||
/// In any other case, the URI is kept as it is. | ||
/// | ||
/// # Arguments | ||
/// * `uri` - The URI to process | ||
/// * `service` - The metadata service to use to upload assets. | ||
/// | ||
/// # Returns | ||
/// The updated URI or a Anyhow error. | ||
async fn upload_uri(uri: &Option<Uri>, service: &mut impl MetadataService) -> Result<Option<Uri>> { | ||
if let Some(Uri::File(path)) = uri { | ||
let data = std::fs::read(path)?; | ||
let uploaded_uri = Uri::Ipfs(service.upload(data).await?); | ||
Ok(Some(uploaded_uri)) | ||
} else { | ||
Ok(uri.clone()) | ||
} | ||
} | ||
|
||
/// Trait to be implemented by metadata structs to be | ||
/// uploadable on a storage system. | ||
#[allow(async_fn_in_trait)] | ||
pub trait MetadataStorage { | ||
/// Upload metadata using the provided service. | ||
/// | ||
/// # Arguments | ||
/// * `service` - service to use to upload metadata | ||
/// | ||
/// # Returns | ||
/// The uploaded metadata URI or a Anyhow error. | ||
async fn upload(&self, service: &mut impl MetadataService) -> Result<String>; | ||
|
||
/// Upload metadata using the provided service, only if it has changed. | ||
/// | ||
/// # Arguments | ||
/// * `service` - service to use to upload metadata | ||
/// * `current_hash` - the hash of the previously uploaded metadata | ||
/// | ||
/// # Returns | ||
/// The uploaded metadata URI or a Anyhow error. | ||
async fn upload_if_changed( | ||
&self, | ||
service: &mut impl MetadataService, | ||
current_hash: Felt, | ||
) -> Result<Option<(String, Felt)>> | ||
where | ||
Self: std::hash::Hash, | ||
{ | ||
let new_hash = compute_metadata_hash(self); | ||
let new_hash = Felt::from_raw([0, 0, 0, new_hash]); | ||
|
||
if new_hash != current_hash { | ||
let new_uri = self.upload(service).await?; | ||
return Ok(Some((new_uri, new_hash))); | ||
} | ||
|
||
Ok(None) | ||
} | ||
} | ||
|
||
#[allow(async_fn_in_trait)] | ||
impl MetadataStorage for WorldMetadata { | ||
async fn upload(&self, service: &mut impl MetadataService) -> Result<String> { | ||
let mut meta = self.clone(); | ||
|
||
meta.icon_uri = | ||
upload_uri(&self.icon_uri, service).await.context("Failed to upload icon URI")?; | ||
meta.cover_uri = | ||
upload_uri(&self.cover_uri, service).await.context("Failed to upload cover URI")?; | ||
|
||
let serialized = json!(meta).to_string(); | ||
service.upload(serialized.as_bytes().to_vec()).await.context("Failed to upload metadata") | ||
} | ||
} | ||
|
||
#[allow(async_fn_in_trait)] | ||
impl MetadataStorage for ResourceMetadata { | ||
async fn upload(&self, service: &mut impl MetadataService) -> Result<String> { | ||
let mut meta = self.clone(); | ||
|
||
meta.icon_uri = | ||
upload_uri(&self.icon_uri, service).await.context("Failed to upload icon URI")?; | ||
|
||
let serialized = json!(meta).to_string(); | ||
service.upload(serialized.as_bytes().to_vec()).await.context("Failed to upload metadata") | ||
} | ||
} |
Oops, something went wrong.