sugar_cli/upload/
process.rs

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    // loading assets
44    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    // creates/loads the cache
56    let mut cache = load_cache(&args.cache, true)?;
57    if asset_pairs.get(&-1).is_none() {
58        cache.items.remove("-1");
59    }
60
61    // list of indices to upload
62    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        // checks if we have complete URIs in the metadata file;
70        // if true, no upload is necessary and we will use the
71        // existing URIs
72
73        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        // retrieve the existing image uri from the metadata
88        let existing_image = if is_complete_uri(&m.image) {
89            m.image.clone()
90        } else {
91            String::new()
92        };
93
94        // retrieve the existing animation uri from the metadata
95        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                    // triggers the image upload
121                    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                    // triggers the animation upload
131                    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                    // triggers the metadata upload
141                    item.metadata_hash = pair.metadata_hash.clone();
142                    item.metadata_link = String::new();
143                    item.on_chain = false;
144                    // we need to upload metadata only
145                    indices.metadata.push(*index);
146                }
147            }
148            None => {
149                let mut item = pair.clone().into_cache_item();
150
151                // check if we need to upload the image
152                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                // and we might need to upload the animation
160                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        // sanity check: verifies that both symbol and seller-fee-basis-points are the
174        // same as the ones in the config file
175        let f = File::open(Path::new(&pair.metadata))?;
176        match serde_json::from_reader(f) {
177            Ok(metadata) => {
178                let metadata: Metadata = metadata;
179                // symbol check, but only if the asset actually has the value
180                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                // seller-fee-basis-points check, but only if the asset actually has the value
192                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    // this should never happen, since every time we update the image file we
229    // need to update the metadata
230    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    // ready to upload data
242
243    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        // clear the interruption handler value ahead of the upload
273        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            // updates the list of metadata indices since the image upload
301            // might fail - removes any index that the image upload failed
302            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                        // no image link, not ready for metadata upload
308                        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            // updates the list of metadata indices since the image upload
337            // might fail - removes any index that the animation upload failed
338            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                        // no animation link, not ready for metadata upload
344                        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    // move all non-numeric keys to the beginning and sort as strings
382    // sort numeric keys as integers
383    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                // string, string
391                key_a.cmp(key_b)
392            } else if a.is_ok() && b.is_err() {
393                // number, string
394                std::cmp::Ordering::Greater
395            } else if a.is_err() && b.is_ok() {
396                // string, number
397                std::cmp::Ordering::Less
398            } else {
399                // number, number
400                a.unwrap().cmp(&b.unwrap())
401            }
402        });
403    cache.sync_file()?;
404
405    // sanity check
406
407    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        // we first check that the asset has an animation file; if there is one,
418        // we need to check that the cache item has the link and the link is not empty
419        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            // the asset does not have animation file
427            false
428        };
429
430        // only increment the count if the cache item is complete (all links are present)
431        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
478/// Upload the data to the selected storage.
479async 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        // chooses the file path based on the data type
497        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    // uploading data
530
531    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        // path to the media/metadata file
539        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            // replaces the media link without modifying the original file to avoid
549            // changing the hash of the metadata file
550            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    // makes sure the cache file is updated
585    cache.sync_file()?;
586
587    Ok(errors)
588}