Skip to content

Commit 7390616

Browse files
committed
refactor: [#301] add independent field root_hash in DB for BEP-30 torrents
instead of reusing the `pieces` field in `torrust_torrents` table. That decouples BEP-30 implementation. In the future we migth want to remove support for BEP 30 since it's decrecated and not supported by clients or libs.
1 parent 4d7091d commit 7390616

File tree

8 files changed

+168
-45
lines changed

8 files changed

+168
-45
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ALTER TABLE torrust_torrents ADD COLUMN root_hash LONGTEXT;
2+
3+
-- Make `pieces` nullable. BEP 30 torrents does have this field.
4+
ALTER TABLE torrust_torrents MODIFY COLUMN pieces LONGTEXT;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
-- add field `root_hash` and make `pieces` nullable
2+
CREATE TABLE
3+
"torrust_torrents_new" (
4+
"torrent_id" INTEGER NOT NULL,
5+
"uploader_id" INTEGER NOT NULL,
6+
"category_id" INTEGER,
7+
"info_hash" TEXT NOT NULL UNIQUE,
8+
"size" INTEGER NOT NULL,
9+
"name" TEXT NOT NULL,
10+
"pieces" TEXT,
11+
"root_hash" TEXT,
12+
"piece_length" INTEGER NOT NULL,
13+
"private" BOOLEAN DEFAULT NULL,
14+
"is_bep_30" INT NOT NULL DEFAULT 0,
15+
"date_uploaded" TEXT NOT NULL,
16+
"source" TEXT DEFAULT NULL,
17+
"comment" TEXT,
18+
"creation_date" BIGINT,
19+
"created_by" TEXT,
20+
"encoding" TEXT,
21+
FOREIGN KEY ("uploader_id") REFERENCES "torrust_users" ("user_id") ON DELETE CASCADE,
22+
FOREIGN KEY ("category_id") REFERENCES "torrust_categories" ("category_id") ON DELETE SET NULL,
23+
PRIMARY KEY ("torrent_id" AUTOINCREMENT)
24+
);
25+
26+
-- Step 2: Copy data from the old table to the new table
27+
INSERT INTO
28+
torrust_torrents_new (
29+
torrent_id,
30+
uploader_id,
31+
category_id,
32+
info_hash,
33+
size,
34+
name,
35+
pieces,
36+
piece_length,
37+
private,
38+
root_hash,
39+
date_uploaded,
40+
source,
41+
comment,
42+
creation_date,
43+
created_by,
44+
encoding
45+
)
46+
SELECT
47+
torrent_id,
48+
uploader_id,
49+
category_id,
50+
info_hash,
51+
size,
52+
name,
53+
CASE
54+
WHEN is_bep_30 = 0 THEN pieces
55+
ELSE NULL
56+
END,
57+
piece_length,
58+
private,
59+
CASE
60+
WHEN is_bep_30 = 1 THEN pieces
61+
ELSE NULL
62+
END,
63+
date_uploaded,
64+
source,
65+
comment,
66+
creation_date,
67+
created_by,
68+
encoding
69+
FROM
70+
torrust_torrents;
71+
72+
-- Step 3: Drop the old table
73+
DROP TABLE torrust_torrents;
74+
75+
-- Step 4: Rename the new table to the original name
76+
ALTER TABLE torrust_torrents_new
77+
RENAME TO torrust_torrents;

src/databases/mysql.rs

+14-9
Original file line numberDiff line numberDiff line change
@@ -438,14 +438,17 @@ impl Database for Mysql {
438438
// start db transaction
439439
let mut tx = conn.begin().await.map_err(|_| database::Error::Error)?;
440440

441-
// torrent file can only hold a `pieces` key or a `root hash` key
442-
// BEP 30: http://www.bittorrent.org/beps/bep_0030.html
443-
let (pieces, is_bep_30): (String, bool) = if let Some(pieces) = &torrent.info.pieces {
444-
(from_bytes(pieces.as_ref()), false)
445-
} else {
446-
let root_hash = torrent.info.root_hash.as_ref().ok_or(database::Error::Error)?;
447-
(root_hash.to_string(), true)
448-
};
441+
// BEP 30: <http://www.bittorrent.org/beps/bep_0030.html>.
442+
// Torrent file can only hold a `pieces` key or a `root hash` key
443+
let is_bep_30 = !matches!(&torrent.info.pieces, Some(_pieces));
444+
445+
let pieces = torrent.info.pieces.as_ref().map(|pieces| from_bytes(pieces.as_ref()));
446+
447+
let root_hash = torrent
448+
.info
449+
.root_hash
450+
.as_ref()
451+
.map(|root_hash| from_bytes(root_hash.as_ref()));
449452

450453
// add torrent
451454
let torrent_id = query(
@@ -456,6 +459,7 @@ impl Database for Mysql {
456459
size,
457460
name,
458461
pieces,
462+
root_hash,
459463
piece_length,
460464
private,
461465
is_bep_30,
@@ -465,14 +469,15 @@ impl Database for Mysql {
465469
creation_date,
466470
created_by,
467471
`encoding`
468-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), ?, ?, ?)",
472+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), ?, ?, ?)",
469473
)
470474
.bind(uploader_id)
471475
.bind(metadata.category_id)
472476
.bind(info_hash.to_lowercase())
473477
.bind(torrent.file_size())
474478
.bind(torrent.info.name.to_string())
475479
.bind(pieces)
480+
.bind(root_hash)
476481
.bind(torrent.info.piece_length)
477482
.bind(torrent.info.private)
478483
.bind(is_bep_30)

src/databases/sqlite.rs

+14-8
Original file line numberDiff line numberDiff line change
@@ -428,13 +428,17 @@ impl Database for Sqlite {
428428
// start db transaction
429429
let mut tx = conn.begin().await.map_err(|_| database::Error::Error)?;
430430

431-
// torrent file can only hold a pieces key or a root hash key: http://www.bittorrent.org/beps/bep_0030.html
432-
let (pieces, is_bep_30): (String, bool) = if let Some(pieces) = &torrent.info.pieces {
433-
(from_bytes(pieces.as_ref()), false)
434-
} else {
435-
let root_hash = torrent.info.root_hash.as_ref().ok_or(database::Error::Error)?;
436-
(root_hash.to_string(), true)
437-
};
431+
// BEP 30: <http://www.bittorrent.org/beps/bep_0030.html>.
432+
// Torrent file can only hold a `pieces` key or a `root hash` key
433+
let is_bep_30 = !matches!(&torrent.info.pieces, Some(_pieces));
434+
435+
let pieces = torrent.info.pieces.as_ref().map(|pieces| from_bytes(pieces.as_ref()));
436+
437+
let root_hash = torrent
438+
.info
439+
.root_hash
440+
.as_ref()
441+
.map(|root_hash| from_bytes(root_hash.as_ref()));
438442

439443
// add torrent
440444
let torrent_id = query(
@@ -445,6 +449,7 @@ impl Database for Sqlite {
445449
size,
446450
name,
447451
pieces,
452+
root_hash,
448453
piece_length,
449454
private,
450455
is_bep_30,
@@ -454,14 +459,15 @@ impl Database for Sqlite {
454459
creation_date,
455460
created_by,
456461
`encoding`
457-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')), ?, ?, ?)",
462+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')), ?, ?, ?)",
458463
)
459464
.bind(uploader_id)
460465
.bind(metadata.category_id)
461466
.bind(info_hash.to_lowercase())
462467
.bind(torrent.file_size())
463468
.bind(torrent.info.name.to_string())
464469
.bind(pieces)
470+
.bind(root_hash)
465471
.bind(torrent.info.piece_length)
466472
.bind(torrent.info.private)
467473
.bind(is_bep_30)

src/models/torrent_file.rs

+48-20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use log::error;
12
use serde::{Deserialize, Serialize};
23
use serde_bencode::ser;
34
use serde_bytes::ByteBuf;
@@ -77,12 +78,31 @@ impl Torrent {
7778
torrent_http_seed_urls: Vec<String>,
7879
torrent_nodes: Vec<(String, i64)>,
7980
) -> Self {
81+
let pieces_or_root_hash = if db_torrent.is_bep_30 == 0 {
82+
match &db_torrent.pieces {
83+
Some(pieces) => pieces.clone(),
84+
None => {
85+
error!("Invalid torrent #{}. Null `pieces` in database", db_torrent.torrent_id);
86+
String::new()
87+
}
88+
}
89+
} else {
90+
// A BEP-30 torrent
91+
match &db_torrent.root_hash {
92+
Some(root_hash) => root_hash.clone(),
93+
None => {
94+
error!("Invalid torrent #{}. Null `root_hash` in database", db_torrent.torrent_id);
95+
String::new()
96+
}
97+
}
98+
};
99+
80100
let info_dict = TorrentInfoDictionary::with(
81101
&db_torrent.name,
82102
db_torrent.piece_length,
83103
db_torrent.private,
84104
db_torrent.is_bep_30,
85-
&db_torrent.pieces,
105+
&pieces_or_root_hash,
86106
torrent_files,
87107
);
88108

@@ -235,7 +255,14 @@ impl TorrentInfoDictionary {
235255
/// - The `pieces` field is not a valid hex string.
236256
/// - For single files torrents the `TorrentFile` path is empty.
237257
#[must_use]
238-
pub fn with(name: &str, piece_length: i64, private: Option<u8>, is_bep_30: i64, pieces: &str, files: &[TorrentFile]) -> Self {
258+
pub fn with(
259+
name: &str,
260+
piece_length: i64,
261+
private: Option<u8>,
262+
is_bep_30: i64,
263+
pieces_or_root_hash: &str,
264+
files: &[TorrentFile],
265+
) -> Self {
239266
let mut info_dict = Self {
240267
name: name.to_string(),
241268
pieces: None,
@@ -249,13 +276,13 @@ impl TorrentInfoDictionary {
249276
source: None,
250277
};
251278

252-
// a torrent file has a root hash or a pieces key, but not both.
253-
if is_bep_30 > 0 {
254-
// If `is_bep_30` is true the `pieces` field contains the `root hash`
255-
info_dict.root_hash = Some(pieces.to_owned());
256-
} else {
257-
let buffer = into_bytes(pieces).expect("variable `torrent_info.pieces` is not a valid hex string");
279+
// BEP 30: <http://www.bittorrent.org/beps/bep_0030.html>.
280+
// Torrent file can only hold a `pieces` key or a `root hash` key
281+
if is_bep_30 == 0 {
282+
let buffer = into_bytes(pieces_or_root_hash).expect("variable `torrent_info.pieces` is not a valid hex string");
258283
info_dict.pieces = Some(ByteBuf::from(buffer));
284+
} else {
285+
info_dict.root_hash = Some(pieces_or_root_hash.to_owned());
259286
}
260287

261288
// either set the single file or the multiple files information
@@ -298,22 +325,22 @@ impl TorrentInfoDictionary {
298325
}
299326
}
300327

301-
/// It returns the root hash as a `i64` value.
302-
///
303-
/// # Panics
304-
///
305-
/// This function will panic if the root hash cannot be converted into a
306-
/// `i64` value.
328+
/// torrent file can only hold a pieces key or a root hash key:
329+
/// [BEP 39](http://www.bittorrent.org/beps/bep_0030.html)
307330
#[must_use]
308-
pub fn get_root_hash_as_i64(&self) -> i64 {
331+
pub fn get_root_hash_as_string(&self) -> String {
309332
match &self.root_hash {
310-
None => 0i64,
311-
Some(root_hash) => root_hash
312-
.parse::<i64>()
313-
.expect("variable `root_hash` cannot be converted into a `i64`"),
333+
None => String::new(),
334+
Some(root_hash) => root_hash.clone(),
314335
}
315336
}
316337

338+
/// It returns true if the torrent is a BEP-30 torrent.
339+
#[must_use]
340+
pub fn is_bep_30(&self) -> bool {
341+
self.root_hash.is_some()
342+
}
343+
317344
#[must_use]
318345
pub fn is_a_single_file_torrent(&self) -> bool {
319346
self.length.is_some()
@@ -330,7 +357,8 @@ pub struct DbTorrent {
330357
pub torrent_id: i64,
331358
pub info_hash: String,
332359
pub name: String,
333-
pub pieces: String,
360+
pub pieces: Option<String>,
361+
pub root_hash: Option<String>,
334362
pub piece_length: i64,
335363
#[serde(default)]
336364
pub private: Option<u8>,

src/services/torrent_file.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::services::hasher::sha1;
1111
pub struct CreateTorrentRequest {
1212
// The `info` dictionary fields
1313
pub name: String,
14-
pub pieces: String,
14+
pub pieces_or_root_hash: String,
1515
pub piece_length: i64,
1616
pub private: Option<u8>,
1717
/// True (1) if it's a BEP 30 torrent.
@@ -60,7 +60,7 @@ impl CreateTorrentRequest {
6060
self.piece_length,
6161
self.private,
6262
self.is_bep_30,
63-
&self.pieces,
63+
&self.pieces_or_root_hash,
6464
&self.files,
6565
)
6666
}
@@ -90,7 +90,7 @@ pub fn generate_random_torrent(id: Uuid) -> Torrent {
9090

9191
let create_torrent_req = CreateTorrentRequest {
9292
name: format!("file-{id}.txt"),
93-
pieces: sha1(&file_contents),
93+
pieces_or_root_hash: sha1(&file_contents),
9494
piece_length: 16384,
9595
private: None,
9696
is_bep_30: 0,

src/upgrades/from_v1_0_0_to_v2_0_0/databases/sqlite_v2_0_0.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ pub struct TorrentRecordV2 {
2323
pub info_hash: String,
2424
pub size: i64,
2525
pub name: String,
26-
pub pieces: String,
26+
pub pieces: Option<String>,
27+
pub root_hash: Option<String>,
2728
pub piece_length: i64,
2829
pub private: Option<u8>,
2930
pub is_bep_30: i64,
@@ -40,10 +41,11 @@ impl TorrentRecordV2 {
4041
info_hash: torrent.info_hash.clone(),
4142
size: torrent.file_size,
4243
name: torrent_info.name.clone(),
43-
pieces: torrent_info.get_pieces_as_string(),
44+
pieces: Some(torrent_info.get_pieces_as_string()),
45+
root_hash: Some(torrent_info.get_root_hash_as_string()),
4446
piece_length: torrent_info.piece_length,
4547
private: torrent_info.private,
46-
is_bep_30: torrent_info.get_root_hash_as_i64(),
48+
is_bep_30: i64::from(torrent_info.is_bep_30()),
4749
date_uploaded: convert_timestamp_to_datetime(torrent.upload_date),
4850
}
4951
}

tests/upgrades/from_v1_0_0_to_v2_0_0/transferrer_testers/torrent_transferrer_tester.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,15 @@ impl TorrentTester {
112112
assert_eq!(imported_torrent.info_hash, torrent.info_hash);
113113
assert_eq!(imported_torrent.size, torrent.file_size);
114114
assert_eq!(imported_torrent.name, torrent_file.info.name);
115-
assert_eq!(imported_torrent.pieces, torrent_file.info.get_pieces_as_string());
115+
assert_eq!(imported_torrent.pieces, Some(torrent_file.info.get_pieces_as_string()));
116+
assert_eq!(imported_torrent.root_hash, None);
116117
assert_eq!(imported_torrent.piece_length, torrent_file.info.piece_length);
117118
if torrent_file.info.private.is_none() {
118119
assert_eq!(imported_torrent.private, Some(0));
119120
} else {
120121
assert_eq!(imported_torrent.private, torrent_file.info.private);
121122
}
122-
assert_eq!(imported_torrent.is_bep_30, torrent_file.info.get_root_hash_as_i64());
123+
assert_eq!(imported_torrent.is_bep_30, i64::from(torrent_file.info.is_bep_30()));
123124
assert_eq!(
124125
imported_torrent.date_uploaded,
125126
convert_timestamp_to_datetime(torrent.upload_date)

0 commit comments

Comments
 (0)