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
539 changes: 304 additions & 235 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ path = "src/main.rs"
askama = "0.14.0"
# askama_web::WebTemplate implements axum::IntoResponse
askama_web = { version = "0.14.6", features = ["axum-0.8"] }
axum = { version = "0.8.4", features = ["macros"] }
axum = { version = "0.8.4", features = ["macros","multipart"] }
axum-extra = { version = "0.12.1", features = ["cookie"] }
# UTF-8 paths for easier String/PathBuf interop
camino = { version = "1.1.12", features = ["serde1"] }
Expand All @@ -36,13 +36,13 @@ env_logger = "0.11.8"
# 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"
log = "0.4.27"
# SQLite ORM
sea-orm = { version = "2.0.0-rc.21", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] }
sea-orm = { version = "=2.0.0-rc.28", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] }
# SQLite migrations
sea-orm-migration = { version = "2.0.0-rc.21" }
sea-orm-migration = { version = "=2.0.0-rc.28" }
# Serialization/deserialization, for example in path extractors
serde = { version = "1.0.219", features = ["derive", "rc"] }
# (De)serialization for operations log
Expand Down
174 changes: 174 additions & 0 deletions src/database/magnet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use chrono::Utc;
use hightorrent_api::hightorrent::{MagnetLink, MagnetLinkError, 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: bool,
}

#[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 {
pub fn new(state: AppState, user: Option<User>) -> Self {
Self { state, user }
}

/// 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)
}

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(false),
..Default::default()
}
.save(&self.state.database)
.await
.context(DBSnafu)?;

// 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,4 +1,5 @@
// 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;
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
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pub fn router(state: state::AppState) -> Router {
Router::new()
// Register dynamic routes
.route("/", get(routes::index::index))
.route("/upload", get(routes::index::upload))
.route("/progress/{view_request}", get(routes::progress::progress))
.route("/categories", post(routes::category::create))
.route("/categories/new", get(routes::category::new))
Expand All @@ -32,6 +31,10 @@ pub fn router(state: state::AppState) -> Router {
)
.route("/folders", post(routes::content_folder::create))
.route("/logs", get(routes::logs::index))
.route("/magnet/upload", post(routes::magnet::upload))
.route("/magnet/upload", get(routes::magnet::get_upload))
.route("/magnet", get(routes::magnet::list))
.route("/magnet/{id}", get(routes::magnet::show))
// Register static assets routes
.nest("/assets", static_router())
// Insert request timing
Expand Down
39 changes: 39 additions & 0 deletions src/migration/m20251114_01_create_table_magnet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use sea_orm_migration::{prelude::*, schema::*};

#[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(boolean(Magnet::Resolved))
.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,
}
3 changes: 3 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,8 @@ 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(m20251110_01_create_table_category::Migration),
Box::new(m20251114_01_create_table_magnet::Migration),
]
}
}
31 changes: 6 additions & 25 deletions src/routes/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use snafu::prelude::*;
// TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
use crate::database::category::{self, CategoryOperator};
use crate::extractors::user::User;
use crate::routes::magnet::MagnetForm;
use crate::state::flash_message::{OperationStatus, get_cookie};
use crate::state::{AppState, AppStateContext, error::*};

Expand All @@ -32,6 +33,11 @@ pub struct UploadTemplate {
pub user: Option<User>,
/// Categories
pub categories: Vec<String>,
// TODO: also support torrent upload
/// Magnet upload form
pub post: Option<MagnetForm>,
/// Error with submitted magnet
pub post_error: Option<AppStateError>,
}

impl IndexTemplate {
Expand Down Expand Up @@ -61,35 +67,10 @@ impl IndexTemplate {
}
}

impl UploadTemplate {
pub async fn new(app_state: AppState, user: Option<User>) -> Result<Self, AppStateError> {
let categories: Vec<String> = CategoryOperator::new(app_state.clone(), user.clone())
.list()
.await
.context(CategorySnafu)?
.into_iter()
.map(|x| x.name.to_string())
.collect();

Ok(UploadTemplate {
state: app_state.context().await?,
user,
categories,
})
}
}

pub async fn index(
State(app_state): State<AppState>,
user: Option<User>,
jar: CookieJar,
) -> Result<(CookieJar, IndexTemplate), AppStateError> {
IndexTemplate::new(app_state, user, jar).await
}

pub async fn upload(
State(app_state): State<AppState>,
user: Option<User>,
) -> Result<UploadTemplate, AppStateError> {
UploadTemplate::new(app_state, user).await
}
Loading
Loading