Skip to content

Commit 91cecac

Browse files
committed
feat: Start supporting magnet upload
1 parent 485f9ca commit 91cecac

File tree

14 files changed

+337
-3
lines changed

14 files changed

+337
-3
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
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"] }

src/database/magnet.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use chrono::Utc;
2+
use hightorrent_api::hightorrent::{MagnetLink, MagnetLinkError};
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+
// TODO: typed model fields
18+
// see https://github.com/SeaQL/sea-orm/issues/2811
19+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
20+
#[sea_orm(table_name = "magnet")]
21+
pub struct Model {
22+
#[sea_orm(primary_key)]
23+
pub id: i32,
24+
pub torrent_id: String,
25+
pub magnet: String,
26+
pub name: String,
27+
pub resolved: bool,
28+
}
29+
30+
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
31+
pub enum Relation {}
32+
33+
#[async_trait::async_trait]
34+
impl ActiveModelBehavior for ActiveModel {}
35+
36+
#[derive(Debug, Snafu)]
37+
#[snafu(visibility(pub))]
38+
pub enum MagnetError {
39+
#[snafu(display("The magnet is invalid"))]
40+
InvalidMagnet { source: MagnetLinkError },
41+
// TODO: this is not an error
42+
// we should redirect to the magnet page (eg. progress)
43+
#[snafu(display("There is already a magnet with the TorrentID `{torrent_id}`"))]
44+
TorrentIDTaken { torrent_id: String },
45+
#[snafu(display("Database error"))]
46+
DB { source: sea_orm::DbErr },
47+
#[snafu(display("The magnet (ID: {id}) does not exist"))]
48+
NotFound { id: i32 },
49+
#[snafu(display("Failed to save the operation log"))]
50+
Logger { source: LoggerError },
51+
}
52+
53+
#[derive(Clone, Debug)]
54+
pub struct MagnetOperator {
55+
pub state: AppState,
56+
pub user: Option<User>,
57+
}
58+
59+
impl MagnetOperator {
60+
pub fn new(state: AppState, user: Option<User>) -> Self {
61+
Self { state, user }
62+
}
63+
64+
/// List categories
65+
///
66+
/// Should not fail, unless SQLite was corrupted for some reason.
67+
pub async fn list(&self) -> Result<Vec<Model>, MagnetError> {
68+
Entity::find()
69+
.all(&self.state.database)
70+
.await
71+
.context(DBSnafu)
72+
}
73+
74+
/// Delete an uploaded magnet
75+
pub async fn delete(&self, id: i32, user: Option<User>) -> Result<String, MagnetError> {
76+
let db = &self.state.database;
77+
78+
let uploaded_magnet = Entity::find_by_id(id)
79+
.one(db)
80+
.await
81+
.context(DBSnafu)?
82+
.ok_or(MagnetError::NotFound { id })?;
83+
84+
let clone: Model = uploaded_magnet.clone();
85+
uploaded_magnet.delete(db).await.context(DBSnafu)?;
86+
87+
let operation_log = OperationLog {
88+
user,
89+
date: Utc::now(),
90+
table: Table::Magnet,
91+
operation: OperationType::Delete,
92+
operation_id: OperationId {
93+
object_id: clone.id,
94+
name: clone.name.to_owned(),
95+
},
96+
operation_form: None,
97+
};
98+
99+
self.state
100+
.logger
101+
.write(operation_log)
102+
.await
103+
.context(LoggerSnafu)?;
104+
105+
Ok(clone.name)
106+
}
107+
108+
/// Create a new uploaded magnet
109+
///
110+
/// Fails if:
111+
///
112+
/// - the magnet is invalid
113+
pub async fn create(&self, f: &MagnetForm, user: Option<User>) -> Result<Model, MagnetError> {
114+
let magnet = MagnetLink::new(&f.magnet).context(InvalidMagnetSnafu)?;
115+
116+
// Check duplicates
117+
let list = self.list().await?;
118+
119+
if list.iter().any(|x| x.torrent_id == magnet.id().as_str()) {
120+
return Err(MagnetError::TorrentIDTaken {
121+
torrent_id: magnet.id().to_string(),
122+
});
123+
}
124+
125+
let model = ActiveModel {
126+
torrent_id: Set(magnet.id().to_string()),
127+
magnet: Set(magnet.to_string()),
128+
name: Set(magnet.name().to_string()),
129+
// TODO: check if we already have the torrent in which case it's already resolved!
130+
resolved: Set(false),
131+
..Default::default()
132+
}
133+
.save(&self.state.database)
134+
.await
135+
.context(DBSnafu)?;
136+
137+
// Should not fail
138+
let model = model.try_into_model().unwrap();
139+
140+
let operation_log = OperationLog {
141+
user,
142+
date: Utc::now(),
143+
table: Table::Magnet,
144+
operation: OperationType::Create,
145+
operation_id: OperationId {
146+
object_id: model.id.to_owned(),
147+
name: model.name.to_string(),
148+
},
149+
operation_form: Some(Operation::Magnet(f.clone())),
150+
};
151+
152+
self.state
153+
.logger
154+
.write(operation_log)
155+
.await
156+
.context(LoggerSnafu)?;
157+
158+
Ok(model)
159+
}
160+
}

src/database/mod.rs

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

src/database/operation.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
44

55
use crate::extractors::user::User;
66
use crate::routes::category::CategoryForm;
7+
use crate::routes::magnet::MagnetForm;
78

89
/// Type of operation applied to the database.
910
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
@@ -22,6 +23,7 @@ pub struct OperationId {
2223
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
2324
pub enum Table {
2425
Category,
26+
Magnet,
2527
}
2628

2729
/// Operation applied to the database.
@@ -31,6 +33,7 @@ pub enum Table {
3133
#[serde(untagged)]
3234
pub enum Operation {
3335
Category(CategoryForm),
36+
Magnet(MagnetForm),
3437
}
3538

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

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub fn router(state: state::AppState) -> Router {
2626
.route("/categories/new", get(routes::category::new))
2727
.route("/categories/{id}/delete", get(routes::category::delete))
2828
.route("/logs", get(routes::logs::index))
29+
.route("/magnet/upload", post(routes::magnet::upload))
2930
// Register static assets routes
3031
.nest("/assets", static_router())
3132
// 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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
pub use sea_orm_migration::prelude::*;
22

33
mod m20251110_01_create_table_category;
4+
mod m20251114_01_create_table_magnet;
45

56
pub struct Migrator;
67

78
#[async_trait::async_trait]
89
impl MigratorTrait for Migrator {
910
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
10-
vec![Box::new(m20251110_01_create_table_category::Migration)]
11+
vec![
12+
Box::new(m20251110_01_create_table_category::Migration),
13+
Box::new(m20251114_01_create_table_magnet::Migration),
14+
]
1115
}
1216
}

src/routes/index.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use snafu::prelude::*;
66
// TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
77
use crate::database::category::CategoryOperator;
88
use crate::extractors::user::User;
9+
use crate::routes::magnet::MagnetForm;
910
use crate::state::{AppState, AppStateContext, error::*};
1011

1112
#[derive(Template, WebTemplate)]
@@ -17,6 +18,12 @@ pub struct IndexTemplate {
1718
pub user: Option<User>,
1819
/// Categories
1920
pub categories: Vec<String>,
21+
// TODO: also support torrent upload
22+
/// Magnet upload form
23+
pub post: Option<MagnetForm>,
24+
/// Error with submitted magnet
25+
/// TODO: typed error
26+
pub post_error: Option<String>,
2027
}
2128

2229
impl IndexTemplate {
@@ -33,8 +40,16 @@ impl IndexTemplate {
3340
state: app_state.context().await?,
3441
user,
3542
categories,
43+
post: None,
44+
post_error: None,
3645
})
3746
}
47+
48+
pub fn with_errored_form(mut self, form: MagnetForm, error: String) -> Self {
49+
self.post = Some(form);
50+
self.post_error = Some(error);
51+
self
52+
}
3853
}
3954

4055
pub async fn index(

0 commit comments

Comments
 (0)