sugar_cli/upload/uploader.rs
1use std::{
2 cmp,
3 collections::HashMap,
4 sync::{
5 atomic::{AtomicBool, Ordering},
6 Arc,
7 },
8};
9
10use anyhow::Result;
11use async_trait::async_trait;
12use console::style;
13use futures::future::select_all;
14pub use indicatif::ProgressBar;
15use tokio::task::JoinHandle;
16
17use crate::{
18 cache::Cache,
19 config::{ConfigData, SugarConfig, UploadMethod},
20 constants::PARALLEL_LIMIT,
21 upload::{
22 assets::{AssetPair, DataType},
23 methods::*,
24 UploadError,
25 },
26};
27
28// Size of the mock media URI for cost calculations.
29pub const MOCK_URI_SIZE: usize = 100;
30
31/// Struct representing an asset ready for upload. An `AssetInfo` can represent
32/// a physical file, in which case the `content` will correspond to the name
33/// of the file; or an in-memory asset, in which case the `content` will correspond
34/// to the content of the asset.
35///
36/// For example, for image files, the `content` contains the path of the file on the
37/// file system. In the case of json metadata files, the `content` contains the string
38/// representation of the json metadata.
39pub struct AssetInfo {
40 /// Id of the asset in the cache.
41 pub asset_id: String,
42 /// Name (file name) of the asset.
43 pub name: String,
44 /// Content of the asset - either a file path or the string representation of the content.
45 pub content: String,
46 /// Type of the asset.
47 pub data_type: DataType,
48 /// MIME content type.
49 pub content_type: String,
50}
51
52/// Types that can be prepared to upload assets (files).
53///
54/// All implementation of [`Uploader`](Uploader) need to implement this trait.
55#[async_trait]
56pub trait Prepare {
57 /// Prepare the upload of the specified media/metadata files, e.g.:
58 /// - check if any file exceeds a size limit;
59 /// - check if there is storage space for the upload;
60 /// - check/add funds for the upload.
61 ///
62 /// The `prepare` receives the information of all files that will be upload.
63 ///
64 /// # Arguments
65 ///
66 /// * `sugar_config` - The current sugar configuration
67 /// * `asset_pairs` - Mapping of `index` to an `AssetPair`
68 /// * `asset_indices` - Vector with the information of which asset pair indices will be upload grouped by type.
69 ///
70 /// The `asset_pairs` contain the complete information of the assets, but only the assets specified in the
71 /// `asset_indices` will be uploaded. E.g., if index `1` is only present in the `DataType::Image` indices' array,
72 /// only the image of asset `1` will the uploaded.
73 async fn prepare(
74 &self,
75 sugar_config: &SugarConfig,
76 asset_pairs: &HashMap<isize, AssetPair>,
77 asset_indices: Vec<(DataType, &[isize])>,
78 ) -> Result<()>;
79}
80
81/// Types that can upload assets (files).
82///
83/// This trait should be implemented directly by upload methods that require full control on how the upload
84/// is performed. For methods that support parallel uploads (threading), consider implementing
85/// [`ParallelUploader`](ParallelUploader) instead.
86#[async_trait]
87pub trait Uploader: Prepare {
88 /// Returns a vector [`UploadError`](super::errors::UploadError) with the errors (if any) after uploading all
89 /// assets to the storage.
90 ///
91 /// This function will be called to upload each type of asset separately.
92 ///
93 /// # Arguments
94 ///
95 /// * `sugar_config` - The current sugar configuration
96 /// * `cache` - Asset [`cache`](crate::cache::Cache) object (mutable)
97 /// * `data_type` - Type of the asset being uploaded
98 /// * `assets` - Vector of [`assets`](AssetInfo) to upload (mutable)
99 /// * `progress` - Reference to the [`progress bar`](indicatif::ProgressBar) to provide feedback to
100 /// the console
101 /// * `interrupted` - Reference to the shared interruption handler [`flag`](std::sync::atomic::AtomicBool)
102 /// to receive notifications
103 ///
104 /// # Examples
105 ///
106 /// Implementations are expected to use the `interrupted` to control when the user aborts the upload process.
107 /// In general, this would involve using it as a control of a loop:
108 ///
109 /// ```ignore
110 /// while !interrupted.load(Ordering::SeqCst) {
111 /// // continue with the upload
112 /// }
113 /// ```
114 ///
115 /// After uploading an asset, its information need to be updated in the cache and the cache
116 /// [`sync`](crate::cache::Cache#method.sync_file)ed to the file system. Syncing the cache to the file system
117 /// might be slow for large collections, therefore it should be done as frequent as practical to avoid slowing
118 /// down the upload process and, at the same time, minimizing the chances of information loss in case
119 /// the user aborts the upload.
120 ///
121 /// ```ignore
122 /// ...
123 /// // once an asset has been upload
124 ///
125 /// let id = asset_info.asset_id.clone();
126 /// let uri = "URI of the asset after upload";
127 /// // cache item to update
128 /// let item = cache.items.get_mut(&id).unwrap();
129 ///
130 /// match data_type {
131 /// DataType::Image => item.image_link = uri,
132 /// DataType::Metadata => item.metadata_link = uri,
133 /// DataType::Animation => item.animation_link = Some(uri),
134 /// }
135 /// // updates the progress bar
136 /// progress.inc(1);
137 ///
138 /// ...
139 ///
140 /// // after several uploads
141 /// cache.sync_file()?;
142 /// ```
143 async fn upload(
144 &self,
145 sugar_config: &SugarConfig,
146 cache: &mut Cache,
147 data_type: DataType,
148 assets: &mut Vec<AssetInfo>,
149 progress: &ProgressBar,
150 interrupted: Arc<AtomicBool>,
151 ) -> Result<Vec<UploadError>>;
152}
153
154/// Types that can upload assets in parallel.
155///
156/// This trait abstracts the threading logic and allows methods to focus on the logic of uploading a single
157/// asset (file).
158#[async_trait]
159pub trait ParallelUploader: Uploader + Send + Sync {
160 /// Returns a [`JoinHandle`](tokio::task::JoinHandle) to the task responsible to upload the specified asset.
161 ///
162 /// # Arguments
163 ///
164 /// * `asset` - The [`asset`](AssetInfo) to upload
165 ///
166 /// # Example
167 ///
168 /// In most cases, the function will return the value from [`tokio::spawn`](tokio::spawn):
169 ///
170 /// ```ignore
171 /// tokio::spawn(async move {
172 /// // code responsible to upload a single asset
173 /// });
174 /// ```
175 ///
176 fn upload_asset(&self, asset: AssetInfo) -> JoinHandle<Result<(String, String)>>;
177
178 /// Return the number of concurrent uploads allowed. The default implementation returns
179 /// the value [PARALLEL_LIMIT].
180 fn parallel_limit(&self) -> usize {
181 PARALLEL_LIMIT
182 }
183}
184
185/// Default implementation of the trait ['Uploader'](Uploader) for all ['ParallelUploader'](ParallelUploader).
186#[async_trait]
187impl<T: ParallelUploader> Uploader for T {
188 /// Uploads assets in parallel. It creates [`self::parallel_limit()`] tasks at a time to avoid
189 /// reaching the limit of concurrent files open and it syncs the cache file at every `self.parallel_limit() / 2`
190 /// step.
191 async fn upload(
192 &self,
193 _sugar_config: &SugarConfig,
194 cache: &mut Cache,
195 data_type: DataType,
196 assets: &mut Vec<AssetInfo>,
197 progress: &ProgressBar,
198 interrupted: Arc<AtomicBool>,
199 ) -> Result<Vec<UploadError>> {
200 let limit = self.parallel_limit();
201 let mut handles = Vec::new();
202
203 for task in assets.drain(0..cmp::min(assets.len(), limit)) {
204 handles.push(self.upload_asset(task));
205 }
206
207 let mut errors = Vec::new();
208
209 while !interrupted.load(Ordering::SeqCst) && !handles.is_empty() {
210 match select_all(handles).await {
211 (Ok(res), _index, remaining) => {
212 // independently if the upload was successful or not
213 // we continue to try the remaining ones
214 handles = remaining;
215 if res.is_ok() {
216 let val = res?;
217 let link = val.clone().1;
218 // cache item to update
219 let item = cache.items.0.get_mut(&val.0).unwrap();
220 match data_type {
221 DataType::Image => item.image_link = link,
222 DataType::Metadata => item.metadata_link = link,
223 DataType::Animation => item.animation_link = Some(link),
224 }
225 // updates the progress bar
226 progress.inc(1);
227 } else {
228 // user will need to retry the upload
229 errors.push(UploadError::SendDataFailed(format!(
230 "Upload error: {:?}",
231 res.err().unwrap()
232 )));
233 }
234 }
235 (Err(err), _index, remaining) => {
236 errors.push(UploadError::SendDataFailed(format!(
237 "Upload error: {:?}",
238 err
239 )));
240 // ignoring all errors
241 handles = remaining;
242 }
243 }
244 if !assets.is_empty() {
245 // if we are half way through, let spawn more transactions
246 if (limit - handles.len()) > (limit / 2) {
247 // syncs cache (checkpoint)
248 cache.sync_file()?;
249 // determine the number of task to release
250 let task_count =
251 cmp::min(assets.len(), if limit < 2 { limit } else { limit / 2 });
252
253 for task in assets.drain(0..task_count) {
254 handles.push(self.upload_asset(task));
255 }
256 }
257 }
258 }
259
260 if errors.is_empty() && !assets.is_empty() {
261 progress.abandon_with_message(format!("{}", style("Upload aborted ").red().bold()));
262 return Err(
263 UploadError::SendDataFailed("Not all files were uploaded.".to_string()).into(),
264 );
265 }
266
267 Ok(errors)
268 }
269}
270
271/// Returns a new uploader trait object based on the configuration `uploadMethod`.
272///
273/// This function acts as a *factory* function for uploader objects.
274pub async fn initialize(
275 sugar_config: &SugarConfig,
276 config_data: &ConfigData,
277) -> Result<Box<dyn Uploader>> {
278 Ok(match config_data.upload_method {
279 UploadMethod::AWS => Box::new(AWSMethod::new(config_data).await?) as Box<dyn Uploader>,
280 UploadMethod::Bundlr => {
281 Box::new(BundlrMethod::new(sugar_config, config_data).await?) as Box<dyn Uploader>
282 }
283 UploadMethod::NftStorage => {
284 Box::new(NftStorageMethod::new(config_data).await?) as Box<dyn Uploader>
285 }
286 UploadMethod::SHDW => {
287 Box::new(SHDWMethod::new(sugar_config, config_data).await?) as Box<dyn Uploader>
288 }
289 UploadMethod::Pinata => {
290 Box::new(PinataMethod::new(config_data).await?) as Box<dyn Uploader>
291 }
292 })
293}