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
47 changes: 43 additions & 4 deletions src/database/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*;
use sea_orm::*;
use snafu::prelude::*;

use crate::database::operation::*;
use crate::database::{content_folder, operation::*};
use crate::extractors::normalized_path::*;
use crate::extractors::user::User;
use crate::routes::category::CategoryForm;
Expand All @@ -15,6 +15,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 {
Expand All @@ -24,11 +25,10 @@ pub struct Model {
pub name: NormalizedPathComponent,
#[sea_orm(unique)]
pub path: NormalizedPathAbsolute,
#[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 {}

Expand Down Expand Up @@ -74,7 +74,24 @@ impl CategoryOperator {
.context(DBSnafu)
}

/// Find one category by ID
///
/// Should not fail, unless SQLite was corrupted for some reason.
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::IDNotFound { id }),
}
}

/// Find one category by Name
///
/// Should not fail, unless SQLite was corrupted for some reason.
pub async fn find_by_name(&self, name: String) -> Result<Model, CategoryError> {
let category = Entity::find()
.filter(Column::Name.contains(name.clone()))
Expand All @@ -88,6 +105,28 @@ impl CategoryOperator {
}
}

/// List folders for 1 category
///
/// Should not fail, unless SQLite was corrupted for some reason.
pub async fn list_folders(&self, id: i32) -> Result<Vec<content_folder::Model>, CategoryError> {
let category = Entity::find_by_id(id)
.one(&self.state.database)
.await
.context(DBSnafu)?;

match category {
Some(category) => {
let folders = category
.find_related(content_folder::Entity)
.all(&self.state.database)
.await
.context(DBSnafu)?;
Ok(folders)
}
None => Err(CategoryError::IDNotFound { id }),
}
}

/// Delete a category
pub async fn delete(&self, id: i32, user: Option<User>) -> Result<String, CategoryError> {
let db = &self.state.database;
Expand Down
180 changes: 180 additions & 0 deletions src/database/content_folder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use chrono::Utc;
use sea_orm::entity::prelude::*;
use sea_orm::*;
use snafu::prelude::*;

use crate::database::category::{self, CategoryError, CategoryOperator};
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 },
#[snafu(display("Category operation failed"))]
Category { source: CategoryError },
}

#[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 All child folders for 1 folder
///
/// Should not fail, unless SQLite was corrupted for some reason.
pub async fn list_child_folders(
&self,
content_folder_id: i32,
) -> Result<Vec<Model>, ContentFolderError> {
Entity::find()
.filter(Column::ParentId.eq(content_folder_id))
.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 = if let Some(parent_id) = f.parent_id {
self.list_child_folders(parent_id).await?
} else {
let category = CategoryOperator::new(self.state.clone(), None);
category
.list_folders(f.category_id)
.await
.context(CategorySnafu)?
};

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)
}
}
1 change: 1 addition & 0 deletions src/database/mod.rs
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;
3 changes: 3 additions & 0 deletions src/database/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};

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

/// Type of operation applied to the database.
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
Expand All @@ -22,6 +23,7 @@ pub struct OperationId {
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
pub enum Table {
Category,
ContentFolder,
}

/// Operation applied to the database.
Expand All @@ -31,6 +33,7 @@ pub enum Table {
#[serde(untagged)]
pub enum Operation {
Category(CategoryForm),
ContentFolder(ContentFolderForm),
}

impl std::fmt::Display for Operation {
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ pub fn router(state: state::AppState) -> Router {
.route("/categories/new", get(routes::category::new))
.route("/categories/{id}/delete", get(routes::category::delete))
.route("/folders/{category_id}", get(routes::category::show))
.route(
"/folders/{category_name}/{*folder_path}",
get(routes::content_folder::show),
)
.route("/folders", post(routes::content_folder::create))
.route("/logs", get(routes::logs::index))
// Register static assets routes
.nest("/assets", static_router())
Expand Down
2 changes: 1 addition & 1 deletion src/migration/m20251110_01_create_table_category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl MigrationTrait for Migration {
}

#[derive(DeriveIden)]
enum Category {
pub enum Category {
Table,
Id,
Name,
Expand Down
57 changes: 57 additions & 0 deletions src/migration/m20251113_203047_add_content_folder.rs
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,
}
Loading