sugar_cli/upload/
assets.rs1use std::{
2 ffi::OsStr,
3 fs::{self, DirEntry, File, OpenOptions},
4 io::{BufReader, Read},
5};
6
7use data_encoding::HEXLOWER;
8use glob::glob;
9use regex::{Regex, RegexBuilder};
10use ring::digest::{Context, SHA256};
11use serde::Serialize;
12use serde_json;
13
14use crate::{common::*, validate::format::Metadata};
15
16#[derive(Debug, Clone)]
17pub enum DataType {
18 Image,
19 Metadata,
20 Animation,
21}
22
23#[derive(Debug, Clone, Serialize)]
24pub struct AssetPair {
25 pub name: String,
26 pub metadata: String,
27 pub metadata_hash: String,
28 pub image: String,
29 pub image_hash: String,
30 pub animation: Option<String>,
31 pub animation_hash: Option<String>,
32}
33
34impl AssetPair {
35 pub fn into_cache_item(self) -> CacheItem {
36 CacheItem {
37 name: self.name,
38 image_hash: self.image_hash,
39 image_link: String::new(),
40 metadata_hash: self.metadata_hash,
41 metadata_link: String::new(),
42 on_chain: false,
43 animation_hash: self.animation_hash,
44 animation_link: None,
45 }
46 }
47}
48
49pub fn get_cache_item<'a>(path: &Path, cache: &'a mut Cache) -> Result<(String, &'a CacheItem)> {
50 let file_stem = String::from(
51 path.file_stem()
52 .and_then(OsStr::to_str)
53 .expect("Failed to get convert path file ext to valid unicode."),
54 );
55
56 let asset_id = if file_stem == "collection" {
58 String::from("-1")
59 } else {
60 file_stem
61 };
62
63 let cache_item: &CacheItem = cache
64 .items
65 .get(&asset_id)
66 .ok_or_else(|| anyhow!("Failed to get config item at index '{}'", asset_id))?;
67
68 Ok((asset_id, cache_item))
69}
70
71pub fn get_data_size(assets_dir: &Path, extension: &str) -> Result<u64> {
72 let path = assets_dir
73 .join(format!("*.{extension}"))
74 .to_str()
75 .expect("Failed to convert asset directory path from unicode.")
76 .to_string();
77
78 let assets = glob(&path)?;
79
80 let mut total_size = 0;
81
82 for asset in assets {
83 let asset_path = asset?;
84 let size = fs::metadata(asset_path)?.len();
85 total_size += size;
86 }
87
88 Ok(total_size)
89}
90
91pub fn list_files(assets_dir: &str, include_collection: bool) -> Result<Vec<DirEntry>> {
92 let files = fs::read_dir(assets_dir)
93 .map_err(|_| anyhow!("Failed to read assets directory"))?
94 .filter_map(|entry| entry.ok())
95 .filter(|entry| {
96 let is_file = entry
97 .metadata()
98 .expect("Failed to retrieve metadata from file")
99 .is_file();
100
101 let path = entry.path();
102 let file_stem = path
103 .file_stem()
104 .unwrap_or_default()
105 .to_str()
106 .expect("Failed to convert file name to valid unicode.");
107
108 let is_collection = include_collection && file_stem == "collection";
109 let is_numeric = file_stem.chars().all(|c| c.is_ascii_digit());
110
111 is_file && (is_numeric || is_collection)
112 });
113
114 Ok(files.collect())
115}
116
117pub fn get_asset_pairs(assets_dir: &str) -> Result<HashMap<isize, AssetPair>> {
118 let filtered_files = list_files(assets_dir, true)?;
120
121 let paths = filtered_files
122 .into_iter()
123 .map(|entry| {
124 let file_name_as_string =
125 String::from(entry.path().file_name().unwrap().to_str().unwrap());
126 file_name_as_string
127 })
128 .collect::<Vec<String>>();
129
130 let mut asset_pairs: HashMap<isize, AssetPair> = HashMap::new();
131
132 let paths_ref = &paths;
133
134 let animation_exists_regex =
135 Regex::new("^(.+)\\.((mp3)|(mp4)|(mov)|(webm)|(glb))$").expect("Failed to create regex.");
136
137 for x in paths_ref {
140 if let Some(captures) = animation_exists_regex.captures(x) {
141 if &captures[1] != "collection" && captures[1].parse::<usize>().is_err() {
142 let error = anyhow!("Couldn't parse filename '{}' to a valid index number.", x);
143 error!("{:?}", error);
144 return Err(error);
145 }
146 }
147 }
148
149 let metadata_filenames = paths_ref
150 .clone()
151 .into_iter()
152 .filter(|p| p.to_lowercase().ends_with(".json"))
153 .collect::<Vec<String>>();
154
155 ensure_sequential_files(metadata_filenames.clone())?;
156
157 for metadata_filename in metadata_filenames {
158 let i = metadata_filename.split('.').next().unwrap();
159 let is_collection_index = i == "collection";
160
161 let index: isize = if is_collection_index {
162 -1
163 } else if let Ok(index) = i.parse::<isize>() {
164 index
165 } else {
166 let error = anyhow!(
167 "Couldn't parse filename '{}' to a valid index number.",
168 metadata_filename
169 );
170 error!("{:?}", error);
171 return Err(error);
172 };
173
174 let img_pattern = format!("^{}\\.((jpg)|(jpeg)|(gif)|(png))$", i);
175
176 let img_regex = RegexBuilder::new(&img_pattern)
177 .case_insensitive(true)
178 .build()
179 .expect("Failed to create regex.");
180
181 let img_filenames = paths_ref
182 .clone()
183 .into_iter()
184 .filter(|p| img_regex.is_match(p))
185 .collect::<Vec<String>>();
186
187 let img_filename = if img_filenames.len() != 1 {
188 let error = if is_collection_index {
189 anyhow!("Couldn't find the collection image filename.")
190 } else {
191 anyhow!(
192 "Couldn't find an image filename at index {}.",
193 i.parse::<isize>().unwrap()
194 )
195 };
196 error!("{:?}", error);
197 return Err(error);
198 } else {
199 &img_filenames[0]
200 };
201
202 let animation_pattern = format!("^{}\\.((mp3)|(mp4)|(mov)|(webm)|(glb))$", i);
206 let animation_regex = RegexBuilder::new(&animation_pattern)
207 .case_insensitive(true)
208 .build()
209 .expect("Failed to create regex.");
210
211 let animation_filenames = paths_ref
212 .clone()
213 .into_iter()
214 .filter(|p| animation_regex.is_match(p))
215 .collect::<Vec<String>>();
216
217 let metadata_filepath = Path::new(assets_dir)
218 .join(&metadata_filename)
219 .to_str()
220 .expect("Failed to convert metadata path from unicode.")
221 .to_string();
222
223 let m = File::open(&metadata_filepath)?;
224 let metadata: Metadata = serde_json::from_reader(m).map_err(|e| {
225 anyhow!("Failed to read metadata file '{metadata_filepath}' with error: {e}")
226 })?;
227 let name = metadata.name.clone();
228
229 let img_filepath = Path::new(assets_dir)
230 .join(img_filename)
231 .to_str()
232 .expect("Failed to convert image path from unicode.")
233 .to_string();
234
235 let animation_filename = if animation_filenames.len() == 1 {
236 let animation_filepath = Path::new(assets_dir)
237 .join(&animation_filenames[0])
238 .to_str()
239 .expect("Failed to convert animation path from unicode.")
240 .to_string();
241
242 Some(animation_filepath)
243 } else {
244 None
245 };
246
247 let animation_hash = if let Some(animation_file) = &animation_filename {
248 let encoded_filename = encode(animation_file)?;
249 Some(encoded_filename)
250 } else {
251 None
252 };
253
254 let asset_pair = AssetPair {
255 name,
256 metadata: metadata_filepath.clone(),
257 metadata_hash: encode(&metadata_filepath)?,
258 image: img_filepath.clone(),
259 image_hash: encode(&img_filepath)?,
260 animation_hash,
261 animation: animation_filename,
262 };
263
264 asset_pairs.insert(index, asset_pair);
265 }
266
267 Ok(asset_pairs)
268}
269
270pub fn encode(file: &str) -> Result<String> {
271 let input = File::open(file)?;
272 let mut reader = BufReader::new(input);
273 let mut context = Context::new(&SHA256);
274 let mut buffer = [0; 1024];
275
276 loop {
277 let count = reader.read(&mut buffer)?;
278 if count == 0 {
279 break;
280 }
281 context.update(&buffer[..count]);
282 }
283
284 Ok(HEXLOWER.encode(context.finish().as_ref()))
285}
286
287fn ensure_sequential_files(metadata_filenames: Vec<String>) -> Result<()> {
288 let mut metadata_indices = metadata_filenames
289 .into_iter()
290 .filter(|f| !f.starts_with("collection"))
291 .map(|f| {
292 f.split('.')
293 .next()
294 .unwrap()
295 .to_string()
296 .parse::<usize>()
297 .map_err(|_| {
298 anyhow!(
299 "Couldn't parse metadata filename '{}' to a valid index number.",
300 f
301 )
302 })
303 })
304 .collect::<Result<Vec<usize>>>()?;
305 metadata_indices.sort_unstable();
306
307 metadata_indices
308 .into_iter()
309 .enumerate()
310 .try_for_each(|(i, file_index)| {
311 if i != file_index {
312 Err(anyhow!("Missing metadata file '{}.json'", i))
313 } else {
314 Ok(())
315 }
316 })
317}
318
319pub fn get_updated_metadata(
320 metadata_file: &str,
321 image_link: &str,
322 animation_link: &Option<String>,
323) -> Result<String> {
324 let mut metadata: Metadata = {
325 let m = OpenOptions::new()
326 .read(true)
327 .open(metadata_file)
328 .map_err(|e| {
329 anyhow!("Failed to read metadata file '{metadata_file}' with error: {e}")
330 })?;
331 serde_json::from_reader(&m)?
332 };
333
334 for file in &mut metadata.properties.files {
335 if file.uri.eq(&metadata.image) {
336 file.uri = image_link.to_string();
337 }
338 if let Some(ref animation_link) = animation_link {
339 if let Some(ref animation_url) = metadata.animation_url {
340 if file.uri.eq(animation_url) {
341 file.uri = animation_link.to_string();
342 }
343 }
344 }
345 }
346
347 metadata.image = image_link.to_string();
348
349 if animation_link.is_some() {
350 metadata.animation_url = animation_link.clone();
352 }
353
354 Ok(serde_json::to_string(&metadata).unwrap())
355}
356
357pub fn is_complete_uri(value: &str) -> bool {
358 matches!(url::Url::parse(value), Ok(_))
359}