Skip to content

Commit d6e4c02

Browse files
Gabatxo1312angrynode
authored andcommitted
Add contennt folders migrations ans views
1 parent 2259109 commit d6e4c02

File tree

16 files changed

+717
-6
lines changed

16 files changed

+717
-6
lines changed

src/database/category.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*;
44
use sea_orm::*;
55
use snafu::prelude::*;
66

7-
use crate::database::operation::*;
7+
use crate::database::{content_folder, operation::*};
88
use crate::extractors::normalized_path::*;
99
use crate::extractors::user::User;
1010
use crate::routes::category::CategoryForm;
@@ -15,6 +15,7 @@ use crate::state::logger::LoggerError;
1515
///
1616
/// Each category has a name and an associated path on disk, where
1717
/// symlinks to the content will be created.
18+
#[sea_orm::model]
1819
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
1920
#[sea_orm(table_name = "category")]
2021
pub struct Model {
@@ -24,11 +25,10 @@ pub struct Model {
2425
pub name: NormalizedPathComponent,
2526
#[sea_orm(unique)]
2627
pub path: NormalizedPathAbsolute,
28+
#[sea_orm(has_many)]
29+
pub content_folders: HasMany<super::content_folder::Entity>,
2730
}
2831

29-
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
30-
pub enum Relation {}
31-
3232
#[async_trait::async_trait]
3333
impl ActiveModelBehavior for ActiveModel {}
3434

@@ -74,7 +74,24 @@ impl CategoryOperator {
7474
.context(DBSnafu)
7575
}
7676

77+
/// Find one category by ID
78+
///
79+
/// Should not fail, unless SQLite was corrupted for some reason.
80+
pub async fn find_by_id(&self, id: i32) -> Result<Model, CategoryError> {
81+
let category = Entity::find_by_id(id)
82+
.one(&self.state.database)
83+
.await
84+
.context(DBSnafu)?;
85+
86+
match category {
87+
Some(category) => Ok(category),
88+
None => Err(CategoryError::IDNotFound { id }),
89+
}
90+
}
91+
7792
/// Find one category by Name
93+
///
94+
/// Should not fail, unless SQLite was corrupted for some reason.
7895
pub async fn find_by_name(&self, name: String) -> Result<Model, CategoryError> {
7996
let category = Entity::find()
8097
.filter(Column::Name.contains(name.clone()))
@@ -88,6 +105,28 @@ impl CategoryOperator {
88105
}
89106
}
90107

108+
/// List folders for 1 category
109+
///
110+
/// Should not fail, unless SQLite was corrupted for some reason.
111+
pub async fn list_folders(&self, id: i32) -> Result<Vec<content_folder::Model>, CategoryError> {
112+
let category = Entity::find_by_id(id)
113+
.one(&self.state.database)
114+
.await
115+
.context(DBSnafu)?;
116+
117+
match category {
118+
Some(category) => {
119+
let folders = category
120+
.find_related(content_folder::Entity)
121+
.all(&self.state.database)
122+
.await
123+
.context(DBSnafu)?;
124+
Ok(folders)
125+
}
126+
None => Err(CategoryError::IDNotFound { id }),
127+
}
128+
}
129+
91130
/// Delete a category
92131
pub async fn delete(&self, id: i32, user: Option<User>) -> Result<String, CategoryError> {
93132
let db = &self.state.database;

src/database/content_folder.rs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use chrono::Utc;
2+
use sea_orm::entity::prelude::*;
3+
use sea_orm::*;
4+
use snafu::prelude::*;
5+
6+
use crate::database::category::{self, CategoryError, CategoryOperator};
7+
use crate::database::operation::{Operation, OperationId, OperationLog, OperationType, Table};
8+
use crate::extractors::user::User;
9+
use crate::routes::content_folder::ContentFolderForm;
10+
use crate::state::AppState;
11+
use crate::state::logger::LoggerError;
12+
13+
/// A content folder to store associated files.
14+
///
15+
/// Each content folder has a name and an associated path on disk, a Category
16+
/// and it can have an Parent Content Folder (None if it's the first folder
17+
/// in category)
18+
#[sea_orm::model]
19+
#[derive(DeriveEntityModel, Clone, Debug, PartialEq, Eq)]
20+
#[sea_orm(table_name = "content_folder")]
21+
pub struct Model {
22+
#[sea_orm(primary_key)]
23+
pub id: i32,
24+
pub name: String,
25+
#[sea_orm(unique)]
26+
pub path: String,
27+
pub category_id: i32,
28+
#[sea_orm(belongs_to, from = "category_id", to = "id")]
29+
pub category: HasOne<category::Entity>,
30+
pub parent_id: Option<i32>,
31+
#[sea_orm(self_ref, relation_enum = "Parent", from = "parent_id", to = "id")]
32+
pub parent: HasOne<Entity>,
33+
}
34+
35+
#[async_trait::async_trait]
36+
impl ActiveModelBehavior for ActiveModel {}
37+
38+
#[derive(Debug, Snafu)]
39+
#[snafu(visibility(pub))]
40+
pub enum ContentFolderError {
41+
#[snafu(display("There is already a content folder called `{name}`"))]
42+
NameTaken { name: String },
43+
#[snafu(display("There is already a content folder in dir `{path}`"))]
44+
PathTaken { path: String },
45+
#[snafu(display("The Content Folder (Path: {path}) does not exist"))]
46+
NotFound { path: String },
47+
#[snafu(display("Database error"))]
48+
DB { source: sea_orm::DbErr },
49+
#[snafu(display("Failed to save the operation log"))]
50+
Logger { source: LoggerError },
51+
#[snafu(display("Category operation failed"))]
52+
Category { source: CategoryError },
53+
}
54+
55+
#[derive(Clone, Debug)]
56+
pub struct ContentFolderOperator {
57+
pub state: AppState,
58+
pub user: Option<User>,
59+
}
60+
61+
impl ContentFolderOperator {
62+
pub fn new(state: AppState, user: Option<User>) -> Self {
63+
Self { state, user }
64+
}
65+
66+
/// list All child folders for 1 folder
67+
///
68+
/// Should not fail, unless SQLite was corrupted for some reason.
69+
pub async fn list_child_folders(
70+
&self,
71+
content_folder_id: i32,
72+
) -> Result<Vec<Model>, ContentFolderError> {
73+
Entity::find()
74+
.filter(Column::ParentId.eq(content_folder_id))
75+
.all(&self.state.database)
76+
.await
77+
.context(DBSnafu)
78+
}
79+
80+
/// Find one Content Folder by path
81+
///
82+
/// Should not fail, unless SQLite was corrupted for some reason.
83+
pub async fn find_by_path(&self, path: String) -> Result<Model, ContentFolderError> {
84+
let content_folder = Entity::find_by_path(path.clone())
85+
.one(&self.state.database)
86+
.await
87+
.context(DBSnafu)?;
88+
89+
match content_folder {
90+
Some(category) => Ok(category),
91+
None => Err(ContentFolderError::NotFound { path }),
92+
}
93+
}
94+
95+
/// Find one Content Folder by ID
96+
///
97+
/// Should not fail, unless SQLite was corrupted for some reason.
98+
pub async fn find_by_id(&self, id: i32) -> Result<Model, ContentFolderError> {
99+
let content_folder = Entity::find_by_id(id)
100+
.one(&self.state.database)
101+
.await
102+
.context(DBSnafu)?;
103+
104+
match content_folder {
105+
Some(category) => Ok(category),
106+
None => Err(ContentFolderError::NotFound {
107+
path: id.to_string(),
108+
}),
109+
}
110+
}
111+
112+
/// Create a new content folder
113+
///
114+
/// Fails if:
115+
///
116+
/// - name or path is already taken (they should be unique in one folder)
117+
/// - path parent directory does not exist (to avoid completely wrong paths)
118+
pub async fn create(
119+
&self,
120+
f: &ContentFolderForm,
121+
user: Option<User>,
122+
) -> Result<Model, ContentFolderError> {
123+
// Check duplicates in same folder
124+
let list = if let Some(parent_id) = f.parent_id {
125+
self.list_child_folders(parent_id).await?
126+
} else {
127+
let category = CategoryOperator::new(self.state.clone(), None);
128+
category
129+
.list_folders(f.category_id)
130+
.await
131+
.context(CategorySnafu)?
132+
};
133+
134+
if list.iter().any(|x| x.name == f.name) {
135+
return Err(ContentFolderError::NameTaken {
136+
name: f.name.clone(),
137+
});
138+
}
139+
140+
if list.iter().any(|x| x.path == f.path) {
141+
return Err(ContentFolderError::PathTaken {
142+
path: f.path.clone(),
143+
});
144+
}
145+
146+
let model = ActiveModel {
147+
name: Set(f.name.clone()),
148+
path: Set(f.path.clone()),
149+
category_id: Set(f.category_id),
150+
parent_id: Set(f.parent_id),
151+
..Default::default()
152+
}
153+
.save(&self.state.database)
154+
.await
155+
.context(DBSnafu)?;
156+
157+
// Should not fail
158+
let model = model.try_into_model().unwrap();
159+
160+
let operation_log = OperationLog {
161+
user,
162+
date: Utc::now(),
163+
table: Table::ContentFolder,
164+
operation: OperationType::Create,
165+
operation_id: OperationId {
166+
object_id: model.id.to_owned(),
167+
name: f.name.to_string(),
168+
},
169+
operation_form: Some(Operation::ContentFolder(f.clone())),
170+
};
171+
172+
self.state
173+
.logger
174+
.write(operation_log)
175+
.await
176+
.context(LoggerSnafu)?;
177+
178+
Ok(model)
179+
}
180+
}

src/database/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// sea_orm example: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/
22
pub mod category;
3+
pub mod content_folder;
34
pub mod operation;

src/database/operation.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
44

55
use crate::extractors::user::User;
66
use crate::routes::category::CategoryForm;
7+
use crate::routes::content_folder::ContentFolderForm;
78

89
/// Type of operation applied to the database.
910
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
@@ -22,6 +23,7 @@ pub struct OperationId {
2223
#[derive(Clone, Debug, Display, Serialize, Deserialize)]
2324
pub enum Table {
2425
Category,
26+
ContentFolder,
2527
}
2628

2729
/// Operation applied to the database.
@@ -31,6 +33,7 @@ pub enum Table {
3133
#[serde(untagged)]
3234
pub enum Operation {
3335
Category(CategoryForm),
36+
ContentFolder(ContentFolderForm),
3437
}
3538

3639
impl std::fmt::Display for Operation {

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ pub fn router(state: state::AppState) -> Router {
2727
.route("/categories/new", get(routes::category::new))
2828
.route("/categories/{id}/delete", get(routes::category::delete))
2929
.route("/folders/{category_id}", get(routes::category::show))
30+
.route(
31+
"/folders/{category_name}/{*folder_path}",
32+
get(routes::content_folder::show),
33+
)
34+
.route("/folders", post(routes::content_folder::create))
3035
.route("/logs", get(routes::logs::index))
3136
// Register static assets routes
3237
.nest("/assets", static_router())

src/migration/m20251110_01_create_table_category.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ impl MigrationTrait for Migration {
2727
}
2828

2929
#[derive(DeriveIden)]
30-
enum Category {
30+
pub enum Category {
3131
Table,
3232
Id,
3333
Name,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use sea_orm_migration::{prelude::*, schema::*};
2+
3+
use super::m20251110_01_create_table_category::Category;
4+
5+
#[derive(DeriveMigrationName)]
6+
pub struct Migration;
7+
8+
#[async_trait::async_trait]
9+
impl MigrationTrait for Migration {
10+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
11+
manager
12+
.create_table(
13+
Table::create()
14+
.table(ContentFolder::Table)
15+
.if_not_exists()
16+
.col(pk_auto(ContentFolder::Id))
17+
.col(string(ContentFolder::Name))
18+
.col(string(ContentFolder::Path))
19+
.col(
20+
ColumnDef::new(ContentFolder::CategoryId)
21+
.integer()
22+
.not_null(),
23+
)
24+
.foreign_key(
25+
ForeignKey::create()
26+
.name("fk-content-file-category_id")
27+
.from(ContentFolder::Table, ContentFolder::CategoryId)
28+
.to(Category::Table, Category::Id),
29+
)
30+
.col(ColumnDef::new(ContentFolder::ParentId).integer())
31+
.foreign_key(
32+
ForeignKey::create()
33+
.name("fk-content-folder-parent_id")
34+
.from(ContentFolder::ParentId, ContentFolder::ParentId)
35+
.to(ContentFolder::Table, ContentFolder::Id),
36+
)
37+
.to_owned(),
38+
)
39+
.await
40+
}
41+
42+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
43+
manager
44+
.drop_table(Table::drop().table(ContentFolder::Table).to_owned())
45+
.await
46+
}
47+
}
48+
49+
#[derive(DeriveIden)]
50+
pub enum ContentFolder {
51+
Table,
52+
Id,
53+
Name,
54+
Path,
55+
CategoryId,
56+
ParentId,
57+
}

0 commit comments

Comments
 (0)