use std::collections::HashMap;
use super::{BlobDatabase, DbError, FileStats, UploadRecord, UserRecord};
pub struct MemoryDatabase {
uploads: HashMap<String, UploadRecord>,
users: HashMap<String, UserRecord>,
stats: HashMap<String, FileStats>,
}
impl MemoryDatabase {
pub fn new() -> Self {
Self {
uploads: HashMap::new(),
users: HashMap::new(),
stats: HashMap::new(),
}
}
}
impl Default for MemoryDatabase {
fn default() -> Self {
Self::new()
}
}
impl BlobDatabase for MemoryDatabase {
fn record_upload(&mut self, record: &UploadRecord) -> Result<(), DbError> {
self.uploads
.entry(record.sha256.clone())
.or_insert_with(|| record.clone());
let user = self.get_or_create_user(&record.pubkey)?;
let new_used = user.used_bytes + record.size;
self.update_used_bytes(&record.pubkey, new_used)?;
Ok(())
}
fn get_upload(&self, sha256: &str) -> Result<UploadRecord, DbError> {
self.uploads.get(sha256).cloned().ok_or(DbError::NotFound)
}
fn list_uploads_by_pubkey(&self, pubkey: &str) -> Result<Vec<UploadRecord>, DbError> {
let mut records: Vec<_> = self
.uploads
.values()
.filter(|r| r.pubkey == pubkey)
.cloned()
.collect();
records.sort_by_key(|r| std::cmp::Reverse(r.created_at));
Ok(records)
}
fn delete_upload(&mut self, sha256: &str) -> Result<bool, DbError> {
if let Some(record) = self.uploads.remove(sha256) {
if let Some(user) = self.users.get_mut(&record.pubkey) {
user.used_bytes = user.used_bytes.saturating_sub(record.size);
}
self.stats.remove(sha256);
Ok(true)
} else {
Ok(false)
}
}
fn get_or_create_user(&mut self, pubkey: &str) -> Result<UserRecord, DbError> {
Ok(self
.users
.entry(pubkey.to_string())
.or_insert_with(|| UserRecord {
pubkey: pubkey.to_string(),
role: "member".to_string(),
quota_bytes: None,
used_bytes: 0,
})
.clone())
}
fn set_quota(&mut self, pubkey: &str, quota_bytes: Option<u64>) -> Result<(), DbError> {
let user = self
.users
.entry(pubkey.to_string())
.or_insert_with(|| UserRecord {
pubkey: pubkey.to_string(),
role: "member".to_string(),
quota_bytes: None,
used_bytes: 0,
});
user.quota_bytes = quota_bytes;
Ok(())
}
fn check_quota(&self, pubkey: &str, additional_bytes: u64) -> Result<(), DbError> {
if let Some(user) = self.users.get(pubkey) {
if let Some(limit) = user.quota_bytes {
if user.used_bytes + additional_bytes > limit {
return Err(DbError::QuotaExceeded {
used: user.used_bytes,
requested: additional_bytes,
limit,
});
}
}
}
Ok(())
}
fn update_used_bytes(&mut self, pubkey: &str, used_bytes: u64) -> Result<(), DbError> {
let user = self
.users
.entry(pubkey.to_string())
.or_insert_with(|| UserRecord {
pubkey: pubkey.to_string(),
role: "member".to_string(),
quota_bytes: None,
used_bytes: 0,
});
user.used_bytes = used_bytes;
Ok(())
}
fn record_access(&mut self, sha256: &str, bytes_served: u64) -> Result<(), DbError> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let stats = self
.stats
.entry(sha256.to_string())
.or_insert_with(|| FileStats {
sha256: sha256.to_string(),
egress_bytes: 0,
last_accessed: 0,
});
stats.egress_bytes += bytes_served;
stats.last_accessed = now;
Ok(())
}
fn get_stats(&self, sha256: &str) -> Result<FileStats, DbError> {
self.stats.get(sha256).cloned().ok_or(DbError::NotFound)
}
fn upload_count(&self) -> usize {
self.uploads.len()
}
fn user_count(&self) -> usize {
self.users.len()
}
fn set_role(&mut self, pubkey: &str, role: &str) -> Result<(), DbError> {
let user = self
.users
.entry(pubkey.to_string())
.or_insert_with(|| UserRecord {
pubkey: pubkey.to_string(),
role: "member".to_string(),
quota_bytes: None,
used_bytes: 0,
});
user.role = role.to_string();
Ok(())
}
fn get_role(&self, pubkey: &str) -> String {
self.users
.get(pubkey)
.map(|u| u.role.clone())
.unwrap_or_else(|| "member".to_string())
}
fn list_users_by_role(&self, role: &str) -> Result<Vec<UserRecord>, DbError> {
Ok(self
.users
.values()
.filter(|u| u.role == role)
.cloned()
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_upload(pubkey: &str) -> UploadRecord {
UploadRecord {
sha256: "a".repeat(64),
size: 1024,
mime_type: "application/octet-stream".into(),
pubkey: pubkey.to_string(),
created_at: 1700000000,
phash: None,
}
}
#[test]
fn test_record_and_get_upload() {
let mut db = MemoryDatabase::new();
let record = sample_upload("deadbeef");
db.record_upload(&record).unwrap();
let retrieved = db.get_upload(&record.sha256).unwrap();
assert_eq!(retrieved.sha256, record.sha256);
assert_eq!(retrieved.size, 1024);
assert_eq!(retrieved.pubkey, "deadbeef");
}
#[test]
fn test_list_uploads_by_pubkey() {
let mut db = MemoryDatabase::new();
let mut r1 = sample_upload("alice");
r1.sha256 = "a".repeat(64);
r1.created_at = 1000;
let mut r2 = sample_upload("alice");
r2.sha256 = "b".repeat(64);
r2.created_at = 2000;
let mut r3 = sample_upload("bob");
r3.sha256 = "c".repeat(64);
db.record_upload(&r1).unwrap();
db.record_upload(&r2).unwrap();
db.record_upload(&r3).unwrap();
let alice_uploads = db.list_uploads_by_pubkey("alice").unwrap();
assert_eq!(alice_uploads.len(), 2);
assert_eq!(alice_uploads[0].created_at, 2000);
assert_eq!(alice_uploads[1].created_at, 1000);
}
#[test]
fn test_delete_upload_updates_used_bytes() {
let mut db = MemoryDatabase::new();
let record = sample_upload("alice");
db.record_upload(&record).unwrap();
let user = db.get_or_create_user("alice").unwrap();
assert_eq!(user.used_bytes, 1024);
db.delete_upload(&record.sha256).unwrap();
let user = db.get_or_create_user("alice").unwrap();
assert_eq!(user.used_bytes, 0);
}
#[test]
fn test_quota_enforcement() {
let mut db = MemoryDatabase::new();
db.set_quota("alice", Some(2000)).unwrap();
db.check_quota("alice", 1024).unwrap();
db.update_used_bytes("alice", 1500).unwrap();
let result = db.check_quota("alice", 600);
assert!(matches!(result, Err(DbError::QuotaExceeded { .. })));
db.check_quota("alice", 400).unwrap();
}
#[test]
fn test_no_quota_means_unlimited() {
let mut db = MemoryDatabase::new();
db.get_or_create_user("bob").unwrap();
db.check_quota("bob", u64::MAX).unwrap();
}
#[test]
fn test_unknown_user_quota_passes() {
let db = MemoryDatabase::new();
db.check_quota("unknown", 999999).unwrap();
}
#[test]
fn test_file_stats() {
let mut db = MemoryDatabase::new();
let sha = "f".repeat(64);
db.record_access(&sha, 500).unwrap();
db.record_access(&sha, 300).unwrap();
let stats = db.get_stats(&sha).unwrap();
assert_eq!(stats.egress_bytes, 800);
assert!(stats.last_accessed > 0);
}
#[test]
fn test_upload_count() {
let mut db = MemoryDatabase::new();
assert_eq!(db.upload_count(), 0);
assert_eq!(db.user_count(), 0);
let record = sample_upload("alice");
db.record_upload(&record).unwrap();
assert_eq!(db.upload_count(), 1);
assert_eq!(db.user_count(), 1);
}
#[test]
fn test_dedup_upload() {
let mut db = MemoryDatabase::new();
let record = sample_upload("alice");
db.record_upload(&record).unwrap();
db.record_upload(&record).unwrap();
assert_eq!(db.upload_count(), 1);
let user = db.get_or_create_user("alice").unwrap();
assert!(user.used_bytes >= 1024);
}
}