Skip to content

Commit 26ffd5a

Browse files
committed
feat: Allow moving (resolved) torrents between folders
1 parent 02a396d commit 26ffd5a

9 files changed

Lines changed: 160 additions & 53 deletions

File tree

Cargo.lock

Lines changed: 20 additions & 44 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
@@ -74,6 +74,7 @@ toml = { version = "1", features = ["preserve_order"] }
7474
uucore = { version = "0.8.0", features = ["fsext"] }
7575
# Finding XDG standard directories (such as ~/.config/torrentmanager/config.toml)
7676
xdg = "3.0.0"
77+
serde_with = "3.20.0"
7778

7879
[dev-dependencies]
7980
async-tempfile = "0.7"

src/database/content_folder/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ pub enum ContentFolderError {
2222
Logger { source: LoggerError },
2323
#[snafu(display("Failed to create the folder on disk"))]
2424
IO { source: std::io::Error },
25+
#[snafu(display("Failed to load the torrent {id} requested to be moved"))]
26+
MovingTorrent {
27+
id: i32,
28+
source: crate::database::torrent::TorrentError,
29+
},
2530
}
2631

2732
impl From<ContentFolderError> for AppStateError {

src/database/content_folder/folder_view.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use snafu::prelude::*;
2+
13
use crate::database::torrent;
24

35
use super::*;
@@ -14,6 +16,7 @@ pub struct FolderView {
1416
pub children: Vec<Model>,
1517
pub folder: Option<Model>,
1618
pub torrents: Vec<torrent::Model>,
19+
pub moving_torrent: Option<torrent::Model>,
1720
}
1821

1922
impl FolderView {
@@ -31,6 +34,7 @@ impl FolderView {
3134
folder: None,
3235
children,
3336
torrents: vec![],
37+
moving_torrent: None,
3438
})
3539
}
3640

@@ -43,6 +47,7 @@ impl FolderView {
4347
pub async fn from_id(
4448
operator: &ContentFolderOperator<'_>,
4549
id: i32,
50+
moving_id: Option<i32>,
4651
) -> Result<Self, ContentFolderError> {
4752
let list = operator.list().await?;
4853

@@ -54,6 +59,19 @@ impl FolderView {
5459
.await
5560
.unwrap();
5661

62+
let moving_torrent = if let Some(moving_id) = moving_id {
63+
Some(
64+
operator
65+
.db()
66+
.torrent()
67+
.get(moving_id)
68+
.await
69+
.context(MovingTorrentSnafu { id: moving_id })?,
70+
)
71+
} else {
72+
None
73+
};
74+
5775
Ok(Self {
5876
ancestors: folder
5977
.ancestors_from_list(&list)
@@ -67,6 +85,7 @@ impl FolderView {
6785
.collect(),
6886
folder: Some(folder.clone()),
6987
torrents,
88+
moving_torrent,
7089
})
7190
} else {
7291
Err(ContentFolderError::NotFound { id })

src/database/torrent/operator.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,33 @@ impl TorrentOperator<'_> {
201201
Ok(model)
202202
}
203203

204+
/// Moves a torrent to a given content folder
205+
///
206+
/// TODO: This should error if there are conflicting files over there,
207+
/// by checking torrent content.
208+
pub async fn move_folder(
209+
&self,
210+
torrent: Model,
211+
folder: &content_folder::Model,
212+
) -> Result<Model, TorrentError> {
213+
let mut active_model: ActiveModel = torrent.into();
214+
active_model.content_folder_id = Set(folder.id);
215+
let torrent = active_model
216+
.update(&self.state.database)
217+
.await
218+
.context(DBSnafu)?;
219+
220+
// TODO: log the move
221+
// self.log_update(TorrentOperation::MoveFolder {
222+
// id: torrent.id,
223+
// name: torrent.name.to_string(),
224+
// previous_folder:
225+
// new_folder:
226+
// }).await.context(LoggerSnafu)?;
227+
228+
Ok(torrent)
229+
}
230+
204231
/// Internal method used by `import_torrent`.
205232
///
206233
/// Resolves a magnet to a torrent, or if it's already resolved, errors because of duplicate.

src/routes/content_folder.rs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ use askama::Template;
22
use askama_web::WebTemplate;
33
use axum::extract::Form;
44
use axum::extract::Path;
5+
use axum::extract::Query;
6+
use axum::response::{IntoResponse, Response};
57
use axum_extra::extract::CookieJar;
68
use serde::{Deserialize, Serialize};
9+
use serde_with::serde_as;
710

811
use crate::database::{content_folder, torrent};
912
use crate::state::AppStateContext;
@@ -17,6 +20,19 @@ pub struct ContentFolderForm {
1720
pub name: String,
1821
}
1922

23+
/// A request to move a torrent around in the categories/folders.
24+
///
25+
/// When validate is set, the requested folder is set to the database.
26+
#[derive(Clone, Debug, Deserialize)]
27+
#[serde_as]
28+
pub struct MoveTorrentQuery {
29+
#[serde(default)]
30+
#[serde_as(as = "DeserializeFromStr")]
31+
pub moving_id: Option<i32>,
32+
#[serde(default)]
33+
pub moving_validate: bool,
34+
}
35+
2036
#[derive(Template, WebTemplate)]
2137
#[template(path = "content_folders/show.html")]
2238
pub struct ContentFolderShowTemplate {
@@ -33,6 +49,8 @@ pub struct ContentFolderShowTemplate {
3349
pub ancestors: Vec<content_folder::Model>,
3450
/// Operation status for UI confirmation (Cookie)
3551
pub flash: Option<OperationStatus>,
52+
/// Torrent being moved (if any)
53+
pub moving_torrent: Option<torrent::Model>,
3654
}
3755

3856
impl ContentFolderShowTemplate {
@@ -42,6 +60,7 @@ impl ContentFolderShowTemplate {
4260
children,
4361
folder,
4462
torrents,
63+
moving_torrent,
4564
} = folder;
4665

4766
Self {
@@ -51,6 +70,7 @@ impl ContentFolderShowTemplate {
5170
torrents,
5271
ancestors,
5372
state: context,
73+
moving_torrent,
5474
}
5575
}
5676
}
@@ -65,10 +85,40 @@ pub async fn show(
6585
context: AppStateContext,
6686
Path(id): Path<i32>,
6787
status: StatusCookie,
68-
) -> Result<FlashTemplate<ContentFolderShowTemplate>, AppStateError> {
88+
Query(moving): Query<MoveTorrentQuery>,
89+
) -> Result<Response, AppStateError> {
6990
// 404 if requested ID does not exist
70-
let view = content_folder::FolderView::from_id(&context.db.content_folder(), id).await?;
71-
Ok(status.with_template(ContentFolderShowTemplate::new(context, view)))
91+
let view =
92+
content_folder::FolderView::from_id(&context.db.content_folder(), id, moving.moving_id)
93+
.await?;
94+
95+
if view.moving_torrent.is_some() && moving.moving_validate {
96+
// Request to effectively move the torrent to this folder
97+
// Once done, redirect to the same page
98+
if let Err(e) = context
99+
.db
100+
.torrent()
101+
.move_folder(
102+
view.moving_torrent.clone().unwrap(),
103+
view.folder.as_ref().unwrap(),
104+
)
105+
.await
106+
{
107+
return Ok(OperationStatus::error(e)
108+
.with_template(ContentFolderShowTemplate::new(context, view))
109+
.into_response());
110+
}
111+
112+
// Success! Perform a redirection
113+
return Ok(status
114+
.with_success("Torrent successfully moved".to_string())
115+
.redirect(&format!("/folders/{}", view.folder.as_ref().unwrap().id))
116+
.into_response());
117+
}
118+
119+
Ok(status
120+
.with_template(ContentFolderShowTemplate::new(context, view))
121+
.into_response())
72122
}
73123

74124
pub async fn index(
@@ -112,14 +162,13 @@ pub async fn create_folder(
112162
}
113163
}
114164

115-
// TODO: create top-level
116165
pub async fn create_subfolder(
117166
context: AppStateContext,
118167
jar: CookieJar,
119168
Path(id): Path<i32>,
120169
Form(form): Form<ContentFolderForm>,
121170
) -> Result<Result<FlashRedirect, ContentFolderShowTemplate>, AppStateError> {
122-
let view = content_folder::FolderView::from_id(&context.db.content_folder(), id).await?;
171+
let view = content_folder::FolderView::from_id(&context.db.content_folder(), id, None).await?;
123172
match context
124173
.db
125174
.content_folder()

src/routes/torrent.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub async fn upload_torrent(
3737
jar: CookieJar,
3838
TypedMultipart(form): TypedMultipart<TorrentForm>,
3939
) -> Result<Response, AppStateError> {
40-
let view = FolderView::from_id(&context.db.content_folder(), id).await?;
40+
let view = FolderView::from_id(&context.db.content_folder(), id, None).await?;
4141

4242
if let Err(e) = context
4343
.db
@@ -64,7 +64,7 @@ pub async fn upload_magnet(
6464
jar: CookieJar,
6565
Form(form): Form<MagnetForm>,
6666
) -> Result<Response, AppStateError> {
67-
let view = FolderView::from_id(&context.db.content_folder(), id).await?;
67+
let view = FolderView::from_id(&context.db.content_folder(), id, None).await?;
6868

6969
if let Err(e) = context
7070
.db

0 commit comments

Comments
 (0)