1use std::collections::HashMap;
4
5use super::{BlobDatabase, DbError, FileStats, UploadRecord, UserRecord};
6
7pub struct MemoryDatabase {
12 uploads: HashMap<String, UploadRecord>,
13 users: HashMap<String, UserRecord>,
14 stats: HashMap<String, FileStats>,
15}
16
17impl MemoryDatabase {
18 pub fn new() -> Self {
19 Self {
20 uploads: HashMap::new(),
21 users: HashMap::new(),
22 stats: HashMap::new(),
23 }
24 }
25}
26
27impl Default for MemoryDatabase {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl BlobDatabase for MemoryDatabase {
34 fn record_upload(&mut self, record: &UploadRecord) -> Result<(), DbError> {
35 self.uploads
36 .entry(record.sha256.clone())
37 .or_insert_with(|| record.clone());
38
39 let user = self.get_or_create_user(&record.pubkey)?;
41 let new_used = user.used_bytes + record.size;
42 self.update_used_bytes(&record.pubkey, new_used)?;
43
44 Ok(())
45 }
46
47 fn get_upload(&self, sha256: &str) -> Result<UploadRecord, DbError> {
48 self.uploads.get(sha256).cloned().ok_or(DbError::NotFound)
49 }
50
51 fn list_uploads_by_pubkey(&self, pubkey: &str) -> Result<Vec<UploadRecord>, DbError> {
52 let mut records: Vec<_> = self
53 .uploads
54 .values()
55 .filter(|r| r.pubkey == pubkey)
56 .cloned()
57 .collect();
58 records.sort_by_key(|r| std::cmp::Reverse(r.created_at));
59 Ok(records)
60 }
61
62 fn delete_upload(&mut self, sha256: &str) -> Result<bool, DbError> {
63 if let Some(record) = self.uploads.remove(sha256) {
64 if let Some(user) = self.users.get_mut(&record.pubkey) {
66 user.used_bytes = user.used_bytes.saturating_sub(record.size);
67 }
68 self.stats.remove(sha256);
69 Ok(true)
70 } else {
71 Ok(false)
72 }
73 }
74
75 fn get_or_create_user(&mut self, pubkey: &str) -> Result<UserRecord, DbError> {
76 Ok(self
77 .users
78 .entry(pubkey.to_string())
79 .or_insert_with(|| UserRecord {
80 pubkey: pubkey.to_string(),
81 role: "member".to_string(),
82 quota_bytes: None,
83 used_bytes: 0,
84 })
85 .clone())
86 }
87
88 fn set_quota(&mut self, pubkey: &str, quota_bytes: Option<u64>) -> Result<(), DbError> {
89 let user = self
90 .users
91 .entry(pubkey.to_string())
92 .or_insert_with(|| UserRecord {
93 pubkey: pubkey.to_string(),
94 role: "member".to_string(),
95 quota_bytes: None,
96 used_bytes: 0,
97 });
98 user.quota_bytes = quota_bytes;
99 Ok(())
100 }
101
102 fn check_quota(&self, pubkey: &str, additional_bytes: u64) -> Result<(), DbError> {
103 if let Some(user) = self.users.get(pubkey) {
104 if let Some(limit) = user.quota_bytes {
105 if user.used_bytes + additional_bytes > limit {
106 return Err(DbError::QuotaExceeded {
107 used: user.used_bytes,
108 requested: additional_bytes,
109 limit,
110 });
111 }
112 }
113 }
114 Ok(())
116 }
117
118 fn update_used_bytes(&mut self, pubkey: &str, used_bytes: u64) -> Result<(), DbError> {
119 let user = self
120 .users
121 .entry(pubkey.to_string())
122 .or_insert_with(|| UserRecord {
123 pubkey: pubkey.to_string(),
124 role: "member".to_string(),
125 quota_bytes: None,
126 used_bytes: 0,
127 });
128 user.used_bytes = used_bytes;
129 Ok(())
130 }
131
132 fn record_access(&mut self, sha256: &str, bytes_served: u64) -> Result<(), DbError> {
133 let now = std::time::SystemTime::now()
134 .duration_since(std::time::UNIX_EPOCH)
135 .unwrap_or_default()
136 .as_secs();
137
138 let stats = self
139 .stats
140 .entry(sha256.to_string())
141 .or_insert_with(|| FileStats {
142 sha256: sha256.to_string(),
143 egress_bytes: 0,
144 last_accessed: 0,
145 });
146 stats.egress_bytes += bytes_served;
147 stats.last_accessed = now;
148 Ok(())
149 }
150
151 fn get_stats(&self, sha256: &str) -> Result<FileStats, DbError> {
152 self.stats.get(sha256).cloned().ok_or(DbError::NotFound)
153 }
154
155 fn upload_count(&self) -> usize {
156 self.uploads.len()
157 }
158
159 fn user_count(&self) -> usize {
160 self.users.len()
161 }
162
163 fn set_role(&mut self, pubkey: &str, role: &str) -> Result<(), DbError> {
164 let user = self
165 .users
166 .entry(pubkey.to_string())
167 .or_insert_with(|| UserRecord {
168 pubkey: pubkey.to_string(),
169 role: "member".to_string(),
170 quota_bytes: None,
171 used_bytes: 0,
172 });
173 user.role = role.to_string();
174 Ok(())
175 }
176
177 fn get_role(&self, pubkey: &str) -> String {
178 self.users
179 .get(pubkey)
180 .map(|u| u.role.clone())
181 .unwrap_or_else(|| "member".to_string())
182 }
183
184 fn list_users_by_role(&self, role: &str) -> Result<Vec<UserRecord>, DbError> {
185 Ok(self
186 .users
187 .values()
188 .filter(|u| u.role == role)
189 .cloned()
190 .collect())
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn sample_upload(pubkey: &str) -> UploadRecord {
199 UploadRecord {
200 sha256: "a".repeat(64),
201 size: 1024,
202 mime_type: "application/octet-stream".into(),
203 pubkey: pubkey.to_string(),
204 created_at: 1700000000,
205 phash: None,
206 }
207 }
208
209 #[test]
210 fn test_record_and_get_upload() {
211 let mut db = MemoryDatabase::new();
212 let record = sample_upload("deadbeef");
213 db.record_upload(&record).unwrap();
214
215 let retrieved = db.get_upload(&record.sha256).unwrap();
216 assert_eq!(retrieved.sha256, record.sha256);
217 assert_eq!(retrieved.size, 1024);
218 assert_eq!(retrieved.pubkey, "deadbeef");
219 }
220
221 #[test]
222 fn test_list_uploads_by_pubkey() {
223 let mut db = MemoryDatabase::new();
224
225 let mut r1 = sample_upload("alice");
226 r1.sha256 = "a".repeat(64);
227 r1.created_at = 1000;
228
229 let mut r2 = sample_upload("alice");
230 r2.sha256 = "b".repeat(64);
231 r2.created_at = 2000;
232
233 let mut r3 = sample_upload("bob");
234 r3.sha256 = "c".repeat(64);
235
236 db.record_upload(&r1).unwrap();
237 db.record_upload(&r2).unwrap();
238 db.record_upload(&r3).unwrap();
239
240 let alice_uploads = db.list_uploads_by_pubkey("alice").unwrap();
241 assert_eq!(alice_uploads.len(), 2);
242 assert_eq!(alice_uploads[0].created_at, 2000);
244 assert_eq!(alice_uploads[1].created_at, 1000);
245 }
246
247 #[test]
248 fn test_delete_upload_updates_used_bytes() {
249 let mut db = MemoryDatabase::new();
250 let record = sample_upload("alice");
251 db.record_upload(&record).unwrap();
252
253 let user = db.get_or_create_user("alice").unwrap();
254 assert_eq!(user.used_bytes, 1024);
255
256 db.delete_upload(&record.sha256).unwrap();
257 let user = db.get_or_create_user("alice").unwrap();
258 assert_eq!(user.used_bytes, 0);
259 }
260
261 #[test]
262 fn test_quota_enforcement() {
263 let mut db = MemoryDatabase::new();
264 db.set_quota("alice", Some(2000)).unwrap();
265
266 db.check_quota("alice", 1024).unwrap();
268
269 db.update_used_bytes("alice", 1500).unwrap();
271
272 let result = db.check_quota("alice", 600);
274 assert!(matches!(result, Err(DbError::QuotaExceeded { .. })));
275
276 db.check_quota("alice", 400).unwrap();
278 }
279
280 #[test]
281 fn test_no_quota_means_unlimited() {
282 let mut db = MemoryDatabase::new();
283 db.get_or_create_user("bob").unwrap();
284 db.check_quota("bob", u64::MAX).unwrap();
286 }
287
288 #[test]
289 fn test_unknown_user_quota_passes() {
290 let db = MemoryDatabase::new();
291 db.check_quota("unknown", 999999).unwrap();
293 }
294
295 #[test]
296 fn test_file_stats() {
297 let mut db = MemoryDatabase::new();
298 let sha = "f".repeat(64);
299
300 db.record_access(&sha, 500).unwrap();
301 db.record_access(&sha, 300).unwrap();
302
303 let stats = db.get_stats(&sha).unwrap();
304 assert_eq!(stats.egress_bytes, 800);
305 assert!(stats.last_accessed > 0);
306 }
307
308 #[test]
309 fn test_upload_count() {
310 let mut db = MemoryDatabase::new();
311 assert_eq!(db.upload_count(), 0);
312 assert_eq!(db.user_count(), 0);
313
314 let record = sample_upload("alice");
315 db.record_upload(&record).unwrap();
316
317 assert_eq!(db.upload_count(), 1);
318 assert_eq!(db.user_count(), 1);
319 }
320
321 #[test]
322 fn test_dedup_upload() {
323 let mut db = MemoryDatabase::new();
324 let record = sample_upload("alice");
325
326 db.record_upload(&record).unwrap();
327 db.record_upload(&record).unwrap();
328
329 assert_eq!(db.upload_count(), 1);
330 let user = db.get_or_create_user("alice").unwrap();
332 assert!(user.used_bytes >= 1024);
339 }
340}