Skip to content

Commit ca6e97c

Browse files
committed
refactor: [#276] move metadata format validation to Metadata struct
And other minor changes.
1 parent a302c22 commit ca6e97c

File tree

9 files changed

+134
-55
lines changed

9 files changed

+134
-55
lines changed

src/databases/mysql.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ impl Database for Mysql {
436436
title: &str,
437437
description: &str,
438438
) -> Result<i64, database::Error> {
439-
let info_hash = torrent.info_hash_hex();
439+
let info_hash = torrent.canonical_info_hash_hex();
440440
let canonical_info_hash = torrent.canonical_info_hash();
441441

442442
// open pool connection

src/databases/sqlite.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ impl Database for Sqlite {
426426
title: &str,
427427
description: &str,
428428
) -> Result<i64, database::Error> {
429-
let info_hash = torrent.info_hash_hex();
429+
let info_hash = torrent.canonical_info_hash_hex();
430430
let canonical_info_hash = torrent.canonical_info_hash();
431431

432432
// open pool connection

src/errors.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use derive_more::{Display, Error};
55
use hyper::StatusCode;
66

77
use crate::databases::database;
8+
use crate::models::torrent::MetadataError;
89

910
pub type ServiceResult<V> = Result<V, ServiceError>;
1011

@@ -204,6 +205,18 @@ impl From<serde_json::Error> for ServiceError {
204205
}
205206
}
206207

208+
impl From<MetadataError> for ServiceError {
209+
fn from(e: MetadataError) -> Self {
210+
eprintln!("{e}");
211+
match e {
212+
MetadataError::MissingTorrentTitle | MetadataError::MissingTorrentCategoryName => {
213+
ServiceError::MissingMandatoryMetadataFields
214+
}
215+
MetadataError::InvalidTorrentTitleLength => ServiceError::InvalidTorrentTitleLength,
216+
}
217+
}
218+
}
219+
207220
#[must_use]
208221
pub fn http_status_code_for_service_error(error: &ServiceError) -> StatusCode {
209222
#[allow(clippy::match_same_arms)]

src/models/torrent.rs

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use derive_more::{Display, Error};
12
use serde::{Deserialize, Serialize};
23

34
use super::torrent_tag::TagId;
4-
use crate::errors::ServiceError;
5+
6+
const MIN_TORRENT_TITLE_LENGTH: usize = 3;
57

68
#[allow(clippy::module_name_repetitions)]
79
pub type TorrentId = i64;
@@ -24,6 +26,18 @@ pub struct TorrentListing {
2426
pub comment: Option<String>,
2527
}
2628

29+
#[derive(Debug, Display, PartialEq, Eq, Error)]
30+
pub enum MetadataError {
31+
#[display(fmt = "Missing mandatory torrent title")]
32+
MissingTorrentTitle,
33+
34+
#[display(fmt = "Missing mandatory torrent category name")]
35+
MissingTorrentCategoryName,
36+
37+
#[display(fmt = "Torrent title is too short.")]
38+
InvalidTorrentTitleLength,
39+
}
40+
2741
#[derive(Debug, Deserialize)]
2842
pub struct Metadata {
2943
pub title: String,
@@ -33,16 +47,48 @@ pub struct Metadata {
3347
}
3448

3549
impl Metadata {
36-
/// Returns the verify of this [`Metadata`].
50+
/// Create a new struct.
51+
///
52+
/// # Errors
53+
///
54+
/// This function will return an error if the metadata fields do not have a
55+
/// valid format.
56+
pub fn new(title: &str, description: &str, category: &str, tag_ids: &[TagId]) -> Result<Self, MetadataError> {
57+
Self::validate_format(title, description, category, tag_ids)?;
58+
59+
Ok(Self {
60+
title: title.to_owned(),
61+
description: description.to_owned(),
62+
category: category.to_owned(),
63+
tags: tag_ids.to_vec(),
64+
})
65+
}
66+
67+
/// It validates the format of the metadata fields.
68+
///
69+
/// It does not validate domain rules, like:
70+
///
71+
/// - Duplicate titles.
72+
/// - Non-existing categories.
73+
/// - ...
3774
///
3875
/// # Errors
3976
///
40-
/// This function will return an error if the any of the mandatory metadata fields are missing.
41-
pub fn verify(&self) -> Result<(), ServiceError> {
42-
if self.title.is_empty() || self.category.is_empty() {
43-
Err(ServiceError::MissingMandatoryMetadataFields)
44-
} else {
45-
Ok(())
77+
/// This function will return an error if any of the metadata fields does
78+
/// not have a valid format.
79+
fn validate_format(title: &str, _description: &str, category: &str, _tag_ids: &[TagId]) -> Result<(), MetadataError> {
80+
if title.is_empty() {
81+
return Err(MetadataError::MissingTorrentTitle);
4682
}
83+
84+
if category.is_empty() {
85+
return Err(MetadataError::MissingTorrentCategoryName);
86+
}
87+
88+
if title.len() < MIN_TORRENT_TITLE_LENGTH {
89+
return Err(MetadataError::InvalidTorrentTitleLength);
90+
}
91+
92+
Ok(())
4793
}
4894
}

src/models/torrent_file.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,13 @@ impl Torrent {
133133
}
134134

135135
#[must_use]
136-
pub fn info_hash_hex(&self) -> String {
137-
from_bytes(&self.calculate_info_hash_as_bytes()).to_lowercase()
136+
pub fn canonical_info_hash(&self) -> InfoHash {
137+
self.calculate_info_hash_as_bytes().into()
138138
}
139139

140140
#[must_use]
141-
pub fn canonical_info_hash(&self) -> InfoHash {
142-
self.calculate_info_hash_as_bytes().into()
141+
pub fn canonical_info_hash_hex(&self) -> String {
142+
self.canonical_info_hash().to_hex_string()
143143
}
144144

145145
#[must_use]
@@ -389,7 +389,7 @@ mod tests {
389389
httpseeds: None,
390390
};
391391

392-
assert_eq!(torrent.info_hash_hex(), "79fa9e4a2927804fe4feab488a76c8c2d3d1cdca");
392+
assert_eq!(torrent.canonical_info_hash_hex(), "79fa9e4a2927804fe4feab488a76c8c2d3d1cdca");
393393
}
394394

395395
mod infohash_should_be_calculated_for {
@@ -430,7 +430,7 @@ mod tests {
430430
httpseeds: None,
431431
};
432432

433-
assert_eq!(torrent.info_hash_hex(), "79fa9e4a2927804fe4feab488a76c8c2d3d1cdca");
433+
assert_eq!(torrent.canonical_info_hash_hex(), "79fa9e4a2927804fe4feab488a76c8c2d3d1cdca");
434434
}
435435

436436
#[test]
@@ -469,7 +469,7 @@ mod tests {
469469
httpseeds: None,
470470
};
471471

472-
assert_eq!(torrent.info_hash_hex(), "aa2aca91ab650c4d249c475ca3fa604f2ccb0d2a");
472+
assert_eq!(torrent.canonical_info_hash_hex(), "aa2aca91ab650c4d249c475ca3fa604f2ccb0d2a");
473473
}
474474

475475
#[test]
@@ -504,7 +504,7 @@ mod tests {
504504
httpseeds: None,
505505
};
506506

507-
assert_eq!(torrent.info_hash_hex(), "ccc1cf4feb59f3fa85c96c9be1ebbafcfe8a9cc8");
507+
assert_eq!(torrent.canonical_info_hash_hex(), "ccc1cf4feb59f3fa85c96c9be1ebbafcfe8a9cc8");
508508
}
509509

510510
#[test]
@@ -539,7 +539,7 @@ mod tests {
539539
httpseeds: None,
540540
};
541541

542-
assert_eq!(torrent.info_hash_hex(), "d3a558d0a19aaa23ba6f9f430f40924d10fefa86");
542+
assert_eq!(torrent.canonical_info_hash_hex(), "d3a558d0a19aaa23ba6f9f430f40924d10fefa86");
543543
}
544544
}
545545
}

src/services/torrent.rs

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ use crate::tracker::statistics_importer::StatisticsImporter;
2020
use crate::utils::parse_torrent;
2121
use crate::{tracker, AsCSV};
2222

23-
const MIN_TORRENT_TITLE_LENGTH: usize = 3;
24-
2523
pub struct Index {
2624
configuration: Arc<Configuration>,
2725
tracker_statistics_importer: Arc<StatisticsImporter>,
@@ -128,22 +126,34 @@ impl Index {
128126
/// * Unable to parse the torrent info-hash.
129127
pub async fn add_torrent(
130128
&self,
131-
add_torrent_form: AddTorrentRequest,
129+
add_torrent_req: AddTorrentRequest,
132130
user_id: UserId,
133131
) -> Result<AddTorrentResponse, ServiceError> {
134-
let metadata = Metadata {
135-
title: add_torrent_form.title,
136-
description: add_torrent_form.description,
137-
category: add_torrent_form.category,
138-
tags: add_torrent_form.tags,
139-
};
132+
// Authorization: only authenticated users ere allowed to upload torrents
133+
134+
let _user = self.user_repository.get_compact(&user_id).await?;
135+
136+
// Validate and build metadata
137+
138+
let metadata = Metadata::new(
139+
&add_torrent_req.title,
140+
&add_torrent_req.description,
141+
&add_torrent_req.category,
142+
&add_torrent_req.tags,
143+
)?;
144+
145+
let category = self
146+
.category_repository
147+
.get_by_name(&metadata.category)
148+
.await
149+
.map_err(|_| ServiceError::InvalidCategory)?;
140150

141-
metadata.verify()?;
151+
// Validate and build torrent file
142152

143-
let original_info_hash = parse_torrent::calculate_info_hash(&add_torrent_form.torrent_buffer);
153+
let original_info_hash = parse_torrent::calculate_info_hash(&add_torrent_req.torrent_buffer);
144154

145155
let mut torrent =
146-
parse_torrent::decode_torrent(&add_torrent_form.torrent_buffer).map_err(|_| ServiceError::InvalidTorrentFile)?;
156+
parse_torrent::decode_torrent(&add_torrent_req.torrent_buffer).map_err(|_| ServiceError::InvalidTorrentFile)?;
147157

148158
// Make sure that the pieces key has a length that is a multiple of 20
149159
if let Some(pieces) = torrent.info.pieces.as_ref() {
@@ -152,22 +162,12 @@ impl Index {
152162
}
153163
}
154164

155-
let _user = self.user_repository.get_compact(&user_id).await?;
156-
157165
torrent.set_announce_urls(&self.configuration).await;
158166

159-
if metadata.title.len() < MIN_TORRENT_TITLE_LENGTH {
160-
return Err(ServiceError::InvalidTorrentTitleLength);
161-
}
162-
163-
let category = self
164-
.category_repository
165-
.get_by_name(&metadata.category)
166-
.await
167-
.map_err(|_| ServiceError::InvalidCategory)?;
168-
169167
let canonical_info_hash = torrent.canonical_info_hash();
170168

169+
// Canonical InfoHash Group checks
170+
171171
let original_info_hashes = self
172172
.torrent_info_hash_repository
173173
.get_canonical_info_hash_group(&canonical_info_hash)
@@ -194,35 +194,44 @@ impl Index {
194194
return Err(ServiceError::CanonicalInfoHashAlreadyExists);
195195
}
196196

197-
// First time a torrent with this original infohash is uploaded.
197+
// Store the torrent into the database
198198

199199
let torrent_id = self
200200
.torrent_repository
201201
.add(&original_info_hash, &torrent, &metadata, user_id, category)
202202
.await?;
203-
let info_hash = torrent.canonical_info_hash();
203+
204+
self.torrent_tag_repository
205+
.link_torrent_to_tags(&torrent_id, &metadata.tags)
206+
.await?;
207+
208+
// Secondary task: import torrent statistics from the tracker
204209

205210
drop(
206211
self.tracker_statistics_importer
207-
.import_torrent_statistics(torrent_id, &torrent.info_hash_hex())
212+
.import_torrent_statistics(torrent_id, &torrent.canonical_info_hash_hex())
208213
.await,
209214
);
210215

216+
// Secondary task: whitelist torrent on the tracker
217+
211218
// We always whitelist the torrent on the tracker because even if the tracker mode is `public`
212219
// it could be changed to `private` later on.
213-
if let Err(e) = self.tracker_service.whitelist_info_hash(torrent.info_hash_hex()).await {
220+
if let Err(e) = self
221+
.tracker_service
222+
.whitelist_info_hash(torrent.canonical_info_hash_hex())
223+
.await
224+
{
214225
// If the torrent can't be whitelisted somehow, remove the torrent from database
215226
drop(self.torrent_repository.delete(&torrent_id).await);
216227
return Err(e);
217228
}
218229

219-
self.torrent_tag_repository
220-
.link_torrent_to_tags(&torrent_id, &metadata.tags)
221-
.await?;
230+
// Build response
222231

223232
Ok(AddTorrentResponse {
224233
torrent_id,
225-
info_hash: info_hash.to_string(),
234+
info_hash: torrent.canonical_info_hash_hex(),
226235
original_info_hash: original_info_hash.to_string(),
227236
})
228237
}

src/utils/parse_torrent.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ mod tests {
9999
// The infohash is not the original infohash of the torrent file,
100100
// but the infohash of the info dictionary without the custom keys.
101101
assert_eq!(
102-
torrent.info_hash_hex(),
102+
torrent.canonical_info_hash_hex(),
103103
"8aa01a4c816332045ffec83247ccbc654547fedf".to_string()
104104
);
105105
}

src/web/api/v1/contexts/torrent/handlers.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ pub async fn download_torrent_handler(
100100
return ServiceError::InternalServerError.into_response();
101101
};
102102

103-
torrent_file_response(bytes, &format!("{}.torrent", torrent.info.name), &torrent.info_hash_hex())
103+
torrent_file_response(
104+
bytes,
105+
&format!("{}.torrent", torrent.info.name),
106+
&torrent.canonical_info_hash_hex(),
107+
)
104108
}
105109
}
106110

@@ -307,7 +311,11 @@ pub async fn create_random_torrent_handler(State(_app_data): State<Arc<AppData>>
307311
return ServiceError::InternalServerError.into_response();
308312
};
309313

310-
torrent_file_response(bytes, &format!("{}.torrent", torrent.info.name), &torrent.info_hash_hex())
314+
torrent_file_response(
315+
bytes,
316+
&format!("{}.torrent", torrent.info.name),
317+
&torrent.canonical_info_hash_hex(),
318+
)
311319
}
312320

313321
/// Extracts the [`TorrentRequest`] from the multipart form payload.

tests/e2e/web/api/v1/contexts/torrent/contract.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,10 @@ mod for_guests {
396396

397397
// The returned torrent info-hash should be the same as the first torrent
398398
assert_eq!(response.status, 200);
399-
assert_eq!(torrent.info_hash_hex(), first_torrent_canonical_info_hash.to_hex_string());
399+
assert_eq!(
400+
torrent.canonical_info_hash_hex(),
401+
first_torrent_canonical_info_hash.to_hex_string()
402+
);
400403
}
401404

402405
#[tokio::test]

0 commit comments

Comments
 (0)