domain: add Storage ports (BYOS, Quota, LibraryPath) and QuotaChecker service

This commit is contained in:
2026-05-31 03:23:38 +02:00
parent 3c5c4ed9b1
commit ed6eb0c28a
9 changed files with 211 additions and 1 deletions

View File

@@ -0,0 +1,20 @@
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>;
}

View File

@@ -0,0 +1,9 @@
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>;
}

View File

@@ -0,0 +1,11 @@
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>;
}

View File

@@ -4,6 +4,11 @@ mod group_repo;
mod role_repo;
mod storage;
mod user_repo;
mod storage_volume_repo;
mod library_path_repo;
mod ingest_session_repo;
mod quota_repo;
mod file_storage;
pub use auth::{PasswordHasher, TokenIssuer};
pub use event_publisher::EventPublisher;
@@ -11,3 +16,8 @@ pub use group_repo::GroupRepository;
pub use role_repo::RoleRepository;
pub use storage::{DataStream, StoragePort, StorageReader, StorageWriter};
pub use user_repo::UserRepository;
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};

View File

@@ -0,0 +1,24 @@
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>;
}

View File

@@ -0,0 +1,10 @@
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>;
}

View File

@@ -1 +1,73 @@
// Quota checker — will be implemented in Task 7
use chrono::{Datelike, NaiveDate, TimeZone, Utc};
use crate::entities::{QuotaDefinition, TimePeriod, UsageType};
use crate::value_objects::DateTimeStamp;
pub struct QuotaCheckResult {
pub allowed: bool,
pub current_usage: u64,
pub limit: u64,
pub is_unlimited: bool,
}
pub fn check_quota(
quota: &QuotaDefinition,
usage_type: UsageType,
current_usage: u64,
requested_amount: u64,
) -> QuotaCheckResult {
if !quota.is_enforced {
return QuotaCheckResult {
allowed: true,
current_usage,
limit: 0,
is_unlimited: true,
};
}
let rule = quota.rules.iter().find(|r| r.dimension == usage_type);
let Some(rule) = rule else {
return QuotaCheckResult {
allowed: true,
current_usage,
limit: 0,
is_unlimited: true,
};
};
if rule.is_unlimited {
return QuotaCheckResult {
allowed: true,
current_usage,
limit: 0,
is_unlimited: true,
};
}
QuotaCheckResult {
allowed: current_usage + requested_amount <= rule.limit_value,
current_usage,
limit: rule.limit_value,
is_unlimited: false,
}
}
pub fn period_start(period: TimePeriod) -> Option<DateTimeStamp> {
let now = Utc::now();
match period {
TimePeriod::Daily => {
let start = NaiveDate::from_ymd_opt(now.year(), now.month(), now.day())
.expect("valid date")
.and_hms_opt(0, 0, 0)
.expect("valid time");
Some(DateTimeStamp::from_datetime(Utc.from_utc_datetime(&start)))
}
TimePeriod::Monthly => {
let start = NaiveDate::from_ymd_opt(now.year(), now.month(), 1)
.expect("valid date")
.and_hms_opt(0, 0, 0)
.expect("valid time");
Some(DateTimeStamp::from_datetime(Utc.from_utc_datetime(&start)))
}
TimePeriod::Lifetime => None,
}
}