Files
Oxicloud/src/domain/entities/file.rs
T

431 lines
12 KiB
Rust
Raw Normal View History

use uuid::Uuid;
2025-03-19 00:44:27 +01:00
use crate::domain::services::path_service::StoragePath;
// Re-export entity errors from the centralized module
2026-02-06 20:57:00 +01:00
pub use super::entity_errors::{FileError, FileResult};
2025-03-17 21:28:08 +01:00
/// Owned parts of a [`File`] entity, produced by [`File::into_parts()`].
///
/// Consuming a `File` into `FileParts` **moves** every field without cloning,
/// eliminating 3-5 heap allocations that previously occurred when converting
/// `File → FileDto` via `.to_string()` on each getter.
pub struct FileParts {
pub id: String,
pub name: String,
pub storage_path: StoragePath,
pub path_string: String,
pub size: u64,
pub mime_type: String,
pub folder_id: Option<String>,
pub created_at: u64,
pub modified_at: u64,
pub owner_id: Option<Uuid>,
}
2025-03-26 19:08:07 +01:00
/**
* Represents a file in the system's domain model.
2026-02-14 01:29:34 +01:00
*
2025-03-26 19:08:07 +01:00
* The File entity is a core domain object that encapsulates all properties and behaviors
* of a file in the system. It implements an immutable design pattern where modification
* operations return new instances rather than modifying the existing one.
2026-02-14 01:29:34 +01:00
*
2025-03-26 19:08:07 +01:00
* This entity maintains both physical storage information and logical metadata about files,
* serving as the bridge between the storage system and the application.
*/
#[derive(Debug, Clone, PartialEq, Eq)]
2025-03-17 21:28:08 +01:00
pub struct File {
2025-03-26 19:08:07 +01:00
/// Unique identifier for the file - used throughout the system for file operations
2025-03-19 00:44:27 +01:00
id: String,
2026-02-14 01:29:34 +01:00
2025-03-26 19:08:07 +01:00
/// Name of the file including extension
2025-03-19 00:44:27 +01:00
name: String,
2026-02-14 01:29:34 +01:00
/// Path to the file in the domain model
2025-03-19 00:44:27 +01:00
storage_path: StoragePath,
2026-02-14 01:29:34 +01:00
/// String representation of the path for API compatibility
2025-03-19 00:44:27 +01:00
path_string: String,
2026-02-14 01:29:34 +01:00
2025-03-17 21:28:08 +01:00
/// Size of the file in bytes
2025-03-19 00:44:27 +01:00
size: u64,
2026-02-14 01:29:34 +01:00
2025-03-26 19:08:07 +01:00
/// MIME type of the file (e.g., "text/plain", "image/jpeg")
2025-03-19 00:44:27 +01:00
mime_type: String,
2026-02-14 01:29:34 +01:00
2025-03-26 19:08:07 +01:00
/// Parent folder ID if the file is within a folder, None if in root
2025-03-19 00:44:27 +01:00
folder_id: Option<String>,
2026-02-14 01:29:34 +01:00
2025-03-26 19:08:07 +01:00
/// Creation timestamp (seconds since UNIX epoch)
2025-03-19 00:44:27 +01:00
created_at: u64,
2026-02-14 01:29:34 +01:00
2025-03-26 19:08:07 +01:00
/// Last modification timestamp (seconds since UNIX epoch)
2025-03-19 00:44:27 +01:00
modified_at: u64,
/// Owner user ID (from storage.files.user_id)
owner_id: Option<Uuid>,
2025-03-17 21:28:08 +01:00
}
// We no longer need this module, now we use a String directly
2025-03-19 00:44:27 +01:00
2025-03-20 09:22:31 +01:00
impl Default for File {
fn default() -> Self {
Self {
id: "stub-id".to_string(),
name: "stub-file.txt".to_string(),
storage_path: StoragePath::from_string("/"),
path_string: "/".to_string(),
size: 0,
mime_type: "application/octet-stream".to_string(),
folder_id: None,
created_at: 0,
modified_at: 0,
owner_id: None,
2025-03-20 09:22:31 +01:00
}
}
}
2025-03-17 21:28:08 +01:00
impl File {
/// Creates a new file with validation
2025-03-17 21:28:08 +01:00
pub fn new(
id: String,
name: String,
2025-03-19 00:44:27 +01:00
storage_path: StoragePath,
2025-03-17 21:28:08 +01:00
size: u64,
mime_type: String,
folder_id: Option<String>,
2025-03-19 00:44:27 +01:00
) -> FileResult<Self> {
// Validate file name
2025-03-19 00:44:27 +01:00
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
2026-02-14 01:29:34 +01:00
2025-03-17 21:28:08 +01:00
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
2025-03-19 00:44:27 +01:00
.unwrap_or_default()
2025-03-17 21:28:08 +01:00
.as_secs();
2026-02-14 01:29:34 +01:00
// Store the path string for serialization compatibility
2025-03-19 00:44:27 +01:00
let path_string = storage_path.to_string();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok(Self {
2025-03-17 21:28:08 +01:00
id,
name,
2025-03-19 00:44:27 +01:00
storage_path,
path_string,
2025-03-17 21:28:08 +01:00
size,
mime_type,
folder_id,
created_at: now,
modified_at: now,
owner_id: None,
2025-03-19 00:44:27 +01:00
})
}
2026-02-14 01:29:34 +01:00
2025-04-09 00:21:20 +02:00
/// Creates a folder entity
pub fn new_folder(
id: String,
name: String,
storage_path: StoragePath,
parent_id: Option<String>,
created_at: u64,
modified_at: u64,
) -> FileResult<Self> {
// Validate folder name
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
2026-02-14 01:29:34 +01:00
2025-04-09 00:21:20 +02:00
// Store the path string for serialization compatibility
let path_string = storage_path.to_string();
2026-02-14 01:29:34 +01:00
2025-04-09 00:21:20 +02:00
Ok(Self {
id,
name,
storage_path,
path_string,
2026-02-14 01:29:34 +01:00
size: 0, // Folders have zero size
2025-04-09 00:21:20 +02:00
mime_type: "directory".to_string(), // Standard MIME type for directories
folder_id: parent_id,
created_at,
modified_at,
owner_id: None,
2025-04-09 00:21:20 +02:00
})
}
2026-02-14 01:29:34 +01:00
#[allow(clippy::too_many_arguments)]
2025-03-19 00:44:27 +01:00
pub fn with_timestamps(
id: String,
name: String,
storage_path: StoragePath,
size: u64,
mime_type: String,
folder_id: Option<String>,
created_at: u64,
modified_at: u64,
owner_id: Option<Uuid>,
2025-03-19 00:44:27 +01:00
) -> FileResult<Self> {
// Validate file name
2025-03-19 00:44:27 +01:00
if name.is_empty() || name.contains('/') || name.contains('\\') {
return Err(FileError::InvalidFileName(name));
}
2026-02-14 01:29:34 +01:00
// Store the path string for serialization compatibility
2025-03-19 00:44:27 +01:00
let path_string = storage_path.to_string();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok(Self {
id,
name,
storage_path,
path_string,
size,
mime_type,
folder_id,
created_at,
modified_at,
owner_id,
2025-03-19 00:44:27 +01:00
})
}
2026-02-14 01:29:34 +01:00
/// Consume the entity and return all fields by ownership.
///
/// Use this when converting `File` into a DTO to avoid cloning
/// every `String` field (saves 3-5 heap allocations per file).
pub fn into_parts(self) -> FileParts {
FileParts {
id: self.id,
name: self.name,
storage_path: self.storage_path,
path_string: self.path_string,
size: self.size,
mime_type: self.mime_type,
folder_id: self.folder_id,
created_at: self.created_at,
modified_at: self.modified_at,
owner_id: self.owner_id,
}
}
2025-03-19 00:44:27 +01:00
// Getters
pub fn id(&self) -> &str {
&self.id
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
pub fn name(&self) -> &str {
&self.name
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
pub fn storage_path(&self) -> &StoragePath {
&self.storage_path
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
pub fn path_string(&self) -> &str {
&self.path_string
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
pub fn size(&self) -> u64 {
self.size
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
pub fn mime_type(&self) -> &str {
&self.mime_type
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
pub fn folder_id(&self) -> Option<&str> {
self.folder_id.as_deref()
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
pub fn created_at(&self) -> u64 {
self.created_at
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
pub fn modified_at(&self) -> u64 {
self.modified_at
}
2026-02-14 01:29:34 +01:00
pub fn owner_id(&self) -> Option<Uuid> {
self.owner_id
}
#[allow(clippy::too_many_arguments)]
2025-03-19 00:44:27 +01:00
pub fn from_dto(
id: String,
name: String,
path: String,
size: u64,
mime_type: String,
folder_id: Option<String>,
created_at: u64,
modified_at: u64,
) -> Self {
// Create storage_path from string
2025-03-19 00:44:27 +01:00
let storage_path = StoragePath::from_string(&path);
2026-02-14 01:29:34 +01:00
// Create directly without validation to avoid errors in DTO conversions
2025-03-19 00:44:27 +01:00
Self {
id,
name,
storage_path,
path_string: path,
size,
mime_type,
folder_id,
created_at,
modified_at,
owner_id: None,
2025-03-19 00:44:27 +01:00
}
}
2026-02-14 01:29:34 +01:00
// Methods to create new versions of the file (immutable)
2026-02-14 01:29:34 +01:00
/// Creates a new version of the file with updated name
2025-03-19 00:44:27 +01:00
pub fn with_name(&self, new_name: String) -> FileResult<Self> {
// Validate file name
2025-03-19 00:44:27 +01:00
if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') {
return Err(FileError::InvalidFileName(new_name));
2025-03-17 21:28:08 +01:00
}
2026-02-14 01:29:34 +01:00
// Update path based on name
2025-03-19 00:44:27 +01:00
let parent_path = self.storage_path.parent();
let new_storage_path = match parent_path {
Some(parent) => parent.join(&new_name),
None => StoragePath::from_string(&new_name),
};
2026-02-14 01:29:34 +01:00
// Update string representation
2025-03-19 00:44:27 +01:00
let new_path_string = new_storage_path.to_string();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok(Self {
id: self.id.clone(),
name: new_name,
storage_path: new_storage_path,
path_string: new_path_string,
size: self.size,
mime_type: self.mime_type.clone(),
folder_id: self.folder_id.clone(),
created_at: self.created_at,
modified_at: now,
owner_id: self.owner_id,
2025-03-19 00:44:27 +01:00
})
2025-03-17 21:28:08 +01:00
}
2026-02-14 01:29:34 +01:00
/// Creates a new version of the file with updated folder
2026-02-14 01:29:34 +01:00
pub fn with_folder(
&self,
folder_id: Option<String>,
folder_path: Option<StoragePath>,
) -> FileResult<Self> {
// We need a folder path to update the file path
2025-03-19 00:44:27 +01:00
let new_storage_path = match folder_path {
Some(path) => path.join(&self.name),
None => StoragePath::from_string(&self.name), // Root
2025-03-19 00:44:27 +01:00
};
2026-02-14 01:29:34 +01:00
// Update string representation
2025-03-19 00:44:27 +01:00
let new_path_string = new_storage_path.to_string();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok(Self {
id: self.id.clone(),
name: self.name.clone(),
storage_path: new_storage_path,
path_string: new_path_string,
size: self.size,
mime_type: self.mime_type.clone(),
folder_id,
created_at: self.created_at,
modified_at: now,
owner_id: self.owner_id,
2025-03-19 00:44:27 +01:00
})
}
2026-02-14 01:29:34 +01:00
/// Creates a new version of the file with updated size
2025-03-19 00:44:27 +01:00
pub fn with_size(&self, new_size: u64) -> Self {
let now = std::time::SystemTime::now()
2025-03-17 21:28:08 +01:00
.duration_since(std::time::UNIX_EPOCH)
2025-03-19 00:44:27 +01:00
.unwrap_or_default()
2025-03-17 21:28:08 +01:00
.as_secs();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Self {
id: self.id.clone(),
name: self.name.clone(),
storage_path: self.storage_path.clone(),
path_string: self.path_string.clone(),
size: new_size,
mime_type: self.mime_type.clone(),
folder_id: self.folder_id.clone(),
created_at: self.created_at,
modified_at: now,
owner_id: self.owner_id,
2025-03-19 00:44:27 +01:00
}
}
}
#[cfg(test)]
mod tests {
use super::*;
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
#[test]
fn test_file_creation_with_valid_name() {
let storage_path = StoragePath::from_string("/test/file.txt");
let file = File::new(
"123".to_string(),
"file.txt".to_string(),
storage_path,
100,
"text/plain".to_string(),
None,
);
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
assert!(file.is_ok());
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
#[test]
fn test_file_creation_with_invalid_name() {
let storage_path = StoragePath::from_string("/test/invalid/file.txt");
let file = File::new(
"123".to_string(),
"file/with/slash.txt".to_string(), // Invalid name
2025-03-19 00:44:27 +01:00
storage_path,
100,
"text/plain".to_string(),
None,
);
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
assert!(file.is_err());
match file {
Err(FileError::InvalidFileName(_)) => (),
_ => panic!("Expected InvalidFileName error"),
}
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
#[test]
fn test_file_with_name() {
let storage_path = StoragePath::from_string("/test/file.txt");
let file = File::new(
"123".to_string(),
"file.txt".to_string(),
storage_path,
100,
"text/plain".to_string(),
None,
2026-02-14 01:29:34 +01:00
)
.unwrap();
2025-03-19 00:44:27 +01:00
let renamed = file.with_name("newname.txt".to_string());
assert!(renamed.is_ok());
let renamed = renamed.unwrap();
assert_eq!(renamed.name(), "newname.txt");
assert_eq!(renamed.id(), "123"); // The ID does not change
2025-03-17 21:28:08 +01:00
}
2026-02-14 01:29:34 +01:00
}