Skip to content

Commit 4df268b

Browse files
committed
feat: Allow moving torrents between folders
1 parent 1a1845c commit 4df268b

10 files changed

Lines changed: 228 additions & 59 deletions

File tree

Cargo.lock

Lines changed: 16 additions & 40 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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ sea-orm-migration = { version = "2.0.0-rc.38" }
4848
serde = { version = "1.0.228", features = ["derive", "rc"] }
4949
# (De)serialization for operations log
5050
serde_json = { version = "1" }
51+
serde_with = "3.20.0"
5152
# Error declaration/context
5253
snafu = "0.9"
5354
# Serve static assets directly from the binary

src/database/operation.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ pub enum Operation {
3737
Category(CategoryForm),
3838
ContentFolder(ContentFolderForm),
3939
Torrent(TorrentForm),
40+
MoveTorrent {
41+
torrent: i32,
42+
category: i32,
43+
content_folder: Option<i32>,
44+
},
4045
}
4146

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

src/database/torrent.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,40 @@ impl TorrentOperator {
263263

264264
Ok(model)
265265
}
266+
267+
pub async fn update_category_content_folder(
268+
&self,
269+
model: Model,
270+
category: category::Model,
271+
content_folder: Option<content_folder::Model>,
272+
) -> Result<Model, TorrentError> {
273+
let mut active: ActiveModel = model.clone().into();
274+
active.category_id = Set(category.id);
275+
active.content_folder_id = Set(content_folder.as_ref().map(|x| x.id));
276+
active.save(&self.state.database).await.context(DBSnafu)?;
277+
278+
let operation_log = OperationLog {
279+
user: self.user.clone(),
280+
date: Utc::now(),
281+
table: Table::Torrent,
282+
operation: OperationType::Update,
283+
operation_id: OperationId {
284+
object_id: model.id.to_owned(),
285+
name: model.name.to_string(),
286+
},
287+
operation_form: Some(Operation::MoveTorrent {
288+
torrent: model.id,
289+
category: category.id,
290+
content_folder: content_folder.map(|x| x.id),
291+
}),
292+
};
293+
294+
self.state
295+
.logger
296+
.write(operation_log)
297+
.await
298+
.context(LoggerSnafu)?;
299+
300+
Ok(model)
301+
}
266302
}

src/extractors/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod category_request;
22
pub mod folder_request;
3+
pub mod moving;
34
pub mod normalized_path;
45
pub mod torrent_list;
56
pub mod user;

src/extractors/moving.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use serde::Deserialize;
2+
use serde_with::serde_as;
3+
4+
/// A request to move a torrent around in the categories/folders.
5+
///
6+
/// When validate is set, the requested folder is set to the database.
7+
#[derive(Clone, Debug, Deserialize)]
8+
#[serde_as]
9+
pub struct MovingQuery {
10+
#[serde(default)]
11+
#[serde_as(as = "DeserializeFromStr")]
12+
pub id: Option<i32>,
13+
#[serde(default)]
14+
pub validate: bool,
15+
}

src/routes/category.rs

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
use askama::Template;
22
use askama_web::WebTemplate;
33
use axum::Form;
4-
use axum::extract::Path;
4+
use axum::extract::{Path, Query};
5+
use axum::response::{IntoResponse, Response};
56
use axum_extra::extract::CookieJar;
67
use serde::{Deserialize, Serialize};
8+
use snafu::prelude::*;
79

810
use crate::database::category;
911
use crate::database::content_folder::PathBreadcrumb;
1012
use crate::database::torrent;
1113
use crate::extractors::category_request::{CategoriesRequest, CategoryRequest};
14+
use crate::extractors::moving::MovingQuery;
1215
use crate::filesystem::FileSystemEntry;
1316
use crate::routes::content_folder::ContentFolderForm;
1417
use crate::routes::index::IndexTemplate;
15-
use crate::state::AppStateContext;
16-
use crate::state::flash_message::{
17-
FallibleTemplate, FlashRedirect, FlashTemplate, OperationStatus, StatusCookie,
18-
};
18+
use crate::state::flash_message::{FallibleTemplate, FlashRedirect, OperationStatus, StatusCookie};
19+
use crate::state::{AppStateContext, error::*};
1920

2021
#[derive(Clone, Debug, Deserialize, Serialize)]
2122
pub struct CategoryForm {
@@ -95,10 +96,16 @@ pub struct CategoryShowTemplate {
9596
pub breadcrumbs: Vec<PathBreadcrumb>,
9697
/// Torrents in this category
9798
pub torrents: Vec<torrent::Model>,
99+
/// If any, the current torrent being moved in the folder.
100+
pub current_torrent: Option<torrent::Model>,
98101
}
99102

100103
impl CategoryShowTemplate {
101-
fn new(context: AppStateContext, category: CategoryRequest) -> Self {
104+
fn new(
105+
context: AppStateContext,
106+
category: CategoryRequest,
107+
current_torrent: Option<torrent::Model>,
108+
) -> Self {
102109
let CategoryRequest {
103110
breadcrumbs,
104111
category,
@@ -113,6 +120,7 @@ impl CategoryShowTemplate {
113120
flash: None,
114121
state: context,
115122
torrents,
123+
current_torrent,
116124
}
117125
}
118126
}
@@ -127,8 +135,41 @@ pub async fn show(
127135
context: AppStateContext,
128136
category: CategoryRequest,
129137
status: StatusCookie,
130-
) -> FlashTemplate<CategoryShowTemplate> {
131-
status.with_template(CategoryShowTemplate::new(context, category))
138+
Query(moving): Query<MovingQuery>,
139+
) -> Result<Response, AppStateError> {
140+
if let Some(id) = moving.id {
141+
// We are currently moving a torrent between folders
142+
let torrent: torrent::Model = context
143+
.db
144+
.torrent()
145+
.get(id)
146+
.await
147+
.context(TorrentUploadSnafu)?;
148+
if moving.validate {
149+
// Save to DB the new location of the torrent
150+
let _torrent = context
151+
.db
152+
.torrent()
153+
.update_category_content_folder(torrent.clone(), category.category.clone(), None)
154+
.await
155+
.context(TorrentUploadSnafu)?;
156+
// Now we produce a redirection (to the same page) in order to refresh the list of torrents
157+
// in this folder, which was already computed.
158+
Ok(status
159+
.with_success("Torrent successfully saved to this folder".to_string())
160+
// Need to remove the query params
161+
.redirect("?")
162+
.into_response())
163+
} else {
164+
Ok(status
165+
.with_template(CategoryShowTemplate::new(context, category, Some(torrent)))
166+
.into_response())
167+
}
168+
} else {
169+
Ok(status
170+
.with_template(CategoryShowTemplate::new(context, category, None))
171+
.into_response())
172+
}
132173
}
133174

134175
pub async fn create_folder(
@@ -152,7 +193,7 @@ pub async fn create_folder(
152193
}
153194
Err(error) => {
154195
let status = OperationStatus::error(error);
155-
Err(status.with_template(CategoryShowTemplate::new(context, category)))
196+
Err(status.with_template(CategoryShowTemplate::new(context, category, None)))
156197
}
157198
}
158199
}

0 commit comments

Comments
 (0)