Skip to content

Commit

Permalink
Merge #516: Refactor BEP 30 implmentation
Browse files Browse the repository at this point in the history
7390616 refactor: [#301] add independent field root_hash in DB for BEP-30 torrents (Jose Celano)
4d7091d refactor: [#301] rename table field root_hash to is_bep_30 (Jose Celano)

Pull request description:

  Refactor BEP 30 implementation to make it clearer and simplify implementation.

  - [x] Rename table field `torrust_torrents::root_hash` to `is_bep_30`. It's not the `root hash` value in BEP 30. It's a flag for BEP 30 torrents.
  - [x] Don't reuse `torrust_torrents::pieces` field for BEP 30 torrents. Add a new field `root_hash`.

  BEP 30: https://www.bittorrent.org/beps/bep_0030.html

ACKs for top commit:
  josecelano:
    ACK 7390616

Tree-SHA512: 44404db360e60951856ff04570a48ab4d4adb8db98e26a1e31409bd86bee2b281fb5b95bbaf726c3089920eb4e720c4093661633f13d4fd8370dd0e5480c4dbd
  • Loading branch information
josecelano committed Mar 5, 2024
2 parents 2e8bd5a + 7390616 commit a149f21
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE torrust_torrents CHANGE COLUMN root_hash is_bep_30 BOOLEAN NOT NULL DEFAULT FALSE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE torrust_torrents ADD COLUMN root_hash LONGTEXT;

-- Make `pieces` nullable. BEP 30 torrents does have this field.
ALTER TABLE torrust_torrents MODIFY COLUMN pieces LONGTEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE torrust_torrents RENAME COLUMN root_hash TO is_bep_30;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
-- add field `root_hash` and make `pieces` nullable
CREATE TABLE
"torrust_torrents_new" (
"torrent_id" INTEGER NOT NULL,
"uploader_id" INTEGER NOT NULL,
"category_id" INTEGER,
"info_hash" TEXT NOT NULL UNIQUE,
"size" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"pieces" TEXT,
"root_hash" TEXT,
"piece_length" INTEGER NOT NULL,
"private" BOOLEAN DEFAULT NULL,
"is_bep_30" INT NOT NULL DEFAULT 0,
"date_uploaded" TEXT NOT NULL,
"source" TEXT DEFAULT NULL,
"comment" TEXT,
"creation_date" BIGINT,
"created_by" TEXT,
"encoding" TEXT,
FOREIGN KEY ("uploader_id") REFERENCES "torrust_users" ("user_id") ON DELETE CASCADE,
FOREIGN KEY ("category_id") REFERENCES "torrust_categories" ("category_id") ON DELETE SET NULL,
PRIMARY KEY ("torrent_id" AUTOINCREMENT)
);

-- Step 2: Copy data from the old table to the new table
INSERT INTO
torrust_torrents_new (
torrent_id,
uploader_id,
category_id,
info_hash,
size,
name,
pieces,
piece_length,
private,
root_hash,
date_uploaded,
source,
comment,
creation_date,
created_by,
encoding
)
SELECT
torrent_id,
uploader_id,
category_id,
info_hash,
size,
name,
CASE
WHEN is_bep_30 = 0 THEN pieces
ELSE NULL
END,
piece_length,
private,
CASE
WHEN is_bep_30 = 1 THEN pieces
ELSE NULL
END,
date_uploaded,
source,
comment,
creation_date,
created_by,
encoding
FROM
torrust_torrents;

-- Step 3: Drop the old table
DROP TABLE torrust_torrents;

-- Step 4: Rename the new table to the original name
ALTER TABLE torrust_torrents_new
RENAME TO torrust_torrents;
26 changes: 16 additions & 10 deletions src/databases/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,17 @@ impl Database for Mysql {
// start db transaction
let mut tx = conn.begin().await.map_err(|_| database::Error::Error)?;

// torrent file can only hold a pieces key or a root hash key: http://www.bittorrent.org/beps/bep_0030.html
let (pieces, root_hash): (String, bool) = if let Some(pieces) = &torrent.info.pieces {
(from_bytes(pieces.as_ref()), false)
} else {
let root_hash = torrent.info.root_hash.as_ref().ok_or(database::Error::Error)?;
(root_hash.to_string(), true)
};
// BEP 30: <http://www.bittorrent.org/beps/bep_0030.html>.
// Torrent file can only hold a `pieces` key or a `root hash` key
let is_bep_30 = !matches!(&torrent.info.pieces, Some(_pieces));

let pieces = torrent.info.pieces.as_ref().map(|pieces| from_bytes(pieces.as_ref()));

let root_hash = torrent
.info
.root_hash
.as_ref()
.map(|root_hash| from_bytes(root_hash.as_ref()));

// add torrent
let torrent_id = query(
Expand All @@ -455,26 +459,28 @@ impl Database for Mysql {
size,
name,
pieces,
root_hash,
piece_length,
private,
root_hash,
is_bep_30,
`source`,
comment,
date_uploaded,
creation_date,
created_by,
`encoding`
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), ?, ?, ?)",
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), ?, ?, ?)",
)
.bind(uploader_id)
.bind(metadata.category_id)
.bind(info_hash.to_lowercase())
.bind(torrent.file_size())
.bind(torrent.info.name.to_string())
.bind(pieces)
.bind(root_hash)
.bind(torrent.info.piece_length)
.bind(torrent.info.private)
.bind(root_hash)
.bind(is_bep_30)
.bind(torrent.info.source.clone())
.bind(torrent.comment.clone())
.bind(torrent.creation_date)
Expand Down
26 changes: 16 additions & 10 deletions src/databases/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,13 +428,17 @@ impl Database for Sqlite {
// start db transaction
let mut tx = conn.begin().await.map_err(|_| database::Error::Error)?;

// torrent file can only hold a pieces key or a root hash key: http://www.bittorrent.org/beps/bep_0030.html
let (pieces, root_hash): (String, bool) = if let Some(pieces) = &torrent.info.pieces {
(from_bytes(pieces.as_ref()), false)
} else {
let root_hash = torrent.info.root_hash.as_ref().ok_or(database::Error::Error)?;
(root_hash.to_string(), true)
};
// BEP 30: <http://www.bittorrent.org/beps/bep_0030.html>.
// Torrent file can only hold a `pieces` key or a `root hash` key
let is_bep_30 = !matches!(&torrent.info.pieces, Some(_pieces));

let pieces = torrent.info.pieces.as_ref().map(|pieces| from_bytes(pieces.as_ref()));

let root_hash = torrent
.info
.root_hash
.as_ref()
.map(|root_hash| from_bytes(root_hash.as_ref()));

// add torrent
let torrent_id = query(
Expand All @@ -445,26 +449,28 @@ impl Database for Sqlite {
size,
name,
pieces,
root_hash,
piece_length,
private,
root_hash,
is_bep_30,
`source`,
comment,
date_uploaded,
creation_date,
created_by,
`encoding`
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')), ?, ?, ?)",
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%S',DATETIME('now', 'utc')), ?, ?, ?)",
)
.bind(uploader_id)
.bind(metadata.category_id)
.bind(info_hash.to_lowercase())
.bind(torrent.file_size())
.bind(torrent.info.name.to_string())
.bind(pieces)
.bind(root_hash)
.bind(torrent.info.piece_length)
.bind(torrent.info.private)
.bind(root_hash)
.bind(is_bep_30)
.bind(torrent.info.source.clone())
.bind(torrent.comment.clone())
.bind(torrent.creation_date)
Expand Down
72 changes: 50 additions & 22 deletions src/models/torrent_file.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use log::error;
use serde::{Deserialize, Serialize};
use serde_bencode::ser;
use serde_bytes::ByteBuf;
Expand Down Expand Up @@ -77,12 +78,31 @@ impl Torrent {
torrent_http_seed_urls: Vec<String>,
torrent_nodes: Vec<(String, i64)>,
) -> Self {
let pieces_or_root_hash = if db_torrent.is_bep_30 == 0 {
match &db_torrent.pieces {
Some(pieces) => pieces.clone(),
None => {
error!("Invalid torrent #{}. Null `pieces` in database", db_torrent.torrent_id);
String::new()
}
}
} else {
// A BEP-30 torrent
match &db_torrent.root_hash {
Some(root_hash) => root_hash.clone(),
None => {
error!("Invalid torrent #{}. Null `root_hash` in database", db_torrent.torrent_id);
String::new()
}
}
};

let info_dict = TorrentInfoDictionary::with(
&db_torrent.name,
db_torrent.piece_length,
db_torrent.private,
db_torrent.root_hash,
&db_torrent.pieces,
db_torrent.is_bep_30,
&pieces_or_root_hash,
torrent_files,
);

Expand Down Expand Up @@ -235,7 +255,14 @@ impl TorrentInfoDictionary {
/// - The `pieces` field is not a valid hex string.
/// - For single files torrents the `TorrentFile` path is empty.
#[must_use]
pub fn with(name: &str, piece_length: i64, private: Option<u8>, root_hash: i64, pieces: &str, files: &[TorrentFile]) -> Self {
pub fn with(
name: &str,
piece_length: i64,
private: Option<u8>,
is_bep_30: i64,
pieces_or_root_hash: &str,
files: &[TorrentFile],
) -> Self {
let mut info_dict = Self {
name: name.to_string(),
pieces: None,
Expand All @@ -249,13 +276,13 @@ impl TorrentInfoDictionary {
source: None,
};

// a torrent file has a root hash or a pieces key, but not both.
if root_hash > 0 {
// If `root_hash` is true the `pieces` field contains the `root hash`
info_dict.root_hash = Some(pieces.to_owned());
} else {
let buffer = into_bytes(pieces).expect("variable `torrent_info.pieces` is not a valid hex string");
// BEP 30: <http://www.bittorrent.org/beps/bep_0030.html>.
// Torrent file can only hold a `pieces` key or a `root hash` key
if is_bep_30 == 0 {
let buffer = into_bytes(pieces_or_root_hash).expect("variable `torrent_info.pieces` is not a valid hex string");
info_dict.pieces = Some(ByteBuf::from(buffer));
} else {
info_dict.root_hash = Some(pieces_or_root_hash.to_owned());
}

// either set the single file or the multiple files information
Expand Down Expand Up @@ -298,22 +325,22 @@ impl TorrentInfoDictionary {
}
}

/// It returns the root hash as a `i64` value.
///
/// # Panics
///
/// This function will panic if the root hash cannot be converted into a
/// `i64` value.
/// torrent file can only hold a pieces key or a root hash key:
/// [BEP 39](http://www.bittorrent.org/beps/bep_0030.html)
#[must_use]
pub fn get_root_hash_as_i64(&self) -> i64 {
pub fn get_root_hash_as_string(&self) -> String {
match &self.root_hash {
None => 0i64,
Some(root_hash) => root_hash
.parse::<i64>()
.expect("variable `root_hash` cannot be converted into a `i64`"),
None => String::new(),
Some(root_hash) => root_hash.clone(),
}
}

/// It returns true if the torrent is a BEP-30 torrent.
#[must_use]
pub fn is_bep_30(&self) -> bool {
self.root_hash.is_some()
}

#[must_use]
pub fn is_a_single_file_torrent(&self) -> bool {
self.length.is_some()
Expand All @@ -330,11 +357,12 @@ pub struct DbTorrent {
pub torrent_id: i64,
pub info_hash: String,
pub name: String,
pub pieces: String,
pub pieces: Option<String>,
pub root_hash: Option<String>,
pub piece_length: i64,
#[serde(default)]
pub private: Option<u8>,
pub root_hash: i64,
pub is_bep_30: i64,
pub comment: Option<String>,
pub creation_date: Option<i64>,
pub created_by: Option<String>,
Expand Down
13 changes: 7 additions & 6 deletions src/services/torrent_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ use crate::services::hasher::sha1;
pub struct CreateTorrentRequest {
// The `info` dictionary fields
pub name: String,
pub pieces: String,
pub pieces_or_root_hash: String,
pub piece_length: i64,
pub private: Option<u8>,
pub root_hash: i64, // True (1) if it's a BEP 30 torrent.
/// True (1) if it's a BEP 30 torrent.
pub is_bep_30: i64,
pub files: Vec<TorrentFile>,
// Other fields of the root level metainfo dictionary
pub announce_urls: Vec<Vec<String>>,
Expand Down Expand Up @@ -58,8 +59,8 @@ impl CreateTorrentRequest {
&self.name,
self.piece_length,
self.private,
self.root_hash,
&self.pieces,
self.is_bep_30,
&self.pieces_or_root_hash,
&self.files,
)
}
Expand Down Expand Up @@ -89,10 +90,10 @@ pub fn generate_random_torrent(id: Uuid) -> Torrent {

let create_torrent_req = CreateTorrentRequest {
name: format!("file-{id}.txt"),
pieces: sha1(&file_contents),
pieces_or_root_hash: sha1(&file_contents),
piece_length: 16384,
private: None,
root_hash: 0,
is_bep_30: 0,
files: torrent_files,
announce_urls: torrent_announce_urls,
comment: None,
Expand Down
Loading

0 comments on commit a149f21

Please sign in to comment.