Skip to content

Commit 2a34db6

Browse files
author
amateurforger
committed
feat: Magnet upload form on the content folder page
1 parent 994dc96 commit 2a34db6

10 files changed

Lines changed: 185 additions & 20 deletions

File tree

src/database/category.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ pub enum CategoryError {
4747
DB { source: sea_orm::DbErr },
4848
#[snafu(display("The category (ID: {id}) does not exist"))]
4949
IDNotFound { id: i32 },
50+
#[snafu(display("The category id is invalid: {id}"))]
51+
IDInvalid { id: String },
5052
#[snafu(display("The category (Name: {name}) does not exist"))]
5153
NameNotFound { name: String },
5254
#[snafu(display("Failed to save the operation log"))]
@@ -76,7 +78,7 @@ impl CategoryOperator {
7678

7779
/// Find one category by ID
7880
///
79-
/// Should not fail, unless SQLite was corrupted for some reason.
81+
/// Fails if the requested category ID does not exist.
8082
pub async fn find_by_id(&self, id: i32) -> Result<Model, CategoryError> {
8183
let category = Entity::find_by_id(id)
8284
.one(&self.state.database)
@@ -89,6 +91,19 @@ impl CategoryOperator {
8991
}
9092
}
9193

94+
/// Find one category by stringy ID
95+
///
96+
/// Fails if:
97+
///
98+
/// - the requested ID does not exist
99+
/// - the requested ID could not be parsed
100+
pub async fn find_by_id_str(&self, id: &str) -> Result<Model, CategoryError> {
101+
let id: i32 = id
102+
.parse()
103+
.map_err(|_e| CategoryError::IDInvalid { id: id.to_string() })?;
104+
self.find_by_id(id).await
105+
}
106+
92107
/// Find one category by Name
93108
///
94109
/// Should not fail, unless SQLite was corrupted for some reason.

src/database/content_folder.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub struct Model {
3030
pub parent_id: Option<i32>,
3131
#[sea_orm(self_ref, relation_enum = "Parent", from = "parent_id", to = "id")]
3232
pub parent: HasOne<Entity>,
33+
#[sea_orm(has_many)]
34+
pub magnets: HasMany<super::magnet::Entity>,
3335
}
3436

3537
#[async_trait::async_trait]
@@ -44,6 +46,8 @@ pub enum ContentFolderError {
4446
PathTaken { path: String },
4547
#[snafu(display("The Content Folder (Path: {path}) does not exist"))]
4648
NotFound { path: String },
49+
#[snafu(display("The content folder id is invalid: {id}"))]
50+
IDInvalid { id: String },
4751
#[snafu(display("Database error"))]
4852
DB { source: sea_orm::DbErr },
4953
#[snafu(display("Failed to save the operation log"))]
@@ -109,6 +113,19 @@ impl ContentFolderOperator {
109113
}
110114
}
111115

116+
/// Find one category by stringy ID
117+
///
118+
/// Fails if:
119+
///
120+
/// - the requested ID does not exist
121+
/// - the requested ID could not be parsed
122+
pub async fn find_by_id_str(&self, id: &str) -> Result<Model, ContentFolderError> {
123+
let id: i32 = id
124+
.parse()
125+
.map_err(|_e| ContentFolderError::IDInvalid { id: id.to_string() })?;
126+
self.find_by_id(id).await
127+
}
128+
112129
/// Create a new content folder
113130
///
114131
/// Fails if:

src/database/magnet.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use sea_orm::entity::prelude::*;
44
use sea_orm::*;
55
use snafu::prelude::*;
66

7+
use crate::database::content_folder;
78
use crate::database::operation::*;
89
use crate::extractors::user::User;
910
use crate::routes::magnet::MagnetForm;
@@ -24,6 +25,9 @@ pub struct Model {
2425
pub link: MagnetLink,
2526
pub name: String,
2627
pub resolved: bool,
28+
pub content_folder_id: i32,
29+
#[sea_orm(belongs_to, from = "content_folder_id", to = "id")]
30+
pub content_folder: HasOne<content_folder::Entity>,
2731
}
2832

2933
#[async_trait::async_trait]
@@ -36,6 +40,10 @@ pub enum MagnetError {
3640
InvalidMagnet { source: MagnetLinkError },
3741
#[snafu(display("Database error"))]
3842
DB { source: sea_orm::DbErr },
43+
#[snafu(display("Error with the requested content folder"))]
44+
ContentFolder {
45+
source: content_folder::ContentFolderError,
46+
},
3947
#[snafu(display("The magnet (ID: {id}) does not exist"))]
4048
NotFound { id: i32 },
4149
#[snafu(display("The magnet (TorrentID: {id}) does not exist"))]
@@ -132,8 +140,22 @@ impl MagnetOperator {
132140
/// Fails if:
133141
///
134142
/// - the magnet is invalid
143+
/// - the requested content folder does not exist
135144
pub async fn create(&self, f: &MagnetForm) -> Result<Model, MagnetError> {
136-
let magnet = MagnetLink::new(&f.magnet).context(InvalidMagnetSnafu)?;
145+
let MagnetForm {
146+
magnet,
147+
content_folder_id,
148+
} = f;
149+
150+
let magnet = MagnetLink::new(magnet).context(InvalidMagnetSnafu)?;
151+
152+
let content_folder = {
153+
let operator = content_folder::ContentFolderOperator::new(self.state.clone(), None);
154+
operator
155+
.find_by_id_str(content_folder_id)
156+
.await
157+
.context(ContentFolderSnafu)?
158+
};
137159

138160
// Check duplicates
139161
let list = self.list().await?;
@@ -149,6 +171,7 @@ impl MagnetOperator {
149171
name: Set(magnet.name().to_string()),
150172
// TODO: check if we already have the torrent in which case it's already resolved!
151173
resolved: Set(false),
174+
content_folder_id: Set(content_folder.id),
152175
..Default::default()
153176
}
154177
.save(&self.state.database)

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ pub fn router(state: state::AppState) -> Router {
2929
"/folders/{category_name}/{*folder_path}",
3030
get(routes::content_folder::show),
3131
)
32+
.route(
33+
"/folders/{category_name}/{*folder_path}",
34+
post(routes::content_folder::post_magnet),
35+
)
3236
.route("/folders", post(routes::content_folder::create))
3337
.route("/logs", get(routes::logs::index))
3438
.route("/magnet/upload", post(routes::magnet::upload))

src/migration/m20251114_01_create_table_magnet.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ impl MigrationTrait for Migration {
1818
.col(string(Magnet::Name))
1919
.col(string(Magnet::Link))
2020
.col(boolean(Magnet::Resolved))
21-
.col(ColumnDef::new(ContentFolder::Id).integer())
21+
.col(ColumnDef::new(Magnet::ContentFolderId).integer())
2222
.foreign_key(
2323
ForeignKey::create()
24-
.name("fk-magnet-content-folder_id")
25-
.from(Magnet::ContentFolder, Magnet::ContentFolder)
24+
.name("fk-magnet-content_folder_id")
25+
.from(Magnet::Table, Magnet::ContentFolderId)
2626
.to(ContentFolder::Table, ContentFolder::Id),
2727
)
2828
.to_owned(),
@@ -45,5 +45,5 @@ enum Magnet {
4545
Name,
4646
Link,
4747
Resolved,
48-
ContentFolder,
48+
ContentFolderId,
4949
}

src/routes/content_folder.rs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use snafu::prelude::*;
1010
use crate::database::content_folder::PathBreadcrumb;
1111
use crate::database::{category, content_folder};
1212
use crate::extractors::folder_request::FolderRequest;
13+
use crate::routes::magnet::MagnetForm;
1314
use crate::state::flash_message::{OperationStatus, get_cookie};
1415
use crate::state::{AppStateContext, error::*};
1516

@@ -38,6 +39,33 @@ pub struct ContentFolderShowTemplate {
3839
pub parent_folder: Option<content_folder::Model>,
3940
/// Operation status for UI confirmation (Cookie)
4041
pub flash: Option<OperationStatus>,
42+
// TODO: WIP
43+
pub error: Option<AppStateError>,
44+
}
45+
46+
impl ContentFolderShowTemplate {
47+
fn new(context: AppStateContext, folder: FolderRequest) -> Self {
48+
Self {
49+
parent_folder: folder.parent,
50+
breadcrumb_items: folder.ancestors,
51+
sub_content_folders: folder.sub_folders,
52+
current_content_folder: folder.folder,
53+
category: folder.category,
54+
state: context,
55+
flash: None,
56+
error: None,
57+
}
58+
}
59+
60+
fn with_flash(mut self, flash: Option<OperationStatus>) -> Self {
61+
self.flash = flash;
62+
self
63+
}
64+
65+
fn with_errored_form(mut self, _form: MagnetForm, error: AppStateError) -> Self {
66+
self.error = Some(error);
67+
self
68+
}
4169
}
4270

4371
pub async fn show(
@@ -49,18 +77,30 @@ pub async fn show(
4977

5078
Ok((
5179
jar,
52-
ContentFolderShowTemplate {
53-
parent_folder: folder.parent,
54-
breadcrumb_items: folder.ancestors,
55-
sub_content_folders: folder.sub_folders,
56-
current_content_folder: folder.folder,
57-
category: folder.category,
58-
state: context,
59-
flash: operation_status,
60-
},
80+
ContentFolderShowTemplate::new(context, folder).with_flash(operation_status),
6181
))
6282
}
6383

84+
pub async fn post_magnet(
85+
context: AppStateContext,
86+
folder: FolderRequest,
87+
Form(form): Form<MagnetForm>,
88+
) -> Result<Redirect, ContentFolderShowTemplate> {
89+
// TODO: proper error type
90+
if let Err(e) = context
91+
.db
92+
.magnet()
93+
.create(&form)
94+
.await
95+
.context(MagnetUploadSnafu)
96+
{
97+
return Err(ContentFolderShowTemplate::new(context, folder).with_errored_form(form, e));
98+
}
99+
100+
// TODO: what to do when upload is successful?
101+
Ok(Redirect::to("/magnet"))
102+
}
103+
64104
pub async fn create(
65105
context: AppStateContext,
66106
jar: CookieJar,

src/routes/magnet.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ use axum::response::{IntoResponse, Redirect, Response};
55
use serde::{Deserialize, Serialize};
66
use snafu::prelude::*;
77

8-
use crate::database::{category, magnet};
8+
use sea_orm::LoaderTrait;
9+
10+
use crate::database::{category, content_folder, magnet};
911
use crate::state::{AppStateContext, error::*};
1012

1113
/// Multipart form submitted to /magnet/upload:
1214
///
1315
/// - magnet: the magnet link to upload
1416
#[derive(Clone, Debug, Serialize, Deserialize)]
1517
pub struct MagnetForm {
18+
pub content_folder_id: String,
1619
pub magnet: String,
1720
}
1821

@@ -43,7 +46,7 @@ pub struct MagnetListTemplate {
4346
/// Global application state (errors/warnings)
4447
pub state: AppStateContext,
4548
/// Magnets stored in database
46-
pub magnets: Vec<magnet::Model>,
49+
pub magnets: Vec<(magnet::Model, content_folder::Model)>,
4750
}
4851

4952
pub async fn list(context: AppStateContext) -> Result<impl IntoResponse, AppStateError> {
@@ -55,6 +58,20 @@ pub async fn list(context: AppStateContext) -> Result<impl IntoResponse, AppStat
5558
.boxed()
5659
.context(OtherSnafu)?;
5760

61+
// In the creation form we guarantee to set the content_folder so we can unwrap
62+
let content_folders: Vec<content_folder::Model> = magnets
63+
.load_one(content_folder::Entity, &context.state.database)
64+
.await
65+
.context(SqliteSnafu)?
66+
.into_iter()
67+
.map(|x| x.unwrap())
68+
.collect();
69+
70+
let magnets = magnets
71+
.into_iter()
72+
.zip(content_folders.into_iter())
73+
.collect();
74+
5875
Ok(MagnetListTemplate {
5976
state: context,
6077
magnets,

templates/content_folders/dropdown_actions.html

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,46 @@
44
</button>
55
<ul class="dropdown-menu">
66
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#createSubFolder">Create a subfolder</a></li>
7-
<li><a class="dropdown-item" href="/magnet/upload">Import magnet link</a></li>
8-
<li><a class="dropdown-item" href="#">Import torrent file</a></li>
7+
8+
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#importMagnetLink" href="#">Import magnet link</a></li>
99
</ul>
1010
</div>
11+
12+
<div class="modal fade" id="importMagnetLink" tabindex="-1" aria-labelledby="importMagnetLink" aria-hidden="false">
13+
<div class="modal-dialog">
14+
<div class="modal-content">
15+
<div class="modal-header">
16+
<h1 class="modal-title fs-5" id="staticBackdropLabel">Import Magnet Link</h1>
17+
</div>
18+
<div class="modal-body">
19+
<form method="POST" accept-charset="utf-8">
20+
<div class="alert alert-info text-start">
21+
<p>
22+
You are about to import a magnet into the category <strong>{{ category.name }}</strong>, in the folder
23+
<code class="ms-2"><span><small>{{ category.name }}{% if current_content_folder is defined %}{{current_content_folder.path }}{% endif %}/</small></span></code>.
24+
</p>
25+
26+
<p>
27+
You will be able to reorganize the torrent content later, once the magnet has been resolved.
28+
</p>
29+
</div>
30+
<div class="row align-items-center">
31+
<div class="col-md-12 text-center">
32+
<img src="/assets/images/magnet.png" class="img-64">
33+
</div>
34+
<div class="col-md-12 mt-3">
35+
{% if current_content_folder is defined %}
36+
<input type="hidden" name="content_folder_id" value="{{ current_content_folder.id }}">
37+
{% endif %}
38+
<input type="text" name="magnet" class="form-control" placeholder="Magnet link to download" id="magnet">
39+
40+
<div class="text-center mt-4">
41+
<input type="Submit" class="btn btn-success btn-lg" value="Add to resolving list">
42+
</div>
43+
</div>
44+
</div>
45+
</form>
46+
</div>
47+
</div>
48+
</div>
49+
</div>

templates/content_folders/show.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@
4646

4747
{% block alert_message %}
4848
{% include "shared/alert_operation_status.html" %}
49+
{% if let Some(error) = error %}
50+
<div class="alert alert-danger mt-4">
51+
<p class="mb-0">{{ error }}</p>
52+
{% for inner_error in error.inner_errors() %}
53+
<p>→ {{ inner_error }}</p>
54+
{% endfor %}
55+
</div>
56+
{% endif %}
4957
{% endblock %}
5058

5159

templates/magnet/list.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ <h1>List of uploaded magnets not yet resolved</h1>
1111
<thead>
1212
<tr>
1313
<th>Name</th>
14+
<th>Content folder</th>
1415
<th>Actions</th>
1516
</tr>
1617
</thead>
17-
{% for magnet in magnets %}
18+
{% for (magnet, content_folder) in magnets %}
1819
<tr>
1920
<td class="is-left" align="left"><a href="/magnet/{{ magnet.id }}">{{ magnet.name }}</a></td>
21+
<td><a href="/folder/{{ content_folder.category_id }}/{{ content_folder.id }}">{{ content_folder.path }}</a></td>
2022
<td>
2123
{% if magnet.resolved %}
2224
<!-- TODO: mettre le lien vers la page des d'import d'un torrent -->

0 commit comments

Comments
 (0)