firecloud_storage/
quota.rs1use crate::{StorageError, StorageResult};
9use serde::{Deserialize, Serialize};
10use sled::Db;
11use std::path::Path;
12use tracing::{debug, info, warn};
13
14const TREE_QUOTA: &str = "quota";
15const KEY_TOTAL_BYTES: &[u8] = b"total_bytes";
16const KEY_TOTAL_CHUNKS: &[u8] = b"total_chunks";
17const KEY_QUOTA_LIMIT: &[u8] = b"quota_limit";
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct StorageQuota {
22 pub total_bytes: u64,
24 pub total_chunks: u64,
26 pub quota_limit: u64,
28}
29
30impl StorageQuota {
31 pub fn has_space(&self, bytes: u64) -> bool {
33 if self.quota_limit == 0 {
34 return true; }
36 self.total_bytes + bytes <= self.quota_limit
37 }
38
39 pub fn remaining_bytes(&self) -> u64 {
41 if self.quota_limit == 0 {
42 return u64::MAX; }
44 self.quota_limit.saturating_sub(self.total_bytes)
45 }
46
47 pub fn usage_percentage(&self) -> f64 {
49 if self.quota_limit == 0 {
50 return 0.0; }
52 (self.total_bytes as f64 / self.quota_limit as f64) * 100.0
53 }
54
55 pub fn is_nearly_full(&self) -> bool {
57 self.usage_percentage() > 90.0
58 }
59
60 pub fn is_full(&self) -> bool {
62 if self.quota_limit == 0 {
63 return false; }
65 self.total_bytes >= self.quota_limit
66 }
67}
68
69impl Default for StorageQuota {
70 fn default() -> Self {
71 Self {
72 total_bytes: 0,
73 total_chunks: 0,
74 quota_limit: 0, }
76 }
77}
78
79pub struct QuotaManager {
81 db: Db,
82}
83
84impl QuotaManager {
85 pub fn open<P: AsRef<Path>>(path: P) -> StorageResult<Self> {
87 let db = sled::open(path).map_err(|e| StorageError::Database(e.to_string()))?;
88 info!("Opened quota manager");
89 Ok(Self { db })
90 }
91
92 pub fn get_quota(&self) -> StorageResult<StorageQuota> {
94 let tree = self
95 .db
96 .open_tree(TREE_QUOTA)
97 .map_err(|e| StorageError::Database(e.to_string()))?;
98
99 let total_bytes = tree
100 .get(KEY_TOTAL_BYTES)
101 .map_err(|e| StorageError::Database(e.to_string()))?
102 .map(|bytes| u64::from_le_bytes(bytes.as_ref().try_into().unwrap_or([0; 8])))
103 .unwrap_or(0);
104
105 let total_chunks = tree
106 .get(KEY_TOTAL_CHUNKS)
107 .map_err(|e| StorageError::Database(e.to_string()))?
108 .map(|bytes| u64::from_le_bytes(bytes.as_ref().try_into().unwrap_or([0; 8])))
109 .unwrap_or(0);
110
111 let quota_limit = tree
112 .get(KEY_QUOTA_LIMIT)
113 .map_err(|e| StorageError::Database(e.to_string()))?
114 .map(|bytes| u64::from_le_bytes(bytes.as_ref().try_into().unwrap_or([0; 8])))
115 .unwrap_or(0);
116
117 Ok(StorageQuota {
118 total_bytes,
119 total_chunks,
120 quota_limit,
121 })
122 }
123
124 pub fn set_quota_limit(&self, limit: u64) -> StorageResult<()> {
126 let tree = self
127 .db
128 .open_tree(TREE_QUOTA)
129 .map_err(|e| StorageError::Database(e.to_string()))?;
130
131 tree.insert(KEY_QUOTA_LIMIT, &limit.to_le_bytes())
132 .map_err(|e| StorageError::Database(e.to_string()))?;
133
134 info!("Set quota limit to {} bytes", limit);
135 Ok(())
136 }
137
138 pub fn add_usage(&self, bytes: u64) -> StorageResult<()> {
140 let tree = self
141 .db
142 .open_tree(TREE_QUOTA)
143 .map_err(|e| StorageError::Database(e.to_string()))?;
144
145 let quota = self.get_quota()?;
147 if !quota.has_space(bytes) {
148 return Err(StorageError::QuotaExceeded {
149 requested: bytes,
150 available: quota.remaining_bytes(),
151 });
152 }
153
154 let new_total = quota.total_bytes + bytes;
156 tree.insert(KEY_TOTAL_BYTES, &new_total.to_le_bytes())
157 .map_err(|e| StorageError::Database(e.to_string()))?;
158
159 let new_chunks = quota.total_chunks + 1;
161 tree.insert(KEY_TOTAL_CHUNKS, &new_chunks.to_le_bytes())
162 .map_err(|e| StorageError::Database(e.to_string()))?;
163
164 debug!(
165 "Added {} bytes, total: {} / {} chunks: {}",
166 bytes, new_total, quota.quota_limit, new_chunks
167 );
168
169 if new_total as f64 / quota.quota_limit as f64 > 0.9 && quota.quota_limit > 0 {
171 warn!(
172 "Storage quota nearly full: {:.1}%",
173 (new_total as f64 / quota.quota_limit as f64) * 100.0
174 );
175 }
176
177 Ok(())
178 }
179
180 pub fn remove_usage(&self, bytes: u64) -> StorageResult<()> {
182 let tree = self
183 .db
184 .open_tree(TREE_QUOTA)
185 .map_err(|e| StorageError::Database(e.to_string()))?;
186
187 let quota = self.get_quota()?;
188
189 let new_total = quota.total_bytes.saturating_sub(bytes);
191 tree.insert(KEY_TOTAL_BYTES, &new_total.to_le_bytes())
192 .map_err(|e| StorageError::Database(e.to_string()))?;
193
194 let new_chunks = quota.total_chunks.saturating_sub(1);
196 tree.insert(KEY_TOTAL_CHUNKS, &new_chunks.to_le_bytes())
197 .map_err(|e| StorageError::Database(e.to_string()))?;
198
199 debug!(
200 "Removed {} bytes, total: {} chunks: {}",
201 bytes, new_total, new_chunks
202 );
203
204 Ok(())
205 }
206
207 pub fn can_store(&self, bytes: u64) -> StorageResult<bool> {
209 let quota = self.get_quota()?;
210 Ok(quota.has_space(bytes))
211 }
212
213 pub fn get_stats(&self) -> StorageResult<QuotaStats> {
215 let quota = self.get_quota()?;
216
217 Ok(QuotaStats {
218 total_bytes: quota.total_bytes,
219 total_chunks: quota.total_chunks,
220 quota_limit: quota.quota_limit,
221 remaining_bytes: quota.remaining_bytes(),
222 usage_percentage: quota.usage_percentage(),
223 is_nearly_full: quota.is_nearly_full(),
224 is_full: quota.is_full(),
225 })
226 }
227
228 pub fn reset(&self) -> StorageResult<()> {
230 let tree = self
231 .db
232 .open_tree(TREE_QUOTA)
233 .map_err(|e| StorageError::Database(e.to_string()))?;
234
235 tree.clear().map_err(|e| StorageError::Database(e.to_string()))?;
236
237 warn!("Storage quota statistics reset!");
238 Ok(())
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct QuotaStats {
245 pub total_bytes: u64,
246 pub total_chunks: u64,
247 pub quota_limit: u64,
248 pub remaining_bytes: u64,
249 pub usage_percentage: f64,
250 pub is_nearly_full: bool,
251 pub is_full: bool,
252}
253
254impl QuotaStats {
255 pub fn format_bytes(bytes: u64) -> String {
257 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
258 let mut size = bytes as f64;
259 let mut unit_idx = 0;
260
261 while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
262 size /= 1024.0;
263 unit_idx += 1;
264 }
265
266 format!("{:.2} {}", size, UNITS[unit_idx])
267 }
268
269 pub fn summary(&self) -> String {
271 if self.quota_limit == 0 {
272 format!(
273 "{} used in {} chunks (unlimited)",
274 Self::format_bytes(self.total_bytes),
275 self.total_chunks
276 )
277 } else {
278 format!(
279 "{} / {} used ({:.1}%) in {} chunks",
280 Self::format_bytes(self.total_bytes),
281 Self::format_bytes(self.quota_limit),
282 self.usage_percentage,
283 self.total_chunks
284 )
285 }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use tempfile::tempdir;
293
294 #[test]
295 fn test_quota_basic() {
296 let dir = tempdir().unwrap();
297 let manager = QuotaManager::open(dir.path().join("quota")).unwrap();
298
299 manager.set_quota_limit(1_000_000_000).unwrap();
301
302 manager.add_usage(500_000_000).unwrap();
304
305 let stats = manager.get_stats().unwrap();
306 assert_eq!(stats.total_bytes, 500_000_000);
307 assert_eq!(stats.total_chunks, 1);
308 assert_eq!(stats.usage_percentage, 50.0);
309
310 assert!(manager.can_store(400_000_000).unwrap());
312
313 assert!(!manager.can_store(600_000_000).unwrap());
315 }
316
317 #[test]
318 fn test_quota_exceeded() {
319 let dir = tempdir().unwrap();
320 let manager = QuotaManager::open(dir.path().join("quota")).unwrap();
321
322 manager.set_quota_limit(1000).unwrap();
323 manager.add_usage(800).unwrap();
324
325 let result = manager.add_usage(300);
327 assert!(matches!(result, Err(StorageError::QuotaExceeded { .. })));
328 }
329
330 #[test]
331 fn test_remove_usage() {
332 let dir = tempdir().unwrap();
333 let manager = QuotaManager::open(dir.path().join("quota")).unwrap();
334
335 manager.add_usage(1000).unwrap();
336 assert_eq!(manager.get_stats().unwrap().total_bytes, 1000);
337
338 manager.remove_usage(400).unwrap();
339 assert_eq!(manager.get_stats().unwrap().total_bytes, 600);
340 assert_eq!(manager.get_stats().unwrap().total_chunks, 0);
341 }
342
343 #[test]
344 fn test_unlimited_quota() {
345 let dir = tempdir().unwrap();
346 let manager = QuotaManager::open(dir.path().join("quota")).unwrap();
347
348 manager.add_usage(u64::MAX / 2).unwrap();
350 assert!(manager.can_store(u64::MAX / 2).unwrap());
351 }
352}