Skip to content

Commit

Permalink
feat: [torrust#56] transfer torrents (1/4 tables) from v1.0.0 to v2.0.0
Browse files Browse the repository at this point in the history
Transferred data to tables in versio v2.0.0:

- [x] Table `torrust_torrents`
- [ ] Table `torrust_torrent_files`
- [ ] Table `torrust_torrent_announce_urls`
- [ ] Table `torrust_torrent_info`
  • Loading branch information
josecelano committed Nov 30, 2022
1 parent 8d26faa commit 0b3aefa
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 20 deletions.
18 changes: 18 additions & 0 deletions src/models/torrent_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ pub struct TorrentInfo {
pub root_hash: Option<String>,
}

impl TorrentInfo {
/// torrent file can only hold a pieces key or a root hash key:
/// http://www.bittorrent.org/beps/bep_0030.html
pub fn get_pieces_as_string(&self) -> String {
match &self.pieces {
None => "".to_string(),
Some(byte_buf) => bytes_to_hex(byte_buf.as_ref())
}
}

pub fn get_root_hash_as_i64(&self) -> i64 {
match &self.root_hash {
None => 0i64,
Some(root_hash) => root_hash.parse::<i64>().unwrap()
}
}
}

#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct Torrent {
pub info: TorrentInfo, //
Expand Down
42 changes: 42 additions & 0 deletions src/upgrades/from_v1_0_0_to_v2_0_0/databases/sqlite_v1_0_0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ pub struct TrackerKey {
pub valid_until: i64,
}

#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct Torrent {
pub torrent_id: i64,
pub uploader: String,
pub info_hash: String,
pub title: String,
pub category_id: i64,
pub description: String,
pub upload_date: i64,
pub file_size: i64,
pub seeders: i64,
pub leechers: i64,
}

#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct TorrentFile {
pub file_id: i64,
pub torrent_uid: i64,
pub number: i64,
pub path: String,
pub length: i64,
}

pub struct SqliteDatabaseV1_0_0 {
pub pool: SqlitePool,
}
Expand Down Expand Up @@ -56,9 +79,28 @@ impl SqliteDatabaseV1_0_0 {
.await
}

pub async fn get_user_by_username(&self, username: &str) -> Result<User, sqlx::Error> {
query_as::<_, User>("SELECT * FROM torrust_users WHERE username = ?")
.bind(username)
.fetch_one(&self.pool)
.await
}

pub async fn get_tracker_keys(&self) -> Result<Vec<TrackerKey>, sqlx::Error> {
query_as::<_, TrackerKey>("SELECT * FROM torrust_tracker_keys ORDER BY key_id ASC")
.fetch_all(&self.pool)
.await
}

pub async fn get_torrents(&self) -> Result<Vec<Torrent>, sqlx::Error> {
query_as::<_, Torrent>("SELECT * FROM torrust_torrents ORDER BY torrent_id ASC")
.fetch_all(&self.pool)
.await
}

pub async fn get_torrent_files(&self) -> Result<Vec<TorrentFile>, sqlx::Error> {
query_as::<_, TorrentFile>("SELECT * FROM torrust_torrent_files ORDER BY file_id ASC")
.fetch_all(&self.pool)
.await
}
}
46 changes: 46 additions & 0 deletions src/upgrades/from_v1_0_0_to_v2_0_0/databases/sqlite_v2_0_0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,52 @@ impl SqliteDatabaseV2_0_0 {
.map(|v| v.last_insert_rowid())
}

pub async fn insert_torrent(
&self,
torrent_id: i64,
uploader_id: i64,
category_id: i64,
info_hash: &str,
size: i64,
name: &str,
pieces: &str,
piece_length: i64,
private: bool,
root_hash: i64,
date_uploaded: &str,
) -> Result<i64, sqlx::Error> {
query(
"
INSERT INTO torrust_torrents (
torrent_id,
uploader_id,
category_id,
info_hash,
size,
name,
pieces,
piece_length,
private,
root_hash,
date_uploaded
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(torrent_id)
.bind(uploader_id)
.bind(category_id)
.bind(info_hash)
.bind(size)
.bind(name)
.bind(pieces)
.bind(piece_length)
.bind(private)
.bind(root_hash)
.bind(date_uploaded)
.execute(&self.pool)
.await
.map(|v| v.last_insert_rowid())
}

pub async fn delete_all_database_rows(&self) -> Result<(), DatabaseError> {
query("DELETE FROM torrust_categories;")
.execute(&self.pool)
Expand Down
167 changes: 147 additions & 20 deletions src/upgrades/from_v1_0_0_to_v2_0_0/upgrader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@
//! - In v2, the table `torrust_user_profiles` contains two new fields: `bio` and `avatar`.
//! Empty string is used as default value.

use crate::upgrades::from_v1_0_0_to_v2_0_0::databases::sqlite_v1_0_0::SqliteDatabaseV1_0_0;
use crate::upgrades::from_v1_0_0_to_v2_0_0::databases::sqlite_v2_0_0::SqliteDatabaseV2_0_0;
use crate::utils::parse_torrent::decode_torrent;
use crate::{
models::torrent_file::Torrent,
upgrades::from_v1_0_0_to_v2_0_0::databases::sqlite_v1_0_0::SqliteDatabaseV1_0_0,
};
use chrono::prelude::{DateTime, Utc};
use chrono::NaiveDateTime;

use std::{error, fs};
use std::{sync::Arc, time::SystemTime};

use crate::config::Configuration;

fn today_iso8601() -> String {
let dt: DateTime<Utc> = SystemTime::now().into();
format!("{}", dt.format("%Y-%m-%d %H:%M:%S"))
}
pub async fn upgrade() {
// TODO: get from command arguments
let database_file = "data_v2.db".to_string(); // The new database
let upload_path = "./uploads".to_string(); // The relative dir where torrent files are stored

async fn current_db() -> Arc<SqliteDatabaseV1_0_0> {
// Connect to the old v1.0.0 DB
let cfg = match Configuration::load_from_file().await {
Ok(config) => Arc::new(config),
Err(error) => {
Expand All @@ -30,10 +35,29 @@ async fn current_db() -> Arc<SqliteDatabaseV1_0_0> {

let settings = cfg.settings.read().await;

Arc::new(SqliteDatabaseV1_0_0::new(&settings.database.connect_url).await)
// Get connection to source database (current DB in settings)
let source_database = current_db(&settings.database.connect_url).await;

// Get connection to destiny database
let dest_database = new_db(&database_file).await;

println!("Upgrading data from version v1.0.0 to v2.0.0 ...");

migrate_destiny_database(dest_database.clone()).await;
reset_destiny_database(dest_database.clone()).await;
transfer_categories(source_database.clone(), dest_database.clone()).await;
transfer_user_data(source_database.clone(), dest_database.clone()).await;
transfer_tracker_keys(source_database.clone(), dest_database.clone()).await;
transfer_torrents(source_database.clone(), dest_database.clone(), &upload_path).await;

// TODO: WIP. We have to transfer data from the 5 tables in V1 and the torrent files in folder `uploads`.
}

async fn new_db(db_filename: String) -> Arc<SqliteDatabaseV2_0_0> {
async fn current_db(connect_url: &str) -> Arc<SqliteDatabaseV1_0_0> {
Arc::new(SqliteDatabaseV1_0_0::new(connect_url).await)
}

async fn new_db(db_filename: &str) -> Arc<SqliteDatabaseV2_0_0> {
let dest_database_connect_url = format!("sqlite://{}?mode=rwc", db_filename);
Arc::new(SqliteDatabaseV2_0_0::new(&dest_database_connect_url).await)
}
Expand Down Expand Up @@ -170,6 +194,11 @@ async fn transfer_user_data(
}
}

fn today_iso8601() -> String {
let dt: DateTime<Utc> = SystemTime::now().into();
format!("{}", dt.format("%Y-%m-%d %H:%M:%S"))
}

async fn transfer_tracker_keys(
source_database: Arc<SqliteDatabaseV1_0_0>,
dest_database: Arc<SqliteDatabaseV2_0_0>,
Expand Down Expand Up @@ -212,18 +241,116 @@ async fn transfer_tracker_keys(
}
}

pub async fn upgrade() {
// Get connections to source adn destiny databases
let source_database = current_db().await;
let dest_database = new_db("data_v2.db".to_string()).await;
async fn transfer_torrents(
source_database: Arc<SqliteDatabaseV1_0_0>,
dest_database: Arc<SqliteDatabaseV2_0_0>,
upload_path: &str,
) {
println!("Transferring torrents ...");

println!("Upgrading data from version v1.0.0 to v2.0.0 ...");
// Transfer table `torrust_torrents_files`

migrate_destiny_database(dest_database.clone()).await;
reset_destiny_database(dest_database.clone()).await;
transfer_categories(source_database.clone(), dest_database.clone()).await;
transfer_user_data(source_database.clone(), dest_database.clone()).await;
transfer_tracker_keys(source_database.clone(), dest_database.clone()).await;
// Although the The table `torrust_torrents_files` existed in version v1.0.0
// it was was not used.

// TODO: WIP. We have to transfer data from the 5 tables in V1 and the torrent files in folder `uploads`.
// Transfer table `torrust_torrents`

let torrents = source_database.get_torrents().await.unwrap();

for torrent in &torrents {
// [v2] table torrust_torrents

println!(
"[v2][torrust_torrents] adding the torrent: {:?} ...",
&torrent.torrent_id
);

// TODO: confirm with @WarmBeer that
// - All torrents were public in version v1.0.0
// - Infohashes were in lowercase en v1.0. and uppercase in version v2.0.0
let private = false;

let uploader = source_database
.get_user_by_username(&torrent.uploader)
.await
.unwrap();

if uploader.username != torrent.uploader {
panic!(
"Error copying torrent {:?}. Uploader in torrent does username",
&torrent.torrent_id
);
}

let filepath = format!("{}/{}.torrent", upload_path, &torrent.torrent_id);

let torrent_from_file = read_torrent_from_file(&filepath).unwrap();

let pieces = torrent_from_file.info.get_pieces_as_string();
let root_hash = torrent_from_file.info.get_root_hash_as_i64();

let id = dest_database
.insert_torrent(
torrent.torrent_id,
uploader.user_id,
torrent.category_id,
&torrent_from_file.info_hash(),
torrent.file_size,
&torrent_from_file.info.name,
&pieces,
torrent_from_file.info.piece_length,
private,
root_hash,
&convert_timestamp_to_datetime(torrent.upload_date),
)
.await
.unwrap();

if id != torrent.torrent_id {
panic!(
"Error copying torrent {:?} from source DB to destiny DB",
&torrent.torrent_id
);
}

println!(
"[v2][torrust_torrents] torrent: {:?} added.",
&torrent.torrent_id
);

// [v2] table torrust_torrent_files

// TODO

// [v2] table torrust_torrent_announce_urls

// TODO

// [v2] table torrust_torrent_info

// TODO
}
}

fn read_torrent_from_file(path: &str) -> Result<Torrent, Box<dyn error::Error>> {
let contents = match fs::read(path) {
Ok(contents) => contents,
Err(e) => return Err(e.into()),
};

match decode_torrent(&contents) {
Ok(torrent) => Ok(torrent),
Err(e) => Err(e),
}
}

fn convert_timestamp_to_datetime(timestamp: i64) -> String {
// The expected format in database is: 2022-11-04 09:53:57
// MySQL uses a DATETIME column and SQLite uses a TEXT column.

let naive_datetime = NaiveDateTime::from_timestamp(timestamp, 0);
let datetime_again: DateTime<Utc> = DateTime::from_utc(naive_datetime, Utc);

// Format without timezone
datetime_again.format("%Y-%m-%d %H:%M:%S").to_string()
}

0 comments on commit 0b3aefa

Please sign in to comment.