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 super::entities::{AssetMetadata, MetadataSource};
|
||||||
use crate::value_objects::{MetadataValue, StructuredData};
|
use crate::common::value_objects::{MetadataValue, StructuredData};
|
||||||
|
|
||||||
/// Merge metadata layers by priority: ExifExtracted < AiGenerated < UserEdited.
|
/// Merge metadata layers by priority: ExifExtracted < AiGenerated < UserEdited.
|
||||||
/// Later (higher-priority) layers overwrite earlier ones.
|
/// 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)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum DomainEvent {
|
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 async_trait::async_trait;
|
||||||
use crate::{errors::DomainError, events::DomainEvent};
|
use crate::common::errors::DomainError;
|
||||||
|
use crate::common::events::DomainEvent;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait EventPublisher: Send + Sync {
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Checksum(String);
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Email(String);
|
pub struct Email(String);
|
||||||
@@ -20,4 +20,3 @@ impl std::fmt::Display for Email {
|
|||||||
write!(f, "{}", self.0)
|
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 std::collections::HashSet;
|
||||||
use crate::entities::{Permission, PermissionAction, ResourceType, Role};
|
use super::entities::{Permission, PermissionAction, ResourceType, Role};
|
||||||
|
|
||||||
pub struct PermissionChecker;
|
pub struct PermissionChecker;
|
||||||
|
|
||||||
@@ -1,6 +1,57 @@
|
|||||||
pub mod entities;
|
pub mod common;
|
||||||
pub mod errors;
|
pub mod identity;
|
||||||
pub mod events;
|
pub mod storage;
|
||||||
pub mod ports;
|
pub mod catalog;
|
||||||
pub mod services;
|
pub mod organization;
|
||||||
pub mod value_objects;
|
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::common::errors::DomainError;
|
||||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
use crate::common::value_objects::{DateTimeStamp, FilterCriteria, SystemId};
|
||||||
|
|
||||||
|
// --- Album ---
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct AlbumEntry {
|
pub struct AlbumEntry {
|
||||||
@@ -62,3 +64,71 @@ impl Album {
|
|||||||
self.entries.len()
|
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 async_trait::async_trait;
|
||||||
use crate::{
|
use crate::common::errors::DomainError;
|
||||||
entities::{InviteCode, ShareLink, ShareScope, ShareTarget},
|
use crate::common::value_objects::SystemId;
|
||||||
errors::DomainError,
|
use super::entities::{InviteCode, ShareLink, ShareScope, ShareTarget, VisibilityFilter};
|
||||||
value_objects::SystemId,
|
|
||||||
};
|
// --- ShareRepository ---
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ShareRepository: Send + Sync {
|
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 save_invite(&self, invite: &InviteCode) -> Result<(), DomainError>;
|
||||||
async fn find_invite_by_id(&self, id: &SystemId) -> Result<Option<InviteCode>, 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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum SyncStatus {
|
pub enum SyncStatus {
|
||||||
@@ -55,3 +57,36 @@ impl SidecarRecord {
|
|||||||
self.error_message = Some(message.into());
|
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 chrono::{Datelike, NaiveDate, TimeZone, Utc};
|
||||||
use crate::entities::{QuotaDefinition, TimePeriod, UsageType};
|
use super::entities::{QuotaDefinition, TimePeriod, UsageType};
|
||||||
use crate::value_objects::DateTimeStamp;
|
use crate::common::value_objects::DateTimeStamp;
|
||||||
|
|
||||||
pub struct QuotaCheckResult {
|
pub struct QuotaCheckResult {
|
||||||
pub allowed: bool,
|
pub allowed: bool,
|
||||||
117
crates/domain/tests/catalog/entities.rs
Normal file
117
crates/domain/tests/catalog/entities.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use domain::entities::{
|
||||||
|
Asset, AssetMetadata, AssetStack, AssetType, DerivativeAsset, DerivativeProfile,
|
||||||
|
DetectionMethod, DuplicateGroup, DuplicateStatus, GenerationStatus,
|
||||||
|
MetadataSource, SourceReference, StackMemberRole, StackType,
|
||||||
|
};
|
||||||
|
use domain::errors::DomainError;
|
||||||
|
use domain::value_objects::{Checksum, MetadataValue, StructuredData, SystemId};
|
||||||
|
|
||||||
|
// --- Asset ---
|
||||||
|
|
||||||
|
fn make_asset() -> Asset {
|
||||||
|
let src = SourceReference {
|
||||||
|
volume_id: SystemId::new(),
|
||||||
|
relative_path: "photos/img.jpg".to_string(),
|
||||||
|
checksum: Checksum::new("a".repeat(64)).unwrap(),
|
||||||
|
};
|
||||||
|
Asset::new(src, AssetType::Image, "image/jpeg", 1024, SystemId::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_asset_is_unprocessed() {
|
||||||
|
let a = make_asset();
|
||||||
|
assert!(!a.is_processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mark_processed() {
|
||||||
|
let mut a = make_asset();
|
||||||
|
a.mark_processed();
|
||||||
|
assert!(a.is_processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AssetMetadata ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn metadata_source_ordering() {
|
||||||
|
assert!(MetadataSource::ExifExtracted < MetadataSource::AiGenerated);
|
||||||
|
assert!(MetadataSource::AiGenerated < MetadataSource::UserEdited);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_metadata_layer() {
|
||||||
|
let mut data = StructuredData::new();
|
||||||
|
data.insert("camera", MetadataValue::String("Canon".to_string()));
|
||||||
|
let meta = AssetMetadata::new(SystemId::new(), MetadataSource::ExifExtracted, data);
|
||||||
|
assert_eq!(meta.metadata_source, MetadataSource::ExifExtracted);
|
||||||
|
assert_eq!(meta.data.get_string("camera"), Some("Canon"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AssetStack ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_stack_contains_primary() {
|
||||||
|
let primary = SystemId::new();
|
||||||
|
let stack = AssetStack::new(StackType::LivePhoto, primary.clone(), SystemId::new());
|
||||||
|
assert_eq!(stack.members.len(), 1);
|
||||||
|
assert_eq!(stack.members[0].asset_id, primary);
|
||||||
|
assert_eq!(stack.members[0].role, StackMemberRole::PrimaryDisplay);
|
||||||
|
assert_eq!(stack.members[0].sort_order, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_motion_clip() {
|
||||||
|
let mut stack = AssetStack::new(StackType::LivePhoto, SystemId::new(), SystemId::new());
|
||||||
|
let clip_id = SystemId::new();
|
||||||
|
stack.add_member(clip_id.clone(), StackMemberRole::MotionClip).unwrap();
|
||||||
|
assert_eq!(stack.members.len(), 2);
|
||||||
|
assert_eq!(stack.members[1].asset_id, clip_id);
|
||||||
|
assert_eq!(stack.members[1].sort_order, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_add_duplicate_to_stack() {
|
||||||
|
let primary = SystemId::new();
|
||||||
|
let mut stack = AssetStack::new(StackType::LivePhoto, primary.clone(), SystemId::new());
|
||||||
|
let result = stack.add_member(primary, StackMemberRole::HighResSource);
|
||||||
|
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DerivativeAsset ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derivative_lifecycle() {
|
||||||
|
let mut d = DerivativeAsset::new_pending(
|
||||||
|
SystemId::new(),
|
||||||
|
DerivativeProfile::ThumbnailSquare,
|
||||||
|
"/thumbs/abc.webp",
|
||||||
|
);
|
||||||
|
assert_eq!(d.generation_status, GenerationStatus::Pending);
|
||||||
|
assert_eq!(d.file_size, 0);
|
||||||
|
assert_eq!(d.dimensions, (0, 0));
|
||||||
|
|
||||||
|
d.mark_ready("image/webp", 4096, (256, 256));
|
||||||
|
assert_eq!(d.generation_status, GenerationStatus::Ready);
|
||||||
|
assert_eq!(d.mime_type, "image/webp");
|
||||||
|
assert_eq!(d.file_size, 4096);
|
||||||
|
assert_eq!(d.dimensions, (256, 256));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Duplicate ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_duplicate_group() {
|
||||||
|
let g = DuplicateGroup::new_exact(SystemId::new(), SystemId::new());
|
||||||
|
assert_eq!(g.detection_method, DetectionMethod::ExactHash);
|
||||||
|
assert_eq!(g.status, DuplicateStatus::Unresolved);
|
||||||
|
assert_eq!(g.candidates.len(), 2);
|
||||||
|
assert_eq!(g.candidates[0].similarity_score, 1.0);
|
||||||
|
assert_eq!(g.candidates[1].similarity_score, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_group() {
|
||||||
|
let mut g = DuplicateGroup::new_exact(SystemId::new(), SystemId::new());
|
||||||
|
g.resolve();
|
||||||
|
assert_eq!(g.status, DuplicateStatus::Resolved);
|
||||||
|
}
|
||||||
2
crates/domain/tests/catalog/mod.rs
Normal file
2
crates/domain/tests/catalog/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod entities;
|
||||||
|
mod services;
|
||||||
2
crates/domain/tests/common/mod.rs
Normal file
2
crates/domain/tests/common/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod value_objects;
|
||||||
|
mod events;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user