Skip to content

Commit eb91fec

Browse files
committed
refactor: Move content folder validation logic to database module
1 parent 7ee28f3 commit eb91fec

8 files changed

Lines changed: 75 additions & 82 deletions

File tree

src/database/content_folder.rs

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

7-
use crate::database::category::{self, CategoryError, CategoryOperator};
7+
use std::str::FromStr;
8+
9+
use crate::database::category::{self, CategoryError};
810
use crate::database::operation::{Operation, OperationId, OperationLog, OperationType, Table};
911
use crate::database::operator::DatabaseOperator;
12+
use crate::extractors::normalized_path::{NormalizedPathAbsolute, NormalizedPathComponent};
1013
use crate::extractors::user::User;
1114
use crate::routes::content_folder::ContentFolderForm;
1215
use crate::state::AppState;
@@ -25,7 +28,9 @@ pub struct Model {
2528
pub id: i32,
2629
pub name: String,
2730
#[sea_orm(unique)]
28-
pub path: String,
31+
// TODO: maybe we'd like relative paths in fact? Why did
32+
// we have a leading slash in the first place?
33+
pub path: NormalizedPathAbsolute,
2934
pub category_id: i32,
3035
#[sea_orm(belongs_to, from = "category_id", to = "id")]
3136
pub category: HasOne<category::Entity>,
@@ -40,10 +45,12 @@ impl ActiveModelBehavior for ActiveModel {}
4045
#[derive(Debug, Snafu)]
4146
#[snafu(visibility(pub))]
4247
pub enum ContentFolderError {
43-
#[snafu(display("There is already a content folder called `{name}`"))]
48+
#[snafu(display("There is already a content folder called `{name}` in the current folder."))]
4449
NameTaken { name: String },
45-
#[snafu(display("There is already a content folder in dir `{path}`"))]
46-
PathTaken { path: String },
50+
#[snafu(display("The folder name is invalid. It must not contain slashes."))]
51+
NameInvalid,
52+
#[snafu(display("The folder path must appear absolute"))]
53+
PathInvalid,
4754
#[snafu(display("The Content Folder (Path: {path}) does not exist"))]
4855
NotFound { path: String },
4956
#[snafu(display("Database error"))]
@@ -52,6 +59,8 @@ pub enum ContentFolderError {
5259
Logger { source: LoggerError },
5360
#[snafu(display("Category operation failed"))]
5461
Category { source: CategoryError },
62+
#[snafu(display("Failed to create the folder on disk"))]
63+
IO { source: std::io::Error },
5564
}
5665

5766
#[derive(Clone, Debug)]
@@ -90,14 +99,19 @@ impl ContentFolderOperator {
9099
///
91100
/// Should not fail, unless SQLite was corrupted for some reason.
92101
pub async fn find_by_path(&self, path: String) -> Result<Model, ContentFolderError> {
102+
let path = NormalizedPathAbsolute::from_str(&path)
103+
.map_err(|_e| ContentFolderError::PathInvalid)?;
104+
93105
let content_folder = Entity::find_by_path(path.clone())
94106
.one(&self.state.database)
95107
.await
96108
.context(DBSnafu)?;
97109

98110
match content_folder {
99111
Some(category) => Ok(category),
100-
None => Err(ContentFolderError::NotFound { path }),
112+
None => Err(ContentFolderError::NotFound {
113+
path: path.to_string(),
114+
}),
101115
}
102116
}
103117

@@ -122,35 +136,48 @@ impl ContentFolderOperator {
122136
///
123137
/// Fails if:
124138
///
125-
/// - name or path is already taken (they should be unique in one folder)
139+
/// - name is already taken (they should be unique in one folder)
126140
/// - path parent directory does not exist (to avoid completely wrong paths)
127141
pub async fn create(&self, f: &ContentFolderForm) -> Result<Model, ContentFolderError> {
128-
// Check duplicates in same folder
129-
let list = if let Some(parent_id) = f.parent_id {
130-
self.list_child_folders(parent_id).await?
131-
} else {
132-
let category = CategoryOperator::new(self.state.clone(), None);
133-
category
134-
.list_folders(f.category_id)
135-
.await
136-
.context(CategorySnafu)?
137-
};
142+
let name = NormalizedPathComponent::from_str(&f.name)
143+
.map_err(|_e| ContentFolderError::NameInvalid)?;
138144

139-
if list.iter().any(|x| x.name == f.name) {
140-
return Err(ContentFolderError::NameTaken {
141-
name: f.name.clone(),
142-
});
143-
}
145+
let category = self
146+
.db()
147+
.category()
148+
.find_by_id(f.category_id)
149+
.await
150+
.context(CategorySnafu)?;
144151

145-
if list.iter().any(|x| x.path == f.path) {
146-
return Err(ContentFolderError::PathTaken {
147-
path: f.path.clone(),
148-
});
152+
// Check duplicates in same category/folder
153+
{
154+
let siblings = if let Some(parent_id) = f.parent_id {
155+
self.list_child_folders(parent_id).await?
156+
} else {
157+
self.db()
158+
.category()
159+
.list_folders(f.category_id)
160+
.await
161+
.context(CategorySnafu)?
162+
};
163+
if siblings.iter().any(|x| x.name == f.name) {
164+
return Err(ContentFolderError::NameTaken {
165+
name: f.name.clone(),
166+
});
167+
}
149168
}
150169

170+
// This path is an absolute path, but relative to a category path
171+
let inner_path = if let Some(parent_id) = f.parent_id {
172+
let parent = self.find_by_id(parent_id).await?;
173+
NormalizedPathAbsolute::from_str(&format!("{}/{}", parent.path, name,)).unwrap()
174+
} else {
175+
NormalizedPathAbsolute::from_str(&format!("/{}", name)).unwrap()
176+
};
177+
151178
let model = ActiveModel {
152-
name: Set(f.name.clone()),
153-
path: Set(f.path.clone()),
179+
name: Set(name.to_string()),
180+
path: Set(inner_path.clone()),
154181
category_id: Set(f.category_id),
155182
parent_id: Set(f.parent_id),
156183
..Default::default()
@@ -159,6 +186,13 @@ impl ContentFolderOperator {
159186
.await
160187
.context(DBSnafu)?;
161188

189+
let real_path =
190+
NormalizedPathAbsolute::from_str(&format!("{}{}", category.path, inner_path)).unwrap();
191+
192+
tokio::fs::create_dir_all(&real_path)
193+
.await
194+
.context(IOSnafu)?;
195+
162196
// Should not fail
163197
let model = model.try_into_model().unwrap();
164198

src/extractors/normalized_path/absolute.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::str::FromStr;
99
use super::*;
1010

1111
/// [NormalizedPath] with extra constraint that it's absolute.
12-
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, DeriveValueType)]
12+
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, DeriveValueType)]
1313
#[sea_orm(value_type = "String")]
1414
#[serde(into = "String", try_from = "String")]
1515
pub struct NormalizedPathAbsolute {

src/extractors/normalized_path/component.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::str::FromStr;
99
use super::*;
1010

1111
/// [NormalizedPath] with extra constraint that it contains no slashes.
12-
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, DeriveValueType)]
12+
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, DeriveValueType)]
1313
#[sea_orm(value_type = "String")]
1414
#[serde(into = "String", try_from = "String")]
1515
pub struct NormalizedPathComponent {

src/extractors/normalized_path/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub use relative::*;
2222
/// - disallows parent dir traversal (`..`)
2323
/// - has no trailing slash
2424
/// - may contain current dir `./` but these will be discarded
25-
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, DeriveValueType)]
25+
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, DeriveValueType)]
2626
#[sea_orm(value_type = "String")]
2727
#[serde(into = "String", try_from = "String")]
2828
pub struct NormalizedPath {

src/extractors/normalized_path/relative.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::str::FromStr;
99
use super::*;
1010

1111
/// [NormalizedPath] with extra constraint that it's relative.
12-
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, DeriveValueType)]
12+
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, DeriveValueType)]
1313
#[sea_orm(value_type = "String")]
1414
#[serde(into = "String", try_from = "String")]
1515
pub struct NormalizedPathRelative {

src/routes/content_folder.rs

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use askama_web::WebTemplate;
33
use axum::Form;
44
use axum::response::Redirect;
55
use axum_extra::extract::CookieJar;
6-
use camino::Utf8PathBuf;
76
use serde::{Deserialize, Serialize};
87
use snafu::prelude::*;
98

@@ -20,7 +19,6 @@ use crate::state::{AppStateContext, error::*};
2019
pub struct ContentFolderForm {
2120
pub name: String,
2221
pub parent_id: Option<i32>,
23-
pub path: String,
2422
pub category_id: i32,
2523
}
2624

@@ -75,56 +73,20 @@ pub async fn show(
7573
status.with_template(ContentFolderShowTemplate::new(context, folder))
7674
}
7775

76+
// TODO: currently an error takes us back to /
7877
pub async fn create(
7978
context: AppStateContext,
8079
jar: CookieJar,
81-
Form(mut form): Form<ContentFolderForm>,
80+
Form(form): Form<ContentFolderForm>,
8281
) -> Result<FlashRedirect, AppStateError> {
83-
let categories = context.db.category();
84-
let content_folders = context.db.content_folder();
85-
86-
// build path with Parent folder path (or category path if parent is None) + folder.name
87-
let parent_path = if let Some(parent_id) = form.parent_id {
88-
let parent_folder = content_folders
89-
.find_by_id(parent_id)
90-
.await
91-
.context(ContentFolderSnafu)?;
92-
Utf8PathBuf::from(parent_folder.path)
93-
} else {
94-
Utf8PathBuf::new()
95-
};
96-
97-
// Get folder category
98-
let category: category::Model = categories
99-
.find_by_id(form.category_id)
100-
.await
101-
.context(CategorySnafu)?;
102-
103-
// If name contains "/" returns an error
104-
if form.name.contains("/") {
105-
let status = StatusCookie::error(
106-
jar,
107-
format!(
108-
"Failed to create Folder, {} is not valid (it contains '/')",
109-
form.name
110-
),
111-
);
112-
113-
let uri = format!("/folders/{}{}", category.name, parent_path.into_string());
114-
return Ok(status.redirect(&uri));
115-
}
116-
117-
// build final path with parent_path and path of form
118-
form.path = format!("{}/{}", parent_path, form.name);
119-
120-
let created = content_folders.create(&form).await;
121-
122-
match created {
82+
match context.db.content_folder().create(&form).await {
12383
Ok(created) => {
124-
tokio::fs::create_dir_all(format!("{}/{}", category.path, created.path.clone()))
84+
let category = context
85+
.db
86+
.category()
87+
.find_by_id(created.category_id)
12588
.await
126-
.context(IOSnafu)?;
127-
89+
.context(CategorySnafu)?;
12890
let status = StatusCookie::success(
12991
jar,
13092
format!(
@@ -137,6 +99,7 @@ pub async fn create(
13799
Ok(status.redirect(&uri))
138100
}
139101
// TODO: why don't we produce an error here?
102+
// We should also redirect to the appropriate page
140103
Err(_error) => Ok((jar, Redirect::to("/"))),
141104
}
142105
}

templates/categories/show.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,12 @@
1515
<input
1616
type="text"
1717
class="form-control"
18-
placeholder="Ex: Saison 1"
1918
name="name"
2019
id="name"
2120
/>
2221
</div>
2322

2423
<input type="hidden" id="category_id" name="category_id" value="{{ category.id }}" />
25-
<input type="hidden" id="path" name="path" value="fake_path" />
2624

2725
<div class="text-center mt-4">
2826
<input type="Submit" class="btn btn-success" value="Create folder">

templates/content_folders/show.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@
1515
<input
1616
type="text"
1717
class="form-control"
18-
placeholder="Ex: Saison 1"
1918
name="name"
2019
id="name"
2120
/>
2221
</div>
2322

2423
<input type="hidden" id="category_id" name="category_id" value="{{ category.id }}" />
2524
<input type="hidden" id="parent_id" name="parent_id" value="{{ folder.id }}" />
26-
<input type="hidden" id="path" name="path" value="fake_path" />
2725

2826
<div class="text-center mt-4">
2927
<input type="Submit" class="btn btn-success" value="Create folder">

0 commit comments

Comments
 (0)