Skip to content
Merged
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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ migration = { path = "migration" }
sea-orm = { version = "1.1", features = [ "runtime-tokio", "debug-print", "sqlx-sqlite"] }
# Serialization/deserialization, for example in path extractors
serde = { version = "1.0.219", features = ["derive", "rc"] }
# (De)serialization for operations log
serde_json = { version = "1" }
# Error declaration/context
snafu = "0.8.8"
# Serve static assets directly from the binary
Expand All @@ -66,3 +68,6 @@ toml = { version = "0.9.5", features = ["preserve_order"] }
uucore = { version = "0.1.0", features = ["fsext"] }
# Finding XDG standard directories (such as ~/.config/torrentmanager/config.toml)
xdg = "3.0.0"

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

#[serde(default = "AppConfig::default_sqlite_path")]
pub sqlite_path: Utf8PathBuf,

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

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -119,6 +122,11 @@ impl AppConfig {
Self::config_dir().join("database.sqlite")
}

pub fn default_log_path() -> Utf8PathBuf {
// At this point the directory has already been successfully created
Self::config_dir().join("operations.log")
}

pub async fn load_from_xdg() -> Result<Self, ConfigError> {
let config_dir = Self::config_dir();
create_dir_all(&config_dir)
Expand Down
34 changes: 32 additions & 2 deletions src/database/category.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use camino::Utf8PathBuf;
use chrono::Utc;
use sea_orm::entity::prelude::*;
use sea_orm::*;
use snafu::prelude::*;

use crate::database::operation::*;
use crate::extractors::user::User;
use crate::routes::category::CategoryForm;
use crate::state::AppState;
use crate::state::logger::LoggerError;

/// A category to store associated files.
///
Expand Down Expand Up @@ -43,6 +46,8 @@ pub enum CategoryError {
DB { source: sea_orm::DbErr },
#[snafu(display("The category (ID: {id}) does not exist"))]
NotFound { id: i32 },
#[snafu(display("Failed to save the operation log"))]
Logger { source: LoggerError },
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -88,7 +93,11 @@ impl CategoryOperator {
///
/// - name or path is already taken (they should be unique)
/// - path parent directory does not exist (to avoid completely wrong paths)
pub async fn create(&self, f: &CategoryForm) -> Result<Model, CategoryError> {
pub async fn create(
&self,
f: &CategoryForm,
user: Option<User>,
) -> Result<Model, CategoryError> {
let dir = Utf8PathBuf::from(&f.path);
let parent = dir.parent().unwrap();

Expand Down Expand Up @@ -121,6 +130,27 @@ impl CategoryOperator {
.await
.context(DBSnafu)?;

Ok(model.try_into_model().unwrap())
// Should not fail
let model = model.try_into_model().unwrap();

let operation_log = OperationLog {
user,
date: Utc::now(),
table: Table::Category,
operation: OperationType::Create,
operation_id: OperationId {
object_id: model.id.to_owned(),
name: f.name.to_string(),
},
operation_form: Operation::Category(f.clone()),
};

self.state
.logger
.write(operation_log)
.await
.context(LoggerSnafu)?;

Ok(model)
}
}
1 change: 1 addition & 0 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// sea_orm example: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
pub mod category;
pub mod operation;
31 changes: 22 additions & 9 deletions src/database/operation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use chrono::{DateTime, Utc};
use derive_more::Display;
use serde::{Deserialize, Serialize};

use crate::extractors::user::User;
use crate::routes::category::CategoryForm;

/// Type of operation applied to the database.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
pub enum OperationType {
Create,
Update,
Expand All @@ -9,11 +15,11 @@ pub enum OperationType {

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OperationId {
pub object_id: i64,
pub object_id: i32,
pub name: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
pub enum Table {
Category,
}
Expand All @@ -22,17 +28,24 @@ pub enum Table {
///
/// Will be saved as an [OperationLog].
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Operation {
Category(CategoryForm),
}

impl std::fmt::Display for Operation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &serde_json::to_string(self).unwrap())
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OperationLog {
user: Option<User>,
date: DateTime<Utc>,
table: Table,
operation: OperationType,
operation_id: OperationId,
pub user: Option<User>,
pub date: DateTime<Utc>,
pub table: Table,
pub operation: OperationType,
pub operation_id: OperationId,
// Raw operation parameters
operation_form: CategoryForm,
pub operation_form: Operation,
}
6 changes: 4 additions & 2 deletions src/extractors/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ use axum::{
http::{StatusCode, request::Parts},
};
use derive_more::Display;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Display)]
/// A logged-in user, as expressed by the Remote-User header.
///
/// Cannot be produced outside of header extraction.
pub struct User(String);
#[derive(Clone, Debug, Display, Deserialize, Serialize)]
#[serde(transparent)]
pub struct User(pub String);

impl<S> OptionalFromRequestParts<S> for User
where
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn router(state: state::AppState) -> Router {
.route("/categories", get(routes::category::index))
.route("/categories", post(routes::category::create))
.route("/categories/new", get(routes::category::new))
.route("/categories/{id}/delete", get(routes::category::delete))
.route("/logs", get(routes::logs::index))
// Register static assets routes
.nest("/assets", static_router())
// Insert request timing
Expand Down
7 changes: 3 additions & 4 deletions src/routes/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ use askama_web::WebTemplate;
use axum::Form;
use axum::extract::{Path, State};
use axum::response::IntoResponse;
// use sea_orm::entity::*;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use snafu::prelude::*;

use crate::database::category::CategoryError;
use crate::database::{category, category::CategoryOperator};
use crate::extractors::user::User;
use crate::state::{AppState, AppStateContext, error::*};

#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CategoryForm {
pub name: String,
pub path: String,
Expand Down Expand Up @@ -105,7 +104,7 @@ pub async fn create(
let app_state_context = app_state.context().await?;
let categories = CategoryOperator::new(app_state.clone(), user.clone());

let created = categories.create(&form).await;
let created = categories.create(&form, user.clone()).await;

match created {
Ok(created) => Ok(CategoriesTemplate {
Expand Down
30 changes: 30 additions & 0 deletions src/routes/logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use askama::Template;
use askama_web::WebTemplate;
use axum::extract::State;
use snafu::prelude::*;

use crate::database::operation::OperationLog;
use crate::extractors::user::User;
use crate::state::{AppState, AppStateContext, error::*};

#[derive(Template, WebTemplate)]
#[template(path = "logs.html")]
pub struct LogTemplate {
pub state: AppStateContext,
pub logs: Vec<OperationLog>,
pub user: Option<User>,
}

pub async fn index(
State(app_state): State<AppState>,
user: Option<User>,
) -> Result<LogTemplate, AppStateError> {
let app_state_context = app_state.context().await?;
let logs = app_state.logger.read().await.context(LoggerSnafu)?;

Ok(LogTemplate {
state: app_state_context,
logs,
user,
})
}
1 change: 1 addition & 0 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod category;
pub mod index;
pub mod logs;
pub mod progress;
3 changes: 3 additions & 0 deletions src/state/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use snafu::prelude::*;

use crate::database::category::CategoryError;
use crate::state::free_space::FreeSpaceError;
use crate::state::logger::LoggerError;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
Expand All @@ -19,6 +20,8 @@ pub enum AppStateError {
FreeSpace { source: FreeSpaceError },
#[snafu(display("SQLite error"))]
Sqlite { source: sea_orm::error::DbErr },
#[snafu(display("Logger error"))]
Logger { source: LoggerError },
#[snafu(display("An other error occurred"))]
Other {
source: Box<dyn snafu::Error + Send + Sync + 'static>,
Expand Down
Loading