sugar_cli/upload/methods/
nft_storage.rs1use std::{
2 fs,
3 path::Path,
4 sync::{
5 atomic::{AtomicBool, Ordering},
6 Arc,
7 },
8};
9
10use async_trait::async_trait;
11use reqwest::{
12 header,
13 multipart::{Form, Part},
14 Client, StatusCode,
15};
16use tokio::time::{sleep, Duration};
17
18use crate::{common::*, config::*, upload::*};
19
20const NFT_STORAGE_API_URL: &str = "https://api.nft.storage";
22const NFT_STORAGE_GATEWAY_URL: &str = "https://nftstorage.link/ipfs";
24const REQUEST_WAIT: u64 = 10000;
26const FILE_SIZE_LIMIT: u64 = 100 * 1024 * 1024;
28const FILE_COUNT_LIMIT: u64 = 100;
30
31pub enum NftStorageError {
32 ApiError(Value),
33}
34
35#[derive(Debug, Deserialize, Default)]
37pub struct StoreNftResponse {
38 pub ok: bool,
40 pub value: NftValue,
42}
43
44#[derive(Debug, Deserialize, Default)]
46#[serde(default)]
47pub struct NftValue {
48 pub cid: String,
50}
51
52#[derive(Debug, Deserialize, Default)]
54pub struct StoreNftError {
55 pub ok: bool,
57 pub error: NftError,
59}
60
61#[derive(Debug, Deserialize, Default)]
63#[serde(default)]
64pub struct NftError {
65 pub message: String,
67}
68
69pub struct NftStorageMethod {
70 client: Arc<Client>,
71}
72
73impl NftStorageMethod {
74 pub async fn new(config_data: &ConfigData) -> Result<Self> {
76 if let Some(auth_token) = &config_data.nft_storage_auth_token {
77 let client_builder = Client::builder();
78
79 let mut headers = header::HeaderMap::new();
80 let bearer_value = format!("Bearer {}", auth_token);
81 let mut auth_value = header::HeaderValue::from_str(&bearer_value)?;
82 auth_value.set_sensitive(true);
83 headers.insert(header::AUTHORIZATION, auth_value);
84
85 let client = client_builder.default_headers(headers).build()?;
86
87 let url = format!("{}/", NFT_STORAGE_API_URL);
88 let response = client.get(url).send().await?;
89
90 match response.status() {
91 StatusCode::OK => Ok(Self {
92 client: Arc::new(client),
93 }),
94 StatusCode::UNAUTHORIZED => {
95 Err(anyhow!("Invalid nft.storage authentication token."))
96 }
97 code => Err(anyhow!("Could not initialize nft.storage client: {code}")),
98 }
99 } else {
100 Err(anyhow!(
101 "Missing 'nftStorageAuthToken' value in config file."
102 ))
103 }
104 }
105}
106
107#[async_trait]
108impl Prepare for NftStorageMethod {
109 async fn prepare(
112 &self,
113 _sugar_config: &SugarConfig,
114 asset_pairs: &HashMap<isize, AssetPair>,
115 asset_indices: Vec<(DataType, &[isize])>,
116 ) -> Result<()> {
117 for (data_type, indices) in asset_indices {
118 for index in indices {
119 let item = asset_pairs.get(index).unwrap();
120 let size = match data_type {
121 DataType::Image => {
122 let path = Path::new(&item.image);
123 fs::metadata(path)?.len()
124 }
125 DataType::Animation => {
126 if let Some(animation) = &item.animation {
127 let path = Path::new(animation);
128 fs::metadata(path)?.len()
129 } else {
130 0
131 }
132 }
133 DataType::Metadata => {
134 let mock_uri = "x".repeat(MOCK_URI_SIZE);
135 let animation = if item.animation.is_some() {
136 Some(mock_uri.clone())
137 } else {
138 None
139 };
140
141 get_updated_metadata(&item.metadata, &mock_uri.clone(), &animation)?
142 .into_bytes()
143 .len() as u64
144 }
145 };
146
147 if size > FILE_SIZE_LIMIT {
148 return Err(anyhow!(
149 "File '{}' exceeds the current 100MB file size limit",
150 item.name,
151 ));
152 }
153 }
154 }
155 Ok(())
156 }
157}
158
159#[async_trait]
160impl Uploader for NftStorageMethod {
161 async fn upload(
163 &self,
164 _sugar_config: &SugarConfig,
165 cache: &mut Cache,
166 data_type: DataType,
167 assets: &mut Vec<AssetInfo>,
168 progress: &ProgressBar,
169 interrupted: Arc<AtomicBool>,
170 ) -> Result<Vec<UploadError>> {
171 let mut batches: Vec<Vec<&AssetInfo>> = Vec::new();
172 let mut current: Vec<&AssetInfo> = Vec::new();
173 let mut upload_size = 0;
174 let mut upload_count = 0;
175
176 for asset_info in assets {
177 let size = match data_type {
178 DataType::Image | DataType::Animation => {
179 let path = Path::new(&asset_info.content);
180 fs::metadata(path)?.len()
181 }
182 DataType::Metadata => {
183 let content = String::from(&asset_info.content);
184 content.into_bytes().len() as u64
185 }
186 };
187
188 if (upload_size + size) > FILE_SIZE_LIMIT || (upload_count + 1) > FILE_COUNT_LIMIT {
189 batches.push(current);
190 current = Vec::new();
191 upload_size = 0;
192 upload_count = 0;
193 }
194
195 upload_size += size;
196 upload_count += 1;
197 current.push(asset_info);
198 }
199 if !current.is_empty() {
201 batches.push(current);
202 }
203
204 let mut errors = Vec::new();
205 progress.set_length(batches.len() as u64);
207
208 while !interrupted.load(Ordering::SeqCst) && !batches.is_empty() {
209 let batch = batches.remove(0);
210 let mut form = Form::new();
211
212 for asset_info in &batch {
213 let data = match asset_info.data_type {
214 DataType::Image | DataType::Animation => fs::read(&asset_info.content)?,
215 DataType::Metadata => {
216 let content = String::from(&asset_info.content);
217 content.into_bytes()
218 }
219 };
220
221 let file = Part::bytes(data)
222 .file_name(asset_info.name.clone())
223 .mime_str(asset_info.content_type.as_str())?;
224 form = form.part("file", file);
225 }
226
227 let response = self
228 .client
229 .post(format!("{NFT_STORAGE_API_URL}/upload"))
230 .multipart(form)
231 .send()
232 .await?;
233 let status = response.status();
234
235 if status.is_success() {
236 let body = response.json::<Value>().await?;
237 let StoreNftResponse {
238 value: NftValue { cid },
239 ..
240 }: StoreNftResponse = serde_json::from_value(body)?;
241
242 for asset_info in batch {
245 let id = asset_info.asset_id.clone();
246 let uri = format!("{NFT_STORAGE_GATEWAY_URL}/{cid}/{}", asset_info.name);
247 let item = cache.items.get_mut(&id).unwrap();
249
250 match data_type {
251 DataType::Image => item.image_link = uri,
252 DataType::Metadata => item.metadata_link = uri,
253 DataType::Animation => item.animation_link = Some(uri),
254 }
255 }
256 cache.sync_file()?;
258 progress.inc(1);
260 } else {
261 let body = response.json::<Value>().await?;
262 let StoreNftError {
263 error: NftError { message },
264 ..
265 }: StoreNftError = serde_json::from_value(body)?;
266
267 errors.push(UploadError::SendDataFailed(format!(
268 "Error uploading batch ({}): {}",
269 status, message
270 )));
271 }
272 if !batches.is_empty() {
273 sleep(Duration::from_millis(REQUEST_WAIT)).await;
275 }
276 }
277
278 Ok(errors)
279 }
280}