1use std::{
2 borrow::Borrow,
3 collections::HashSet,
4 ffi::OsStr,
5 fmt::Write as _,
6 fs::OpenOptions,
7 sync::{
8 atomic::{AtomicBool, Ordering},
9 Arc,
10 },
11};
12
13use console::style;
14
15use crate::{
16 cache::{load_cache, Cache},
17 common::*,
18 config::{get_config_data, SugarConfig},
19 upload::*,
20 utils::*,
21 validate::format::Metadata,
22};
23
24pub struct UploadArgs {
25 pub assets_dir: String,
26 pub config: String,
27 pub keypair: Option<String>,
28 pub rpc_url: Option<String>,
29 pub cache: String,
30 pub interrupted: Arc<AtomicBool>,
31}
32
33pub struct AssetType {
34 pub image: Vec<isize>,
35 pub metadata: Vec<isize>,
36 pub animation: Vec<isize>,
37}
38
39pub async fn process_upload(args: UploadArgs) -> Result<()> {
40 let sugar_config = sugar_setup(args.keypair, args.rpc_url)?;
41 let config_data = get_config_data(&args.config)?;
42
43 println!(
45 "{} {}Loading assets",
46 style("[1/4]").bold().dim(),
47 ASSETS_EMOJI
48 );
49
50 let pb = spinner_with_style();
51 pb.enable_steady_tick(120);
52 pb.set_message("Reading files...");
53 let asset_pairs = get_asset_pairs(&args.assets_dir)?;
54
55 let mut cache = load_cache(&args.cache, true)?;
57 if asset_pairs.get(&-1).is_none() {
58 cache.items.remove("-1");
59 }
60
61 let mut indices = AssetType {
63 image: Vec::new(),
64 metadata: Vec::new(),
65 animation: Vec::new(),
66 };
67
68 for (index, pair) in &asset_pairs {
69 let m: Metadata = {
74 let m = OpenOptions::new()
75 .read(true)
76 .open(&pair.metadata)
77 .map_err(|e| {
78 anyhow!(
79 "Failed to read metadata file '{}' with error: {}",
80 &pair.metadata,
81 e
82 )
83 })?;
84 serde_json::from_reader(&m)?
85 };
86
87 let existing_image = if is_complete_uri(&m.image) {
89 m.image.clone()
90 } else {
91 String::new()
92 };
93
94 let existing_animation = match m.animation_url {
96 Some(ref url) => {
97 if is_complete_uri(url) {
98 url.clone()
99 } else {
100 String::new()
101 }
102 }
103 None => String::new(),
104 };
105
106 match cache.items.get_mut(&index.to_string()) {
107 Some(item) => {
108 let image_changed = (!item.image_hash.eq(&pair.image_hash)
109 || item.image_link.is_empty())
110 && existing_image.is_empty();
111
112 let animation_changed = (!item.animation_hash.eq(&pair.animation_hash)
113 || (item.animation_link.is_none() && pair.animation.is_some()))
114 && existing_animation.is_empty();
115
116 let metadata_changed =
117 !item.metadata_hash.eq(&pair.metadata_hash) || item.metadata_link.is_empty();
118
119 if image_changed {
120 item.image_hash = pair.image_hash.clone();
122 item.image_link = String::new();
123 indices.image.push(*index);
124 } else if !existing_image.is_empty() {
125 item.image_hash = pair.image_hash.clone();
126 item.image_link = existing_image;
127 }
128
129 if animation_changed {
130 item.animation_hash = pair.animation_hash.clone();
132 item.animation_link = None;
133 indices.animation.push(*index);
134 } else if !existing_animation.is_empty() {
135 item.animation_hash = pair.animation_hash.clone();
136 item.animation_link = Some(existing_animation);
137 }
138
139 if metadata_changed || image_changed || animation_changed {
140 item.metadata_hash = pair.metadata_hash.clone();
142 item.metadata_link = String::new();
143 item.on_chain = false;
144 indices.metadata.push(*index);
146 }
147 }
148 None => {
149 let mut item = pair.clone().into_cache_item();
150
151 if existing_image.is_empty() {
153 indices.image.push(*index);
154 } else {
155 item.image_hash = pair.image_hash.clone();
156 item.image_link = existing_image;
157 }
158
159 if pair.animation.is_some() {
161 if existing_animation.is_empty() {
162 indices.animation.push(*index);
163 } else {
164 item.animation_hash = pair.animation_hash.clone();
165 item.animation_link = Some(existing_animation);
166 }
167 }
168
169 indices.metadata.push(*index);
170 cache.items.insert(index.to_string(), item);
171 }
172 }
173 let f = File::open(Path::new(&pair.metadata))?;
176 match serde_json::from_reader(f) {
177 Ok(metadata) => {
178 let metadata: Metadata = metadata;
179 if let Some(symbol) = metadata.symbol {
181 if config_data.symbol.ne(&symbol) {
182 return Err(UploadError::MismatchValue(
183 "symbol".to_string(),
184 pair.metadata.clone(),
185 config_data.symbol,
186 symbol,
187 )
188 .into());
189 }
190 }
191 if let Some(seller_fee_basis_points) = metadata.seller_fee_basis_points {
193 if config_data.seller_fee_basis_points != seller_fee_basis_points {
194 return Err(UploadError::MismatchValue(
195 "seller_fee_basis_points".to_string(),
196 pair.metadata.clone(),
197 config_data.seller_fee_basis_points.to_string(),
198 seller_fee_basis_points.to_string(),
199 )
200 .into());
201 }
202 }
203 }
204 Err(err) => {
205 let error = anyhow!("Error parsing metadata ({}): {}", pair.metadata, err);
206 error!("{:?}", error);
207 return Err(error);
208 }
209 }
210 }
211
212 pb.finish_and_clear();
213
214 println!(
215 "Found {} asset pair(s), uploading files:",
216 asset_pairs.len()
217 );
218 println!("+--------------------+");
219 println!("| images | {:>6} |", indices.image.len());
220 println!("| metadata | {:>6} |", indices.metadata.len());
221
222 if !indices.animation.is_empty() {
223 println!("| animation | {:>6} |", indices.animation.len());
224 }
225
226 println!("+--------------------+");
227
228 if indices.image.len() > indices.metadata.len() {
231 return Err(anyhow!(format!(
232 "There are more image files ({}) to upload than metadata ({})",
233 indices.image.len(),
234 indices.metadata.len(),
235 )));
236 }
237
238 let need_upload =
239 !indices.image.is_empty() || !indices.metadata.is_empty() || !indices.animation.is_empty();
240
241 let mut errors = Vec::new();
244
245 if need_upload {
246 let total_steps = if indices.animation.is_empty() { 4 } else { 5 };
247 println!(
248 "\n{} {}Initializing upload",
249 style(format!("[2/{}]", total_steps)).bold().dim(),
250 COMPUTER_EMOJI
251 );
252
253 let pb = spinner_with_style();
254 pb.set_message("Connecting...");
255
256 let storage = initialize(&sugar_config, &config_data).await?;
257
258 pb.finish_with_message("Connected");
259
260 storage
261 .prepare(
262 &sugar_config,
263 &asset_pairs,
264 vec![
265 (DataType::Image, &indices.image),
266 (DataType::Animation, &indices.animation),
267 (DataType::Metadata, &indices.metadata),
268 ],
269 )
270 .await?;
271
272 args.interrupted.store(false, Ordering::SeqCst);
274
275 println!(
276 "\n{} {}Uploading image files {}",
277 style(format!("[3/{}]", total_steps)).bold().dim(),
278 UPLOAD_EMOJI,
279 if indices.image.is_empty() {
280 "(skipping)"
281 } else {
282 ""
283 }
284 );
285
286 if !indices.image.is_empty() {
287 errors.extend(
288 upload_data(
289 &sugar_config,
290 &asset_pairs,
291 &mut cache,
292 &indices.image,
293 DataType::Image,
294 storage.borrow(),
295 args.interrupted.clone(),
296 )
297 .await?,
298 );
299
300 if !indices.metadata.is_empty() {
303 for index in indices.image {
304 let item = cache.items.get(&index.to_string()).unwrap();
305
306 if item.image_link.is_empty() {
307 indices.metadata.retain(|&x| x != index);
309 }
310 }
311 }
312 }
313
314 if !indices.animation.is_empty() {
315 println!(
316 "\n{} {}Uploading animation files",
317 style("[4/5]").bold().dim(),
318 UPLOAD_EMOJI
319 );
320 }
321
322 if !indices.animation.is_empty() {
323 errors.extend(
324 upload_data(
325 &sugar_config,
326 &asset_pairs,
327 &mut cache,
328 &indices.animation,
329 DataType::Animation,
330 storage.borrow(),
331 args.interrupted.clone(),
332 )
333 .await?,
334 );
335
336 if !indices.metadata.is_empty() {
339 for index in indices.animation {
340 let item = cache.items.get(&index.to_string()).unwrap();
341
342 if item.animation_link.is_none() {
343 indices.metadata.retain(|&x| x != index);
345 }
346 }
347 }
348 }
349
350 println!(
351 "\n{} {}Uploading metadata files {}",
352 style(format!("[{}/{}]", total_steps, total_steps))
353 .bold()
354 .dim(),
355 UPLOAD_EMOJI,
356 if indices.metadata.is_empty() {
357 "(skipping)"
358 } else {
359 ""
360 }
361 );
362
363 if !indices.metadata.is_empty() {
364 errors.extend(
365 upload_data(
366 &sugar_config,
367 &asset_pairs,
368 &mut cache,
369 &indices.metadata,
370 DataType::Metadata,
371 storage.borrow(),
372 args.interrupted.clone(),
373 )
374 .await?,
375 );
376 }
377 } else {
378 println!("\n....no files need uploading, skipping remaining steps.");
379 }
380
381 cache
384 .items
385 .sort_by(|key_a, _, key_b, _| -> std::cmp::Ordering {
386 let a = key_a.parse::<i32>();
387 let b = key_b.parse::<i32>();
388
389 if a.is_err() && b.is_err() {
390 key_a.cmp(key_b)
392 } else if a.is_ok() && b.is_err() {
393 std::cmp::Ordering::Greater
395 } else if a.is_err() && b.is_ok() {
396 std::cmp::Ordering::Less
398 } else {
399 a.unwrap().cmp(&b.unwrap())
401 }
402 });
403 cache.sync_file()?;
404
405 let mut count = 0;
408
409 for (index, item) in &cache.items.0 {
410 let asset_pair = asset_pairs.get(&isize::from_str(index)?).ok_or_else(|| {
411 anyhow!(
412 "cache item {} does not have a corresponding asset pair",
413 index
414 )
415 })?;
416
417 let missing_animation_link = if asset_pair.animation.is_some() {
420 if let Some(link) = &item.animation_link {
421 link.is_empty()
422 } else {
423 true
424 }
425 } else {
426 false
428 };
429
430 if !(item.image_link.is_empty() || item.metadata_link.is_empty() || missing_animation_link)
432 {
433 count += 1;
434 }
435 }
436
437 println!(
438 "\n{}",
439 style(format!(
440 "{}/{} asset pair(s) uploaded.",
441 count,
442 asset_pairs.len()
443 ))
444 .bold()
445 );
446
447 if count != asset_pairs.len() {
448 let message = if !errors.is_empty() {
449 let mut message = String::new();
450 write!(
451 message,
452 "Failed to upload all files, {0} error(s) occurred:",
453 errors.len()
454 )?;
455
456 let mut unique = HashSet::new();
457
458 for err in errors {
459 unique.insert(err.to_string());
460 }
461
462 for u in unique {
463 message.push_str(&style("\n=> ").dim().to_string());
464 message.push_str(&u);
465 }
466
467 message
468 } else {
469 "Not all files were uploaded.".to_string()
470 };
471
472 return Err(UploadError::Incomplete(message).into());
473 }
474
475 Ok(())
476}
477
478async fn upload_data(
480 sugar_config: &SugarConfig,
481 asset_pairs: &HashMap<isize, AssetPair>,
482 cache: &mut Cache,
483 indices: &[isize],
484 data_type: DataType,
485 uploader: &dyn Uploader,
486 interrupted: Arc<AtomicBool>,
487) -> Result<Vec<UploadError>> {
488 let mut extension = String::new();
489 let mut paths = Vec::new();
490
491 for index in indices {
492 let item = match asset_pairs.get(index) {
493 Some(asset_index) => asset_index,
494 None => return Err(anyhow::anyhow!("Failed to get asset at index {}", index)),
495 };
496 let file_path = match data_type {
498 DataType::Image => item.image.clone(),
499 DataType::Metadata => item.metadata.clone(),
500 DataType::Animation => {
501 if let Some(animation) = item.animation.clone() {
502 animation
503 } else {
504 return Err(anyhow::anyhow!(
505 "Missing animation path for asset at index {}",
506 index
507 ));
508 }
509 }
510 };
511
512 let path = Path::new(&file_path);
513 let ext = path
514 .extension()
515 .and_then(OsStr::to_str)
516 .expect("Failed to convert extension from unicode");
517
518 extension = String::from(ext);
519
520 paths.push(file_path);
521 }
522
523 let content_type = match data_type {
524 DataType::Image => format!("image/{}", extension),
525 DataType::Metadata => "application/json".to_string(),
526 DataType::Animation => format!("video/{}", extension),
527 };
528
529 println!("\nSending data: (Ctrl+C to abort)");
532
533 let pb = progress_bar_with_style(paths.len() as u64);
534
535 let mut assets = Vec::new();
536
537 for file_path in paths {
538 let path = Path::new(&file_path);
540 let file_name = String::from(
541 path.file_name()
542 .and_then(OsStr::to_str)
543 .expect("Filed to get file name."),
544 );
545 let (asset_id, cache_item) = get_cache_item(path, cache)?;
546
547 let content = match data_type {
548 DataType::Metadata => get_updated_metadata(
551 &file_path,
552 &cache_item.image_link,
553 &cache_item.animation_link,
554 )?,
555 _ => file_path.clone(),
556 };
557
558 assets.push(AssetInfo {
559 asset_id: asset_id.to_string(),
560 name: file_name,
561 content,
562 data_type: data_type.clone(),
563 content_type: content_type.clone(),
564 });
565 }
566
567 let errors = uploader
568 .upload(
569 sugar_config,
570 cache,
571 data_type,
572 &mut assets,
573 &pb,
574 interrupted,
575 )
576 .await?;
577
578 if !errors.is_empty() {
579 pb.abandon_with_message(format!("{}", style("Upload failed ").red().bold()));
580 } else {
581 pb.finish_with_message(format!("{}", style("Upload successful ").green().bold()));
582 }
583
584 cache.sync_file()?;
586
587 Ok(errors)
588}