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
File renamed without changes.
44 changes: 27 additions & 17 deletions src/database/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use snafu::prelude::*;
use crate::extractors::user::User;
use crate::routes::category::CategoryForm;
use crate::state::AppState;
use crate::state::error::{self as state_error, AppStateError};

/// A category to store associated files.
///
Expand Down Expand Up @@ -42,6 +41,8 @@ pub enum CategoryError {
IO { source: std::io::Error },
#[snafu(display("Database error"))]
DB { source: sea_orm::DbErr },
#[snafu(display("The category (ID: {id}) does not exist"))]
NotFound { id: i32 },
}

#[derive(Clone, Debug)]
Expand All @@ -58,11 +59,27 @@ impl CategoryOperator {
/// List categories
///
/// Should not fail, unless SQLite was corrupted for some reason.
pub async fn list(&self) -> Result<Vec<Model>, AppStateError> {
pub async fn list(&self) -> Result<Vec<Model>, CategoryError> {
Entity::find()
.all(&self.state.database)
.await
.context(state_error::SqliteSnafu)
.context(DBSnafu)
}

/// Delete a category
pub async fn delete(&self, id: i32) -> Result<String, CategoryError> {
let db = &self.state.database;
let category: Option<Model> = Entity::find_by_id(id).one(db).await.context(DBSnafu)?;

match category {
Some(category) => {
let category_name: String = category.clone().name;
category.delete(db).await.context(DBSnafu)?;

Ok(category_name)
}
None => Err(CategoryError::NotFound { id }),
}
}

/// Create a new category, creating the corresponding directory.
Expand All @@ -71,34 +88,28 @@ 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, AppStateError> {
pub async fn create(&self, f: &CategoryForm) -> Result<Model, CategoryError> {
let dir = Utf8PathBuf::from(&f.path);
let parent = dir.parent().unwrap();

if !tokio::fs::try_exists(parent)
.await
.context(IOSnafu)
.context(state_error::CategorySnafu)?
{
if !tokio::fs::try_exists(parent).await.context(IOSnafu)? {
return Err(CategoryError::ParentDir {
path: parent.to_string(),
})
.context(state_error::CategorySnafu);
});
}

// Check duplicates
let list = self.list().await?;

if list.iter().any(|x| x.name == f.name) {
return Err(CategoryError::NameTaken {
name: f.name.clone(),
})
.context(state_error::CategorySnafu);
});
}
if list.iter().any(|x| x.path == f.path) {
return Err(CategoryError::PathTaken {
path: f.path.clone(),
})
.context(state_error::CategorySnafu);
});
}

let model = ActiveModel {
Expand All @@ -108,8 +119,7 @@ impl CategoryOperator {
}
.save(&self.state.database)
.await
.context(DBSnafu)
.context(state_error::CategorySnafu)?;
.context(DBSnafu)?;

Ok(model.try_into_model().unwrap())
}
Expand Down
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ pub fn router(state: state::AppState) -> Router {
// Register dynamic routes
.route("/", get(routes::index::index))
.route("/progress/{view_request}", get(routes::progress::progress))
.route("/category", get(routes::category::index))
.route("/category", post(routes::category::create))
.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))
// Register static assets routes
.nest("/assets", static_router())
// Insert request timing
Expand Down
119 changes: 102 additions & 17 deletions src/routes/category.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use askama::Template;
use askama_web::WebTemplate;
use axum::Form;
use axum::extract::State;
use axum::extract::{Path, State};
use axum::response::IntoResponse;
// use sea_orm::entity::*;
use serde::Deserialize;
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::*};
Expand All @@ -15,48 +18,130 @@ pub struct CategoryForm {
pub path: String,
}

pub struct OperationStatus {
/// Status of operation
pub success: bool,
/// Message for confirmation alert
pub message: String,
}

#[derive(Template, WebTemplate)]
#[template(path = "category.html")]
pub struct CategoryTemplate {
#[template(path = "categories/index.html")]
pub struct CategoriesTemplate {
/// Global application state
pub state: AppStateContext,
/// Category that was just created, to confirm in the UI
pub created: Option<category::Model>,
/// Categories found in database
pub categories: Vec<category::Model>,
/// Logged-in user.
pub user: Option<User>,
/// Operation status for UI confirmation
pub operation_status: Option<OperationStatus>,
}

pub async fn create(
#[derive(Template, WebTemplate)]
#[template(path = "categories/new.html")]
pub struct NewCategoryTemplate {
/// Global application state
pub state: AppStateContext,
/// Logged-in user.
pub user: Option<User>,
/// Error
pub error: Option<CategoryError>,
/// Default form with value
pub category_form: Option<CategoryForm>,
}

pub async fn new(
State(app_state): State<AppState>,
user: Option<User>,
Form(form): Form<CategoryForm>,
// ) -> Result<CategoryTemplate, AppStateError> {
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let app_state_context = app_state.context().await?;
let categories = CategoryOperator::new(app_state.clone(), user.clone());

let created = categories.create(&form).await?;
Ok(CategoryTemplate {
categories: categories.list().await?,
created: Some(created),
Ok(NewCategoryTemplate {
state: app_state_context,
user,
category_form: None,
error: None,
})
}

pub async fn delete(
State(app_state): State<AppState>,
user: Option<User>,
Path(id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let app_state_context = app_state.context().await?;
let categories = CategoryOperator::new(app_state.clone(), user.clone());

let deleted = categories.delete(id).await;

match deleted {
Ok(name) => Ok(CategoriesTemplate {
categories: categories.list().await.context(CategorySnafu)?,
operation_status: Some(OperationStatus {
success: true,
message: format!("The category {} has been successfully deleted", name),
}),
state: app_state_context,
user,
}),
Err(error) => Ok(CategoriesTemplate {
categories: categories.list().await.context(CategorySnafu)?,
operation_status: Some(OperationStatus {
success: false,
message: format!("{}", error),
}),
state: app_state_context,
user,
}),
}
}

pub async fn create(
State(app_state): State<AppState>,
user: Option<User>,
Form(form): Form<CategoryForm>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let app_state_context = app_state.context().await?;
let categories = CategoryOperator::new(app_state.clone(), user.clone());

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

match created {
Ok(created) => Ok(CategoriesTemplate {
categories: categories.list().await.context(CategorySnafu)?,
state: app_state_context,
user,
operation_status: Some(OperationStatus {
success: true,
message: format!(
"The category {} has been successfully created (ID {})",
created.name, created.id
),
}),
}
.into_response()),
Err(error) => Ok(NewCategoryTemplate {
state: app_state_context,
user,
category_form: Some(form),
error: Some(error),
}
.into_response()),
}
}

pub async fn index(
State(app_state): State<AppState>,
user: Option<User>,
) -> Result<CategoryTemplate, AppStateError> {
) -> Result<CategoriesTemplate, AppStateError> {
let app_state_context = app_state.context().await?;
let categories = CategoryOperator::new(app_state.clone(), user.clone());

Ok(CategoryTemplate {
categories: categories.list().await?,
created: None,
Ok(CategoriesTemplate {
categories: categories.list().await.context(CategorySnafu)?,
state: app_state_context,
user,
operation_status: None,
})
}
26 changes: 13 additions & 13 deletions src/routes/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ use askama::Template;
use askama_web::WebTemplate;
use axum::extract::State;
use axum::response::{IntoResponse, Response};
use snafu::prelude::*;

// TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
use crate::database::category::CategoryOperator;
use crate::extractors::user::User;
use crate::routes::category::CategoryForm;
use crate::state::{AppState, AppStateContext, error::*};

use std::collections::HashMap;
Expand All @@ -24,6 +26,8 @@ pub struct IndexTemplate {
pub user: Option<User>,
/// Categories
pub categories: Vec<String>,
/// Category Form Data
pub category_form: Option<CategoryForm>,
}

pub async fn index(
Expand All @@ -34,22 +38,18 @@ pub async fn index(

let categories: Vec<String> = CategoryOperator::new(app_state.clone(), user.clone())
.list()
.await?
.await
.context(CategorySnafu)?
.into_iter()
.map(|x| x.name)
.collect();

if categories.is_empty() {
Ok(crate::routes::category::index(State(app_state), user)
.await?
.into_response())
} else {
Ok(IndexTemplate {
state: app_state_context,
post: HashMap::new(),
user,
categories,
}
.into_response())
Ok(IndexTemplate {
state: app_state_context,
post: HashMap::new(),
user,
categories,
category_form: None,
}
.into_response())
}
2 changes: 1 addition & 1 deletion templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@
{% endblock %}
</footer>

<script src="/assets/javascript/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="/assets/js/bootstrap.min.js"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions templates/categories/form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<form method="POST" action="/categories" accept-charset="utf-8">
<div class="form-group">
<label for="name" class="form-label">Name</label>
<input
type="text"
class="form-control"
placeholder="Ex: Movies"
name="name"
id="name"
{% if let Some(category_form) = category_form %} value="{{ category_form.name }}" {% endif %}
/>
</div>

<div class="form-group mt-3">
<label for="path" class="form-label">Path</label>
<input
type="text"
class="form-control"
placeholder="Ex: /mnt/MyBigHdd/DATA"
name="path"
id="path"
{% if let Some(category_form) = category_form %} value="{{ category_form.path}}" {% endif %}
/>
</div>

<div class="text-center mt-4">
<input type="Submit" class="btn btn-success" value="Create category">
</div>
</form>

Loading