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
32 changes: 21 additions & 11 deletions src/database/category.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use camino::Utf8PathBuf;
use chrono::Utc;
use sea_orm::entity::prelude::*;
use sea_orm::*;
use snafu::prelude::*;

use std::str::FromStr;

use crate::database::operator::DatabaseOperator;
use crate::database::{content_folder, operation::*};
use crate::extractors::normalized_path::*;
Expand Down Expand Up @@ -38,8 +39,12 @@ impl ActiveModelBehavior for ActiveModel {}
pub enum CategoryError {
#[snafu(display("There is already a category called `{name}`"))]
NameTaken { name: String },
#[snafu(display("The category name is invalid. It must not contain slashes."))]
NameInvalid,
#[snafu(display("There is already a category in dir `{path}`"))]
PathTaken { path: String },
#[snafu(display("The category path is invalid. It must be an absolute path."))]
PathInvalid,
#[snafu(display("The parent directory does not exist: {path}"))]
ParentDir { path: String },
#[snafu(display("Other disk error"))]
Expand Down Expand Up @@ -177,8 +182,13 @@ 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, CategoryError> {
let dir = Utf8PathBuf::from(&f.path);
pub async fn create(&self, form: &CategoryForm) -> Result<Model, CategoryError> {
let name = NormalizedPathComponent::from_str(&form.name)
.map_err(|_e| CategoryError::NameInvalid)?;
let path = NormalizedPathAbsolute::from_str(&form.path)
.map_err(|_e| CategoryError::PathInvalid)?;

let dir = path.to_path_buf();
let parent = dir.parent().unwrap();

if !tokio::fs::try_exists(parent).await.context(IOSnafu)? {
Expand All @@ -190,20 +200,20 @@ impl CategoryOperator {
// Check duplicates
let list = self.list().await?;

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

let model = ActiveModel {
name: Set(f.name.clone()),
path: Set(f.path.clone()),
name: Set(name.clone()),
path: Set(path.clone()),
..Default::default()
}
.save(&self.state.database)
Expand All @@ -220,9 +230,9 @@ impl CategoryOperator {
operation: OperationType::Create,
operation_id: OperationId {
object_id: model.id.to_owned(),
name: f.name.to_string(),
name: name.to_string(),
},
operation_form: Some(Operation::Category(f.clone())),
operation_form: Some(Operation::Category(form.clone())),
};

self.state
Expand Down
90 changes: 62 additions & 28 deletions src/database/content_folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ use sea_orm::entity::prelude::*;
use sea_orm::*;
use snafu::prelude::*;

use crate::database::category::{self, CategoryError, CategoryOperator};
use std::str::FromStr;

use crate::database::category::{self, CategoryError};
use crate::database::operation::{Operation, OperationId, OperationLog, OperationType, Table};
use crate::database::operator::DatabaseOperator;
use crate::extractors::normalized_path::{NormalizedPathAbsolute, NormalizedPathComponent};
use crate::extractors::user::User;
use crate::routes::content_folder::ContentFolderForm;
use crate::state::AppState;
Expand All @@ -25,7 +28,9 @@ pub struct Model {
pub id: i32,
pub name: String,
#[sea_orm(unique)]
pub path: String,
// TODO: maybe we'd like relative paths in fact? Why did
// we have a leading slash in the first place?
pub path: NormalizedPathAbsolute,
pub category_id: i32,
#[sea_orm(belongs_to, from = "category_id", to = "id")]
pub category: HasOne<category::Entity>,
Expand All @@ -40,10 +45,12 @@ impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum ContentFolderError {
#[snafu(display("There is already a content folder called `{name}`"))]
#[snafu(display("There is already a content folder called `{name}` in the current folder."))]
NameTaken { name: String },
#[snafu(display("There is already a content folder in dir `{path}`"))]
PathTaken { path: String },
#[snafu(display("The folder name is invalid. It must not contain slashes."))]
NameInvalid,
#[snafu(display("The folder path must appear absolute"))]
PathInvalid,
#[snafu(display("The Content Folder (Path: {path}) does not exist"))]
NotFound { path: String },
#[snafu(display("Database error"))]
Expand All @@ -52,6 +59,8 @@ pub enum ContentFolderError {
Logger { source: LoggerError },
#[snafu(display("Category operation failed"))]
Category { source: CategoryError },
#[snafu(display("Failed to create the folder on disk"))]
IO { source: std::io::Error },
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -90,14 +99,19 @@ impl ContentFolderOperator {
///
/// Should not fail, unless SQLite was corrupted for some reason.
pub async fn find_by_path(&self, path: String) -> Result<Model, ContentFolderError> {
let path = NormalizedPathAbsolute::from_str(&path)
.map_err(|_e| ContentFolderError::PathInvalid)?;

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 }),
None => Err(ContentFolderError::NotFound {
path: path.to_string(),
}),
}
}

Expand All @@ -122,35 +136,48 @@ impl ContentFolderOperator {
///
/// Fails if:
///
/// - name or path is already taken (they should be unique in one folder)
/// - name 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) -> 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)?
};
let name = NormalizedPathComponent::from_str(&f.name)
.map_err(|_e| ContentFolderError::NameInvalid)?;

if list.iter().any(|x| x.name == f.name) {
return Err(ContentFolderError::NameTaken {
name: f.name.clone(),
});
}
let category = self
.db()
.category()
.find_by_id(f.category_id)
.await
.context(CategorySnafu)?;

if list.iter().any(|x| x.path == f.path) {
return Err(ContentFolderError::PathTaken {
path: f.path.clone(),
});
// Check duplicates in same category/folder
{
let siblings = if let Some(parent_id) = f.parent_id {
self.list_child_folders(parent_id).await?
} else {
self.db()
.category()
.list_folders(f.category_id)
.await
.context(CategorySnafu)?
};
if siblings.iter().any(|x| x.name == f.name) {
return Err(ContentFolderError::NameTaken {
name: f.name.clone(),
});
}
}

// This path is an absolute path, but relative to a category path
let inner_path = if let Some(parent_id) = f.parent_id {
let parent = self.find_by_id(parent_id).await?;
NormalizedPathAbsolute::from_str(&format!("{}/{}", parent.path, name,)).unwrap()
} else {
NormalizedPathAbsolute::from_str(&format!("/{}", name)).unwrap()
};

let model = ActiveModel {
name: Set(f.name.clone()),
path: Set(f.path.clone()),
name: Set(name.to_string()),
path: Set(inner_path.clone()),
category_id: Set(f.category_id),
parent_id: Set(f.parent_id),
..Default::default()
Expand All @@ -159,6 +186,13 @@ impl ContentFolderOperator {
.await
.context(DBSnafu)?;

let real_path =
NormalizedPathAbsolute::from_str(&format!("{}{}", category.path, inner_path)).unwrap();

tokio::fs::create_dir_all(&real_path)
.await
.context(IOSnafu)?;

// Should not fail
let model = model.try_into_model().unwrap();

Expand Down
22 changes: 22 additions & 0 deletions src/extractors/category_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ use crate::database::content_folder::PathBreadcrumb;
use crate::filesystem::FileSystemEntry;
use crate::state::{AppState, error::*};

#[derive(Clone, Debug)]
pub struct CategoriesRequest {
pub children: Vec<FileSystemEntry>,
}

impl FromRequestParts<AppState> for CategoriesRequest {
type Rejection = AppStateError;

async fn from_request_parts(
_parts: &mut Parts,
app_state: &AppState,
) -> Result<Self, Self::Rejection> {
let categories = CategoryOperator::new(app_state.clone(), None)
.list()
.await
.context(CategorySnafu)?;

let children = FileSystemEntry::from_categories(&categories);
Ok(Self { children })
}
}

#[derive(Clone, Debug)]
pub struct CategoryRequest {
pub category: category::Model,
Expand Down
2 changes: 1 addition & 1 deletion src/extractors/normalized_path/absolute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::str::FromStr;
use super::*;

/// [NormalizedPath] with extra constraint that it's absolute.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, DeriveValueType)]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, DeriveValueType)]
#[sea_orm(value_type = "String")]
#[serde(into = "String", try_from = "String")]
pub struct NormalizedPathAbsolute {
Expand Down
2 changes: 1 addition & 1 deletion src/extractors/normalized_path/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::str::FromStr;
use super::*;

/// [NormalizedPath] with extra constraint that it contains no slashes.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, DeriveValueType)]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, DeriveValueType)]
#[sea_orm(value_type = "String")]
#[serde(into = "String", try_from = "String")]
pub struct NormalizedPathComponent {
Expand Down
2 changes: 1 addition & 1 deletion src/extractors/normalized_path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub use relative::*;
/// - disallows parent dir traversal (`..`)
/// - has no trailing slash
/// - may contain current dir `./` but these will be discarded
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, DeriveValueType)]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, DeriveValueType)]
#[sea_orm(value_type = "String")]
#[serde(into = "String", try_from = "String")]
pub struct NormalizedPath {
Expand Down
2 changes: 1 addition & 1 deletion src/extractors/normalized_path/relative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::str::FromStr;
use super::*;

/// [NormalizedPath] with extra constraint that it's relative.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, DeriveValueType)]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, DeriveValueType)]
#[sea_orm(value_type = "String")]
#[serde(into = "String", try_from = "String")]
pub struct NormalizedPathRelative {
Expand Down
8 changes: 6 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ pub fn router(state: state::AppState) -> Router {
.route("/categories", post(routes::category::create))
.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}", get(routes::category::show))
.route("/folders/{category}", post(routes::category::create_folder))
.route(
"/folders/{category_name}/{*folder_path}",
get(routes::content_folder::show),
)
.route(
"/folders/{category_name}/{*folder_path}",
post(routes::content_folder::create_subfolder),
)
.route("/folders", get(routes::index::index))
.route("/folders", post(routes::content_folder::create))
.route("/logs", get(routes::logs::index))
// Register static assets routes
.nest("/assets", static_router())
Expand Down
Loading