backbone_bucket/application/service/
locking_service.rs1use std::sync::Arc;
9
10use chrono::{Duration, Utc};
11use uuid::Uuid;
12
13use super::error::{ServiceError, ServiceResult};
14use crate::domain::entity::{FileLock, LockStatus};
15use crate::infrastructure::persistence::FileLockRepository;
16use crate::infrastructure::persistence::StoredFileRepository;
17
18const DEFAULT_LOCK_DURATION_MINUTES: i64 = 30;
20
21pub struct LockingService {
23 lock_repo: Arc<FileLockRepository>,
24 file_repo: Arc<StoredFileRepository>,
25}
26
27impl LockingService {
28 pub fn new(lock_repo: Arc<FileLockRepository>, file_repo: Arc<StoredFileRepository>) -> Self {
29 Self { lock_repo, file_repo }
30 }
31
32 pub async fn acquire_lock(
38 &self,
39 file_id: Uuid,
40 user_id: Uuid,
41 duration_minutes: Option<i64>,
42 ) -> ServiceResult<FileLock> {
43 let _file = self.file_repo
45 .find_by_id(&file_id.to_string())
46 .await
47 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
48 .ok_or(ServiceError::NotFound)?;
49
50 if let Some(existing) = self.lock_repo.find_by_file_id(file_id).await.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))? {
52 if existing.is_expired() {
53 self.lock_repo.delete(&existing.id.to_string()).await.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
55 } else if existing.user_id == user_id {
56 return self.refresh_lock(file_id, user_id).await;
58 } else {
59 return Err(ServiceError::AlreadyExists(format!(
61 "File {} is locked by user {} until {}",
62 file_id, existing.user_id, existing.expires_at
63 )));
64 }
65 }
66
67 let minutes = duration_minutes.unwrap_or(DEFAULT_LOCK_DURATION_MINUTES);
68 let now = Utc::now();
69 let expires_at = now + Duration::minutes(minutes);
70
71 let lock = FileLock::new(file_id, user_id, now, expires_at, LockStatus::Active);
72
73 let created = self.lock_repo
74 .create(&lock)
75 .await
76 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
77
78 Ok(created)
79 }
80
81 pub async fn release_lock(&self, file_id: Uuid, user_id: Uuid) -> ServiceResult<()> {
83 let lock = self.lock_repo
84 .find_by_file_id(file_id)
85 .await
86 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
87 .ok_or(ServiceError::NotFound)?;
88
89 if lock.user_id != user_id {
90 return Err(ServiceError::Validation(
91 format!("Lock on file {} is owned by a different user", file_id)
92 ));
93 }
94
95 self.lock_repo.delete(&lock.id.to_string()).await.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
96 Ok(())
97 }
98
99 pub async fn refresh_lock(&self, file_id: Uuid, user_id: Uuid) -> ServiceResult<FileLock> {
101 let mut lock = self.lock_repo
102 .find_by_file_id(file_id)
103 .await
104 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
105 .ok_or(ServiceError::NotFound)?;
106
107 if lock.user_id != user_id {
108 return Err(ServiceError::Validation(
109 format!("Lock on file {} is owned by a different user", file_id)
110 ));
111 }
112
113 lock.expires_at = Utc::now() + Duration::minutes(DEFAULT_LOCK_DURATION_MINUTES);
114 lock.refreshed_at = Some(Utc::now());
115 lock.metadata.touch();
116
117 let id_str = lock.id.to_string();
118 let updated = self.lock_repo
119 .update(&id_str, &lock)
120 .await
121 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
122 .ok_or(ServiceError::NotFound)?;
123
124 Ok(updated)
125 }
126
127 pub async fn break_lock(&self, file_id: Uuid) -> ServiceResult<()> {
129 if let Some(lock) = self.lock_repo.find_by_file_id(file_id).await.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))? {
130 self.lock_repo.delete(&lock.id.to_string()).await.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
131 }
132 Ok(())
133 }
134
135 pub async fn get_active_lock(&self, file_id: Uuid) -> ServiceResult<Option<FileLock>> {
138 if let Some(lock) = self.lock_repo.find_by_file_id(file_id).await.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))? {
139 if lock.is_expired() {
140 self.lock_repo.delete(&lock.id.to_string()).await.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
141 return Ok(None);
142 }
143 return Ok(Some(lock));
144 }
145 Ok(None)
146 }
147
148 pub async fn cleanup_expired(&self) -> ServiceResult<u64> {
150 self.lock_repo.empty_trash().await.map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))
151 }
152}