/ bin / fud / fud / src / resource.rs
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  }