use std::{
cmp,
collections::HashMap,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use anyhow::Result;
use async_trait::async_trait;
use console::style;
use futures::future::select_all;
pub use indicatif::ProgressBar;
use tokio::task::JoinHandle;
use crate::{
cache::Cache,
config::{ConfigData, SugarConfig, UploadMethod},
constants::PARALLEL_LIMIT,
upload::{
assets::{AssetPair, DataType},
methods::*,
UploadError,
},
};
pub const MOCK_URI_SIZE: usize = 100;
pub struct AssetInfo {
pub asset_id: String,
pub name: String,
pub content: String,
pub data_type: DataType,
pub content_type: String,
}
#[async_trait]
pub trait Prepare {
async fn prepare(
&self,
sugar_config: &SugarConfig,
asset_pairs: &HashMap<isize, AssetPair>,
asset_indices: Vec<(DataType, &[isize])>,
) -> Result<()>;
}
#[async_trait]
pub trait Uploader: Prepare {
async fn upload(
&self,
sugar_config: &SugarConfig,
cache: &mut Cache,
data_type: DataType,
assets: &mut Vec<AssetInfo>,
progress: &ProgressBar,
interrupted: Arc<AtomicBool>,
) -> Result<Vec<UploadError>>;
}
#[async_trait]
pub trait ParallelUploader: Uploader + Send + Sync {
fn upload_asset(&self, asset: AssetInfo) -> JoinHandle<Result<(String, String)>>;
fn parallel_limit(&self) -> usize {
PARALLEL_LIMIT
}
}
#[async_trait]
impl<T: ParallelUploader> Uploader for T {
async fn upload(
&self,
_sugar_config: &SugarConfig,
cache: &mut Cache,
data_type: DataType,
assets: &mut Vec<AssetInfo>,
progress: &ProgressBar,
interrupted: Arc<AtomicBool>,
) -> Result<Vec<UploadError>> {
let limit = self.parallel_limit();
let mut handles = Vec::new();
for task in assets.drain(0..cmp::min(assets.len(), limit)) {
handles.push(self.upload_asset(task));
}
let mut errors = Vec::new();
while !interrupted.load(Ordering::SeqCst) && !handles.is_empty() {
match select_all(handles).await {
(Ok(res), _index, remaining) => {
handles = remaining;
if res.is_ok() {
let val = res?;
let link = val.clone().1;
let item = cache.items.0.get_mut(&val.0).unwrap();
match data_type {
DataType::Image => item.image_link = link,
DataType::Metadata => item.metadata_link = link,
DataType::Animation => item.animation_link = Some(link),
}
progress.inc(1);
} else {
errors.push(UploadError::SendDataFailed(format!(
"Upload error: {:?}",
res.err().unwrap()
)));
}
}
(Err(err), _index, remaining) => {
errors.push(UploadError::SendDataFailed(format!(
"Upload error: {err:?}"
)));
handles = remaining;
}
}
if !assets.is_empty() {
if (limit - handles.len()) > (limit / 2) {
cache.sync_file()?;
let task_count =
cmp::min(assets.len(), if limit < 2 { limit } else { limit / 2 });
for task in assets.drain(0..task_count) {
handles.push(self.upload_asset(task));
}
}
}
}
if errors.is_empty() && !assets.is_empty() {
progress.abandon_with_message(format!("{}", style("Upload aborted ").red().bold()));
return Err(
UploadError::SendDataFailed("Not all files were uploaded.".to_string()).into(),
);
}
Ok(errors)
}
}
pub async fn initialize(
sugar_config: &SugarConfig,
config_data: &ConfigData,
) -> Result<Box<dyn Uploader>> {
Ok(match config_data.upload_method {
UploadMethod::AWS => Box::new(AWSMethod::new(config_data).await?) as Box<dyn Uploader>,
UploadMethod::Bundlr => {
Box::new(BundlrMethod::new(sugar_config, config_data).await?) as Box<dyn Uploader>
}
UploadMethod::Pinata => {
Box::new(pinata::PinataMethod::new(config_data).await?) as Box<dyn Uploader>
}
UploadMethod::Sdrive => {
Box::new(sdrive::SdriveMethod::new(config_data).await?) as Box<dyn Uploader>
}
})
}