torrust_index/services/
torrent_file.rs

1//! This module contains the services related to torrent file management.
2use uuid::Uuid;
3
4use crate::models::torrent_file::{Torrent, TorrentFile, TorrentInfoDictionary};
5use crate::services::hasher::sha1;
6
7/// It contains the information required to create a new torrent file.
8///
9/// It's not the full in-memory representation of a torrent file. The full
10/// in-memory representation is the `Torrent` struct.
11pub struct CreateTorrentRequest {
12    // The `info` dictionary fields
13    pub name: String,
14    pub pieces_or_root_hash: String,
15    pub piece_length: i64,
16    pub private: Option<u8>,
17    /// True (1) if it's a BEP 30 torrent.
18    pub is_bep_30: i64,
19    pub files: Vec<TorrentFile>,
20    // Other fields of the root level metainfo dictionary
21    pub announce_urls: Vec<Vec<String>>,
22    pub comment: Option<String>,
23    pub creation_date: Option<i64>,
24    pub created_by: Option<String>,
25    pub encoding: Option<String>,
26}
27
28impl CreateTorrentRequest {
29    /// It builds a `Torrent` from a request.
30    ///
31    /// # Panics
32    ///
33    /// This function will panic if the `torrent_info.pieces` is not a valid hex string.
34    #[must_use]
35    pub fn build_torrent(&self) -> Torrent {
36        let info_dict = self.build_info_dictionary();
37
38        Torrent {
39            info: info_dict,
40            announce: None,
41            nodes: None,
42            encoding: self.encoding.clone(),
43            httpseeds: None,
44            announce_list: Some(self.announce_urls.clone()),
45            creation_date: self.creation_date,
46            comment: self.comment.clone(),
47            created_by: self.created_by.clone(),
48        }
49    }
50
51    /// It builds a `TorrentInfoDictionary` from the current torrent request.
52    ///
53    /// # Panics
54    ///
55    /// This function will panic if the `pieces` field is not a valid hex string.
56    #[must_use]
57    fn build_info_dictionary(&self) -> TorrentInfoDictionary {
58        TorrentInfoDictionary::with(
59            &self.name,
60            self.piece_length,
61            self.private,
62            self.is_bep_30,
63            &self.pieces_or_root_hash,
64            &self.files,
65        )
66    }
67}
68
69/// It generates a random single-file torrent for testing purposes.
70///
71/// The torrent will contain a single text file with the UUID as its content.
72///
73/// # Panics
74///
75/// This function will panic if the sample file contents length in bytes is
76/// greater than `i64::MAX`.
77#[must_use]
78pub fn generate_random_torrent(id: Uuid) -> Torrent {
79    // Content of the file from which the torrent will be generated.
80    // We use the UUID as the content of the file.
81    let file_contents = format!("{id}\n");
82
83    let torrent_files: Vec<TorrentFile> = vec![TorrentFile {
84        path: vec![String::new()],
85        length: i64::try_from(file_contents.len()).expect("file contents size in bytes cannot exceed i64::MAX"),
86        md5sum: None,
87    }];
88
89    let torrent_announce_urls: Vec<Vec<String>> = vec![];
90
91    let create_torrent_req = CreateTorrentRequest {
92        name: format!("file-{id}.txt"),
93        pieces_or_root_hash: sha1(&file_contents),
94        piece_length: 16384,
95        private: None,
96        is_bep_30: 0,
97        files: torrent_files,
98        announce_urls: torrent_announce_urls,
99        comment: None,
100        creation_date: None,
101        created_by: None,
102        encoding: None,
103    };
104
105    create_torrent_req.build_torrent()
106}
107
108#[cfg(test)]
109mod tests {
110    use serde_bytes::ByteBuf;
111    use uuid::Uuid;
112
113    use crate::models::torrent_file::{Torrent, TorrentInfoDictionary};
114    use crate::services::torrent_file::generate_random_torrent;
115
116    #[test]
117    fn it_should_generate_a_random_meta_info_file() {
118        let uuid = Uuid::parse_str("d6170378-2c14-4ccc-870d-2a8e15195e23").unwrap();
119
120        let torrent = generate_random_torrent(uuid);
121
122        let expected_torrent = Torrent {
123            info: TorrentInfoDictionary {
124                name: "file-d6170378-2c14-4ccc-870d-2a8e15195e23.txt".to_string(),
125                pieces: Some(ByteBuf::from(vec![
126                    62, 231, 243, 51, 234, 165, 204, 209, 51, 132, 163, 133, 249, 50, 107, 46, 24, 15, 251, 32,
127                ])),
128                piece_length: 16384,
129                md5sum: None,
130                length: Some(37),
131                files: None,
132                private: None,
133                path: None,
134                root_hash: None,
135                source: None,
136            },
137            announce: None,
138            announce_list: Some(vec![]),
139            creation_date: None,
140            comment: None,
141            created_by: None,
142            nodes: None,
143            encoding: None,
144            httpseeds: None,
145        };
146
147        assert_eq!(torrent, expected_torrent);
148    }
149}