//! Managed temporary storage use cases. use std::sync::Arc; use domain::ports::{Clock, IdGenerator, ManagedTempStore}; use domain::{ ManagedTempAllocation, ManagedTempGcReport, ManagedTempHandle, ManagedTempKind, ManagedTempLease, ProjectId, }; use crate::error::AppError; /// Default lease TTL for managed temp allocations: 24 hours. pub const MANAGED_TEMP_DEFAULT_TTL_MS: u64 = 24 * 60 * 60 * 1_000; /// Maximum purpose length retained in metadata. pub const MANAGED_TEMP_PURPOSE_MAX_CHARS: usize = 120; /// Input for [`AllocateManagedTemp`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AllocateManagedTempInput { /// Optional project that requested this allocation. pub owner_project_id: Option, /// Physical allocation kind. pub kind: ManagedTempKind, /// Short human/debug purpose. pub purpose: String, /// Optional TTL override in milliseconds. `None` uses the default. pub ttl_ms: Option, } /// Output for [`AllocateManagedTemp`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AllocateManagedTempOutput { /// Allocated lease. pub lease: ManagedTempLease, } /// Allocates an IdeA-managed temporary file/directory. pub struct AllocateManagedTemp { store: Arc, ids: Arc, clock: Arc, } impl AllocateManagedTemp { /// Builds the use case. #[must_use] pub fn new( store: Arc, ids: Arc, clock: Arc, ) -> Self { Self { store, ids, clock } } /// Executes allocation. pub async fn execute( &self, input: AllocateManagedTempInput, ) -> Result { let purpose = bounded_purpose(input.purpose)?; let now = now_ms(&self.clock); let ttl_ms = input.ttl_ms.unwrap_or(MANAGED_TEMP_DEFAULT_TTL_MS); if ttl_ms == 0 { return Err(AppError::Invalid( "managed temp ttl must be positive".to_owned(), )); } let expires_at_ms = now.checked_add(ttl_ms).ok_or_else(|| { AppError::Invalid("managed temp ttl exceeds timestamp range".to_owned()) })?; let handle = ManagedTempHandle::new(self.ids.new_uuid().to_string()) .map_err(|err| AppError::Invalid(err.to_string()))?; let lease = self .store .allocate(ManagedTempAllocation { handle, kind: input.kind, owner_project_id: input.owner_project_id, purpose, created_at_ms: now, expires_at_ms: Some(expires_at_ms), }) .await?; Ok(AllocateManagedTempOutput { lease }) } } /// Input for [`ResolveManagedTemp`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolveManagedTempInput { /// Opaque handle to resolve. pub handle: String, } /// Output for [`ResolveManagedTemp`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolveManagedTempOutput { /// Resolved active lease. pub lease: ManagedTempLease, } /// Resolves a managed temp handle. pub struct ResolveManagedTemp { store: Arc, clock: Arc, } impl ResolveManagedTemp { /// Builds the use case. #[must_use] pub fn new(store: Arc, clock: Arc) -> Self { Self { store, clock } } /// Executes resolution. pub async fn execute( &self, input: ResolveManagedTempInput, ) -> Result { let handle = ManagedTempHandle::new(input.handle) .map_err(|err| AppError::Invalid(err.to_string()))?; let lease = self.store.resolve(&handle, now_ms(&self.clock)).await?; Ok(ResolveManagedTempOutput { lease }) } } /// Input for [`ReleaseManagedTemp`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReleaseManagedTempInput { /// Opaque handle to release. pub handle: String, } /// Output for [`ReleaseManagedTemp`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReleaseManagedTempOutput { /// Released lease. pub lease: ManagedTempLease, } /// Releases a managed temp handle. pub struct ReleaseManagedTemp { store: Arc, clock: Arc, } impl ReleaseManagedTemp { /// Builds the use case. #[must_use] pub fn new(store: Arc, clock: Arc) -> Self { Self { store, clock } } /// Executes release. pub async fn execute( &self, input: ReleaseManagedTempInput, ) -> Result { let handle = ManagedTempHandle::new(input.handle) .map_err(|err| AppError::Invalid(err.to_string()))?; let lease = self.store.release(&handle, now_ms(&self.clock)).await?; Ok(ReleaseManagedTempOutput { lease }) } } /// Output for [`GcManagedTemp`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct GcManagedTempOutput { /// GC report. pub report: ManagedTempGcReport, } /// Runs managed temp garbage collection. pub struct GcManagedTemp { store: Arc, clock: Arc, } impl GcManagedTemp { /// Builds the use case. #[must_use] pub fn new(store: Arc, clock: Arc) -> Self { Self { store, clock } } /// Executes GC. pub async fn execute(&self) -> Result { let report = self.store.gc(now_ms(&self.clock)).await?; Ok(GcManagedTempOutput { report }) } } /// Output for [`PurgeManagedTemp`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PurgeManagedTempOutput { /// Purge report. pub report: ManagedTempGcReport, } /// Eagerly purges all managed temporary allocations. pub struct PurgeManagedTemp { store: Arc, } impl PurgeManagedTemp { /// Builds the use case. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Executes the purge. pub async fn execute(&self) -> Result { let report = self.store.purge_all().await?; Ok(PurgeManagedTempOutput { report }) } } fn now_ms(clock: &Arc) -> u64 { u64::try_from(clock.now_millis()).unwrap_or(0) } fn bounded_purpose(raw: String) -> Result { let trimmed = raw.trim(); if trimmed.is_empty() { return Err(AppError::Invalid( "managed temp purpose must not be empty".to_owned(), )); } Ok(trimmed .chars() .take(MANAGED_TEMP_PURPOSE_MAX_CHARS) .collect()) }