fix(storage names): enure folder and file names are not using reserved chars
- create common function `validate_storage_name()` to check files & folder name - replace previous duplicate check with this one - better check on create_folder rename_folder (was only checking non empty) - use correct error class to ensure 400 on API (was 500) fix: #345 note: no unit test here, but plan e2e test for that next: improve UI to display error
This commit is contained in:
@@ -4,7 +4,7 @@ use crate::application::dtos::folder_dto::{
|
|||||||
use crate::application::ports::inbound::FolderUseCase;
|
use crate::application::ports::inbound::FolderUseCase;
|
||||||
use crate::common::errors::{DomainError, ErrorKind};
|
use crate::common::errors::{DomainError, ErrorKind};
|
||||||
use crate::domain::repositories::folder_repository::FolderRepository;
|
use crate::domain::repositories::folder_repository::FolderRepository;
|
||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::{StoragePath, validate_storage_name};
|
||||||
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -136,12 +136,11 @@ impl FolderUseCase for FolderService {
|
|||||||
/// Creates a new folder
|
/// Creates a new folder
|
||||||
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
|
async fn create_folder(&self, dto: CreateFolderDto) -> Result<FolderDto, DomainError> {
|
||||||
// Input validation
|
// Input validation
|
||||||
if dto.name.is_empty() {
|
if let Err(reason) = validate_storage_name(&dto.name) {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::validation_error(format!(
|
||||||
ErrorKind::InvalidInput,
|
"Invalid folder name '{}': {reason}",
|
||||||
"Folder",
|
dto.name
|
||||||
"Folder name cannot be empty",
|
)));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a parent_id is provided, verify it exists
|
// If a parent_id is provided, verify it exists
|
||||||
@@ -397,12 +396,11 @@ impl FolderUseCase for FolderService {
|
|||||||
caller_id: Uuid,
|
caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
// Input validation
|
// Input validation
|
||||||
if dto.name.is_empty() {
|
if let Err(reason) = validate_storage_name(&dto.name) {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::validation_error(format!(
|
||||||
ErrorKind::InvalidInput,
|
"Invalid folder name '{}': {reason}",
|
||||||
"Folder",
|
dto.name
|
||||||
"New folder name cannot be empty",
|
)));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the folder exists and belongs to the caller
|
// Verify the folder exists and belongs to the caller
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::{StoragePath, validate_storage_name};
|
||||||
|
|
||||||
// Re-export entity errors from the centralized module
|
// Re-export entity errors from the centralized module
|
||||||
pub use super::entity_errors::{FileError, FileResult};
|
pub use super::entity_errors::{FileError, FileResult};
|
||||||
@@ -100,9 +100,8 @@ impl File {
|
|||||||
mime_type: String,
|
mime_type: String,
|
||||||
folder_id: Option<String>,
|
folder_id: Option<String>,
|
||||||
) -> FileResult<Self> {
|
) -> FileResult<Self> {
|
||||||
// Validate file name
|
if let Err(reason) = validate_storage_name(&name) {
|
||||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
return Err(FileError::InvalidFileName(format!("{name}: {reason}")));
|
||||||
return Err(FileError::InvalidFileName(name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
@@ -137,9 +136,8 @@ impl File {
|
|||||||
created_at: u64,
|
created_at: u64,
|
||||||
modified_at: u64,
|
modified_at: u64,
|
||||||
) -> FileResult<Self> {
|
) -> FileResult<Self> {
|
||||||
// Validate folder name
|
if let Err(reason) = validate_storage_name(&name) {
|
||||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
return Err(FileError::InvalidFileName(format!("{name}: {reason}")));
|
||||||
return Err(FileError::InvalidFileName(name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the path string for serialization compatibility
|
// Store the path string for serialization compatibility
|
||||||
@@ -199,9 +197,8 @@ impl File {
|
|||||||
owner_id: Option<Uuid>,
|
owner_id: Option<Uuid>,
|
||||||
etag: String,
|
etag: String,
|
||||||
) -> FileResult<Self> {
|
) -> FileResult<Self> {
|
||||||
// Validate file name
|
if let Err(reason) = validate_storage_name(&name) {
|
||||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
return Err(FileError::InvalidFileName(format!("{name}: {reason}")));
|
||||||
return Err(FileError::InvalidFileName(name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the path string for serialization compatibility
|
// Store the path string for serialization compatibility
|
||||||
@@ -321,9 +318,8 @@ impl File {
|
|||||||
|
|
||||||
/// Creates a new version of the file with updated name
|
/// Creates a new version of the file with updated name
|
||||||
pub fn with_name(&self, new_name: String) -> FileResult<Self> {
|
pub fn with_name(&self, new_name: String) -> FileResult<Self> {
|
||||||
// Validate file name
|
if let Err(reason) = validate_storage_name(&new_name) {
|
||||||
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
|
return Err(FileError::InvalidFileName(format!("{new_name}: {reason}")));
|
||||||
return Err(FileError::InvalidFileName(new_name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update path based on name
|
// Update path based on name
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::{StoragePath, validate_storage_name};
|
||||||
|
|
||||||
// Re-export entity errors from the centralized module
|
// Re-export entity errors from the centralized module
|
||||||
pub use super::entity_errors::{FolderError, FolderResult};
|
pub use super::entity_errors::{FolderError, FolderResult};
|
||||||
@@ -71,8 +71,8 @@ impl Folder {
|
|||||||
owner_id: Option<Uuid>,
|
owner_id: Option<Uuid>,
|
||||||
) -> FolderResult<Self> {
|
) -> FolderResult<Self> {
|
||||||
// Validate folder name
|
// Validate folder name
|
||||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
if let Err(reason) = validate_storage_name(&name) {
|
||||||
return Err(FolderError::InvalidFolderName(name));
|
return Err(FolderError::InvalidFolderName(format!("{name}: {reason}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
@@ -126,8 +126,8 @@ impl Folder {
|
|||||||
modified_at: u64,
|
modified_at: u64,
|
||||||
) -> FolderResult<Self> {
|
) -> FolderResult<Self> {
|
||||||
// Validate folder name
|
// Validate folder name
|
||||||
if name.is_empty() || name.contains('/') || name.contains('\\') {
|
if let Err(reason) = validate_storage_name(&name) {
|
||||||
return Err(FolderError::InvalidFolderName(name));
|
return Err(FolderError::InvalidFolderName(format!("{name}: {reason}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the path string for serialization compatibility
|
// Store the path string for serialization compatibility
|
||||||
@@ -208,9 +208,10 @@ impl Folder {
|
|||||||
|
|
||||||
/// Creates a new version of the folder with updated name
|
/// Creates a new version of the folder with updated name
|
||||||
pub fn with_name(&self, new_name: String) -> FolderResult<Self> {
|
pub fn with_name(&self, new_name: String) -> FolderResult<Self> {
|
||||||
// Validate folder name
|
if let Err(reason) = validate_storage_name(&new_name) {
|
||||||
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
|
return Err(FolderError::InvalidFolderName(format!(
|
||||||
return Err(FolderError::InvalidFolderName(new_name));
|
"{new_name}: {reason}"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update path based on the name
|
// Update path based on the name
|
||||||
|
|||||||
@@ -6,6 +6,26 @@
|
|||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Validates a single file or folder name component.
|
||||||
|
///
|
||||||
|
/// Returns `Err` with a human-readable reason if the name is rejected.
|
||||||
|
/// Callers should wrap the reason into their own error type.
|
||||||
|
pub fn validate_storage_name(name: &str) -> Result<(), &'static str> {
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err("name cannot be empty");
|
||||||
|
}
|
||||||
|
if name.contains('/') || name.contains('\\') {
|
||||||
|
return Err("name must not contain '/' or '\\'");
|
||||||
|
}
|
||||||
|
if name.contains('\0') {
|
||||||
|
return Err("name must not contain null bytes");
|
||||||
|
}
|
||||||
|
if name == "." || name == ".." {
|
||||||
|
return Err("'.' and '..' are not valid names");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a storage path in the domain (Value Object)
|
/// Represents a storage path in the domain (Value Object)
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct StoragePath {
|
pub struct StoragePath {
|
||||||
|
|||||||
Reference in New Issue
Block a user