use bytes::Bytes;
use dashmap::DashMap;
use http::{StatusCode, Version};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ChunkedEntry {
pub metadata: ChunkMetadata,
pub chunks: Arc<DashMap<u64, Bytes>>,
pub chunk_size: usize,
}
#[derive(Debug, Clone)]
pub struct ChunkMetadata {
pub total_size: u64,
pub content_type: String,
pub etag: Option<String>,
pub last_modified: Option<String>,
pub status: StatusCode,
pub version: Version,
pub headers: Vec<(String, Vec<u8>)>,
}
impl ChunkedEntry {
pub fn new(metadata: ChunkMetadata, chunk_size: usize) -> Self {
Self {
metadata,
chunks: Arc::new(DashMap::new()),
chunk_size,
}
}
pub fn add_chunk(&self, index: u64, data: Bytes) {
self.chunks.insert(index, data);
}
pub fn get_chunk(&self, index: u64) -> Option<Bytes> {
self.chunks.get(&index).map(|r| r.value().clone())
}
pub fn get_range(&self, start: u64, end: u64) -> Option<Bytes> {
let start_chunk = start / self.chunk_size as u64;
let end_chunk = end / self.chunk_size as u64;
let mut result = Vec::new();
for chunk_idx in start_chunk..=end_chunk {
let chunk = self.get_chunk(chunk_idx)?;
let chunk_start = chunk_idx * self.chunk_size as u64;
let chunk_end = chunk_start + chunk.len() as u64;
let data_start = if start > chunk_start {
(start - chunk_start) as usize
} else {
0
};
let data_end = if end < chunk_end {
(end - chunk_start + 1) as usize
} else {
chunk.len()
};
result.extend_from_slice(&chunk[data_start..data_end]);
}
Some(Bytes::from(result))
}
pub fn is_complete(&self) -> bool {
let total_chunks = self.metadata.total_size.div_ceil(self.chunk_size as u64);
self.chunks.len() as u64 == total_chunks
}
pub fn coverage(&self) -> f64 {
let total_chunks = self.metadata.total_size.div_ceil(self.chunk_size as u64);
if total_chunks == 0 {
return 0.0;
}
(self.chunks.len() as f64 / total_chunks as f64) * 100.0
}
}
pub struct ChunkCache {
entries: Arc<DashMap<String, Arc<ChunkedEntry>>>,
default_chunk_size: usize,
}
impl ChunkCache {
pub fn new(default_chunk_size: usize) -> Self {
Self {
entries: Arc::new(DashMap::new()),
default_chunk_size,
}
}
pub fn get_or_create(&self, key: String, metadata: ChunkMetadata) -> Arc<ChunkedEntry> {
self.entries
.entry(key)
.or_insert_with(|| Arc::new(ChunkedEntry::new(metadata, self.default_chunk_size)))
.clone()
}
pub fn get(&self, key: &str) -> Option<Arc<ChunkedEntry>> {
self.entries.get(key).map(|r| r.value().clone())
}
pub fn remove(&self, key: &str) -> Option<Arc<ChunkedEntry>> {
self.entries.remove(key).map(|(_, v)| v)
}
pub fn stats(&self) -> ChunkCacheStats {
let mut total_entries = 0;
let mut total_chunks = 0;
let mut total_bytes = 0u64;
let mut complete_entries = 0;
for entry in self.entries.iter() {
total_entries += 1;
let chunk_entry = entry.value();
let chunk_count = chunk_entry.chunks.len();
total_chunks += chunk_count;
for chunk in chunk_entry.chunks.iter() {
total_bytes += chunk.value().len() as u64;
}
if chunk_entry.is_complete() {
complete_entries += 1;
}
}
ChunkCacheStats {
total_entries,
complete_entries,
total_chunks,
total_bytes,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ChunkCacheStats {
pub total_entries: usize,
pub complete_entries: usize,
pub total_chunks: usize,
pub total_bytes: u64,
}
#[cfg(test)]
mod tests {
use super::*;
fn test_metadata(size: u64) -> ChunkMetadata {
ChunkMetadata {
total_size: size,
content_type: "application/octet-stream".to_string(),
etag: Some("test-etag".to_string()),
last_modified: None,
status: StatusCode::OK,
version: Version::HTTP_11,
headers: vec![],
}
}
#[test]
fn test_chunk_entry_new() {
let metadata = test_metadata(1024);
let entry = ChunkedEntry::new(metadata, 512);
assert_eq!(entry.chunk_size, 512);
assert_eq!(entry.metadata.total_size, 1024);
assert_eq!(entry.chunks.len(), 0);
}
#[test]
fn test_add_and_get_chunk() {
let metadata = test_metadata(2048);
let entry = ChunkedEntry::new(metadata, 1024);
let chunk_data = Bytes::from(vec![1u8; 1024]);
entry.add_chunk(0, chunk_data.clone());
let retrieved = entry.get_chunk(0);
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap(), chunk_data);
}
#[test]
fn test_get_nonexistent_chunk() {
let metadata = test_metadata(2048);
let entry = ChunkedEntry::new(metadata, 1024);
assert!(entry.get_chunk(0).is_none());
assert!(entry.get_chunk(999).is_none());
}
#[test]
fn test_get_range_single_chunk() {
let metadata = test_metadata(1024);
let entry = ChunkedEntry::new(metadata, 1024);
let chunk_data = Bytes::from((0..1024).map(|i| i as u8).collect::<Vec<u8>>());
entry.add_chunk(0, chunk_data);
let range = entry.get_range(0, 511);
assert!(range.is_some());
let data = range.unwrap();
assert_eq!(data.len(), 512);
assert_eq!(data[0], 0);
assert_eq!(data[511], 255);
}
#[test]
fn test_get_range_multiple_chunks() {
let metadata = test_metadata(3072);
let entry = ChunkedEntry::new(metadata, 1024);
entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
entry.add_chunk(2, Bytes::from(vec![2u8; 1024]));
let range = entry.get_range(512, 1535);
assert!(range.is_some());
let data = range.unwrap();
assert_eq!(data.len(), 1024);
assert_eq!(data[0], 0); assert_eq!(data[512], 1); }
#[test]
fn test_get_range_missing_chunk() {
let metadata = test_metadata(2048);
let entry = ChunkedEntry::new(metadata, 1024);
entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
let range = entry.get_range(512, 1535);
assert!(range.is_none()); }
#[test]
fn test_is_complete_empty() {
let metadata = test_metadata(2048);
let entry = ChunkedEntry::new(metadata, 1024);
assert!(!entry.is_complete());
}
#[test]
fn test_is_complete_partial() {
let metadata = test_metadata(2048);
let entry = ChunkedEntry::new(metadata, 1024);
entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
assert!(!entry.is_complete());
}
#[test]
fn test_is_complete_full() {
let metadata = test_metadata(2048);
let entry = ChunkedEntry::new(metadata, 1024);
entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
assert!(entry.is_complete());
}
#[test]
fn test_coverage() {
let metadata = test_metadata(4096);
let entry = ChunkedEntry::new(metadata, 1024);
assert_eq!(entry.coverage(), 0.0);
entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
assert_eq!(entry.coverage(), 25.0);
entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
assert_eq!(entry.coverage(), 50.0);
entry.add_chunk(2, Bytes::from(vec![2u8; 1024]));
assert_eq!(entry.coverage(), 75.0);
entry.add_chunk(3, Bytes::from(vec![3u8; 1024]));
assert_eq!(entry.coverage(), 100.0);
}
#[test]
fn test_chunk_cache_new() {
let cache = ChunkCache::new(1024 * 1024);
let stats = cache.stats();
assert_eq!(stats.total_entries, 0);
assert_eq!(stats.total_chunks, 0);
assert_eq!(stats.total_bytes, 0);
}
#[test]
fn test_chunk_cache_get_or_create() {
let cache = ChunkCache::new(1024);
let metadata = test_metadata(2048);
let entry1 = cache.get_or_create("key1".to_string(), metadata.clone());
let entry2 = cache.get_or_create("key1".to_string(), test_metadata(4096));
assert_eq!(entry1.metadata.total_size, entry2.metadata.total_size);
assert_eq!(entry1.metadata.total_size, 2048);
}
#[test]
fn test_chunk_cache_get() {
let cache = ChunkCache::new(1024);
assert!(cache.get("nonexistent").is_none());
let metadata = test_metadata(1024);
cache.get_or_create("exists".to_string(), metadata);
assert!(cache.get("exists").is_some());
}
#[test]
fn test_chunk_cache_remove() {
let cache = ChunkCache::new(1024);
let metadata = test_metadata(1024);
cache.get_or_create("temp".to_string(), metadata);
assert!(cache.get("temp").is_some());
let removed = cache.remove("temp");
assert!(removed.is_some());
assert!(cache.get("temp").is_none());
}
#[test]
fn test_chunk_cache_stats() {
let cache = ChunkCache::new(1024);
let metadata1 = test_metadata(2048);
let entry1 = cache.get_or_create("file1".to_string(), metadata1);
entry1.add_chunk(0, Bytes::from(vec![0u8; 1024]));
entry1.add_chunk(1, Bytes::from(vec![1u8; 1024]));
let metadata2 = test_metadata(3072);
let entry2 = cache.get_or_create("file2".to_string(), metadata2);
entry2.add_chunk(0, Bytes::from(vec![2u8; 1024]));
let stats = cache.stats();
assert_eq!(stats.total_entries, 2);
assert_eq!(stats.complete_entries, 1); assert_eq!(stats.total_chunks, 3);
assert_eq!(stats.total_bytes, 3072);
}
}