-
Notifications
You must be signed in to change notification settings - Fork 1
Add ContentFolders crud and category show #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ use crate::state::logger::LoggerError; | |
| /// | ||
| /// 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 = "category")] | ||
| pub struct Model { | ||
|
|
@@ -23,11 +24,10 @@ pub struct Model { | |
| pub name: String, | ||
| #[sea_orm(unique)] | ||
| pub path: String, | ||
| #[sea_orm(has_many)] | ||
| pub content_folders: HasMany<super::content_folder::Entity>, | ||
| } | ||
|
|
||
| #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||
| pub enum Relation {} | ||
|
|
||
| #[async_trait::async_trait] | ||
| impl ActiveModelBehavior for ActiveModel {} | ||
|
|
||
|
|
@@ -44,8 +44,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 }, | ||
| #[snafu(display("The category ({details}) does not exist"))] | ||
| NotFound { details: String }, | ||
| #[snafu(display("Failed to save the operation log"))] | ||
| Logger { source: LoggerError }, | ||
| } | ||
|
|
@@ -71,6 +71,37 @@ impl CategoryOperator { | |
| .context(DBSnafu) | ||
| } | ||
|
|
||
| /// Find one category by ID | ||
| pub async fn find_by_id(&self, id: i32) -> Result<Model, CategoryError> { | ||
| let category = Entity::find_by_id(id) | ||
| .one(&self.state.database) | ||
| .await | ||
| .context(DBSnafu)?; | ||
|
|
||
| match category { | ||
| Some(category) => Ok(category), | ||
| None => Err(CategoryError::NotFound { | ||
| details: format!("ID: {}", id), | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| /// Find one category by Name | ||
| pub async fn find_by_name(&self, name: String) -> Result<Model, CategoryError> { | ||
| let category = Entity::find() | ||
| .filter(Column::Name.contains(name.clone())) | ||
| .one(&self.state.database) | ||
| .await | ||
| .context(DBSnafu)?; | ||
|
|
||
| match category { | ||
| Some(category) => Ok(category), | ||
| None => Err(CategoryError::NotFound { | ||
| details: format!("NAME: {}", name), | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| /// Delete a category | ||
| pub async fn delete(&self, id: i32, user: Option<User>) -> Result<String, CategoryError> { | ||
| let db = &self.state.database; | ||
|
|
@@ -101,7 +132,9 @@ impl CategoryOperator { | |
|
|
||
| Ok(category_clone.name) | ||
| } | ||
| None => Err(CategoryError::NotFound { id }), | ||
| None => Err(CategoryError::NotFound { | ||
| details: format!("ID: {}", id), | ||
| }), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -139,9 +172,11 @@ impl CategoryOperator { | |
| }); | ||
| } | ||
|
|
||
| // Normalized path to avoid trailing slash | ||
| let normalized_path = dir.components().collect::<Utf8PathBuf>(); | ||
| let model = ActiveModel { | ||
| name: Set(f.name.clone()), | ||
| path: Set(f.path.clone()), | ||
| path: Set(normalized_path.into_string()), | ||
| ..Default::default() | ||
| } | ||
|
Comment on lines
+175
to
181
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is where I normalize the path when creating the category
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably use |
||
| .save(&self.state.database) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| use chrono::Utc; | ||
| use sea_orm::entity::prelude::*; | ||
| use sea_orm::*; | ||
| use snafu::prelude::*; | ||
|
|
||
| use crate::database::category; | ||
| use crate::database::operation::{Operation, OperationId, OperationLog, OperationType, Table}; | ||
| use crate::extractors::user::User; | ||
| use crate::routes::content_folder::ContentFolderForm; | ||
| use crate::state::AppState; | ||
| use crate::state::logger::LoggerError; | ||
|
|
||
| /// A content folder to store associated files. | ||
| /// | ||
| /// Each content folder has a name and an associated path on disk, a Category | ||
| /// and it can have an Parent Content Folder (None if it's the first folder | ||
| /// in category) | ||
| #[sea_orm::model] | ||
| #[derive(DeriveEntityModel, Clone, Debug, PartialEq, Eq)] | ||
| #[sea_orm(table_name = "content_folder")] | ||
| pub struct Model { | ||
| #[sea_orm(primary_key)] | ||
| pub id: i32, | ||
| pub name: String, | ||
| #[sea_orm(unique)] | ||
| pub path: String, | ||
| pub category_id: i32, | ||
| #[sea_orm(belongs_to, from = "category_id", to = "id")] | ||
| pub category: HasOne<category::Entity>, | ||
| pub parent_id: Option<i32>, | ||
| #[sea_orm(self_ref, relation_enum = "Parent", from = "parent_id", to = "id")] | ||
| pub parent: HasOne<Entity>, | ||
| } | ||
|
|
||
| #[async_trait::async_trait] | ||
| impl ActiveModelBehavior for ActiveModel {} | ||
|
|
||
| #[derive(Debug, Snafu)] | ||
| #[snafu(visibility(pub))] | ||
| pub enum ContentFolderError { | ||
| #[snafu(display("There is already a content folder called `{name}`"))] | ||
| NameTaken { name: String }, | ||
| #[snafu(display("There is already a content folder in dir `{path}`"))] | ||
| PathTaken { path: String }, | ||
| #[snafu(display("The Content Folder (Path: {path}) does not exist"))] | ||
| NotFound { path: String }, | ||
| #[snafu(display("Database error"))] | ||
| DB { source: sea_orm::DbErr }, | ||
| #[snafu(display("Failed to save the operation log"))] | ||
| Logger { source: LoggerError }, | ||
| } | ||
|
|
||
| #[derive(Clone, Debug)] | ||
| pub struct ContentFolderOperator { | ||
| pub state: AppState, | ||
| pub user: Option<User>, | ||
| } | ||
|
|
||
| impl ContentFolderOperator { | ||
| pub fn new(state: AppState, user: Option<User>) -> Self { | ||
| Self { state, user } | ||
| } | ||
|
|
||
| /// List Content Folders | ||
| /// | ||
| /// Should not fail, unless SQLite was corrupted for some reason. | ||
| pub async fn list_by_parent_and_category( | ||
| &self, | ||
| parent_id: Option<i32>, | ||
| category_id: i32, | ||
| ) -> Result<Vec<Model>, ContentFolderError> { | ||
| let mut query = Entity::find().filter(Column::CategoryId.eq(category_id)); | ||
| // parent_id can be None when it's the first folder in categories | ||
| match parent_id { | ||
| Some(parent_id) => query = query.filter(Column::ParentId.eq(parent_id)), | ||
| None => query = query.filter(Column::ParentId.is_null()), | ||
| } | ||
|
|
||
| query.all(&self.state.database).await.context(DBSnafu) | ||
| } | ||
|
|
||
| /// Find one Content Folder by path | ||
| /// | ||
| /// Should not fail, unless SQLite was corrupted for some reason. | ||
| pub async fn find_by_path(&self, path: String) -> Result<Model, ContentFolderError> { | ||
| let content_folder = Entity::find_by_path(path.clone()) | ||
| .one(&self.state.database) | ||
| .await | ||
| .context(DBSnafu)?; | ||
|
|
||
| match content_folder { | ||
| Some(category) => Ok(category), | ||
| None => Err(ContentFolderError::NotFound { path }), | ||
| } | ||
| } | ||
|
|
||
| /// Find one Content Folder by ID | ||
| /// | ||
| /// Should not fail, unless SQLite was corrupted for some reason. | ||
| pub async fn find_by_id(&self, id: i32) -> Result<Model, ContentFolderError> { | ||
| let content_folder = Entity::find_by_id(id) | ||
| .one(&self.state.database) | ||
| .await | ||
| .context(DBSnafu)?; | ||
|
|
||
| match content_folder { | ||
| Some(category) => Ok(category), | ||
| None => Err(ContentFolderError::NotFound { | ||
| path: id.to_string(), | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| /// Create a new content folder | ||
| /// | ||
| /// Fails if: | ||
| /// | ||
| /// - name or path is already taken (they should be unique in one folder) | ||
| /// - path parent directory does not exist (to avoid completely wrong paths) | ||
| pub async fn create( | ||
| &self, | ||
| f: &ContentFolderForm, | ||
| user: Option<User>, | ||
| ) -> Result<Model, ContentFolderError> { | ||
| // Check duplicates in same folder | ||
| let list = self | ||
| .list_by_parent_and_category(f.parent_id, f.category_id) | ||
Gabatxo1312 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .await?; | ||
|
|
||
| if list.iter().any(|x| x.name == f.name) { | ||
| return Err(ContentFolderError::NameTaken { | ||
| name: f.name.clone(), | ||
| }); | ||
| } | ||
|
|
||
| if list.iter().any(|x| x.path == f.path) { | ||
| return Err(ContentFolderError::PathTaken { | ||
| path: f.path.clone(), | ||
| }); | ||
| } | ||
|
|
||
| let model = ActiveModel { | ||
| name: Set(f.name.clone()), | ||
| path: Set(f.path.clone()), | ||
| category_id: Set(f.category_id), | ||
| parent_id: Set(f.parent_id), | ||
| ..Default::default() | ||
| } | ||
| .save(&self.state.database) | ||
| .await | ||
| .context(DBSnafu)?; | ||
|
|
||
| // Should not fail | ||
| let model = model.try_into_model().unwrap(); | ||
|
|
||
| let operation_log = OperationLog { | ||
| user, | ||
| date: Utc::now(), | ||
| table: Table::ContentFolder, | ||
| operation: OperationType::Create, | ||
| operation_id: OperationId { | ||
| object_id: model.id.to_owned(), | ||
| name: f.name.to_string(), | ||
| }, | ||
| operation_form: Some(Operation::ContentFolder(f.clone())), | ||
| }; | ||
|
|
||
| self.state | ||
| .logger | ||
| .write(operation_log) | ||
| .await | ||
| .context(LoggerSnafu)?; | ||
|
|
||
| Ok(model) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| // sea_orm example: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/ | ||
| pub mod category; | ||
| pub mod content_folder; | ||
| pub mod operation; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| use sea_orm_migration::{prelude::*, schema::*}; | ||
|
|
||
| use super::m20251110_01_create_table_category::Category; | ||
|
|
||
| #[derive(DeriveMigrationName)] | ||
| pub struct Migration; | ||
|
|
||
| #[async_trait::async_trait] | ||
| impl MigrationTrait for Migration { | ||
| async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
| manager | ||
| .create_table( | ||
| Table::create() | ||
| .table(ContentFolder::Table) | ||
| .if_not_exists() | ||
| .col(pk_auto(ContentFolder::Id)) | ||
| .col(string(ContentFolder::Name)) | ||
| .col(string(ContentFolder::Path)) | ||
| .col( | ||
| ColumnDef::new(ContentFolder::CategoryId) | ||
| .integer() | ||
| .not_null(), | ||
| ) | ||
| .foreign_key( | ||
| ForeignKey::create() | ||
| .name("fk-content-file-category_id") | ||
| .from(ContentFolder::Table, ContentFolder::CategoryId) | ||
| .to(Category::Table, Category::Id), | ||
| ) | ||
| .col(ColumnDef::new(ContentFolder::ParentId).integer()) | ||
| .foreign_key( | ||
| ForeignKey::create() | ||
| .name("fk-content-folder-parent_id") | ||
| .from(ContentFolder::ParentId, ContentFolder::ParentId) | ||
| .to(ContentFolder::Table, ContentFolder::Id), | ||
| ) | ||
| .to_owned(), | ||
| ) | ||
| .await | ||
| } | ||
|
|
||
| async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
| manager | ||
| .drop_table(Table::drop().table(ContentFolder::Table).to_owned()) | ||
| .await | ||
| } | ||
| } | ||
|
|
||
| #[derive(DeriveIden)] | ||
| pub enum ContentFolder { | ||
| Table, | ||
| Id, | ||
| Name, | ||
| Path, | ||
| CategoryId, | ||
| ParentId, | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.