nft_server/generators/
disk.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use async_trait::async_trait;
4use ethers::types::U256;
5use eyre::Result;
6use serde::de::DeserializeOwned;
7use tokio::sync::RwLock;
8
9use crate::{open_sea::ContractMetadata, MetadataGenerator, NftMetadata};
10
11/// A `MetadataGenerator` that consults stored JSON files in the local
12/// filesystem.
13///
14/// ## Notes
15///
16/// Files must be stored at `contract.json` for contract-level metadata, and
17/// `{token-id}.json` for tokens, where `token-id` is the string representation
18/// of the decimal token id. e.g. `0.json`, `384510.json`, etc
19///
20/// This generator caches files in memory the first time they're opened. If NFT
21/// metadata changes, the server will need to be re-booted to clear the cache.
22/// In addition, if you're serving an egregious number of NFTs (or have large
23/// image-data properties), you may run out of memory as the cache grows
24#[derive(Debug)]
25pub struct LocalJson {
26    location: PathBuf,
27    cache: RwLock<HashMap<U256, NftMetadata>>,
28    contract_cache: RwLock<Option<ContractMetadata>>,
29}
30
31/// `LocalJson` errors
32#[derive(thiserror::Error, Debug)]
33pub enum LocalJsonError {
34    /// Serde
35    #[error("{0}")]
36    Serde(#[from] serde_json::Error),
37    /// Filesystem
38    #[error("{0}")]
39    Filesystem(#[from] std::io::Error),
40}
41
42impl LocalJson {
43    /// Instantiate a `LocalJson` metadata generator. Creates directories up to
44    /// the specified path
45    ///
46    /// # Errors
47    ///
48    /// - If the location exists and is not a directory
49    /// - If the directory can't be created
50    pub fn new(location: PathBuf) -> Result<Self> {
51        eyre::ensure!(
52            !location.exists() || location.is_dir(),
53            "location exists and is not a directory"
54        );
55        std::fs::create_dir_all(&location)?;
56        Ok(Self {
57            location,
58            cache: Default::default(),
59            contract_cache: Default::default(),
60        })
61    }
62
63    /// Load JSON from a specific file
64    async fn load_json<T, S>(&self, file_name: S) -> Result<Option<T>, LocalJsonError>
65    where
66        T: DeserializeOwned,
67        S: AsRef<str>,
68    {
69        let path = self.location.with_file_name(file_name.as_ref());
70        let raw = tokio::fs::read(path).await;
71        match raw {
72            Ok(raw) => Ok(serde_json::from_slice(&raw)?),
73            Err(e) => {
74                if e.kind() == tokio::io::ErrorKind::NotFound {
75                    Ok(None)
76                } else {
77                    Err(e.into())
78                }
79            }
80        }
81    }
82
83    async fn load_metadata(&self, token_id: U256) -> Result<Option<NftMetadata>, LocalJsonError> {
84        if let Some(metadata) = self.cache.read().await.get(&token_id).cloned() {
85            return Ok(Some(metadata));
86        } else if let Some(metadata) = self
87            .load_json::<NftMetadata, _>(format!("{}.json", token_id))
88            .await?
89        {
90            self.cache.write().await.insert(token_id, metadata.clone());
91            return Ok(Some(metadata));
92        }
93        Ok(None)
94    }
95
96    async fn load_contract_metadata(&self) -> Option<ContractMetadata> {
97        match *(self.contract_cache.read().await) {
98            Some(ref metadata) => Some(metadata.clone()),
99            None => match self.load_json::<ContractMetadata, _>("contract.json").await {
100                Ok(Some(metadata)) => {
101                    self.contract_cache.write().await.replace(metadata.clone());
102                    Some(metadata)
103                }
104                _ => None,
105            },
106        }
107    }
108}
109
110#[async_trait]
111impl MetadataGenerator for LocalJson {
112    type Error = LocalJsonError;
113
114    async fn metadata_for(&self, token_id: U256) -> Result<Option<NftMetadata>, Self::Error> {
115        self.load_metadata(token_id).await
116    }
117
118    async fn contract_metadata(&self) -> Option<ContractMetadata> {
119        self.load_contract_metadata().await
120    }
121}