Skip to content

Commit 9db740e

Browse files
angrynodeamateurforger
authored andcommitted
feat: Start supporting magnet upload
1 parent 13005ca commit 9db740e

18 files changed

Lines changed: 828 additions & 281 deletions

File tree

Cargo.lock

Lines changed: 304 additions & 235 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ path = "src/main.rs"
1515
askama = "0.14.0"
1616
# askama_web::WebTemplate implements axum::IntoResponse
1717
askama_web = { version = "0.14.6", features = ["axum-0.8"] }
18-
axum = { version = "0.8.4", features = ["macros"] }
18+
axum = { version = "0.8.4", features = ["macros","multipart"] }
1919
axum-extra = { version = "0.12.1", features = ["cookie"] }
2020
# UTF-8 paths for easier String/PathBuf interop
2121
camino = { version = "1.1.12", features = ["serde1"] }
@@ -36,13 +36,13 @@ env_logger = "0.11.8"
3636
# Interactions with the torrent client
3737
# Comment/uncomment below for development version
3838
# hightorrent_api = { path = "../hightorrent_api" }
39-
hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api" }
39+
hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api", branch = "feat-sea-orm", features = [ "sea_orm" ]}
4040
# hightorrent_api = "0.2"
4141
log = "0.4.27"
4242
# SQLite ORM
43-
sea-orm = { version = "2.0.0-rc.21", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] }
43+
sea-orm = { version = "=2.0.0-rc.28", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] }
4444
# SQLite migrations
45-
sea-orm-migration = { version = "2.0.0-rc.21" }
45+
sea-orm-migration = { version = "=2.0.0-rc.28" }
4646
# Serialization/deserialization, for example in path extractors
4747
serde = { version = "1.0.219", features = ["derive", "rc"] }
4848
# (De)serialization for operations log

src/database/magnet.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
use chrono::Utc;
2+
use hightorrent_api::hightorrent::{MagnetLink, MagnetLinkError, TorrentID};
3+
use sea_orm::entity::prelude::*;
4+
use sea_orm::*;
5+
use snafu::prelude::*;
6+
7+
use crate::database::operation::*;
8+
use crate::extractors::user::User;
9+
use crate::routes::magnet::MagnetForm;
10+
use crate::state::AppState;
11+
use crate::state::logger::LoggerError;
12+
13+
/// A category to store associated files.
14+
///
15+
/// Each category has a name and an associated path on disk, where
16+
/// symlinks to the content will be created.
17+
#[sea_orm::model]
18+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
19+
#[sea_orm(table_name = "magnet")]
20+
pub struct Model {
21+
#[sea_orm(primary_key)]
22+
pub id: i32,
23+
pub torrent_id: TorrentID,
24+
pub link: MagnetLink,
25+
pub name: String,
26+
pub resolved: bool,
27+
}
28+
29+
#[async_trait::async_trait]
30+
impl ActiveModelBehavior for ActiveModel {}
31+
32+
#[derive(Debug, Snafu)]
33+
#[snafu(visibility(pub))]
34+
pub enum MagnetError {
35+
#[snafu(display("The magnet is invalid"))]
36+
InvalidMagnet { source: MagnetLinkError },
37+
#[snafu(display("Database error"))]
38+
DB { source: sea_orm::DbErr },
39+
#[snafu(display("The magnet (ID: {id}) does not exist"))]
40+
NotFound { id: i32 },
41+
#[snafu(display("The magnet (TorrentID: {id}) does not exist"))]
42+
NotFoundTorrentID { id: TorrentID },
43+
#[snafu(display("Failed to save the operation log"))]
44+
Logger { source: LoggerError },
45+
}
46+
47+
#[derive(Clone, Debug)]
48+
pub struct MagnetOperator {
49+
pub state: AppState,
50+
pub user: Option<User>,
51+
}
52+
53+
impl MagnetOperator {
54+
pub fn new(state: AppState, user: Option<User>) -> Self {
55+
Self { state, user }
56+
}
57+
58+
/// List magnets
59+
///
60+
/// Should not fail, unless SQLite was corrupted for some reason.
61+
pub async fn list(&self) -> Result<Vec<Model>, MagnetError> {
62+
Entity::find()
63+
.all(&self.state.database)
64+
.await
65+
.context(DBSnafu)
66+
}
67+
68+
pub async fn get(&self, id: i32) -> Result<Model, MagnetError> {
69+
let db = &self.state.database;
70+
71+
Entity::find_by_id(id)
72+
.one(db)
73+
.await
74+
.context(DBSnafu)?
75+
.ok_or(MagnetError::NotFound { id })
76+
}
77+
78+
pub async fn get_by_torrent_id(&self, id: &TorrentID) -> Result<Model, MagnetError> {
79+
let db = &self.state.database;
80+
81+
Entity::find()
82+
.filter(Column::TorrentId.eq(id.clone()))
83+
.one(db)
84+
.await
85+
.context(DBSnafu)?
86+
.ok_or(MagnetError::NotFoundTorrentID { id: id.clone() })
87+
}
88+
89+
/// Delete an uploaded magnet
90+
pub async fn delete(&self, id: i32) -> Result<String, MagnetError> {
91+
let db = &self.state.database;
92+
93+
let uploaded_magnet = Entity::find_by_id(id)
94+
.one(db)
95+
.await
96+
.context(DBSnafu)?
97+
.ok_or(MagnetError::NotFound { id })?;
98+
99+
let clone: Model = uploaded_magnet.clone();
100+
uploaded_magnet.delete(db).await.context(DBSnafu)?;
101+
102+
let operation_log = OperationLog {
103+
user: self.user.clone(),
104+
date: Utc::now(),
105+
table: Table::Magnet,
106+
operation: OperationType::Delete,
107+
operation_id: OperationId {
108+
object_id: clone.id,
109+
name: clone.name.to_owned(),
110+
},
111+
operation_form: None,
112+
};
113+
114+
self.state
115+
.logger
116+
.write(operation_log)
117+
.await
118+
.context(LoggerSnafu)?;
119+
120+
Ok(clone.name)
121+
}
122+
123+
/// Create a new uploaded magnet
124+
///
125+
/// Fails if:
126+
///
127+
/// - the magnet is invalid
128+
pub async fn create(&self, f: &MagnetForm) -> Result<Model, MagnetError> {
129+
let magnet = MagnetLink::new(&f.magnet).context(InvalidMagnetSnafu)?;
130+
131+
// Check duplicates
132+
let list = self.list().await?;
133+
134+
if list.iter().any(|x| x.torrent_id == magnet.id()) {
135+
// The magnet is already known
136+
return self.get_by_torrent_id(&magnet.id()).await;
137+
}
138+
139+
let model = ActiveModel {
140+
torrent_id: Set(magnet.id()),
141+
link: Set(magnet.clone()),
142+
name: Set(magnet.name().to_string()),
143+
// TODO: check if we already have the torrent in which case it's already resolved!
144+
resolved: Set(false),
145+
..Default::default()
146+
}
147+
.save(&self.state.database)
148+
.await
149+
.context(DBSnafu)?;
150+
151+
// Should not fail
152+
let model = model.try_into_model().unwrap();
153+
154+
let operation_log = OperationLog {
155+
user: self.user.clone(),
156+
date: Utc::now(),
157+
table: Table::Magnet,
158+
operation: OperationType::Create,
159+
operation_id: OperationId {
160+
object_id: model.id.to_owned(),
161+
name: model.name.to_string(),
162+
},
163+
operation_form: Some(Operation::Magnet(f.clone())),
164+
};
165+
166+
self.state
167+
.logger
168+
.write(operation_log)
169+
.await
170+
.context(LoggerSnafu)?;
171+
172+
Ok(model)
173+
}
174+
}

src/database/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// sea_orm example: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
22
pub mod category;
33
pub mod content_folder;
4+
pub mod magnet;
45
pub mod operation;

src/database/operation.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
55
use crate::extractors::user::User;
66
use crate::routes::category::CategoryForm;
77
use crate::routes::content_folder::ContentFolderForm;
8+
use crate::routes::magnet::MagnetForm;
89

910
/// Type of operation applied to the database.
1011
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
@@ -24,6 +25,7 @@ pub struct OperationId {
2425
pub enum Table {
2526
Category,
2627
ContentFolder,
28+
Magnet,
2729
}
2830

2931
/// Operation applied to the database.
@@ -34,6 +36,7 @@ pub enum Table {
3436
pub enum Operation {
3537
Category(CategoryForm),
3638
ContentFolder(ContentFolderForm),
39+
Magnet(MagnetForm),
3740
}
3841

3942
impl std::fmt::Display for Operation {

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ pub fn router(state: state::AppState) -> Router {
2020
Router::new()
2121
// Register dynamic routes
2222
.route("/", get(routes::index::index))
23-
.route("/upload", get(routes::index::upload))
2423
.route("/progress/{view_request}", get(routes::progress::progress))
2524
.route("/categories", post(routes::category::create))
2625
.route("/categories/new", get(routes::category::new))
@@ -32,6 +31,10 @@ pub fn router(state: state::AppState) -> Router {
3231
)
3332
.route("/folders", post(routes::content_folder::create))
3433
.route("/logs", get(routes::logs::index))
34+
.route("/magnet/upload", post(routes::magnet::upload))
35+
.route("/magnet/upload", get(routes::magnet::get_upload))
36+
.route("/magnet", get(routes::magnet::list))
37+
.route("/magnet/{id}", get(routes::magnet::show))
3538
// Register static assets routes
3639
.nest("/assets", static_router())
3740
// Insert request timing
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use sea_orm_migration::{prelude::*, schema::*};
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
impl MigrationTrait for Migration {
8+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9+
manager
10+
.create_table(
11+
Table::create()
12+
.table(Magnet::Table)
13+
.if_not_exists()
14+
.col(pk_auto(Magnet::Id))
15+
.col(string(Magnet::TorrentID).unique_key())
16+
.col(string(Magnet::Name))
17+
.col(string(Magnet::Link))
18+
.col(boolean(Magnet::Resolved))
19+
.to_owned(),
20+
)
21+
.await
22+
}
23+
24+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
25+
manager
26+
.drop_table(Table::drop().table(Magnet::Table).to_owned())
27+
.await
28+
}
29+
}
30+
31+
#[derive(DeriveIden)]
32+
enum Magnet {
33+
Table,
34+
Id,
35+
TorrentID,
36+
Name,
37+
Link,
38+
Resolved,
39+
}

src/migration/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub use sea_orm_migration::prelude::*;
33
mod m20251110_01_create_table_category;
44
mod m20251113_203047_add_content_folder;
55
mod m20251113_203899_add_uniq_to_content_folder;
6+
mod m20251114_01_create_table_magnet;
67

78
pub struct Migrator;
89

@@ -13,6 +14,8 @@ impl MigratorTrait for Migrator {
1314
Box::new(m20251110_01_create_table_category::Migration),
1415
Box::new(m20251113_203047_add_content_folder::Migration),
1516
Box::new(m20251113_203899_add_uniq_to_content_folder::Migration),
17+
Box::new(m20251110_01_create_table_category::Migration),
18+
Box::new(m20251114_01_create_table_magnet::Migration),
1619
]
1720
}
1821
}

src/routes/index.rs

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use snafu::prelude::*;
77
// TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
88
use crate::database::category::{self, CategoryOperator};
99
use crate::extractors::user::User;
10+
use crate::routes::magnet::MagnetForm;
1011
use crate::state::flash_message::{OperationStatus, get_cookie};
1112
use crate::state::{AppState, AppStateContext, error::*};
1213

@@ -32,6 +33,11 @@ pub struct UploadTemplate {
3233
pub user: Option<User>,
3334
/// Categories
3435
pub categories: Vec<String>,
36+
// TODO: also support torrent upload
37+
/// Magnet upload form
38+
pub post: Option<MagnetForm>,
39+
/// Error with submitted magnet
40+
pub post_error: Option<AppStateError>,
3541
}
3642

3743
impl IndexTemplate {
@@ -61,35 +67,10 @@ impl IndexTemplate {
6167
}
6268
}
6369

64-
impl UploadTemplate {
65-
pub async fn new(app_state: AppState, user: Option<User>) -> Result<Self, AppStateError> {
66-
let categories: Vec<String> = CategoryOperator::new(app_state.clone(), user.clone())
67-
.list()
68-
.await
69-
.context(CategorySnafu)?
70-
.into_iter()
71-
.map(|x| x.name.to_string())
72-
.collect();
73-
74-
Ok(UploadTemplate {
75-
state: app_state.context().await?,
76-
user,
77-
categories,
78-
})
79-
}
80-
}
81-
8270
pub async fn index(
8371
State(app_state): State<AppState>,
8472
user: Option<User>,
8573
jar: CookieJar,
8674
) -> Result<(CookieJar, IndexTemplate), AppStateError> {
8775
IndexTemplate::new(app_state, user, jar).await
8876
}
89-
90-
pub async fn upload(
91-
State(app_state): State<AppState>,
92-
user: Option<User>,
93-
) -> Result<UploadTemplate, AppStateError> {
94-
UploadTemplate::new(app_state, user).await
95-
}

0 commit comments

Comments
 (0)