Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,535 changes: 1,473 additions & 62 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ askama = "0.16"
askama_web = { version = "0.16", features = ["axum-0.8"] }
axum = { version = "0.8.9", features = ["macros"] }
axum-extra = { version = "0.12.6", features = ["cookie"] }
# For torrent uploads
axum_typed_multipart = { version = "0.16.5", default-features = false }
# UTF-8 paths for easier String/PathBuf interop
camino = { version = "1.2.2", features = ["serde1"] }
# Date/time management
Expand All @@ -36,9 +38,11 @@ env_logger = "0.11.10"
# Interactions with the torrent client
# Comment/uncomment below for development version
# hightorrent_api = { path = "../hightorrent_api" }
hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api" }
# hightorrent_api = "0.2"
# hightorrent_api = { git = "https://github.com/angrynode/hightorrent_api", branch = "feat-sea-orm", features = [ "sea_orm" ] }
hightorrent_api = { version = "0.2.2", features = [ "sea_orm" ] }
log = "0.4.29"
# rqbit torrent client to resolve magnets
librqbit = { git = "https://github.com/ikatson/rqbit" }
# SQLite ORM
sea-orm = { version = "2.0.0-rc.38", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] }
# SQLite migrations
Expand Down Expand Up @@ -70,6 +74,7 @@ toml = { version = "1", features = ["preserve_order"] }
uucore = { version = "0.8.0", features = ["fsext"] }
# Finding XDG standard directories (such as ~/.config/torrentmanager/config.toml)
xdg = "3.0.0"
serde_with = "3.20.0"

[dev-dependencies]
async-tempfile = "0.7"
Expand Down
11 changes: 11 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ pub struct AppConfig {

#[serde(default = "AppConfig::default_log_path")]
pub log_path: Utf8PathBuf,

/// Where rqbit saves its internal state.
#[serde(default = "AppConfig::default_rqbit_path")]
pub rqbit_path: Utf8PathBuf,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -127,6 +131,13 @@ impl AppConfig {
Self::config_dir().join("operations.log")
}

pub fn default_rqbit_path() -> Utf8PathBuf {
// TODO: it looks like rqbit actually saves stuff in
// ~/.cache/com.rqbit.dht/dht.json and nothing in this
// folder we provide.
Self::config_dir().join("rqbit")
}

pub async fn load_from_xdg() -> Result<Self, ConfigError> {
let config_dir = Self::config_dir();
create_dir_all(&config_dir)
Expand Down
5 changes: 5 additions & 0 deletions src/database/content_folder/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ pub enum ContentFolderError {
Logger { source: LoggerError },
#[snafu(display("Failed to create the folder on disk"))]
IO { source: std::io::Error },
#[snafu(display("Failed to load the torrent {id} requested to be moved"))]
MovingTorrent {
id: i32,
source: crate::database::torrent::TorrentError,
},
}

impl From<ContentFolderError> for AppStateError {
Expand Down
25 changes: 25 additions & 0 deletions src/database/content_folder/folder_view.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use snafu::prelude::*;

use crate::database::torrent;

use super::*;

/// A loaded folder, with all surrounding entities loaded as well:
Expand All @@ -11,6 +15,8 @@ pub struct FolderView {
pub ancestors: Vec<Model>,
pub children: Vec<Model>,
pub folder: Option<Model>,
pub torrents: Vec<torrent::Model>,
pub moving_torrent: Option<torrent::Model>,
}

impl FolderView {
Expand All @@ -27,6 +33,8 @@ impl FolderView {
ancestors: vec![],
folder: None,
children,
torrents: vec![],
moving_torrent: None,
})
}

Expand All @@ -39,10 +47,25 @@ impl FolderView {
pub async fn from_id(
operator: &ContentFolderOperator<'_>,
id: i32,
moving_id: Option<i32>,
) -> Result<Self, ContentFolderError> {
let list = operator.list().await?;

if let Some(folder) = list.iter().find(|x| x.id == id) {
let torrents = operator.torrent().list_for_folder(folder).await.unwrap();

let moving_torrent = if let Some(moving_id) = moving_id {
Some(
operator
.torrent()
.get(moving_id)
.await
.context(MovingTorrentSnafu { id: moving_id })?,
)
} else {
None
};

Ok(Self {
ancestors: folder
.ancestors_from_list(&list)
Expand All @@ -55,6 +78,8 @@ impl FolderView {
.cloned()
.collect(),
folder: Some(folder.clone()),
torrents,
moving_torrent,
})
} else {
Err(ContentFolderError::NotFound { id })
Expand Down
3 changes: 3 additions & 0 deletions src/database/content_folder/model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use camino::{Utf8Path, Utf8PathBuf};
use sea_orm::entity::prelude::*;

use crate::database::torrent;
use crate::extractors::normalized_path::NormalizedPathComponent;

/// A content folder to store associated files.
Expand All @@ -18,6 +19,8 @@ pub struct Model {
pub parent_id: Option<i32>,
#[sea_orm(self_ref, relation_enum = "Parent", from = "parent_id", to = "id")]
pub parent: HasOne<Entity>,
#[sea_orm(has_many)]
pub torrents: HasMany<torrent::Entity>,
}

#[async_trait::async_trait]
Expand Down
1 change: 1 addition & 0 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
pub mod content_folder;
pub mod operation;
pub mod operator;
pub mod torrent;
3 changes: 3 additions & 0 deletions src/database/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use derive_more::Display;
use serde::{Deserialize, Serialize};

use crate::database::content_folder;
use crate::database::torrent;
use crate::extractors::user::User;

/// Type of operation applied to the database.
Expand All @@ -16,6 +17,7 @@ pub enum OperationType {
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
pub enum Table {
ContentFolder,
Torrent,
}

/// Operation applied to the database.
Expand All @@ -25,6 +27,7 @@ pub enum Table {
#[serde(untagged)]
pub enum Operation {
ContentFolder(content_folder::ContentFolderOperation),
Torrent(torrent::TorrentOperation),
}

impl std::fmt::Display for Operation {
Expand Down
5 changes: 5 additions & 0 deletions src/database/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::ops::Deref;

use crate::database::content_folder::ContentFolderOperator;
use crate::database::operation::{Operation, OperationLog, OperationType, Table};
use crate::database::torrent::TorrentOperator;
use crate::extractors::user::User;
use crate::state::AppState;
use crate::state::logger::LoggerError;
Expand Down Expand Up @@ -64,4 +65,8 @@ impl DatabaseOperator {
pub fn content_folder<'a>(&'a self) -> ContentFolderOperator<'a> {
ContentFolderOperator { db: self }
}

pub fn torrent<'a>(&'a self) -> TorrentOperator<'a> {
TorrentOperator { db: self }
}
}
25 changes: 25 additions & 0 deletions src/database/torrent/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use hightorrent_api::hightorrent::{MagnetLinkError, TorrentFileError, TorrentID};
use snafu::prelude::*;

use crate::state::logger::LoggerError;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum TorrentError {
#[snafu(display("Submitted torrent file is invalid"))]
InvalidFile { source: TorrentFileError },
#[snafu(display("The magnet is invalid"))]
InvalidLink { source: MagnetLinkError },
#[snafu(display("Database error"))]
DB { source: sea_orm::DbErr },
#[snafu(display("The torrent (ID: {id}) does not exist"))]
NotFound { id: i32 },
#[snafu(display("The torrent (TorrentID: {id}) does not exist"))]
NotFoundTorrentID { id: TorrentID },
#[snafu(display("Failed to save the operation log"))]
Logger { source: LoggerError },
#[snafu(display("Requested content folder not found"))]
NoSuchContentFolder { id: i32 },
#[snafu(display("The torrent ID {torrent_id} is already imported"))]
Duplicate { torrent_id: TorrentID },
}
8 changes: 8 additions & 0 deletions src/database/torrent/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mod error;
pub use error::*;
mod model;
pub use model::*;
mod operation;
pub use operation::*;
mod operator;
pub use operator::*;
26 changes: 26 additions & 0 deletions src/database/torrent/model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use hightorrent_api::hightorrent::{MagnetLink, TorrentFile, TorrentID};
use sea_orm::entity::prelude::*;

use crate::database::content_folder;

/// A category to store associated files.
///
/// Each category has a name and an associated path on disk, where
/// symlinks to the content will be created.
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "torrent")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub torrent_id: TorrentID,
pub torrent_file: Option<TorrentFile>,
pub magnet_link: MagnetLink,
pub name: String,
pub content_folder_id: i32,
#[sea_orm(belongs_to, from = "content_folder_id", to = "id")]
pub content_folder: HasOne<content_folder::Entity>,
}

#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {}
33 changes: 33 additions & 0 deletions src/database/torrent/operation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};

use crate::database::operation::Operation;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum TorrentOperation {
ImportTorrent {
id: i32,
name: String,
folder: (i32, String),
},
ImportMagnet {
id: i32,
name: String,
folder: (i32, String),
},
ResolveMagnet {
id: i32,
name: String,
},
MoveTorrent {
id: i32,
name: String,
previous_folder: (i32, String),
new_folder: (i32, String),
},
}

impl From<TorrentOperation> for Operation {
fn from(o: TorrentOperation) -> Self {
Self::Torrent(o)
}
}
Loading
Loading