refactor: restructure domain crate by bounded context
This commit is contained in:
251
crates/domain/src/catalog/entities.rs
Normal file
251
crates/domain/src/catalog/entities.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Checksum, DateTimeStamp, StructuredData, SystemId};
|
||||
|
||||
// --- Asset ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AssetType {
|
||||
Image,
|
||||
Video,
|
||||
LivePhoto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SourceReference {
|
||||
pub volume_id: SystemId,
|
||||
pub relative_path: String,
|
||||
pub checksum: Checksum,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Asset {
|
||||
pub asset_id: SystemId,
|
||||
pub source_reference: SourceReference,
|
||||
pub asset_type: AssetType,
|
||||
pub mime_type: String,
|
||||
pub file_size: u64,
|
||||
pub is_processed: bool,
|
||||
pub owner_user_id: SystemId,
|
||||
pub created_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
pub fn new(
|
||||
source_reference: SourceReference,
|
||||
asset_type: AssetType,
|
||||
mime_type: impl Into<String>,
|
||||
file_size: u64,
|
||||
owner: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
asset_id: SystemId::new(),
|
||||
source_reference,
|
||||
asset_type,
|
||||
mime_type: mime_type.into(),
|
||||
file_size,
|
||||
is_processed: false,
|
||||
owner_user_id: owner,
|
||||
created_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_processed(&mut self) {
|
||||
self.is_processed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- AssetMetadata ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
pub enum MetadataSource {
|
||||
ExifExtracted,
|
||||
AiGenerated,
|
||||
UserEdited,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetMetadata {
|
||||
pub asset_id: SystemId,
|
||||
pub metadata_source: MetadataSource,
|
||||
pub data: StructuredData,
|
||||
pub updated_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl AssetMetadata {
|
||||
pub fn new(asset_id: SystemId, source: MetadataSource, data: StructuredData) -> Self {
|
||||
Self {
|
||||
asset_id,
|
||||
metadata_source: source,
|
||||
data,
|
||||
updated_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- AssetStack ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum StackType {
|
||||
LivePhoto,
|
||||
FormatPair,
|
||||
BurstSequence,
|
||||
ExposureBracket,
|
||||
ManualGroup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum StackMemberRole {
|
||||
PrimaryDisplay,
|
||||
HighResSource,
|
||||
MotionClip,
|
||||
AlternateFrame,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetStackMember {
|
||||
pub asset_id: SystemId,
|
||||
pub role: StackMemberRole,
|
||||
pub sort_order: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetStack {
|
||||
pub stack_id: SystemId,
|
||||
pub stack_type: StackType,
|
||||
pub primary_asset_id: SystemId,
|
||||
pub owner_user_id: SystemId,
|
||||
pub members: Vec<AssetStackMember>,
|
||||
}
|
||||
|
||||
impl AssetStack {
|
||||
pub fn new(stack_type: StackType, primary_asset_id: SystemId, owner: SystemId) -> Self {
|
||||
let primary_member = AssetStackMember {
|
||||
asset_id: primary_asset_id,
|
||||
role: StackMemberRole::PrimaryDisplay,
|
||||
sort_order: 0,
|
||||
};
|
||||
Self {
|
||||
stack_id: SystemId::new(),
|
||||
stack_type,
|
||||
primary_asset_id,
|
||||
owner_user_id: owner,
|
||||
members: vec![primary_member],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_member(&mut self, asset_id: SystemId, role: StackMemberRole) -> Result<(), DomainError> {
|
||||
if self.members.iter().any(|m| m.asset_id == asset_id) {
|
||||
return Err(DomainError::Conflict(
|
||||
"Asset already exists in stack".to_string(),
|
||||
));
|
||||
}
|
||||
let next_order = self.members.iter().map(|m| m.sort_order).max().unwrap_or(0) + 1;
|
||||
self.members.push(AssetStackMember {
|
||||
asset_id,
|
||||
role,
|
||||
sort_order: next_order,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- DerivativeAsset ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DerivativeProfile {
|
||||
ThumbnailSquare,
|
||||
ThumbnailLarge,
|
||||
WebOptimized,
|
||||
VideoSd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GenerationStatus {
|
||||
Pending,
|
||||
Ready,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DerivativeAsset {
|
||||
pub derivative_id: SystemId,
|
||||
pub parent_asset_id: SystemId,
|
||||
pub profile_type: DerivativeProfile,
|
||||
pub storage_path: String,
|
||||
pub mime_type: String,
|
||||
pub file_size: u64,
|
||||
pub dimensions: (u32, u32),
|
||||
pub generation_status: GenerationStatus,
|
||||
}
|
||||
|
||||
impl DerivativeAsset {
|
||||
pub fn new_pending(parent: SystemId, profile: DerivativeProfile, path: impl Into<String>) -> Self {
|
||||
Self {
|
||||
derivative_id: SystemId::new(),
|
||||
parent_asset_id: parent,
|
||||
profile_type: profile,
|
||||
storage_path: path.into(),
|
||||
mime_type: String::new(),
|
||||
file_size: 0,
|
||||
dimensions: (0, 0),
|
||||
generation_status: GenerationStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_ready(&mut self, mime_type: impl Into<String>, size: u64, dims: (u32, u32)) {
|
||||
self.mime_type = mime_type.into();
|
||||
self.file_size = size;
|
||||
self.dimensions = dims;
|
||||
self.generation_status = GenerationStatus::Ready;
|
||||
}
|
||||
|
||||
pub fn mark_failed(&mut self) {
|
||||
self.generation_status = GenerationStatus::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Duplicate ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DetectionMethod {
|
||||
ExactHash,
|
||||
PerceptualHash,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DuplicateStatus {
|
||||
Unresolved,
|
||||
Resolved,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DuplicateCandidate {
|
||||
pub asset_id: SystemId,
|
||||
pub similarity_score: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DuplicateGroup {
|
||||
pub group_id: SystemId,
|
||||
pub detection_method: DetectionMethod,
|
||||
pub status: DuplicateStatus,
|
||||
pub candidates: Vec<DuplicateCandidate>,
|
||||
}
|
||||
|
||||
impl DuplicateGroup {
|
||||
pub fn new_exact(asset_a: SystemId, asset_b: SystemId) -> Self {
|
||||
Self {
|
||||
group_id: SystemId::new(),
|
||||
detection_method: DetectionMethod::ExactHash,
|
||||
status: DuplicateStatus::Unresolved,
|
||||
candidates: vec![
|
||||
DuplicateCandidate { asset_id: asset_a, similarity_score: 1.0 },
|
||||
DuplicateCandidate { asset_id: asset_b, similarity_score: 1.0 },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&mut self) {
|
||||
self.status = DuplicateStatus::Resolved;
|
||||
}
|
||||
}
|
||||
7
crates/domain/src/catalog/mod.rs
Normal file
7
crates/domain/src/catalog/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod entities;
|
||||
pub mod ports;
|
||||
pub mod services;
|
||||
|
||||
pub use entities::*;
|
||||
pub use ports::*;
|
||||
pub use services::*;
|
||||
58
crates/domain/src/catalog/ports.rs
Normal file
58
crates/domain/src/catalog/ports.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Checksum, SystemId};
|
||||
use super::entities::{
|
||||
Asset, AssetMetadata, AssetStack, DerivativeAsset, DerivativeProfile,
|
||||
DuplicateGroup, MetadataSource,
|
||||
};
|
||||
|
||||
// --- AssetRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Asset>, DomainError>;
|
||||
async fn find_by_checksum(&self, checksum: &Checksum) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn find_by_owner(&self, owner_id: &SystemId, limit: u32, offset: u32) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn save(&self, asset: &Asset) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- AssetMetadataRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetMetadataRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<AssetMetadata>, DomainError>;
|
||||
async fn find_by_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<Option<AssetMetadata>, DomainError>;
|
||||
async fn save(&self, metadata: &AssetMetadata) -> Result<(), DomainError>;
|
||||
async fn delete_by_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- AssetStackRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetStackRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<AssetStack>, DomainError>;
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<AssetStack>, DomainError>;
|
||||
async fn save(&self, stack: &AssetStack) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- DerivativeRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait DerivativeRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<DerivativeAsset>, DomainError>;
|
||||
async fn find_by_asset_and_profile(&self, asset_id: &SystemId, profile: DerivativeProfile) -> Result<Option<DerivativeAsset>, DomainError>;
|
||||
async fn save(&self, derivative: &DerivativeAsset) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- DuplicateRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait DuplicateRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<DuplicateGroup>, DomainError>;
|
||||
async fn find_unresolved(&self) -> Result<Vec<DuplicateGroup>, DomainError>;
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<DuplicateGroup>, DomainError>;
|
||||
async fn save(&self, group: &DuplicateGroup) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::entities::{AssetMetadata, MetadataSource};
|
||||
use crate::value_objects::{MetadataValue, StructuredData};
|
||||
use super::entities::{AssetMetadata, MetadataSource};
|
||||
use crate::common::value_objects::{MetadataValue, StructuredData};
|
||||
|
||||
/// Merge metadata layers by priority: ExifExtracted < AiGenerated < UserEdited.
|
||||
/// Later (higher-priority) layers overwrite earlier ones.
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
use crate::common::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DomainEvent {
|
||||
4
crates/domain/src/common/mod.rs
Normal file
4
crates/domain/src/common/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod errors;
|
||||
pub mod events;
|
||||
pub mod ports;
|
||||
pub mod value_objects;
|
||||
@@ -1,5 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{errors::DomainError, events::DomainEvent};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::events::DomainEvent;
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventPublisher: Send + Sync {
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Checksum(String);
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Email(String);
|
||||
@@ -20,4 +20,3 @@ impl std::fmt::Display for Email {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
use crate::value_objects::{Checksum, DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AssetType {
|
||||
Image,
|
||||
Video,
|
||||
LivePhoto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SourceReference {
|
||||
pub volume_id: SystemId,
|
||||
pub relative_path: String,
|
||||
pub checksum: Checksum,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Asset {
|
||||
pub asset_id: SystemId,
|
||||
pub source_reference: SourceReference,
|
||||
pub asset_type: AssetType,
|
||||
pub mime_type: String,
|
||||
pub file_size: u64,
|
||||
pub is_processed: bool,
|
||||
pub owner_user_id: SystemId,
|
||||
pub created_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
pub fn new(
|
||||
source_reference: SourceReference,
|
||||
asset_type: AssetType,
|
||||
mime_type: impl Into<String>,
|
||||
file_size: u64,
|
||||
owner: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
asset_id: SystemId::new(),
|
||||
source_reference,
|
||||
asset_type,
|
||||
mime_type: mime_type.into(),
|
||||
file_size,
|
||||
is_processed: false,
|
||||
owner_user_id: owner,
|
||||
created_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_processed(&mut self) {
|
||||
self.is_processed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use crate::value_objects::{DateTimeStamp, StructuredData, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
pub enum MetadataSource {
|
||||
ExifExtracted,
|
||||
AiGenerated,
|
||||
UserEdited,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetMetadata {
|
||||
pub asset_id: SystemId,
|
||||
pub metadata_source: MetadataSource,
|
||||
pub data: StructuredData,
|
||||
pub updated_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl AssetMetadata {
|
||||
pub fn new(asset_id: SystemId, source: MetadataSource, data: StructuredData) -> Self {
|
||||
Self {
|
||||
asset_id,
|
||||
metadata_source: source,
|
||||
data,
|
||||
updated_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum StackType {
|
||||
LivePhoto,
|
||||
FormatPair,
|
||||
BurstSequence,
|
||||
ExposureBracket,
|
||||
ManualGroup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum StackMemberRole {
|
||||
PrimaryDisplay,
|
||||
HighResSource,
|
||||
MotionClip,
|
||||
AlternateFrame,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetStackMember {
|
||||
pub asset_id: SystemId,
|
||||
pub role: StackMemberRole,
|
||||
pub sort_order: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetStack {
|
||||
pub stack_id: SystemId,
|
||||
pub stack_type: StackType,
|
||||
pub primary_asset_id: SystemId,
|
||||
pub owner_user_id: SystemId,
|
||||
pub members: Vec<AssetStackMember>,
|
||||
}
|
||||
|
||||
impl AssetStack {
|
||||
pub fn new(stack_type: StackType, primary_asset_id: SystemId, owner: SystemId) -> Self {
|
||||
let primary_member = AssetStackMember {
|
||||
asset_id: primary_asset_id,
|
||||
role: StackMemberRole::PrimaryDisplay,
|
||||
sort_order: 0,
|
||||
};
|
||||
Self {
|
||||
stack_id: SystemId::new(),
|
||||
stack_type,
|
||||
primary_asset_id,
|
||||
owner_user_id: owner,
|
||||
members: vec![primary_member],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_member(&mut self, asset_id: SystemId, role: StackMemberRole) -> Result<(), DomainError> {
|
||||
if self.members.iter().any(|m| m.asset_id == asset_id) {
|
||||
return Err(DomainError::Conflict(
|
||||
"Asset already exists in stack".to_string(),
|
||||
));
|
||||
}
|
||||
let next_order = self.members.iter().map(|m| m.sort_order).max().unwrap_or(0) + 1;
|
||||
self.members.push(AssetStackMember {
|
||||
asset_id,
|
||||
role,
|
||||
sort_order: next_order,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::value_objects::{DateTimeStamp, FilterCriteria, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Collection {
|
||||
pub collection_id: SystemId,
|
||||
pub name: String,
|
||||
pub creator_user_id: SystemId,
|
||||
pub criteria: FilterCriteria,
|
||||
pub created_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn new(name: impl Into<String>, creator: SystemId, criteria: FilterCriteria) -> Self {
|
||||
Self {
|
||||
collection_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
creator_user_id: creator,
|
||||
criteria,
|
||||
created_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DerivativeProfile {
|
||||
ThumbnailSquare,
|
||||
ThumbnailLarge,
|
||||
WebOptimized,
|
||||
VideoSd,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GenerationStatus {
|
||||
Pending,
|
||||
Ready,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DerivativeAsset {
|
||||
pub derivative_id: SystemId,
|
||||
pub parent_asset_id: SystemId,
|
||||
pub profile_type: DerivativeProfile,
|
||||
pub storage_path: String,
|
||||
pub mime_type: String,
|
||||
pub file_size: u64,
|
||||
pub dimensions: (u32, u32),
|
||||
pub generation_status: GenerationStatus,
|
||||
}
|
||||
|
||||
impl DerivativeAsset {
|
||||
pub fn new_pending(parent: SystemId, profile: DerivativeProfile, path: impl Into<String>) -> Self {
|
||||
Self {
|
||||
derivative_id: SystemId::new(),
|
||||
parent_asset_id: parent,
|
||||
profile_type: profile,
|
||||
storage_path: path.into(),
|
||||
mime_type: String::new(),
|
||||
file_size: 0,
|
||||
dimensions: (0, 0),
|
||||
generation_status: GenerationStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_ready(&mut self, mime_type: impl Into<String>, size: u64, dims: (u32, u32)) {
|
||||
self.mime_type = mime_type.into();
|
||||
self.file_size = size;
|
||||
self.dimensions = dims;
|
||||
self.generation_status = GenerationStatus::Ready;
|
||||
}
|
||||
|
||||
pub fn mark_failed(&mut self) {
|
||||
self.generation_status = GenerationStatus::Failed;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DetectionMethod {
|
||||
ExactHash,
|
||||
PerceptualHash,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DuplicateStatus {
|
||||
Unresolved,
|
||||
Resolved,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DuplicateCandidate {
|
||||
pub asset_id: SystemId,
|
||||
pub similarity_score: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DuplicateGroup {
|
||||
pub group_id: SystemId,
|
||||
pub detection_method: DetectionMethod,
|
||||
pub status: DuplicateStatus,
|
||||
pub candidates: Vec<DuplicateCandidate>,
|
||||
}
|
||||
|
||||
impl DuplicateGroup {
|
||||
pub fn new_exact(asset_a: SystemId, asset_b: SystemId) -> Self {
|
||||
Self {
|
||||
group_id: SystemId::new(),
|
||||
detection_method: DetectionMethod::ExactHash,
|
||||
status: DuplicateStatus::Unresolved,
|
||||
candidates: vec![
|
||||
DuplicateCandidate { asset_id: asset_a, similarity_score: 1.0 },
|
||||
DuplicateCandidate { asset_id: asset_b, similarity_score: 1.0 },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&mut self) {
|
||||
self.status = DuplicateStatus::Resolved;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use crate::errors::DomainError;
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Group {
|
||||
pub group_id: SystemId,
|
||||
pub name: String,
|
||||
pub owner_user_id: SystemId,
|
||||
pub members: HashSet<SystemId>,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
pub fn new(name: impl Into<String>, owner_user_id: SystemId) -> Self {
|
||||
let mut members = HashSet::new();
|
||||
members.insert(owner_user_id);
|
||||
Self {
|
||||
group_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
owner_user_id,
|
||||
members,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
|
||||
if self.members.contains(&user_id) {
|
||||
return Err(DomainError::Conflict(format!("User {user_id} is already a member")));
|
||||
}
|
||||
self.members.insert(user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
|
||||
if user_id == self.owner_user_id {
|
||||
return Err(DomainError::Validation("Cannot remove the group owner".to_string()));
|
||||
}
|
||||
if !self.members.remove(&user_id) {
|
||||
return Err(DomainError::NotFound(format!("User {user_id} is not a member")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_member(&self, user_id: &SystemId) -> bool {
|
||||
self.members.contains(user_id)
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::value_objects::{Checksum, DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum IngestStatus {
|
||||
Uploading,
|
||||
AwaitingProcessing,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct IngestSession {
|
||||
pub session_id: SystemId,
|
||||
pub uploader_user_id: SystemId,
|
||||
pub client_device_id: String,
|
||||
pub original_filename: String,
|
||||
pub client_checksum: Checksum,
|
||||
pub target_library_path_id: SystemId,
|
||||
pub status: IngestStatus,
|
||||
pub created_at: DateTimeStamp,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl IngestSession {
|
||||
pub fn new(
|
||||
uploader: SystemId,
|
||||
device_id: impl Into<String>,
|
||||
filename: impl Into<String>,
|
||||
checksum: Checksum,
|
||||
target_path: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
session_id: SystemId::new(),
|
||||
uploader_user_id: uploader,
|
||||
client_device_id: device_id.into(),
|
||||
original_filename: filename.into(),
|
||||
client_checksum: checksum,
|
||||
target_library_path_id: target_path,
|
||||
status: IngestStatus::Uploading,
|
||||
created_at: DateTimeStamp::now(),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_to(&mut self, status: IngestStatus) -> Result<(), DomainError> {
|
||||
let valid = matches!(
|
||||
(self.status, status),
|
||||
(IngestStatus::Uploading, IngestStatus::AwaitingProcessing)
|
||||
| (IngestStatus::AwaitingProcessing, IngestStatus::Processing)
|
||||
| (IngestStatus::Processing, IngestStatus::Completed)
|
||||
) || (status == IngestStatus::Failed && !self.is_terminal());
|
||||
|
||||
if !valid {
|
||||
return Err(DomainError::Validation(format!(
|
||||
"Invalid transition from {:?} to {:?}",
|
||||
self.status, status
|
||||
)));
|
||||
}
|
||||
self.status = status;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fail(&mut self, message: impl Into<String>) {
|
||||
self.status = IngestStatus::Failed;
|
||||
self.error_message = Some(message.into());
|
||||
}
|
||||
|
||||
fn is_terminal(&self) -> bool {
|
||||
matches!(self.status, IngestStatus::Completed | IngestStatus::Failed)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use chrono::Utc;
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InviteCode {
|
||||
pub code_id: SystemId,
|
||||
pub scope_id: SystemId,
|
||||
pub created_by_user_id: SystemId,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub max_uses: Option<u32>,
|
||||
pub use_count: u32,
|
||||
pub assigned_role_id: SystemId,
|
||||
}
|
||||
|
||||
impl InviteCode {
|
||||
pub fn new(scope_id: SystemId, created_by: SystemId, role_id: SystemId) -> Self {
|
||||
Self {
|
||||
code_id: SystemId::new(),
|
||||
scope_id,
|
||||
created_by_user_id: created_by,
|
||||
expires_at: None,
|
||||
max_uses: None,
|
||||
use_count: 0,
|
||||
assigned_role_id: role_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if self.expires_at.is_some_and(|exp| exp.as_datetime() < &Utc::now()) {
|
||||
return false;
|
||||
}
|
||||
if self.max_uses.is_some_and(|max| self.use_count >= max) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_use(&mut self) {
|
||||
self.use_count += 1;
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::value_objects::{DateTimeStamp, StructuredData, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum JobType {
|
||||
ScanDirectory,
|
||||
ExtractMetadata,
|
||||
GenerateDerivative,
|
||||
SyncSidecar,
|
||||
DetectDuplicates,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl PartialEq for JobType {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::ScanDirectory, Self::ScanDirectory) => true,
|
||||
(Self::ExtractMetadata, Self::ExtractMetadata) => true,
|
||||
(Self::GenerateDerivative, Self::GenerateDerivative) => true,
|
||||
(Self::SyncSidecar, Self::SyncSidecar) => true,
|
||||
(Self::DetectDuplicates, Self::DetectDuplicates) => true,
|
||||
(Self::Custom(a), Self::Custom(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for JobType {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum JobStatus {
|
||||
Queued,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Job {
|
||||
pub job_id: SystemId,
|
||||
pub job_type: JobType,
|
||||
pub target_asset_id: Option<SystemId>,
|
||||
pub batch_id: Option<SystemId>,
|
||||
pub status: JobStatus,
|
||||
pub priority: u32,
|
||||
pub payload: StructuredData,
|
||||
pub result_data: Option<StructuredData>,
|
||||
pub retry_count: u32,
|
||||
pub max_retries: u32,
|
||||
pub created_at: DateTimeStamp,
|
||||
pub started_at: Option<DateTimeStamp>,
|
||||
pub completed_at: Option<DateTimeStamp>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl Job {
|
||||
pub fn new(job_type: JobType, priority: u32, payload: StructuredData) -> Self {
|
||||
Self {
|
||||
job_id: SystemId::new(),
|
||||
job_type,
|
||||
target_asset_id: None,
|
||||
batch_id: None,
|
||||
status: JobStatus::Queued,
|
||||
priority,
|
||||
payload,
|
||||
result_data: None,
|
||||
retry_count: 0,
|
||||
max_retries: 3,
|
||||
created_at: DateTimeStamp::now(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_target(mut self, asset_id: SystemId) -> Self {
|
||||
self.target_asset_id = Some(asset_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_batch(mut self, batch_id: SystemId) -> Self {
|
||||
self.batch_id = Some(batch_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> Result<(), DomainError> {
|
||||
if self.status != JobStatus::Queued {
|
||||
return Err(DomainError::Conflict(
|
||||
format!("Job can only start from Queued, currently {:?}", self.status),
|
||||
));
|
||||
}
|
||||
self.status = JobStatus::Processing;
|
||||
self.started_at = Some(DateTimeStamp::now());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, result: StructuredData) {
|
||||
self.status = JobStatus::Completed;
|
||||
self.result_data = Some(result);
|
||||
self.completed_at = Some(DateTimeStamp::now());
|
||||
}
|
||||
|
||||
pub fn fail(&mut self, error: impl Into<String>) {
|
||||
self.retry_count += 1;
|
||||
self.error_message = Some(error.into());
|
||||
self.started_at = None;
|
||||
if self.retry_count >= self.max_retries {
|
||||
self.status = JobStatus::Failed;
|
||||
} else {
|
||||
self.status = JobStatus::Queued;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
self.status = JobStatus::Cancelled;
|
||||
self.completed_at = Some(DateTimeStamp::now());
|
||||
}
|
||||
|
||||
pub fn can_retry(&self) -> bool {
|
||||
self.retry_count < self.max_retries
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum BatchStatus {
|
||||
InProgress,
|
||||
CompletedWithErrors,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct JobBatch {
|
||||
pub batch_id: SystemId,
|
||||
pub batch_type: String,
|
||||
pub total_jobs: u32,
|
||||
pub completed_count: u32,
|
||||
pub failed_count: u32,
|
||||
pub status: BatchStatus,
|
||||
}
|
||||
|
||||
impl JobBatch {
|
||||
pub fn new(batch_type: impl Into<String>, total_jobs: u32) -> Self {
|
||||
Self {
|
||||
batch_id: SystemId::new(),
|
||||
batch_type: batch_type.into(),
|
||||
total_jobs,
|
||||
completed_count: 0,
|
||||
failed_count: 0,
|
||||
status: BatchStatus::InProgress,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_completion(&mut self) {
|
||||
self.completed_count += 1;
|
||||
self.check_finished();
|
||||
}
|
||||
|
||||
pub fn record_failure(&mut self) {
|
||||
self.failed_count += 1;
|
||||
self.check_finished();
|
||||
}
|
||||
|
||||
pub fn progress_percent(&self) -> f64 {
|
||||
if self.total_jobs == 0 {
|
||||
return 100.0;
|
||||
}
|
||||
((self.completed_count + self.failed_count) as f64 / self.total_jobs as f64) * 100.0
|
||||
}
|
||||
|
||||
fn check_finished(&mut self) {
|
||||
if self.completed_count + self.failed_count >= self.total_jobs {
|
||||
self.status = if self.failed_count > 0 {
|
||||
BatchStatus::CompletedWithErrors
|
||||
} else {
|
||||
BatchStatus::Completed
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum OwnershipPolicy {
|
||||
UserOwned,
|
||||
GroupOwned,
|
||||
Unassigned,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct LibraryPath {
|
||||
pub path_id: SystemId,
|
||||
pub volume_id: SystemId,
|
||||
pub relative_path: String,
|
||||
pub is_ingest_destination: bool,
|
||||
pub ownership_policy: OwnershipPolicy,
|
||||
pub designated_owner_id: Option<SystemId>,
|
||||
}
|
||||
|
||||
impl LibraryPath {
|
||||
pub fn new_user_owned(
|
||||
volume_id: SystemId,
|
||||
relative_path: impl Into<String>,
|
||||
owner_id: SystemId,
|
||||
is_ingest_destination: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
path_id: SystemId::new(),
|
||||
volume_id,
|
||||
relative_path: relative_path.into(),
|
||||
is_ingest_destination,
|
||||
ownership_policy: OwnershipPolicy::UserOwned,
|
||||
designated_owner_id: Some(owner_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_unassigned(volume_id: SystemId, relative_path: impl Into<String>) -> Self {
|
||||
Self {
|
||||
path_id: SystemId::new(),
|
||||
volume_id,
|
||||
relative_path: relative_path.into(),
|
||||
is_ingest_destination: false,
|
||||
ownership_policy: OwnershipPolicy::Unassigned,
|
||||
designated_owner_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Identity & Access (Tasks 3-4)
|
||||
pub mod permission;
|
||||
pub mod role;
|
||||
mod user;
|
||||
mod group;
|
||||
|
||||
pub use permission::{Permission, PermissionAction, ResourceType};
|
||||
pub use role::Role;
|
||||
pub use user::User;
|
||||
pub use group::Group;
|
||||
|
||||
// Storage & Sources (Task 6)
|
||||
mod storage_volume;
|
||||
mod library_path;
|
||||
mod ingest_session;
|
||||
mod quota;
|
||||
|
||||
pub use storage_volume::StorageVolume;
|
||||
pub use library_path::{LibraryPath, OwnershipPolicy};
|
||||
pub use ingest_session::{IngestSession, IngestStatus};
|
||||
pub use quota::{QuotaDefinition, QuotaRule, TimePeriod, UsageLedgerEntry, UsageType};
|
||||
|
||||
// Media Catalog (Task 8)
|
||||
mod asset;
|
||||
mod asset_metadata;
|
||||
mod asset_stack;
|
||||
mod derivative_asset;
|
||||
mod duplicate;
|
||||
|
||||
pub use asset::{Asset, AssetType, SourceReference};
|
||||
pub use asset_metadata::{AssetMetadata, MetadataSource};
|
||||
pub use asset_stack::{AssetStack, AssetStackMember, StackMemberRole, StackType};
|
||||
pub use derivative_asset::{DerivativeAsset, DerivativeProfile, GenerationStatus};
|
||||
pub use duplicate::{DetectionMethod, DuplicateCandidate, DuplicateGroup, DuplicateStatus};
|
||||
|
||||
// Organization (Task 10)
|
||||
mod album;
|
||||
mod tag;
|
||||
mod collection;
|
||||
|
||||
pub use album::{Album, AlbumEntry};
|
||||
pub use tag::{AssetTag, Tag, TagSource};
|
||||
pub use collection::Collection;
|
||||
|
||||
// Sharing (Task 11)
|
||||
mod share_scope;
|
||||
mod share_target;
|
||||
mod share_link;
|
||||
mod invite_code;
|
||||
mod visibility_filter;
|
||||
|
||||
pub use share_scope::{ScopeType, ShareScope, ShareableType};
|
||||
pub use share_target::{ShareTarget, TargetType};
|
||||
pub use share_link::{LinkAccessLevel, ShareLink};
|
||||
pub use invite_code::InviteCode;
|
||||
pub use visibility_filter::VisibilityFilter;
|
||||
|
||||
// Sidecar Sync (Task 12)
|
||||
mod sidecar_record;
|
||||
mod sidecar_config;
|
||||
|
||||
pub use sidecar_record::{SidecarRecord, SyncStatus};
|
||||
pub use sidecar_config::{ConflictPolicy, SidecarConfig, SyncMode};
|
||||
|
||||
// Processing (Task 13)
|
||||
mod job;
|
||||
mod job_batch;
|
||||
mod plugin;
|
||||
mod processing_pipeline;
|
||||
|
||||
pub use job::{Job, JobStatus, JobType};
|
||||
pub use job_batch::{BatchStatus, JobBatch};
|
||||
pub use plugin::{Plugin, PluginType};
|
||||
pub use processing_pipeline::{PipelineStep, ProcessingPipeline};
|
||||
@@ -1,60 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PermissionAction {
|
||||
ReadAsset,
|
||||
ReadMetadata,
|
||||
ReadLocation,
|
||||
ReadPerson,
|
||||
WriteMetadata,
|
||||
DeleteAsset,
|
||||
ManageAccess,
|
||||
ManageUsers,
|
||||
ManageSystem,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ResourceType {
|
||||
Asset,
|
||||
Album,
|
||||
Collection,
|
||||
Person,
|
||||
Directory,
|
||||
Global,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Permission {
|
||||
pub action: PermissionAction,
|
||||
pub resource_type: ResourceType,
|
||||
}
|
||||
|
||||
impl Permission {
|
||||
pub fn new(action: PermissionAction, resource_type: ResourceType) -> Self {
|
||||
Self { action, resource_type }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn viewer_permissions() -> HashSet<Permission> {
|
||||
HashSet::from([
|
||||
Permission::new(PermissionAction::ReadAsset, ResourceType::Global),
|
||||
Permission::new(PermissionAction::ReadMetadata, ResourceType::Global),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn contributor_permissions() -> HashSet<Permission> {
|
||||
let mut perms = viewer_permissions();
|
||||
perms.insert(Permission::new(PermissionAction::WriteMetadata, ResourceType::Global));
|
||||
perms
|
||||
}
|
||||
|
||||
pub fn admin_permissions() -> HashSet<Permission> {
|
||||
let mut perms = contributor_permissions();
|
||||
perms.insert(Permission::new(PermissionAction::DeleteAsset, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageAccess, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageUsers, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageSystem, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ReadLocation, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ReadPerson, ResourceType::Global));
|
||||
perms
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use crate::value_objects::{StructuredData, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PluginType {
|
||||
MediaProcessor,
|
||||
ScheduledTask,
|
||||
SidecarWriter,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Plugin {
|
||||
pub plugin_id: SystemId,
|
||||
pub name: String,
|
||||
pub plugin_type: PluginType,
|
||||
pub is_enabled: bool,
|
||||
pub configuration: StructuredData,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub fn new(name: impl Into<String>, plugin_type: PluginType) -> Self {
|
||||
Self {
|
||||
plugin_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
plugin_type,
|
||||
is_enabled: true,
|
||||
configuration: StructuredData::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable(&mut self) {
|
||||
self.is_enabled = false;
|
||||
}
|
||||
|
||||
pub fn enable(&mut self) {
|
||||
self.is_enabled = true;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use crate::value_objects::{StructuredData, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PipelineStep {
|
||||
pub plugin_id: SystemId,
|
||||
pub step_order: u32,
|
||||
pub configuration: StructuredData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProcessingPipeline {
|
||||
pub pipeline_id: SystemId,
|
||||
pub trigger_event: String,
|
||||
pub steps: Vec<PipelineStep>,
|
||||
}
|
||||
|
||||
impl ProcessingPipeline {
|
||||
pub fn new(trigger_event: impl Into<String>) -> Self {
|
||||
Self {
|
||||
pipeline_id: SystemId::new(),
|
||||
trigger_event: trigger_event.into(),
|
||||
steps: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_step(&mut self, plugin_id: SystemId, config: StructuredData) {
|
||||
let next_order = self.steps.iter().map(|s| s.step_order).max().unwrap_or(0)
|
||||
+ if self.steps.is_empty() { 0 } else { 1 };
|
||||
self.steps.push(PipelineStep {
|
||||
plugin_id,
|
||||
step_order: next_order,
|
||||
configuration: config,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum UsageType {
|
||||
StorageBytes,
|
||||
ProcessJobs,
|
||||
ApiCalls,
|
||||
IndexingSize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TimePeriod {
|
||||
Daily,
|
||||
Monthly,
|
||||
Lifetime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct QuotaRule {
|
||||
pub rule_id: SystemId,
|
||||
pub dimension: UsageType,
|
||||
pub limit_value: u64,
|
||||
pub time_period: TimePeriod,
|
||||
pub is_unlimited: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct QuotaDefinition {
|
||||
pub quota_id: SystemId,
|
||||
pub owner_scope: SystemId,
|
||||
pub is_enforced: bool,
|
||||
pub rules: Vec<QuotaRule>,
|
||||
}
|
||||
|
||||
impl QuotaDefinition {
|
||||
pub fn new(owner_scope: SystemId) -> Self {
|
||||
Self {
|
||||
quota_id: SystemId::new(),
|
||||
owner_scope,
|
||||
is_enforced: true,
|
||||
rules: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_rule(&mut self, dimension: UsageType, limit_value: u64, time_period: TimePeriod) {
|
||||
self.rules.push(QuotaRule {
|
||||
rule_id: SystemId::new(),
|
||||
dimension,
|
||||
limit_value,
|
||||
time_period,
|
||||
is_unlimited: false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_unlimited_rule(&mut self, dimension: UsageType) {
|
||||
self.rules.push(QuotaRule {
|
||||
rule_id: SystemId::new(),
|
||||
dimension,
|
||||
limit_value: 0,
|
||||
time_period: TimePeriod::Lifetime,
|
||||
is_unlimited: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageLedgerEntry {
|
||||
pub entry_id: SystemId,
|
||||
pub user_id: SystemId,
|
||||
pub usage_type: UsageType,
|
||||
pub consumed_amount: u64,
|
||||
pub timestamp: DateTimeStamp,
|
||||
pub context: String,
|
||||
}
|
||||
|
||||
impl UsageLedgerEntry {
|
||||
pub fn new(
|
||||
user_id: SystemId,
|
||||
usage_type: UsageType,
|
||||
amount: u64,
|
||||
context: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
entry_id: SystemId::new(),
|
||||
user_id,
|
||||
usage_type,
|
||||
consumed_amount: amount,
|
||||
timestamp: DateTimeStamp::now(),
|
||||
context: context.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use crate::value_objects::SystemId;
|
||||
use super::permission::{Permission, PermissionAction, ResourceType};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Role {
|
||||
pub role_id: SystemId,
|
||||
pub name: String,
|
||||
pub permissions: HashSet<Permission>,
|
||||
pub is_system_default: bool,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn new(name: impl Into<String>, permissions: HashSet<Permission>, is_system_default: bool) -> Self {
|
||||
Self {
|
||||
role_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
permissions,
|
||||
is_system_default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_permission(&self, action: PermissionAction, resource_type: ResourceType) -> bool {
|
||||
self.permissions.contains(&Permission::new(action, resource_type))
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use chrono::Utc;
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum LinkAccessLevel {
|
||||
ViewOnly,
|
||||
LimitedSearch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareLink {
|
||||
pub scope_id: SystemId,
|
||||
pub token: String,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub access_level: LinkAccessLevel,
|
||||
pub is_active: bool,
|
||||
pub max_uses: Option<u32>,
|
||||
pub use_count: u32,
|
||||
}
|
||||
|
||||
impl ShareLink {
|
||||
pub fn new(scope_id: SystemId, token: impl Into<String>, access_level: LinkAccessLevel) -> Self {
|
||||
Self {
|
||||
scope_id,
|
||||
token: token.into(),
|
||||
expires_at: None,
|
||||
access_level,
|
||||
is_active: true,
|
||||
max_uses: None,
|
||||
use_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if !self.is_active {
|
||||
return false;
|
||||
}
|
||||
if self.expires_at.is_some_and(|exp| exp.as_datetime() < &Utc::now()) {
|
||||
return false;
|
||||
}
|
||||
if self.max_uses.is_some_and(|max| self.use_count >= max) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_use(&mut self) {
|
||||
self.use_count += 1;
|
||||
}
|
||||
|
||||
pub fn deactivate(&mut self) {
|
||||
self.is_active = false;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use chrono::Utc;
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ScopeType {
|
||||
Private,
|
||||
User,
|
||||
Group,
|
||||
Link,
|
||||
Public,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ShareableType {
|
||||
Asset,
|
||||
Album,
|
||||
Collection,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareScope {
|
||||
pub scope_id: SystemId,
|
||||
pub scope_type: ScopeType,
|
||||
pub shareable_type: ShareableType,
|
||||
pub shareable_id: SystemId,
|
||||
pub created_by_user_id: SystemId,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub created_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl ShareScope {
|
||||
pub fn new(
|
||||
scope_type: ScopeType,
|
||||
shareable_type: ShareableType,
|
||||
shareable_id: SystemId,
|
||||
created_by: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
scope_id: SystemId::new(),
|
||||
scope_type,
|
||||
shareable_type,
|
||||
shareable_id,
|
||||
created_by_user_id: created_by,
|
||||
expires_at: None,
|
||||
created_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
match &self.expires_at {
|
||||
Some(exp) => exp.as_datetime() < &Utc::now(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TargetType {
|
||||
User,
|
||||
Group,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareTarget {
|
||||
pub scope_id: SystemId,
|
||||
pub target_type: TargetType,
|
||||
pub target_id: SystemId,
|
||||
pub role_id: SystemId,
|
||||
}
|
||||
|
||||
impl ShareTarget {
|
||||
pub fn new(scope_id: SystemId, target_type: TargetType, target_id: SystemId, role_id: SystemId) -> Self {
|
||||
Self { scope_id, target_type, target_id, role_id }
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SyncMode {
|
||||
Auto,
|
||||
Scheduled,
|
||||
Manual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ConflictPolicy {
|
||||
DbWins,
|
||||
FileWins,
|
||||
RequireUserDecision,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SidecarConfig {
|
||||
pub export_base_path: String,
|
||||
pub sync_mode: SyncMode,
|
||||
pub conflict_resolution_policy: ConflictPolicy,
|
||||
}
|
||||
|
||||
impl Default for SidecarConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
export_base_path: "/kphotos/sidecars".to_string(),
|
||||
sync_mode: SyncMode::Auto,
|
||||
conflict_resolution_policy: ConflictPolicy::DbWins,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StorageVolume {
|
||||
pub volume_id: SystemId,
|
||||
pub volume_name: String,
|
||||
pub uri_prefix: String,
|
||||
pub is_writable: bool,
|
||||
pub available_bytes: u64,
|
||||
}
|
||||
|
||||
impl StorageVolume {
|
||||
pub fn new(name: impl Into<String>, uri_prefix: impl Into<String>, is_writable: bool) -> Self {
|
||||
Self {
|
||||
volume_id: SystemId::new(),
|
||||
volume_name: name.into(),
|
||||
uri_prefix: uri_prefix.into(),
|
||||
is_writable,
|
||||
available_bytes: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TagSource {
|
||||
UserManual,
|
||||
AiGenerated,
|
||||
ExifExtracted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Tag {
|
||||
pub tag_id: SystemId,
|
||||
pub name: String,
|
||||
pub tag_source: TagSource,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
pub fn new_manual(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
tag_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
tag_source: TagSource::UserManual,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetTag {
|
||||
pub asset_id: SystemId,
|
||||
pub tag_id: SystemId,
|
||||
pub tagged_by_user_id: Option<SystemId>,
|
||||
pub confidence: f64,
|
||||
}
|
||||
|
||||
impl AssetTag {
|
||||
pub fn new_manual(asset_id: SystemId, tag_id: SystemId, user_id: SystemId) -> Self {
|
||||
Self {
|
||||
asset_id,
|
||||
tag_id,
|
||||
tagged_by_user_id: Some(user_id),
|
||||
confidence: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::value_objects::{Email, PasswordHash, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct User {
|
||||
pub id: SystemId,
|
||||
pub username: String,
|
||||
pub email: Email,
|
||||
pub password_hash: PasswordHash,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(username: impl Into<String>, email: Email, password_hash: PasswordHash) -> Self {
|
||||
Self {
|
||||
id: SystemId::new(),
|
||||
username: username.into(),
|
||||
email,
|
||||
password_hash,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use crate::value_objects::{StructuredData, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VisibilityFilter {
|
||||
pub filter_id: SystemId,
|
||||
pub scope_id: SystemId,
|
||||
pub role_id: SystemId,
|
||||
pub hidden_fields: Vec<String>,
|
||||
}
|
||||
|
||||
impl VisibilityFilter {
|
||||
pub fn new(scope_id: SystemId, role_id: SystemId, hidden_fields: Vec<String>) -> Self {
|
||||
Self {
|
||||
filter_id: SystemId::new(),
|
||||
scope_id,
|
||||
role_id,
|
||||
hidden_fields,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, data: &StructuredData) -> StructuredData {
|
||||
let mut result = data.clone();
|
||||
for field in &self.hidden_fields {
|
||||
result.remove(field);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
158
crates/domain/src/identity/entities.rs
Normal file
158
crates/domain/src/identity/entities.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use std::collections::HashSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Email, PasswordHash, SystemId};
|
||||
|
||||
// --- Permission ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PermissionAction {
|
||||
ReadAsset,
|
||||
ReadMetadata,
|
||||
ReadLocation,
|
||||
ReadPerson,
|
||||
WriteMetadata,
|
||||
DeleteAsset,
|
||||
ManageAccess,
|
||||
ManageUsers,
|
||||
ManageSystem,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ResourceType {
|
||||
Asset,
|
||||
Album,
|
||||
Collection,
|
||||
Person,
|
||||
Directory,
|
||||
Global,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Permission {
|
||||
pub action: PermissionAction,
|
||||
pub resource_type: ResourceType,
|
||||
}
|
||||
|
||||
impl Permission {
|
||||
pub fn new(action: PermissionAction, resource_type: ResourceType) -> Self {
|
||||
Self { action, resource_type }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn viewer_permissions() -> HashSet<Permission> {
|
||||
HashSet::from([
|
||||
Permission::new(PermissionAction::ReadAsset, ResourceType::Global),
|
||||
Permission::new(PermissionAction::ReadMetadata, ResourceType::Global),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn contributor_permissions() -> HashSet<Permission> {
|
||||
let mut perms = viewer_permissions();
|
||||
perms.insert(Permission::new(PermissionAction::WriteMetadata, ResourceType::Global));
|
||||
perms
|
||||
}
|
||||
|
||||
pub fn admin_permissions() -> HashSet<Permission> {
|
||||
let mut perms = contributor_permissions();
|
||||
perms.insert(Permission::new(PermissionAction::DeleteAsset, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageAccess, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageUsers, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ManageSystem, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ReadLocation, ResourceType::Global));
|
||||
perms.insert(Permission::new(PermissionAction::ReadPerson, ResourceType::Global));
|
||||
perms
|
||||
}
|
||||
|
||||
// --- Role ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Role {
|
||||
pub role_id: SystemId,
|
||||
pub name: String,
|
||||
pub permissions: HashSet<Permission>,
|
||||
pub is_system_default: bool,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn new(name: impl Into<String>, permissions: HashSet<Permission>, is_system_default: bool) -> Self {
|
||||
Self {
|
||||
role_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
permissions,
|
||||
is_system_default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_permission(&self, action: PermissionAction, resource_type: ResourceType) -> bool {
|
||||
self.permissions.contains(&Permission::new(action, resource_type))
|
||||
}
|
||||
}
|
||||
|
||||
// --- User ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct User {
|
||||
pub id: SystemId,
|
||||
pub username: String,
|
||||
pub email: Email,
|
||||
pub password_hash: PasswordHash,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(username: impl Into<String>, email: Email, password_hash: PasswordHash) -> Self {
|
||||
Self {
|
||||
id: SystemId::new(),
|
||||
username: username.into(),
|
||||
email,
|
||||
password_hash,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Group ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Group {
|
||||
pub group_id: SystemId,
|
||||
pub name: String,
|
||||
pub owner_user_id: SystemId,
|
||||
pub members: HashSet<SystemId>,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
pub fn new(name: impl Into<String>, owner_user_id: SystemId) -> Self {
|
||||
let mut members = HashSet::new();
|
||||
members.insert(owner_user_id);
|
||||
Self {
|
||||
group_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
owner_user_id,
|
||||
members,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
|
||||
if self.members.contains(&user_id) {
|
||||
return Err(DomainError::Conflict(format!("User {user_id} is already a member")));
|
||||
}
|
||||
self.members.insert(user_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
|
||||
if user_id == self.owner_user_id {
|
||||
return Err(DomainError::Validation("Cannot remove the group owner".to_string()));
|
||||
}
|
||||
if !self.members.remove(&user_id) {
|
||||
return Err(DomainError::NotFound(format!("User {user_id} is not a member")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_member(&self, user_id: &SystemId) -> bool {
|
||||
self.members.contains(user_id)
|
||||
}
|
||||
}
|
||||
7
crates/domain/src/identity/mod.rs
Normal file
7
crates/domain/src/identity/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod entities;
|
||||
pub mod ports;
|
||||
pub mod services;
|
||||
|
||||
pub use entities::*;
|
||||
pub use ports::*;
|
||||
pub use services::*;
|
||||
50
crates/domain/src/identity/ports.rs
Normal file
50
crates/domain/src/identity/ports.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Email, PasswordHash, SystemId};
|
||||
use super::entities::{Group, Role, User};
|
||||
|
||||
// --- UserRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<User>, DomainError>;
|
||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
||||
async fn find_by_username(&self, username: &str) -> Result<Option<User>, DomainError>;
|
||||
async fn save(&self, user: &User) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- RoleRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait RoleRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Role>, DomainError>;
|
||||
async fn find_by_name(&self, name: &str) -> Result<Option<Role>, DomainError>;
|
||||
async fn find_defaults(&self) -> Result<Vec<Role>, DomainError>;
|
||||
async fn save(&self, role: &Role) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- GroupRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait GroupRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Group>, DomainError>;
|
||||
async fn find_by_user(&self, user_id: &SystemId) -> Result<Vec<Group>, DomainError>;
|
||||
async fn save(&self, group: &Group) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- Auth ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait PasswordHasher: Send + Sync {
|
||||
async fn hash(&self, password: &str) -> Result<PasswordHash, DomainError>;
|
||||
async fn verify(&self, password: &str, hash: &PasswordHash) -> Result<bool, DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait TokenIssuer: Send + Sync {
|
||||
async fn issue(&self, user_id: &SystemId, role: &str) -> Result<String, DomainError>;
|
||||
async fn verify(&self, token: &str) -> Result<(SystemId, String), DomainError>;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
use crate::entities::{Permission, PermissionAction, ResourceType, Role};
|
||||
use super::entities::{Permission, PermissionAction, ResourceType, Role};
|
||||
|
||||
pub struct PermissionChecker;
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
pub mod entities;
|
||||
pub mod errors;
|
||||
pub mod events;
|
||||
pub mod ports;
|
||||
pub mod services;
|
||||
pub mod value_objects;
|
||||
pub mod common;
|
||||
pub mod identity;
|
||||
pub mod storage;
|
||||
pub mod catalog;
|
||||
pub mod organization;
|
||||
pub mod sharing;
|
||||
pub mod sidecar;
|
||||
pub mod processing;
|
||||
|
||||
// Facade — old import paths still work
|
||||
pub mod errors {
|
||||
pub use crate::common::errors::*;
|
||||
}
|
||||
pub mod events {
|
||||
pub use crate::common::events::*;
|
||||
}
|
||||
pub mod value_objects {
|
||||
pub use crate::common::value_objects::*;
|
||||
}
|
||||
pub mod entities {
|
||||
pub use crate::identity::entities::*;
|
||||
pub use crate::storage::entities::*;
|
||||
pub use crate::catalog::entities::*;
|
||||
pub use crate::organization::entities::*;
|
||||
pub use crate::sharing::entities::*;
|
||||
pub use crate::sidecar::entities::*;
|
||||
pub use crate::processing::entities::*;
|
||||
|
||||
// Sub-module alias for `domain::entities::permission::` imports
|
||||
pub mod permission {
|
||||
pub use crate::identity::entities::{
|
||||
Permission, PermissionAction, ResourceType,
|
||||
viewer_permissions, contributor_permissions, admin_permissions,
|
||||
};
|
||||
}
|
||||
}
|
||||
pub mod ports {
|
||||
pub use crate::common::ports::*;
|
||||
pub use crate::identity::ports::*;
|
||||
pub use crate::storage::ports::*;
|
||||
pub use crate::catalog::ports::*;
|
||||
pub use crate::organization::ports::*;
|
||||
pub use crate::sharing::ports::*;
|
||||
pub use crate::sidecar::ports::*;
|
||||
pub use crate::processing::ports::*;
|
||||
}
|
||||
pub mod services {
|
||||
pub mod permission_service {
|
||||
pub use crate::identity::services::*;
|
||||
}
|
||||
pub mod quota_checker {
|
||||
pub use crate::storage::services::*;
|
||||
}
|
||||
pub mod metadata_resolver {
|
||||
pub use crate::catalog::services::*;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::errors::DomainError;
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{DateTimeStamp, FilterCriteria, SystemId};
|
||||
|
||||
// --- Album ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AlbumEntry {
|
||||
@@ -62,3 +64,71 @@ impl Album {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tag ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TagSource {
|
||||
UserManual,
|
||||
AiGenerated,
|
||||
ExifExtracted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Tag {
|
||||
pub tag_id: SystemId,
|
||||
pub name: String,
|
||||
pub tag_source: TagSource,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
pub fn new_manual(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
tag_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
tag_source: TagSource::UserManual,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AssetTag {
|
||||
pub asset_id: SystemId,
|
||||
pub tag_id: SystemId,
|
||||
pub tagged_by_user_id: Option<SystemId>,
|
||||
pub confidence: f64,
|
||||
}
|
||||
|
||||
impl AssetTag {
|
||||
pub fn new_manual(asset_id: SystemId, tag_id: SystemId, user_id: SystemId) -> Self {
|
||||
Self {
|
||||
asset_id,
|
||||
tag_id,
|
||||
tagged_by_user_id: Some(user_id),
|
||||
confidence: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Collection ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Collection {
|
||||
pub collection_id: SystemId,
|
||||
pub name: String,
|
||||
pub creator_user_id: SystemId,
|
||||
pub criteria: FilterCriteria,
|
||||
pub created_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn new(name: impl Into<String>, creator: SystemId, criteria: FilterCriteria) -> Self {
|
||||
Self {
|
||||
collection_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
creator_user_id: creator,
|
||||
criteria,
|
||||
created_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/domain/src/organization/mod.rs
Normal file
5
crates/domain/src/organization/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod entities;
|
||||
pub mod ports;
|
||||
|
||||
pub use entities::*;
|
||||
pub use ports::*;
|
||||
36
crates/domain/src/organization/ports.rs
Normal file
36
crates/domain/src/organization/ports.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::SystemId;
|
||||
use super::entities::{Album, AssetTag, Collection, Tag};
|
||||
|
||||
// --- AlbumRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait AlbumRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Album>, DomainError>;
|
||||
async fn find_by_creator(&self, creator_id: &SystemId) -> Result<Vec<Album>, DomainError>;
|
||||
async fn save(&self, album: &Album) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- TagRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait TagRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Tag>, DomainError>;
|
||||
async fn find_by_name(&self, name: &str) -> Result<Option<Tag>, DomainError>;
|
||||
async fn find_tags_for_asset(&self, asset_id: &SystemId) -> Result<Vec<(Tag, AssetTag)>, DomainError>;
|
||||
async fn save_tag(&self, tag: &Tag) -> Result<(), DomainError>;
|
||||
async fn save_asset_tag(&self, asset_tag: &AssetTag) -> Result<(), DomainError>;
|
||||
async fn remove_asset_tag(&self, asset_id: &SystemId, tag_id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- CollectionRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait CollectionRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Collection>, DomainError>;
|
||||
async fn find_by_creator(&self, creator_id: &SystemId) -> Result<Vec<Collection>, DomainError>;
|
||||
async fn save(&self, collection: &Collection) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::Album, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AlbumRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Album>, DomainError>;
|
||||
async fn find_by_creator(&self, creator_id: &SystemId) -> Result<Vec<Album>, DomainError>;
|
||||
async fn save(&self, album: &Album) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::{AssetMetadata, MetadataSource}, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetMetadataRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<AssetMetadata>, DomainError>;
|
||||
async fn find_by_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<Option<AssetMetadata>, DomainError>;
|
||||
async fn save(&self, metadata: &AssetMetadata) -> Result<(), DomainError>;
|
||||
async fn delete_by_asset_and_source(&self, asset_id: &SystemId, source: MetadataSource) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::Asset, errors::DomainError, value_objects::{Checksum, SystemId}};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Asset>, DomainError>;
|
||||
async fn find_by_checksum(&self, checksum: &Checksum) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn find_by_owner(&self, owner_id: &SystemId, limit: u32, offset: u32) -> Result<Vec<Asset>, DomainError>;
|
||||
async fn save(&self, asset: &Asset) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::AssetStack, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AssetStackRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<AssetStack>, DomainError>;
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<AssetStack>, DomainError>;
|
||||
async fn save(&self, stack: &AssetStack) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{errors::DomainError, value_objects::{PasswordHash, SystemId}};
|
||||
|
||||
#[async_trait]
|
||||
pub trait PasswordHasher: Send + Sync {
|
||||
async fn hash(&self, password: &str) -> Result<PasswordHash, DomainError>;
|
||||
async fn verify(&self, password: &str, hash: &PasswordHash) -> Result<bool, DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait TokenIssuer: Send + Sync {
|
||||
async fn issue(&self, user_id: &SystemId, role: &str) -> Result<String, DomainError>;
|
||||
async fn verify(&self, token: &str) -> Result<(SystemId, String), DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::Collection, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait CollectionRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Collection>, DomainError>;
|
||||
async fn find_by_creator(&self, creator_id: &SystemId) -> Result<Vec<Collection>, DomainError>;
|
||||
async fn save(&self, collection: &Collection) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::{DerivativeAsset, DerivativeProfile}, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait DerivativeRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<DerivativeAsset>, DomainError>;
|
||||
async fn find_by_asset_and_profile(&self, asset_id: &SystemId, profile: DerivativeProfile) -> Result<Option<DerivativeAsset>, DomainError>;
|
||||
async fn save(&self, derivative: &DerivativeAsset) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::DuplicateGroup, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait DuplicateRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<DuplicateGroup>, DomainError>;
|
||||
async fn find_unresolved(&self) -> Result<Vec<DuplicateGroup>, DomainError>;
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Vec<DuplicateGroup>, DomainError>;
|
||||
async fn save(&self, group: &DuplicateGroup) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use crate::errors::DomainError;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileEntry {
|
||||
pub path: String,
|
||||
pub size_bytes: u64,
|
||||
pub is_directory: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait FileStoragePort: Send + Sync {
|
||||
async fn store_file(&self, path: &str, data: Bytes) -> Result<(), DomainError>;
|
||||
async fn read_file(&self, path: &str) -> Result<Bytes, DomainError>;
|
||||
async fn delete_file(&self, path: &str) -> Result<(), DomainError>;
|
||||
async fn list_directory(&self, path: &str) -> Result<Vec<FileEntry>, DomainError>;
|
||||
async fn file_exists(&self, path: &str) -> Result<bool, DomainError>;
|
||||
async fn available_space(&self) -> Result<u64, DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::Group, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait GroupRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Group>, DomainError>;
|
||||
async fn find_by_user(&self, user_id: &SystemId) -> Result<Vec<Group>, DomainError>;
|
||||
async fn save(&self, group: &Group) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::IngestSession, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait IngestSessionRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<IngestSession>, DomainError>;
|
||||
async fn find_by_user(&self, user_id: &SystemId) -> Result<Vec<IngestSession>, DomainError>;
|
||||
async fn save(&self, session: &IngestSession) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::JobBatch, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait JobBatchRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<JobBatch>, DomainError>;
|
||||
async fn save(&self, batch: &JobBatch) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::Job, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait JobRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Job>, DomainError>;
|
||||
async fn find_next_queued(&self) -> Result<Option<Job>, DomainError>;
|
||||
async fn find_by_batch(&self, batch_id: &SystemId) -> Result<Vec<Job>, DomainError>;
|
||||
async fn save(&self, job: &Job) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::LibraryPath, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait LibraryPathRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<LibraryPath>, DomainError>;
|
||||
async fn find_by_volume(&self, volume_id: &SystemId) -> Result<Vec<LibraryPath>, DomainError>;
|
||||
async fn find_ingest_destinations(&self, owner_id: &SystemId) -> Result<Vec<LibraryPath>, DomainError>;
|
||||
async fn save(&self, path: &LibraryPath) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Identity & Access (Tasks 4-5)
|
||||
mod auth;
|
||||
mod event_publisher;
|
||||
mod group_repo;
|
||||
mod role_repo;
|
||||
mod storage;
|
||||
mod user_repo;
|
||||
|
||||
pub use auth::{PasswordHasher, TokenIssuer};
|
||||
pub use event_publisher::EventPublisher;
|
||||
pub use group_repo::GroupRepository;
|
||||
pub use role_repo::RoleRepository;
|
||||
pub use storage::{DataStream, StoragePort, StorageReader, StorageWriter};
|
||||
pub use user_repo::UserRepository;
|
||||
|
||||
// Storage & Sources (Task 7)
|
||||
mod storage_volume_repo;
|
||||
mod library_path_repo;
|
||||
mod ingest_session_repo;
|
||||
mod quota_repo;
|
||||
mod file_storage;
|
||||
|
||||
pub use storage_volume_repo::StorageVolumeRepository;
|
||||
pub use library_path_repo::LibraryPathRepository;
|
||||
pub use ingest_session_repo::IngestSessionRepository;
|
||||
pub use quota_repo::{QuotaRepository, UsageLedgerRepository};
|
||||
pub use file_storage::{FileEntry, FileStoragePort};
|
||||
|
||||
// Media Catalog (Task 9)
|
||||
mod asset_repo;
|
||||
mod asset_metadata_repo;
|
||||
mod asset_stack_repo;
|
||||
mod derivative_repo;
|
||||
mod duplicate_repo;
|
||||
|
||||
pub use asset_repo::AssetRepository;
|
||||
pub use asset_metadata_repo::AssetMetadataRepository;
|
||||
pub use asset_stack_repo::AssetStackRepository;
|
||||
pub use derivative_repo::DerivativeRepository;
|
||||
pub use duplicate_repo::DuplicateRepository;
|
||||
|
||||
// Organization (Task 10)
|
||||
mod album_repo;
|
||||
mod tag_repo;
|
||||
mod collection_repo;
|
||||
|
||||
pub use album_repo::AlbumRepository;
|
||||
pub use tag_repo::TagRepository;
|
||||
pub use collection_repo::CollectionRepository;
|
||||
|
||||
// Sharing (Task 11)
|
||||
mod share_repo;
|
||||
mod visibility_filter_repo;
|
||||
|
||||
pub use share_repo::ShareRepository;
|
||||
pub use visibility_filter_repo::VisibilityFilterRepository;
|
||||
|
||||
// Sidecar Sync (Task 12)
|
||||
mod sidecar_repo;
|
||||
mod sidecar_writer;
|
||||
|
||||
pub use sidecar_repo::SidecarRepository;
|
||||
pub use sidecar_writer::SidecarWriterPort;
|
||||
|
||||
// Processing (Task 13)
|
||||
mod job_repo;
|
||||
mod job_batch_repo;
|
||||
mod plugin_repo;
|
||||
mod pipeline_repo;
|
||||
|
||||
pub use job_repo::JobRepository;
|
||||
pub use job_batch_repo::JobBatchRepository;
|
||||
pub use plugin_repo::PluginRepository;
|
||||
pub use pipeline_repo::PipelineRepository;
|
||||
@@ -1,9 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::ProcessingPipeline, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait PipelineRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<ProcessingPipeline>, DomainError>;
|
||||
async fn find_by_trigger(&self, event: &str) -> Result<Vec<ProcessingPipeline>, DomainError>;
|
||||
async fn save(&self, pipeline: &ProcessingPipeline) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::Plugin, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait PluginRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Plugin>, DomainError>;
|
||||
async fn find_enabled(&self) -> Result<Vec<Plugin>, DomainError>;
|
||||
async fn save(&self, plugin: &Plugin) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
entities::{QuotaDefinition, UsageLedgerEntry, UsageType},
|
||||
errors::DomainError,
|
||||
value_objects::{DateTimeStamp, SystemId},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait QuotaRepository: Send + Sync {
|
||||
async fn find_by_owner(&self, owner_id: &SystemId) -> Result<Option<QuotaDefinition>, DomainError>;
|
||||
async fn save(&self, quota: &QuotaDefinition) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UsageLedgerRepository: Send + Sync {
|
||||
async fn record(&self, entry: &UsageLedgerEntry) -> Result<(), DomainError>;
|
||||
async fn sum_usage(
|
||||
&self,
|
||||
user_id: &SystemId,
|
||||
usage_type: UsageType,
|
||||
since: Option<DateTimeStamp>,
|
||||
) -> Result<u64, DomainError>;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::Role, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait RoleRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Role>, DomainError>;
|
||||
async fn find_by_name(&self, name: &str) -> Result<Option<Role>, DomainError>;
|
||||
async fn find_defaults(&self) -> Result<Vec<Role>, DomainError>;
|
||||
async fn save(&self, role: &Role) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::{SidecarRecord, SyncStatus}, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait SidecarRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Option<SidecarRecord>, DomainError>;
|
||||
async fn find_by_status(&self, status: SyncStatus) -> Result<Vec<SidecarRecord>, DomainError>;
|
||||
async fn save(&self, record: &SidecarRecord) -> Result<(), DomainError>;
|
||||
async fn delete(&self, asset_id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{errors::DomainError, value_objects::StructuredData};
|
||||
|
||||
#[async_trait]
|
||||
pub trait SidecarWriterPort: Send + Sync {
|
||||
fn format_name(&self) -> &str;
|
||||
async fn write_sidecar(&self, data: &StructuredData, path: &str) -> Result<(), DomainError>;
|
||||
async fn read_sidecar(&self, path: &str) -> Result<StructuredData, DomainError>;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::{self, BoxStream, StreamExt};
|
||||
use crate::errors::DomainError;
|
||||
|
||||
pub type DataStream = BoxStream<'static, Result<Bytes, DomainError>>;
|
||||
|
||||
/// Read operations on object storage. Keys are full paths relative to the adapter root.
|
||||
#[async_trait]
|
||||
pub trait StorageReader: Send + Sync {
|
||||
/// Returns the content of `key` as a stream. Returns `DomainError::NotFound` if absent.
|
||||
async fn get(&self, key: &str) -> Result<DataStream, DomainError>;
|
||||
|
||||
/// Lists all keys whose path begins with `prefix`, or all keys when `prefix` is `None`.
|
||||
/// Returned keys are **full paths from the adapter root**, not relative to `prefix`.
|
||||
/// Example: `list(Some("docs"))` returns `["docs/readme.txt"]`, not `["readme.txt"]`.
|
||||
async fn list(&self, prefix: Option<&str>) -> Result<Vec<String>, DomainError>;
|
||||
|
||||
/// Convenience: reads the entire content of `key` into memory. Wraps `get`.
|
||||
async fn get_bytes(&self, key: &str) -> Result<Bytes, DomainError> {
|
||||
let mut stream = self.get(key).await?;
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
buf.extend_from_slice(&chunk?);
|
||||
}
|
||||
Ok(Bytes::from(buf))
|
||||
}
|
||||
}
|
||||
|
||||
/// Write operations on object storage.
|
||||
#[async_trait]
|
||||
pub trait StorageWriter: Send + Sync {
|
||||
/// Stores `data` at `key`. Overwrites any existing content at that key silently.
|
||||
async fn put(&self, key: &str, data: DataStream) -> Result<(), DomainError>;
|
||||
|
||||
/// Deletes `key`. Returns `Ok(())` even if the key does not exist (idempotent).
|
||||
async fn delete(&self, key: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Convenience: stores an in-memory buffer at `key`. Wraps `put`.
|
||||
async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), DomainError> {
|
||||
self.put(key, Box::pin(stream::once(async move { Ok(data) }))).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Combined read + write storage interface.
|
||||
///
|
||||
/// **Usage note:** `Arc<dyn StoragePort>` is the intended DI type everywhere.
|
||||
/// `StorageReader` and `StorageWriter` exist for implementation clarity, but Rust does not
|
||||
/// support narrowing `Arc<dyn StoragePort>` to `Arc<dyn StorageReader>` at runtime.
|
||||
/// Inject `Arc<dyn StoragePort>` into constructors and pass `.clone()` from the factory.
|
||||
pub trait StoragePort: StorageReader + StorageWriter {}
|
||||
impl<T: StorageReader + StorageWriter> StoragePort for T {}
|
||||
@@ -1,10 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::StorageVolume, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait StorageVolumeRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<StorageVolume>, DomainError>;
|
||||
async fn find_all(&self) -> Result<Vec<StorageVolume>, DomainError>;
|
||||
async fn save(&self, volume: &StorageVolume) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::{AssetTag, Tag}, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait TagRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Tag>, DomainError>;
|
||||
async fn find_by_name(&self, name: &str) -> Result<Option<Tag>, DomainError>;
|
||||
async fn find_tags_for_asset(&self, asset_id: &SystemId) -> Result<Vec<(Tag, AssetTag)>, DomainError>;
|
||||
async fn save_tag(&self, tag: &Tag) -> Result<(), DomainError>;
|
||||
async fn save_asset_tag(&self, asset_tag: &AssetTag) -> Result<(), DomainError>;
|
||||
async fn remove_asset_tag(&self, asset_id: &SystemId, tag_id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::User, errors::DomainError, value_objects::{Email, SystemId}};
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<User>, DomainError>;
|
||||
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError>;
|
||||
async fn find_by_username(&self, username: &str) -> Result<Option<User>, DomainError>;
|
||||
async fn save(&self, user: &User) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::VisibilityFilter, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait VisibilityFilterRepository: Send + Sync {
|
||||
async fn find_by_scope_and_role(&self, scope_id: &SystemId, role_id: &SystemId) -> Result<Option<VisibilityFilter>, DomainError>;
|
||||
async fn save(&self, filter: &VisibilityFilter) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
259
crates/domain/src/processing/entities.rs
Normal file
259
crates/domain/src/processing/entities.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{DateTimeStamp, StructuredData, SystemId};
|
||||
|
||||
// --- Job ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum JobType {
|
||||
ScanDirectory,
|
||||
ExtractMetadata,
|
||||
GenerateDerivative,
|
||||
SyncSidecar,
|
||||
DetectDuplicates,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl PartialEq for JobType {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::ScanDirectory, Self::ScanDirectory) => true,
|
||||
(Self::ExtractMetadata, Self::ExtractMetadata) => true,
|
||||
(Self::GenerateDerivative, Self::GenerateDerivative) => true,
|
||||
(Self::SyncSidecar, Self::SyncSidecar) => true,
|
||||
(Self::DetectDuplicates, Self::DetectDuplicates) => true,
|
||||
(Self::Custom(a), Self::Custom(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for JobType {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum JobStatus {
|
||||
Queued,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Job {
|
||||
pub job_id: SystemId,
|
||||
pub job_type: JobType,
|
||||
pub target_asset_id: Option<SystemId>,
|
||||
pub batch_id: Option<SystemId>,
|
||||
pub status: JobStatus,
|
||||
pub priority: u32,
|
||||
pub payload: StructuredData,
|
||||
pub result_data: Option<StructuredData>,
|
||||
pub retry_count: u32,
|
||||
pub max_retries: u32,
|
||||
pub created_at: DateTimeStamp,
|
||||
pub started_at: Option<DateTimeStamp>,
|
||||
pub completed_at: Option<DateTimeStamp>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl Job {
|
||||
pub fn new(job_type: JobType, priority: u32, payload: StructuredData) -> Self {
|
||||
Self {
|
||||
job_id: SystemId::new(),
|
||||
job_type,
|
||||
target_asset_id: None,
|
||||
batch_id: None,
|
||||
status: JobStatus::Queued,
|
||||
priority,
|
||||
payload,
|
||||
result_data: None,
|
||||
retry_count: 0,
|
||||
max_retries: 3,
|
||||
created_at: DateTimeStamp::now(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_target(mut self, asset_id: SystemId) -> Self {
|
||||
self.target_asset_id = Some(asset_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_batch(mut self, batch_id: SystemId) -> Self {
|
||||
self.batch_id = Some(batch_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> Result<(), DomainError> {
|
||||
if self.status != JobStatus::Queued {
|
||||
return Err(DomainError::Conflict(
|
||||
format!("Job can only start from Queued, currently {:?}", self.status),
|
||||
));
|
||||
}
|
||||
self.status = JobStatus::Processing;
|
||||
self.started_at = Some(DateTimeStamp::now());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, result: StructuredData) {
|
||||
self.status = JobStatus::Completed;
|
||||
self.result_data = Some(result);
|
||||
self.completed_at = Some(DateTimeStamp::now());
|
||||
}
|
||||
|
||||
pub fn fail(&mut self, error: impl Into<String>) {
|
||||
self.retry_count += 1;
|
||||
self.error_message = Some(error.into());
|
||||
self.started_at = None;
|
||||
if self.retry_count >= self.max_retries {
|
||||
self.status = JobStatus::Failed;
|
||||
} else {
|
||||
self.status = JobStatus::Queued;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
self.status = JobStatus::Cancelled;
|
||||
self.completed_at = Some(DateTimeStamp::now());
|
||||
}
|
||||
|
||||
pub fn can_retry(&self) -> bool {
|
||||
self.retry_count < self.max_retries
|
||||
}
|
||||
}
|
||||
|
||||
// --- JobBatch ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum BatchStatus {
|
||||
InProgress,
|
||||
CompletedWithErrors,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct JobBatch {
|
||||
pub batch_id: SystemId,
|
||||
pub batch_type: String,
|
||||
pub total_jobs: u32,
|
||||
pub completed_count: u32,
|
||||
pub failed_count: u32,
|
||||
pub status: BatchStatus,
|
||||
}
|
||||
|
||||
impl JobBatch {
|
||||
pub fn new(batch_type: impl Into<String>, total_jobs: u32) -> Self {
|
||||
Self {
|
||||
batch_id: SystemId::new(),
|
||||
batch_type: batch_type.into(),
|
||||
total_jobs,
|
||||
completed_count: 0,
|
||||
failed_count: 0,
|
||||
status: BatchStatus::InProgress,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_completion(&mut self) {
|
||||
self.completed_count += 1;
|
||||
self.check_finished();
|
||||
}
|
||||
|
||||
pub fn record_failure(&mut self) {
|
||||
self.failed_count += 1;
|
||||
self.check_finished();
|
||||
}
|
||||
|
||||
pub fn progress_percent(&self) -> f64 {
|
||||
if self.total_jobs == 0 {
|
||||
return 100.0;
|
||||
}
|
||||
((self.completed_count + self.failed_count) as f64 / self.total_jobs as f64) * 100.0
|
||||
}
|
||||
|
||||
fn check_finished(&mut self) {
|
||||
if self.completed_count + self.failed_count >= self.total_jobs {
|
||||
self.status = if self.failed_count > 0 {
|
||||
BatchStatus::CompletedWithErrors
|
||||
} else {
|
||||
BatchStatus::Completed
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Plugin ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PluginType {
|
||||
MediaProcessor,
|
||||
ScheduledTask,
|
||||
SidecarWriter,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Plugin {
|
||||
pub plugin_id: SystemId,
|
||||
pub name: String,
|
||||
pub plugin_type: PluginType,
|
||||
pub is_enabled: bool,
|
||||
pub configuration: StructuredData,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub fn new(name: impl Into<String>, plugin_type: PluginType) -> Self {
|
||||
Self {
|
||||
plugin_id: SystemId::new(),
|
||||
name: name.into(),
|
||||
plugin_type,
|
||||
is_enabled: true,
|
||||
configuration: StructuredData::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable(&mut self) {
|
||||
self.is_enabled = false;
|
||||
}
|
||||
|
||||
pub fn enable(&mut self) {
|
||||
self.is_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- ProcessingPipeline ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PipelineStep {
|
||||
pub plugin_id: SystemId,
|
||||
pub step_order: u32,
|
||||
pub configuration: StructuredData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProcessingPipeline {
|
||||
pub pipeline_id: SystemId,
|
||||
pub trigger_event: String,
|
||||
pub steps: Vec<PipelineStep>,
|
||||
}
|
||||
|
||||
impl ProcessingPipeline {
|
||||
pub fn new(trigger_event: impl Into<String>) -> Self {
|
||||
Self {
|
||||
pipeline_id: SystemId::new(),
|
||||
trigger_event: trigger_event.into(),
|
||||
steps: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_step(&mut self, plugin_id: SystemId, config: StructuredData) {
|
||||
let next_order = self.steps.iter().map(|s| s.step_order).max().unwrap_or(0)
|
||||
+ if self.steps.is_empty() { 0 } else { 1 };
|
||||
self.steps.push(PipelineStep {
|
||||
plugin_id,
|
||||
step_order: next_order,
|
||||
configuration: config,
|
||||
});
|
||||
}
|
||||
}
|
||||
5
crates/domain/src/processing/mod.rs
Normal file
5
crates/domain/src/processing/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod entities;
|
||||
pub mod ports;
|
||||
|
||||
pub use entities::*;
|
||||
pub use ports::*;
|
||||
40
crates/domain/src/processing/ports.rs
Normal file
40
crates/domain/src/processing/ports.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::SystemId;
|
||||
use super::entities::{Job, JobBatch, Plugin, ProcessingPipeline};
|
||||
|
||||
// --- JobRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait JobRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Job>, DomainError>;
|
||||
async fn find_next_queued(&self) -> Result<Option<Job>, DomainError>;
|
||||
async fn find_by_batch(&self, batch_id: &SystemId) -> Result<Vec<Job>, DomainError>;
|
||||
async fn save(&self, job: &Job) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- JobBatchRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait JobBatchRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<JobBatch>, DomainError>;
|
||||
async fn save(&self, batch: &JobBatch) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- PluginRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait PluginRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Plugin>, DomainError>;
|
||||
async fn find_enabled(&self) -> Result<Vec<Plugin>, DomainError>;
|
||||
async fn save(&self, plugin: &Plugin) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- PipelineRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait PipelineRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<ProcessingPipeline>, DomainError>;
|
||||
async fn find_by_trigger(&self, event: &str) -> Result<Vec<ProcessingPipeline>, DomainError>;
|
||||
async fn save(&self, pipeline: &ProcessingPipeline) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod metadata_resolver;
|
||||
pub mod permission_service;
|
||||
pub mod quota_checker;
|
||||
204
crates/domain/src/sharing/entities.rs
Normal file
204
crates/domain/src/sharing/entities.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use chrono::Utc;
|
||||
use crate::common::value_objects::{DateTimeStamp, StructuredData, SystemId};
|
||||
|
||||
// --- ShareScope ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ScopeType {
|
||||
Private,
|
||||
User,
|
||||
Group,
|
||||
Link,
|
||||
Public,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ShareableType {
|
||||
Asset,
|
||||
Album,
|
||||
Collection,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareScope {
|
||||
pub scope_id: SystemId,
|
||||
pub scope_type: ScopeType,
|
||||
pub shareable_type: ShareableType,
|
||||
pub shareable_id: SystemId,
|
||||
pub created_by_user_id: SystemId,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub created_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl ShareScope {
|
||||
pub fn new(
|
||||
scope_type: ScopeType,
|
||||
shareable_type: ShareableType,
|
||||
shareable_id: SystemId,
|
||||
created_by: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
scope_id: SystemId::new(),
|
||||
scope_type,
|
||||
shareable_type,
|
||||
shareable_id,
|
||||
created_by_user_id: created_by,
|
||||
expires_at: None,
|
||||
created_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
match &self.expires_at {
|
||||
Some(exp) => exp.as_datetime() < &Utc::now(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ShareTarget ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TargetType {
|
||||
User,
|
||||
Group,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareTarget {
|
||||
pub scope_id: SystemId,
|
||||
pub target_type: TargetType,
|
||||
pub target_id: SystemId,
|
||||
pub role_id: SystemId,
|
||||
}
|
||||
|
||||
impl ShareTarget {
|
||||
pub fn new(scope_id: SystemId, target_type: TargetType, target_id: SystemId, role_id: SystemId) -> Self {
|
||||
Self { scope_id, target_type, target_id, role_id }
|
||||
}
|
||||
}
|
||||
|
||||
// --- ShareLink ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum LinkAccessLevel {
|
||||
ViewOnly,
|
||||
LimitedSearch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareLink {
|
||||
pub scope_id: SystemId,
|
||||
pub token: String,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub access_level: LinkAccessLevel,
|
||||
pub is_active: bool,
|
||||
pub max_uses: Option<u32>,
|
||||
pub use_count: u32,
|
||||
}
|
||||
|
||||
impl ShareLink {
|
||||
pub fn new(scope_id: SystemId, token: impl Into<String>, access_level: LinkAccessLevel) -> Self {
|
||||
Self {
|
||||
scope_id,
|
||||
token: token.into(),
|
||||
expires_at: None,
|
||||
access_level,
|
||||
is_active: true,
|
||||
max_uses: None,
|
||||
use_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if !self.is_active {
|
||||
return false;
|
||||
}
|
||||
if self.expires_at.is_some_and(|exp| exp.as_datetime() < &Utc::now()) {
|
||||
return false;
|
||||
}
|
||||
if self.max_uses.is_some_and(|max| self.use_count >= max) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_use(&mut self) {
|
||||
self.use_count += 1;
|
||||
}
|
||||
|
||||
pub fn deactivate(&mut self) {
|
||||
self.is_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- InviteCode ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InviteCode {
|
||||
pub code_id: SystemId,
|
||||
pub scope_id: SystemId,
|
||||
pub created_by_user_id: SystemId,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub max_uses: Option<u32>,
|
||||
pub use_count: u32,
|
||||
pub assigned_role_id: SystemId,
|
||||
}
|
||||
|
||||
impl InviteCode {
|
||||
pub fn new(scope_id: SystemId, created_by: SystemId, role_id: SystemId) -> Self {
|
||||
Self {
|
||||
code_id: SystemId::new(),
|
||||
scope_id,
|
||||
created_by_user_id: created_by,
|
||||
expires_at: None,
|
||||
max_uses: None,
|
||||
use_count: 0,
|
||||
assigned_role_id: role_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if self.expires_at.is_some_and(|exp| exp.as_datetime() < &Utc::now()) {
|
||||
return false;
|
||||
}
|
||||
if self.max_uses.is_some_and(|max| self.use_count >= max) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_use(&mut self) {
|
||||
self.use_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// --- VisibilityFilter ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VisibilityFilter {
|
||||
pub filter_id: SystemId,
|
||||
pub scope_id: SystemId,
|
||||
pub role_id: SystemId,
|
||||
pub hidden_fields: Vec<String>,
|
||||
}
|
||||
|
||||
impl VisibilityFilter {
|
||||
pub fn new(scope_id: SystemId, role_id: SystemId, hidden_fields: Vec<String>) -> Self {
|
||||
Self {
|
||||
filter_id: SystemId::new(),
|
||||
scope_id,
|
||||
role_id,
|
||||
hidden_fields,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, data: &StructuredData) -> StructuredData {
|
||||
let mut result = data.clone();
|
||||
for field in &self.hidden_fields {
|
||||
result.remove(field);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
5
crates/domain/src/sharing/mod.rs
Normal file
5
crates/domain/src/sharing/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod entities;
|
||||
pub mod ports;
|
||||
|
||||
pub use entities::*;
|
||||
pub use ports::*;
|
||||
@@ -1,9 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
entities::{InviteCode, ShareLink, ShareScope, ShareTarget},
|
||||
errors::DomainError,
|
||||
value_objects::SystemId,
|
||||
};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::SystemId;
|
||||
use super::entities::{InviteCode, ShareLink, ShareScope, ShareTarget, VisibilityFilter};
|
||||
|
||||
// --- ShareRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait ShareRepository: Send + Sync {
|
||||
@@ -22,3 +22,12 @@ pub trait ShareRepository: Send + Sync {
|
||||
async fn save_invite(&self, invite: &InviteCode) -> Result<(), DomainError>;
|
||||
async fn find_invite_by_id(&self, id: &SystemId) -> Result<Option<InviteCode>, DomainError>;
|
||||
}
|
||||
|
||||
// --- VisibilityFilterRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait VisibilityFilterRepository: Send + Sync {
|
||||
async fn find_by_scope_and_role(&self, scope_id: &SystemId, role_id: &SystemId) -> Result<Option<VisibilityFilter>, DomainError>;
|
||||
async fn save(&self, filter: &VisibilityFilter) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::value_objects::{Checksum, DateTimeStamp, SystemId};
|
||||
use crate::common::value_objects::{Checksum, DateTimeStamp, SystemId};
|
||||
|
||||
// --- SidecarRecord ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SyncStatus {
|
||||
@@ -55,3 +57,36 @@ impl SidecarRecord {
|
||||
self.error_message = Some(message.into());
|
||||
}
|
||||
}
|
||||
|
||||
// --- SidecarConfig ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SyncMode {
|
||||
Auto,
|
||||
Scheduled,
|
||||
Manual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ConflictPolicy {
|
||||
DbWins,
|
||||
FileWins,
|
||||
RequireUserDecision,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SidecarConfig {
|
||||
pub export_base_path: String,
|
||||
pub sync_mode: SyncMode,
|
||||
pub conflict_resolution_policy: ConflictPolicy,
|
||||
}
|
||||
|
||||
impl Default for SidecarConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
export_base_path: "/kphotos/sidecars".to_string(),
|
||||
sync_mode: SyncMode::Auto,
|
||||
conflict_resolution_policy: ConflictPolicy::DbWins,
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/domain/src/sidecar/mod.rs
Normal file
5
crates/domain/src/sidecar/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod entities;
|
||||
pub mod ports;
|
||||
|
||||
pub use entities::*;
|
||||
pub use ports::*;
|
||||
23
crates/domain/src/sidecar/ports.rs
Normal file
23
crates/domain/src/sidecar/ports.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{StructuredData, SystemId};
|
||||
use super::entities::{SidecarRecord, SyncStatus};
|
||||
|
||||
// --- SidecarRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait SidecarRepository: Send + Sync {
|
||||
async fn find_by_asset(&self, asset_id: &SystemId) -> Result<Option<SidecarRecord>, DomainError>;
|
||||
async fn find_by_status(&self, status: SyncStatus) -> Result<Vec<SidecarRecord>, DomainError>;
|
||||
async fn save(&self, record: &SidecarRecord) -> Result<(), DomainError>;
|
||||
async fn delete(&self, asset_id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- SidecarWriterPort ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait SidecarWriterPort: Send + Sync {
|
||||
fn format_name(&self) -> &str;
|
||||
async fn write_sidecar(&self, data: &StructuredData, path: &str) -> Result<(), DomainError>;
|
||||
async fn read_sidecar(&self, path: &str) -> Result<StructuredData, DomainError>;
|
||||
}
|
||||
239
crates/domain/src/storage/entities.rs
Normal file
239
crates/domain/src/storage/entities.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{Checksum, DateTimeStamp, SystemId};
|
||||
|
||||
// --- StorageVolume ---
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StorageVolume {
|
||||
pub volume_id: SystemId,
|
||||
pub volume_name: String,
|
||||
pub uri_prefix: String,
|
||||
pub is_writable: bool,
|
||||
pub available_bytes: u64,
|
||||
}
|
||||
|
||||
impl StorageVolume {
|
||||
pub fn new(name: impl Into<String>, uri_prefix: impl Into<String>, is_writable: bool) -> Self {
|
||||
Self {
|
||||
volume_id: SystemId::new(),
|
||||
volume_name: name.into(),
|
||||
uri_prefix: uri_prefix.into(),
|
||||
is_writable,
|
||||
available_bytes: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- LibraryPath ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum OwnershipPolicy {
|
||||
UserOwned,
|
||||
GroupOwned,
|
||||
Unassigned,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct LibraryPath {
|
||||
pub path_id: SystemId,
|
||||
pub volume_id: SystemId,
|
||||
pub relative_path: String,
|
||||
pub is_ingest_destination: bool,
|
||||
pub ownership_policy: OwnershipPolicy,
|
||||
pub designated_owner_id: Option<SystemId>,
|
||||
}
|
||||
|
||||
impl LibraryPath {
|
||||
pub fn new_user_owned(
|
||||
volume_id: SystemId,
|
||||
relative_path: impl Into<String>,
|
||||
owner_id: SystemId,
|
||||
is_ingest_destination: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
path_id: SystemId::new(),
|
||||
volume_id,
|
||||
relative_path: relative_path.into(),
|
||||
is_ingest_destination,
|
||||
ownership_policy: OwnershipPolicy::UserOwned,
|
||||
designated_owner_id: Some(owner_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_unassigned(volume_id: SystemId, relative_path: impl Into<String>) -> Self {
|
||||
Self {
|
||||
path_id: SystemId::new(),
|
||||
volume_id,
|
||||
relative_path: relative_path.into(),
|
||||
is_ingest_destination: false,
|
||||
ownership_policy: OwnershipPolicy::Unassigned,
|
||||
designated_owner_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- IngestSession ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum IngestStatus {
|
||||
Uploading,
|
||||
AwaitingProcessing,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct IngestSession {
|
||||
pub session_id: SystemId,
|
||||
pub uploader_user_id: SystemId,
|
||||
pub client_device_id: String,
|
||||
pub original_filename: String,
|
||||
pub client_checksum: Checksum,
|
||||
pub target_library_path_id: SystemId,
|
||||
pub status: IngestStatus,
|
||||
pub created_at: DateTimeStamp,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl IngestSession {
|
||||
pub fn new(
|
||||
uploader: SystemId,
|
||||
device_id: impl Into<String>,
|
||||
filename: impl Into<String>,
|
||||
checksum: Checksum,
|
||||
target_path: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
session_id: SystemId::new(),
|
||||
uploader_user_id: uploader,
|
||||
client_device_id: device_id.into(),
|
||||
original_filename: filename.into(),
|
||||
client_checksum: checksum,
|
||||
target_library_path_id: target_path,
|
||||
status: IngestStatus::Uploading,
|
||||
created_at: DateTimeStamp::now(),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_to(&mut self, status: IngestStatus) -> Result<(), DomainError> {
|
||||
let valid = matches!(
|
||||
(self.status, status),
|
||||
(IngestStatus::Uploading, IngestStatus::AwaitingProcessing)
|
||||
| (IngestStatus::AwaitingProcessing, IngestStatus::Processing)
|
||||
| (IngestStatus::Processing, IngestStatus::Completed)
|
||||
) || (status == IngestStatus::Failed && !self.is_terminal());
|
||||
|
||||
if !valid {
|
||||
return Err(DomainError::Validation(format!(
|
||||
"Invalid transition from {:?} to {:?}",
|
||||
self.status, status
|
||||
)));
|
||||
}
|
||||
self.status = status;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fail(&mut self, message: impl Into<String>) {
|
||||
self.status = IngestStatus::Failed;
|
||||
self.error_message = Some(message.into());
|
||||
}
|
||||
|
||||
fn is_terminal(&self) -> bool {
|
||||
matches!(self.status, IngestStatus::Completed | IngestStatus::Failed)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Quota ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum UsageType {
|
||||
StorageBytes,
|
||||
ProcessJobs,
|
||||
ApiCalls,
|
||||
IndexingSize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TimePeriod {
|
||||
Daily,
|
||||
Monthly,
|
||||
Lifetime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct QuotaRule {
|
||||
pub rule_id: SystemId,
|
||||
pub dimension: UsageType,
|
||||
pub limit_value: u64,
|
||||
pub time_period: TimePeriod,
|
||||
pub is_unlimited: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct QuotaDefinition {
|
||||
pub quota_id: SystemId,
|
||||
pub owner_scope: SystemId,
|
||||
pub is_enforced: bool,
|
||||
pub rules: Vec<QuotaRule>,
|
||||
}
|
||||
|
||||
impl QuotaDefinition {
|
||||
pub fn new(owner_scope: SystemId) -> Self {
|
||||
Self {
|
||||
quota_id: SystemId::new(),
|
||||
owner_scope,
|
||||
is_enforced: true,
|
||||
rules: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_rule(&mut self, dimension: UsageType, limit_value: u64, time_period: TimePeriod) {
|
||||
self.rules.push(QuotaRule {
|
||||
rule_id: SystemId::new(),
|
||||
dimension,
|
||||
limit_value,
|
||||
time_period,
|
||||
is_unlimited: false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_unlimited_rule(&mut self, dimension: UsageType) {
|
||||
self.rules.push(QuotaRule {
|
||||
rule_id: SystemId::new(),
|
||||
dimension,
|
||||
limit_value: 0,
|
||||
time_period: TimePeriod::Lifetime,
|
||||
is_unlimited: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageLedgerEntry {
|
||||
pub entry_id: SystemId,
|
||||
pub user_id: SystemId,
|
||||
pub usage_type: UsageType,
|
||||
pub consumed_amount: u64,
|
||||
pub timestamp: DateTimeStamp,
|
||||
pub context: String,
|
||||
}
|
||||
|
||||
impl UsageLedgerEntry {
|
||||
pub fn new(
|
||||
user_id: SystemId,
|
||||
usage_type: UsageType,
|
||||
amount: u64,
|
||||
context: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
entry_id: SystemId::new(),
|
||||
user_id,
|
||||
usage_type,
|
||||
consumed_amount: amount,
|
||||
timestamp: DateTimeStamp::now(),
|
||||
context: context.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/domain/src/storage/mod.rs
Normal file
7
crates/domain/src/storage/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod entities;
|
||||
pub mod ports;
|
||||
pub mod services;
|
||||
|
||||
pub use entities::*;
|
||||
pub use ports::*;
|
||||
pub use services::*;
|
||||
128
crates/domain/src/storage/ports.rs
Normal file
128
crates/domain/src/storage/ports.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::{self, BoxStream, StreamExt};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::common::value_objects::{DateTimeStamp, SystemId};
|
||||
use super::entities::{
|
||||
IngestSession, LibraryPath, QuotaDefinition, StorageVolume,
|
||||
UsageLedgerEntry, UsageType,
|
||||
};
|
||||
|
||||
// --- StorageVolumeRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait StorageVolumeRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<StorageVolume>, DomainError>;
|
||||
async fn find_all(&self) -> Result<Vec<StorageVolume>, DomainError>;
|
||||
async fn save(&self, volume: &StorageVolume) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- LibraryPathRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait LibraryPathRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<LibraryPath>, DomainError>;
|
||||
async fn find_by_volume(&self, volume_id: &SystemId) -> Result<Vec<LibraryPath>, DomainError>;
|
||||
async fn find_ingest_destinations(&self, owner_id: &SystemId) -> Result<Vec<LibraryPath>, DomainError>;
|
||||
async fn save(&self, path: &LibraryPath) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- IngestSessionRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait IngestSessionRepository: Send + Sync {
|
||||
async fn find_by_id(&self, id: &SystemId) -> Result<Option<IngestSession>, DomainError>;
|
||||
async fn find_by_user(&self, user_id: &SystemId) -> Result<Vec<IngestSession>, DomainError>;
|
||||
async fn save(&self, session: &IngestSession) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// --- QuotaRepository ---
|
||||
|
||||
#[async_trait]
|
||||
pub trait QuotaRepository: Send + Sync {
|
||||
async fn find_by_owner(&self, owner_id: &SystemId) -> Result<Option<QuotaDefinition>, DomainError>;
|
||||
async fn save(&self, quota: &QuotaDefinition) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UsageLedgerRepository: Send + Sync {
|
||||
async fn record(&self, entry: &UsageLedgerEntry) -> Result<(), DomainError>;
|
||||
async fn sum_usage(
|
||||
&self,
|
||||
user_id: &SystemId,
|
||||
usage_type: UsageType,
|
||||
since: Option<DateTimeStamp>,
|
||||
) -> Result<u64, DomainError>;
|
||||
}
|
||||
|
||||
// --- FileStoragePort ---
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileEntry {
|
||||
pub path: String,
|
||||
pub size_bytes: u64,
|
||||
pub is_directory: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait FileStoragePort: Send + Sync {
|
||||
async fn store_file(&self, path: &str, data: Bytes) -> Result<(), DomainError>;
|
||||
async fn read_file(&self, path: &str) -> Result<Bytes, DomainError>;
|
||||
async fn delete_file(&self, path: &str) -> Result<(), DomainError>;
|
||||
async fn list_directory(&self, path: &str) -> Result<Vec<FileEntry>, DomainError>;
|
||||
async fn file_exists(&self, path: &str) -> Result<bool, DomainError>;
|
||||
async fn available_space(&self) -> Result<u64, DomainError>;
|
||||
}
|
||||
|
||||
// --- StoragePort (object storage) ---
|
||||
|
||||
pub type DataStream = BoxStream<'static, Result<Bytes, DomainError>>;
|
||||
|
||||
/// Read operations on object storage. Keys are full paths relative to the adapter root.
|
||||
#[async_trait]
|
||||
pub trait StorageReader: Send + Sync {
|
||||
/// Returns the content of `key` as a stream. Returns `DomainError::NotFound` if absent.
|
||||
async fn get(&self, key: &str) -> Result<DataStream, DomainError>;
|
||||
|
||||
/// Lists all keys whose path begins with `prefix`, or all keys when `prefix` is `None`.
|
||||
/// Returned keys are **full paths from the adapter root**, not relative to `prefix`.
|
||||
/// Example: `list(Some("docs"))` returns `["docs/readme.txt"]`, not `["readme.txt"]`.
|
||||
async fn list(&self, prefix: Option<&str>) -> Result<Vec<String>, DomainError>;
|
||||
|
||||
/// Convenience: reads the entire content of `key` into memory. Wraps `get`.
|
||||
async fn get_bytes(&self, key: &str) -> Result<Bytes, DomainError> {
|
||||
let mut stream = self.get(key).await?;
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
buf.extend_from_slice(&chunk?);
|
||||
}
|
||||
Ok(Bytes::from(buf))
|
||||
}
|
||||
}
|
||||
|
||||
/// Write operations on object storage.
|
||||
#[async_trait]
|
||||
pub trait StorageWriter: Send + Sync {
|
||||
/// Stores `data` at `key`. Overwrites any existing content at that key silently.
|
||||
async fn put(&self, key: &str, data: DataStream) -> Result<(), DomainError>;
|
||||
|
||||
/// Deletes `key`. Returns `Ok(())` even if the key does not exist (idempotent).
|
||||
async fn delete(&self, key: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Convenience: stores an in-memory buffer at `key`. Wraps `put`.
|
||||
async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), DomainError> {
|
||||
self.put(key, Box::pin(stream::once(async move { Ok(data) }))).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Combined read + write storage interface.
|
||||
///
|
||||
/// **Usage note:** `Arc<dyn StoragePort>` is the intended DI type everywhere.
|
||||
/// `StorageReader` and `StorageWriter` exist for implementation clarity, but Rust does not
|
||||
/// support narrowing `Arc<dyn StoragePort>` to `Arc<dyn StorageReader>` at runtime.
|
||||
/// Inject `Arc<dyn StoragePort>` into constructors and pass `.clone()` from the factory.
|
||||
pub trait StoragePort: StorageReader + StorageWriter {}
|
||||
impl<T: StorageReader + StorageWriter> StoragePort for T {}
|
||||
@@ -1,6 +1,6 @@
|
||||
use chrono::{Datelike, NaiveDate, TimeZone, Utc};
|
||||
use crate::entities::{QuotaDefinition, TimePeriod, UsageType};
|
||||
use crate::value_objects::DateTimeStamp;
|
||||
use super::entities::{QuotaDefinition, TimePeriod, UsageType};
|
||||
use crate::common::value_objects::DateTimeStamp;
|
||||
|
||||
pub struct QuotaCheckResult {
|
||||
pub allowed: bool,
|
||||
Reference in New Issue
Block a user