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
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion 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 Down
160 changes: 160 additions & 0 deletions src/database/magnet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use chrono::Utc;
use hightorrent_api::hightorrent::{MagnetLink, MagnetLinkError};
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.
// TODO: typed model fields
// see https://github.com/SeaQL/sea-orm/issues/2811
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "magnet")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub torrent_id: String,
pub magnet: String,
pub name: String,
pub resolved: bool,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

#[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 },
// TODO: this is not an error
// we should redirect to the magnet page (eg. progress)
#[snafu(display("There is already a magnet with the TorrentID `{torrent_id}`"))]
TorrentIDTaken { torrent_id: String },
#[snafu(display("Database error"))]
DB { source: sea_orm::DbErr },
#[snafu(display("The magnet (ID: {id}) does not exist"))]
NotFound { id: i32 },
#[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 categories
///
/// 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)
}

/// Delete an uploaded magnet
pub async fn delete(&self, id: i32, user: Option<User>) -> 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,
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, user: Option<User>) -> 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().as_str()) {
return Err(MagnetError::TorrentIDTaken {
torrent_id: magnet.id().to_string(),
});
}

let model = ActiveModel {
torrent_id: Set(magnet.id().to_string()),
magnet: Set(magnet.to_string()),
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,
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,3 +1,4 @@
// sea_orm example: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
pub mod category;
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 @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};

use crate::extractors::user::User;
use crate::routes::category::CategoryForm;
use crate::routes::magnet::MagnetForm;

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

/// Operation applied to the database.
Expand All @@ -31,6 +33,7 @@ pub enum Table {
#[serde(untagged)]
pub enum Operation {
Category(CategoryForm),
Magnet(MagnetForm),
}

impl std::fmt::Display for Operation {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub fn router(state: state::AppState) -> Router {
.route("/categories/new", get(routes::category::new))
.route("/categories/{id}/delete", get(routes::category::delete))
.route("/logs", get(routes::logs::index))
.route("/magnet/upload", post(routes::magnet::upload))
// 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,
}
6 changes: 5 additions & 1 deletion src/migration/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
pub use sea_orm_migration::prelude::*;

mod m20251110_01_create_table_category;
mod m20251114_01_create_table_magnet;

pub struct Migrator;

#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20251110_01_create_table_category::Migration)]
vec![
Box::new(m20251110_01_create_table_category::Migration),
Box::new(m20251114_01_create_table_magnet::Migration),
]
}
}
15 changes: 15 additions & 0 deletions src/routes/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use snafu::prelude::*;
// TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
use crate::database::category::CategoryOperator;
use crate::extractors::user::User;
use crate::routes::magnet::MagnetForm;
use crate::state::{AppState, AppStateContext, error::*};

#[derive(Template, WebTemplate)]
Expand All @@ -17,6 +18,12 @@ pub struct IndexTemplate {
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
/// TODO: typed error
pub post_error: Option<String>,
}

impl IndexTemplate {
Expand All @@ -33,8 +40,16 @@ impl IndexTemplate {
state: app_state.context().await?,
user,
categories,
post: None,
post_error: None,
})
}

pub fn with_errored_form(mut self, form: MagnetForm, error: String) -> Self {
self.post = Some(form);
self.post_error = Some(error);
self
}
}

pub async fn index(
Expand Down
Loading
Loading