domain: add Storage ports (BYOS, Quota, LibraryPath) and QuotaChecker service
This commit is contained in:
20
crates/domain/src/ports/file_storage.rs
Normal file
20
crates/domain/src/ports/file_storage.rs
Normal 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>;
|
||||||
|
}
|
||||||
9
crates/domain/src/ports/ingest_session_repo.rs
Normal file
9
crates/domain/src/ports/ingest_session_repo.rs
Normal 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>;
|
||||||
|
}
|
||||||
11
crates/domain/src/ports/library_path_repo.rs
Normal file
11
crates/domain/src/ports/library_path_repo.rs
Normal 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>;
|
||||||
|
}
|
||||||
@@ -4,6 +4,11 @@ mod group_repo;
|
|||||||
mod role_repo;
|
mod role_repo;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod user_repo;
|
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 auth::{PasswordHasher, TokenIssuer};
|
||||||
pub use event_publisher::EventPublisher;
|
pub use event_publisher::EventPublisher;
|
||||||
@@ -11,3 +16,8 @@ pub use group_repo::GroupRepository;
|
|||||||
pub use role_repo::RoleRepository;
|
pub use role_repo::RoleRepository;
|
||||||
pub use storage::{DataStream, StoragePort, StorageReader, StorageWriter};
|
pub use storage::{DataStream, StoragePort, StorageReader, StorageWriter};
|
||||||
pub use user_repo::UserRepository;
|
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};
|
||||||
|
|||||||
24
crates/domain/src/ports/quota_repo.rs
Normal file
24
crates/domain/src/ports/quota_repo.rs
Normal 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>;
|
||||||
|
}
|
||||||
10
crates/domain/src/ports/storage_volume_repo.rs
Normal file
10
crates/domain/src/ports/storage_volume_repo.rs
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
mod permission_service;
|
mod permission_service;
|
||||||
|
mod quota_checker;
|
||||||
|
|||||||
53
crates/domain/tests/services/quota_checker.rs
Normal file
53
crates/domain/tests/services/quota_checker.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use domain::entities::{QuotaDefinition, TimePeriod, UsageType};
|
||||||
|
use domain::services::quota_checker::check_quota;
|
||||||
|
use domain::value_objects::SystemId;
|
||||||
|
|
||||||
|
fn make_quota(limit: u64) -> QuotaDefinition {
|
||||||
|
let mut q = QuotaDefinition::new(SystemId::new());
|
||||||
|
q.add_rule(UsageType::StorageBytes, limit, TimePeriod::Monthly);
|
||||||
|
q
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allows_within_limit() {
|
||||||
|
let q = make_quota(1000);
|
||||||
|
let r = check_quota(&q, UsageType::StorageBytes, 500, 400);
|
||||||
|
assert!(r.allowed);
|
||||||
|
assert!(!r.is_unlimited);
|
||||||
|
assert_eq!(r.limit, 1000);
|
||||||
|
assert_eq!(r.current_usage, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_over_limit() {
|
||||||
|
let q = make_quota(1000);
|
||||||
|
let r = check_quota(&q, UsageType::StorageBytes, 800, 300);
|
||||||
|
assert!(!r.allowed);
|
||||||
|
assert_eq!(r.limit, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unlimited_always_allowed() {
|
||||||
|
let mut q = QuotaDefinition::new(SystemId::new());
|
||||||
|
q.add_unlimited_rule(UsageType::StorageBytes);
|
||||||
|
let r = check_quota(&q, UsageType::StorageBytes, u64::MAX, 1);
|
||||||
|
assert!(r.allowed);
|
||||||
|
assert!(r.is_unlimited);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unenforced_allows_all() {
|
||||||
|
let mut q = make_quota(100);
|
||||||
|
q.is_enforced = false;
|
||||||
|
let r = check_quota(&q, UsageType::StorageBytes, 9999, 9999);
|
||||||
|
assert!(r.allowed);
|
||||||
|
assert!(r.is_unlimited);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_rule_allows() {
|
||||||
|
let q = make_quota(1000); // rule for StorageBytes only
|
||||||
|
let r = check_quota(&q, UsageType::ApiCalls, 9999, 9999);
|
||||||
|
assert!(r.allowed);
|
||||||
|
assert!(r.is_unlimited);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user