fud/
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
19use std::{
20    collections::HashSet,
21    path::{Path, PathBuf},
22};
23use tinyjson::JsonValue;
24
25use darkfi::{
26    geode::{hash_to_string, ChunkedStorage, MAX_CHUNK_SIZE},
27    rpc::util::json_map,
28    Error, Result,
29};
30
31use crate::FileSelection;
32
33#[derive(Clone, Debug)]
34pub enum ResourceStatus {
35    Downloading,
36    Seeding,
37    Discovering,
38    Incomplete,
39    Verifying,
40}
41
42impl 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)]
65pub enum ResourceType {
66    Unknown,
67    File,
68    Directory,
69}
70
71impl 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)]
92pub 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
128impl 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
266impl 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
292impl 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}