resource.rs
1 /* This file is part of DarkFi (https://dark.fi) 2 * 3 * Copyright (C) 2020-2025 Dyne.org foundation 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation, either version 3 of the 8 * License, or (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU Affero General Public License for more details. 14 * 15 * You should have received a copy of the GNU Affero General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19 use std::{ 20 collections::HashSet, 21 path::{Path, PathBuf}, 22 }; 23 use tinyjson::JsonValue; 24 25 use darkfi::{ 26 geode::{hash_to_string, ChunkedStorage, MAX_CHUNK_SIZE}, 27 rpc::util::json_map, 28 Error, Result, 29 }; 30 31 use crate::FileSelection; 32 33 #[derive(Clone, Debug)] 34 pub enum ResourceStatus { 35 Downloading, 36 Seeding, 37 Discovering, 38 Incomplete, 39 Verifying, 40 } 41 42 impl ResourceStatus { 43 pub fn as_str(&self) -> &str { 44 match self { 45 ResourceStatus::Downloading => "downloading", 46 ResourceStatus::Seeding => "seeding", 47 ResourceStatus::Discovering => "discovering", 48 ResourceStatus::Incomplete => "incomplete", 49 ResourceStatus::Verifying => "verifying", 50 } 51 } 52 fn from_str(s: &str) -> Result<Self> { 53 match s { 54 "downloading" => Ok(ResourceStatus::Downloading), 55 "seeding" => Ok(ResourceStatus::Seeding), 56 "discovering" => Ok(ResourceStatus::Discovering), 57 "incomplete" => Ok(ResourceStatus::Incomplete), 58 "verifying" => Ok(ResourceStatus::Verifying), 59 _ => Err(Error::Custom("Invalid resource status".to_string())), 60 } 61 } 62 } 63 64 #[derive(Clone, Debug, PartialEq)] 65 pub enum ResourceType { 66 Unknown, 67 File, 68 Directory, 69 } 70 71 impl ResourceType { 72 pub fn as_str(&self) -> &str { 73 match self { 74 ResourceType::Unknown => "unknown", 75 ResourceType::File => "file", 76 ResourceType::Directory => "directory", 77 } 78 } 79 fn from_str(s: &str) -> Result<Self> { 80 match s { 81 "unknown" => Ok(ResourceType::Unknown), 82 "file" => Ok(ResourceType::File), 83 "directory" => Ok(ResourceType::Directory), 84 _ => Err(Error::Custom("Invalid resource type".to_string())), 85 } 86 } 87 } 88 89 /// Structure representing the current state of a file or directory on fud. 90 /// It is used in most `FudEvent`. 91 #[derive(Clone, Debug)] 92 pub struct Resource { 93 /// Resource hash (used as key in the DHT) 94 pub hash: blake3::Hash, 95 /// Resource type (file or directory) 96 pub rtype: ResourceType, 97 /// Path of the resource on the filesystem 98 pub path: PathBuf, 99 /// Current status of the resource 100 pub status: ResourceStatus, 101 /// The files the user wants to download 102 pub file_selection: FileSelection, 103 104 /// Total number of chunks 105 pub total_chunks_count: u64, 106 /// Number of chunks we want to download 107 pub target_chunks_count: u64, 108 /// Number of chunks we already downloaded 109 pub total_chunks_downloaded: u64, 110 /// Number of chunks we already downloaded, 111 /// but only those we want to download on the last fetch request 112 pub target_chunks_downloaded: u64, 113 114 /// Total size (in bytes) of the resource 115 pub total_bytes_size: u64, 116 /// Data (in bytes) we want to download 117 pub target_bytes_size: u64, 118 /// Data (in bytes) we already downloaded 119 pub total_bytes_downloaded: u64, 120 /// Data (in bytes) we already downloaded, 121 /// but only data we want to download on the last fetch request 122 pub target_bytes_downloaded: u64, 123 124 /// Recent speeds in bytes/sec, used to compute the download ETA. 125 pub speeds: Vec<f64>, 126 } 127 128 impl Resource { 129 pub fn new( 130 hash: blake3::Hash, 131 rtype: ResourceType, 132 path: &Path, 133 status: ResourceStatus, 134 file_selection: FileSelection, 135 ) -> Self { 136 Self { 137 hash, 138 rtype, 139 path: path.to_path_buf(), 140 status, 141 file_selection, 142 total_chunks_count: 0, 143 target_chunks_count: 0, 144 total_chunks_downloaded: 0, 145 target_chunks_downloaded: 0, 146 total_bytes_size: 0, 147 target_bytes_size: 0, 148 total_bytes_downloaded: 0, 149 target_bytes_downloaded: 0, 150 speeds: vec![], 151 } 152 } 153 154 /// Computes and returns download ETA in seconds using the `speeds` list. 155 pub fn get_eta(&self) -> u64 { 156 if self.speeds.is_empty() { 157 return 0 158 } 159 160 let remaining_chunks = self.target_chunks_count - self.target_chunks_downloaded; 161 let mean_speed = self.speeds.iter().sum::<f64>() / self.speeds.len() as f64; 162 163 ((remaining_chunks * MAX_CHUNK_SIZE as u64) as f64 / mean_speed) as u64 164 } 165 166 /// Returns the list of selected files (absolute paths). 167 pub fn get_selected_files(&self, chunked: &ChunkedStorage) -> Vec<PathBuf> { 168 match &self.file_selection { 169 FileSelection::Set(files) => files 170 .iter() 171 .map(|file| self.path.join(file)) 172 .filter(|abs| chunked.get_files().iter().any(|(f, _)| f == abs)) 173 .collect(), 174 FileSelection::All => chunked.get_files().iter().map(|(f, _)| f.clone()).collect(), 175 } 176 } 177 178 /// Returns the (sub)set of chunk hashes in a ChunkedStorage for a file selection. 179 pub fn get_selected_chunks(&self, chunked: &ChunkedStorage) -> HashSet<blake3::Hash> { 180 match &self.file_selection { 181 FileSelection::Set(files) => { 182 let mut chunks = HashSet::new(); 183 for file in files { 184 chunks.extend(chunked.get_chunks_of_file(&self.path.join(file))); 185 } 186 chunks 187 } 188 FileSelection::All => chunked.iter().cloned().map(|(hash, _)| hash).collect(), 189 } 190 } 191 192 /// Returns the number of bytes we want from a chunk (depends on the file selection). 193 pub fn get_selected_bytes(&self, chunked: &ChunkedStorage, chunk: &[u8]) -> usize { 194 // If `FileSelection` is not a set, we want all bytes from a chunk 195 let file_set = if let FileSelection::Set(files) = &self.file_selection { 196 files 197 } else { 198 return chunk.len(); 199 }; 200 201 let chunk_hash = blake3::hash(chunk); 202 let chunk_index = match chunked.iter().position(|(h, _)| *h == chunk_hash) { 203 Some(index) => index, 204 None => { 205 return 0; 206 } 207 }; 208 209 let files = chunked.get_files(); 210 let chunk_length = chunk.len(); 211 let position = (chunk_index as u64) * (MAX_CHUNK_SIZE as u64); 212 let mut total_selected_bytes = 0; 213 214 // Find the starting file index based on the position 215 let mut file_index = 0; 216 let mut file_start_pos = 0; 217 218 while file_index < files.len() { 219 if file_start_pos + files[file_index].1 > position { 220 break; 221 } 222 file_start_pos += files[file_index].1; 223 file_index += 1; 224 } 225 226 if file_index >= files.len() { 227 // Out of bounds 228 return 0; 229 } 230 231 // Calculate the end position of the chunk 232 let end_position = position + chunk_length as u64; 233 234 // Iterate through the files and count selected bytes 235 while file_index < files.len() { 236 let (file_path, file_size) = &files[file_index]; 237 let file_end_pos = file_start_pos + *file_size; 238 239 // Check if the file is in the selection 240 if let Ok(rel_file_path) = file_path.strip_prefix(&self.path) { 241 if file_set.contains(rel_file_path) { 242 // Calculate the overlap with the chunk 243 let overlap_start = position.max(file_start_pos); 244 let overlap_end = end_position.min(file_end_pos); 245 246 if overlap_start < overlap_end { 247 total_selected_bytes += (overlap_end - overlap_start) as usize; 248 } 249 } 250 } 251 252 // Move to the next file 253 file_start_pos += *file_size; 254 file_index += 1; 255 256 // Stop if we've reached the end of the chunk 257 if file_start_pos >= end_position { 258 break; 259 } 260 } 261 262 total_selected_bytes 263 } 264 } 265 266 impl From<Resource> for JsonValue { 267 fn from(rs: Resource) -> JsonValue { 268 json_map([ 269 ("hash", JsonValue::String(hash_to_string(&rs.hash))), 270 ("type", JsonValue::String(rs.rtype.as_str().to_string())), 271 ( 272 "path", 273 JsonValue::String(match rs.path.clone().into_os_string().into_string() { 274 Ok(path) => path, 275 Err(_) => "".to_string(), 276 }), 277 ), 278 ("status", JsonValue::String(rs.status.as_str().to_string())), 279 ("total_chunks_count", JsonValue::Number(rs.total_chunks_count as f64)), 280 ("target_chunks_count", JsonValue::Number(rs.target_chunks_count as f64)), 281 ("total_chunks_downloaded", JsonValue::Number(rs.total_chunks_downloaded as f64)), 282 ("target_chunks_downloaded", JsonValue::Number(rs.target_chunks_downloaded as f64)), 283 ("total_bytes_size", JsonValue::Number(rs.total_bytes_size as f64)), 284 ("target_bytes_size", JsonValue::Number(rs.target_bytes_size as f64)), 285 ("total_bytes_downloaded", JsonValue::Number(rs.total_bytes_downloaded as f64)), 286 ("target_bytes_downloaded", JsonValue::Number(rs.target_bytes_downloaded as f64)), 287 ("speeds", JsonValue::Array(rs.speeds.into_iter().map(JsonValue::Number).collect())), 288 ]) 289 } 290 } 291 292 impl From<JsonValue> for Resource { 293 fn from(value: JsonValue) -> Self { 294 let mut hash_buf = vec![]; 295 let _ = bs58::decode(value["hash"].get::<String>().unwrap().as_str()).onto(&mut hash_buf); 296 let mut hash_buf_arr = [0u8; 32]; 297 hash_buf_arr.copy_from_slice(&hash_buf); 298 let hash = blake3::Hash::from_bytes(hash_buf_arr); 299 300 let rtype = ResourceType::from_str(value["type"].get::<String>().unwrap()).unwrap(); 301 let path = PathBuf::from(value["path"].get::<String>().unwrap()); 302 let status = ResourceStatus::from_str(value["status"].get::<String>().unwrap()).unwrap(); 303 304 let total_chunks_count = *value["total_chunks_count"].get::<f64>().unwrap() as u64; 305 let target_chunks_count = *value["target_chunks_count"].get::<f64>().unwrap() as u64; 306 let total_chunks_downloaded = 307 *value["total_chunks_downloaded"].get::<f64>().unwrap() as u64; 308 let target_chunks_downloaded = 309 *value["target_chunks_downloaded"].get::<f64>().unwrap() as u64; 310 let total_bytes_size = *value["total_bytes_size"].get::<f64>().unwrap() as u64; 311 let target_bytes_size = *value["target_bytes_size"].get::<f64>().unwrap() as u64; 312 let total_bytes_downloaded = *value["total_bytes_downloaded"].get::<f64>().unwrap() as u64; 313 let target_bytes_downloaded = 314 *value["target_bytes_downloaded"].get::<f64>().unwrap() as u64; 315 316 let speeds = value["speeds"] 317 .get::<Vec<JsonValue>>() 318 .unwrap() 319 .iter() 320 .map(|s| *s.get::<f64>().unwrap()) 321 .collect::<Vec<f64>>(); 322 323 Resource { 324 hash, 325 rtype, 326 path, 327 status, 328 file_selection: FileSelection::All, // TODO 329 total_chunks_count, 330 target_chunks_count, 331 total_chunks_downloaded, 332 target_chunks_downloaded, 333 total_bytes_size, 334 target_bytes_size, 335 total_bytes_downloaded, 336 target_bytes_downloaded, 337 speeds, 338 } 339 } 340 }