Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,984 changes: 1,624 additions & 360 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ env_logger = "0.11.10"
# Interactions with the torrent client
# Comment/uncomment below for development version
# hightorrent_api = { path = "../hightorrent_api" }
hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api" }
hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api", branch = "feat-sea-orm", features = [ "sea_orm" ] }
# hightorrent_api = "0.2"
# rqbit torrent client to resolve magnets
librqbit = { git = "https://github.com/ikatson/rqbit" }
log = "0.4.29"
# SQLite ORM
sea-orm = { version = "2.0.0-rc.38", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] }
Expand Down
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ pub struct AppConfig {

#[serde(default = "AppConfig::default_log_path")]
pub log_path: Utf8PathBuf,

#[serde(default = "AppConfig::default_rqbit_path")]
pub rqbit_path: Utf8PathBuf,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -127,6 +130,10 @@ impl AppConfig {
Self::config_dir().join("operations.log")
}

pub fn default_rqbit_path() -> Utf8PathBuf {
Self::config_dir().join("rqbit")
}

pub async fn load_from_xdg() -> Result<Self, ConfigError> {
let config_dir = Self::config_dir();
create_dir_all(&config_dir)
Expand Down
185 changes: 185 additions & 0 deletions src/database/magnet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use chrono::Utc;
use hightorrent_api::hightorrent::{MagnetLink, MagnetLinkError, TorrentFile, TorrentID};
use sea_orm::entity::prelude::*;
use sea_orm::*;
use snafu::prelude::*;

use crate::database::operation::*;
use crate::extractors::user::User;
use crate::routes::magnet::MagnetForm;
use crate::state::AppState;
use crate::state::logger::LoggerError;

/// A category to store associated files.
///
/// Each category has a name and an associated path on disk, where
/// symlinks to the content will be created.
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "magnet")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub torrent_id: TorrentID,
pub link: MagnetLink,
pub name: String,
pub resolved: Option<TorrentFile>,
}

#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {}

#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum MagnetError {
#[snafu(display("The magnet is invalid"))]
InvalidMagnet { source: MagnetLinkError },
#[snafu(display("Database error"))]
DB { source: sea_orm::DbErr },
#[snafu(display("The magnet (ID: {id}) does not exist"))]
NotFound { id: i32 },
#[snafu(display("The magnet (TorrentID: {id}) does not exist"))]
NotFoundTorrentID { id: TorrentID },
#[snafu(display("Failed to save the operation log"))]
Logger { source: LoggerError },
}

#[derive(Clone, Debug)]
pub struct MagnetOperator {
pub state: AppState,
pub user: Option<User>,
}

impl MagnetOperator {
/// List magnets
///
/// Should not fail, unless SQLite was corrupted for some reason.
pub async fn list(&self) -> Result<Vec<Model>, MagnetError> {
Entity::find()
.all(&self.state.database)
.await
.context(DBSnafu)
}

/// Count magnets
///
/// Should not fail, unless SQLite was corrupted for some reason.
pub async fn count(&self) -> Result<usize, MagnetError> {
// TODO: there may be a faster sea_orm operation for this
Ok(self.list().await?.len())
}

pub async fn get(&self, id: i32) -> Result<Model, MagnetError> {
let db = &self.state.database;

Entity::find_by_id(id)
.one(db)
.await
.context(DBSnafu)?
.ok_or(MagnetError::NotFound { id })
}

pub async fn get_by_torrent_id(&self, id: &TorrentID) -> Result<Model, MagnetError> {
let db = &self.state.database;

Entity::find()
.filter(Column::TorrentId.eq(id.clone()))
.one(db)
.await
.context(DBSnafu)?
.ok_or(MagnetError::NotFoundTorrentID { id: id.clone() })
}

/// Delete an uploaded magnet
pub async fn delete(&self, id: i32) -> Result<String, MagnetError> {
let db = &self.state.database;

let uploaded_magnet = Entity::find_by_id(id)
.one(db)
.await
.context(DBSnafu)?
.ok_or(MagnetError::NotFound { id })?;

let clone: Model = uploaded_magnet.clone();
uploaded_magnet.delete(db).await.context(DBSnafu)?;

let operation_log = OperationLog {
user: self.user.clone(),
date: Utc::now(),
table: Table::Magnet,
operation: OperationType::Delete,
operation_id: OperationId {
object_id: clone.id,
name: clone.name.to_owned(),
},
operation_form: None,
};

self.state
.logger
.write(operation_log)
.await
.context(LoggerSnafu)?;

Ok(clone.name)
}

/// Create a new uploaded magnet
///
/// Fails if:
///
/// - the magnet is invalid
pub async fn create(&self, f: &MagnetForm) -> Result<Model, MagnetError> {
let magnet = MagnetLink::new(&f.magnet).context(InvalidMagnetSnafu)?;

// Check duplicates
let list = self.list().await?;

if list.iter().any(|x| x.torrent_id == magnet.id()) {
// The magnet is already known
return self.get_by_torrent_id(&magnet.id()).await;
}

let model = ActiveModel {
torrent_id: Set(magnet.id()),
link: Set(magnet.clone()),
name: Set(magnet.name().to_string()),
// TODO: check if we already have the torrent in which case it's already resolved!
resolved: Set(None),
..Default::default()
}
.save(&self.state.database)
.await
.context(DBSnafu)?;

// Now that the magnet has been summoned into the DB,
// we should let the resolver know about it.
self.state
.resolver
.send(magnet.clone())
.expect("resolver sender channel has been closed");

// Should not fail
let model = model.try_into_model().unwrap();

let operation_log = OperationLog {
user: self.user.clone(),
date: Utc::now(),
table: Table::Magnet,
operation: OperationType::Create,
operation_id: OperationId {
object_id: model.id.to_owned(),
name: model.name.to_string(),
},
operation_form: Some(Operation::Magnet(f.clone())),
};

self.state
.logger
.write(operation_log)
.await
.context(LoggerSnafu)?;

Ok(model)
}
}
1 change: 1 addition & 0 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// sea_orm example: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
pub mod category;
pub mod content_folder;
pub mod magnet;
pub mod operation;
pub mod operator;
3 changes: 3 additions & 0 deletions src/database/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use crate::extractors::user::User;
use crate::routes::category::CategoryForm;
use crate::routes::content_folder::ContentFolderForm;
use crate::routes::magnet::MagnetForm;

/// Type of operation applied to the database.
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
Expand All @@ -24,6 +25,7 @@ pub struct OperationId {
pub enum Table {
Category,
ContentFolder,
Magnet,
}

/// Operation applied to the database.
Expand All @@ -34,6 +36,7 @@ pub enum Table {
pub enum Operation {
Category(CategoryForm),
ContentFolder(ContentFolderForm),
Magnet(MagnetForm),
}

impl std::fmt::Display for Operation {
Expand Down
11 changes: 10 additions & 1 deletion src/database/operator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::database::{category::CategoryOperator, content_folder::ContentFolderOperator};
use crate::database::{
category::CategoryOperator, content_folder::ContentFolderOperator, magnet::MagnetOperator,
};
use crate::extractors::user::User;
use crate::state::AppState;

Expand Down Expand Up @@ -26,4 +28,11 @@ impl DatabaseOperator {
user: self.user.clone(),
}
}

pub fn magnet(&self) -> MagnetOperator {
MagnetOperator {
state: self.state.clone(),
user: self.user.clone(),
}
}
}
16 changes: 14 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod extractors;
pub mod filesystem;
pub mod middleware;
pub mod migration;
pub mod resolver;
pub mod routes;
pub mod state;

Expand Down Expand Up @@ -37,6 +38,8 @@ pub fn router(state: state::AppState) -> Router {
)
.route("/folders", get(routes::index::index))
.route("/logs", get(routes::logs::index))
.route("/magnet/upload", post(routes::magnet::upload))
.route("/magnet", get(routes::magnet::list))
// Register static assets routes
.nest("/assets", static_router())
// Insert request timing
Expand All @@ -50,8 +53,17 @@ where
L: Listener,
L::Addr: std::fmt::Debug,
{
let state = state::AppState::new(config).await?;
let app = router(state);
// Create a channel so the webapp can let the resolver know
// to resolve new magnets.
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();

let state = state::AppState::new(config, sender).await?;
let app = router(state.clone());

tokio::task::spawn(async move {
// Spawn the background task to resolve magnets
resolver::Resolver::new(state, receiver).await.serve().await
});

axum::serve(listener, app.into_make_service())
.await
Expand Down
58 changes: 58 additions & 0 deletions src/migration/m20251114_01_create_table_magnet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use sea_orm_migration::{prelude::*, schema::*};

use crate::migration::m20251110_01_create_table_category::Category;
use crate::migration::m20251113_203047_add_content_folder::ContentFolder;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Magnet::Table)
.if_not_exists()
.col(pk_auto(Magnet::Id))
.col(string(Magnet::TorrentID).unique_key())
.col(string(Magnet::Name))
.col(string(Magnet::Link))
.col(var_binary(Magnet::Resolved, 0).null())
.col(ColumnDef::new(Magnet::ContentFolderId).integer())
.foreign_key(
ForeignKey::create()
.name("fk-magnet-content_folder_id")
.from(Magnet::Table, Magnet::ContentFolderId)
.to(ContentFolder::Table, ContentFolder::Id),
)
.col(ColumnDef::new(Magnet::CategoryId).integer())
.foreign_key(
ForeignKey::create()
.name("fk-magnet-category_id")
.from(Magnet::Table, Magnet::CategoryId)
.to(Category::Table, Category::Id),
)
.to_owned(),
)
.await
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Magnet::Table).to_owned())
.await
}
}

#[derive(DeriveIden)]
enum Magnet {
Table,
Id,
TorrentID,
Name,
Link,
Resolved,
ContentFolderId,
CategoryId,
}
2 changes: 2 additions & 0 deletions src/migration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use sea_orm_migration::prelude::*;
mod m20251110_01_create_table_category;
mod m20251113_203047_add_content_folder;
mod m20251113_203899_add_uniq_to_content_folder;
mod m20251114_01_create_table_magnet;

pub struct Migrator;

Expand All @@ -13,6 +14,7 @@ impl MigratorTrait for Migrator {
Box::new(m20251110_01_create_table_category::Migration),
Box::new(m20251113_203047_add_content_folder::Migration),
Box::new(m20251113_203899_add_uniq_to_content_folder::Migration),
Box::new(m20251114_01_create_table_magnet::Migration),
]
}
}
Loading
Loading