tower_http_cache/
chunks.rs

1//! Chunk-based caching for large files and efficient range request handling.
2//!
3//! This module provides a chunk-based caching system that splits large files into
4//! fixed-size chunks, enabling efficient storage and retrieval of partial content.
5//! This is particularly useful for:
6//! - Video streaming with range requests
7//! - Large file downloads with resume support
8//! - Efficient memory usage for large responses
9//!
10//! # Examples
11//!
12//! ```rust
13//! use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
14//! use bytes::Bytes;
15//! use http::StatusCode;
16//!
17//! # tokio_test::block_on(async {
18//! let chunk_cache = ChunkCache::new(1024 * 1024); // 1MB chunks
19//!
20//! let metadata = ChunkMetadata {
21//!     total_size: 10 * 1024 * 1024, // 10MB file
22//!     content_type: "video/mp4".to_string(),
23//!     etag: Some("abc123".to_string()),
24//!     last_modified: None,
25//!     status: StatusCode::OK,
26//!     version: http::Version::HTTP_11,
27//!     headers: vec![],
28//! };
29//!
30//! let entry = chunk_cache.get_or_create("video.mp4".to_string(), metadata);
31//!
32//! // Add chunks as they're received
33//! entry.add_chunk(0, Bytes::from(vec![0u8; 1024 * 1024]));
34//! entry.add_chunk(1, Bytes::from(vec![1u8; 1024 * 1024]));
35//!
36//! // Retrieve a range
37//! let range = entry.get_range(0, 1024 * 1024 - 1);
38//! assert!(range.is_some());
39//! # });
40//! ```
41
42use bytes::Bytes;
43use dashmap::DashMap;
44use http::{StatusCode, Version};
45use std::sync::Arc;
46
47/// Chunk-based cache entry for large files.
48///
49/// A `ChunkedEntry` splits a large file into fixed-size chunks, allowing
50/// efficient storage and retrieval of partial content. This is particularly
51/// useful for range requests where only a portion of the file is needed.
52#[derive(Debug, Clone)]
53pub struct ChunkedEntry {
54    /// Metadata about the full file
55    pub metadata: ChunkMetadata,
56
57    /// Map of chunk index to chunk data
58    pub chunks: Arc<DashMap<u64, Bytes>>,
59
60    /// Chunk size in bytes (default: 1MB)
61    pub chunk_size: usize,
62}
63
64/// Metadata for a chunked cache entry.
65///
66/// Contains HTTP response metadata without the body, which is stored
67/// separately in chunks.
68#[derive(Debug, Clone)]
69pub struct ChunkMetadata {
70    /// Total size of the file in bytes
71    pub total_size: u64,
72
73    /// Content-Type header value
74    pub content_type: String,
75
76    /// ETag header value, if present
77    pub etag: Option<String>,
78
79    /// Last-Modified header value, if present
80    pub last_modified: Option<String>,
81
82    /// HTTP status code
83    pub status: StatusCode,
84
85    /// HTTP version
86    pub version: Version,
87
88    /// Additional headers (name, value) pairs
89    pub headers: Vec<(String, Vec<u8>)>,
90}
91
92impl ChunkedEntry {
93    /// Creates a new chunked entry with the given metadata and chunk size.
94    ///
95    /// # Arguments
96    ///
97    /// * `metadata` - File metadata including size, content type, and headers
98    /// * `chunk_size` - Size of each chunk in bytes
99    ///
100    /// # Examples
101    ///
102    /// ```rust
103    /// use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
104    /// use http::StatusCode;
105    ///
106    /// let metadata = ChunkMetadata {
107    ///     total_size: 10_000_000,
108    ///     content_type: "video/mp4".to_string(),
109    ///     etag: None,
110    ///     last_modified: None,
111    ///     status: StatusCode::OK,
112    ///     version: http::Version::HTTP_11,
113    ///     headers: vec![],
114    /// };
115    ///
116    /// let entry = ChunkedEntry::new(metadata, 1024 * 1024);
117    /// assert_eq!(entry.chunk_size, 1024 * 1024);
118    /// ```
119    pub fn new(metadata: ChunkMetadata, chunk_size: usize) -> Self {
120        Self {
121            metadata,
122            chunks: Arc::new(DashMap::new()),
123            chunk_size,
124        }
125    }
126
127    /// Adds a chunk to the entry.
128    ///
129    /// # Arguments
130    ///
131    /// * `index` - Chunk index (0-based)
132    /// * `data` - Chunk data
133    ///
134    /// # Examples
135    ///
136    /// ```rust
137    /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
138    /// # use bytes::Bytes;
139    /// # use http::StatusCode;
140    /// # let metadata = ChunkMetadata {
141    /// #     total_size: 1024,
142    /// #     content_type: "text/plain".to_string(),
143    /// #     etag: None,
144    /// #     last_modified: None,
145    /// #     status: StatusCode::OK,
146    /// #     version: http::Version::HTTP_11,
147    /// #     headers: vec![],
148    /// # };
149    /// let entry = ChunkedEntry::new(metadata, 512);
150    /// entry.add_chunk(0, Bytes::from("Hello, world!"));
151    /// ```
152    pub fn add_chunk(&self, index: u64, data: Bytes) {
153        self.chunks.insert(index, data);
154    }
155
156    /// Gets a chunk by index.
157    ///
158    /// Returns `None` if the chunk is not cached.
159    ///
160    /// # Arguments
161    ///
162    /// * `index` - Chunk index (0-based)
163    ///
164    /// # Examples
165    ///
166    /// ```rust
167    /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
168    /// # use bytes::Bytes;
169    /// # use http::StatusCode;
170    /// # let metadata = ChunkMetadata {
171    /// #     total_size: 1024,
172    /// #     content_type: "text/plain".to_string(),
173    /// #     etag: None,
174    /// #     last_modified: None,
175    /// #     status: StatusCode::OK,
176    /// #     version: http::Version::HTTP_11,
177    /// #     headers: vec![],
178    /// # };
179    /// let entry = ChunkedEntry::new(metadata, 512);
180    /// entry.add_chunk(0, Bytes::from("Hello"));
181    ///
182    /// let chunk = entry.get_chunk(0);
183    /// assert!(chunk.is_some());
184    /// assert_eq!(chunk.unwrap(), "Hello");
185    /// ```
186    pub fn get_chunk(&self, index: u64) -> Option<Bytes> {
187        self.chunks.get(&index).map(|r| r.value().clone())
188    }
189
190    /// Gets a byte range from the cached chunks.
191    ///
192    /// Returns `None` if any required chunks are missing from the cache.
193    /// The range is inclusive: [start, end].
194    ///
195    /// # Arguments
196    ///
197    /// * `start` - Starting byte offset (inclusive)
198    /// * `end` - Ending byte offset (inclusive)
199    ///
200    /// # Examples
201    ///
202    /// ```rust
203    /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
204    /// # use bytes::Bytes;
205    /// # use http::StatusCode;
206    /// # let metadata = ChunkMetadata {
207    /// #     total_size: 2048,
208    /// #     content_type: "text/plain".to_string(),
209    /// #     etag: None,
210    /// #     last_modified: None,
211    /// #     status: StatusCode::OK,
212    /// #     version: http::Version::HTTP_11,
213    /// #     headers: vec![],
214    /// # };
215    /// let entry = ChunkedEntry::new(metadata, 1024);
216    /// entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
217    /// entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
218    ///
219    /// // Get first 512 bytes
220    /// let range = entry.get_range(0, 511);
221    /// assert!(range.is_some());
222    /// assert_eq!(range.unwrap().len(), 512);
223    /// ```
224    pub fn get_range(&self, start: u64, end: u64) -> Option<Bytes> {
225        let start_chunk = start / self.chunk_size as u64;
226        let end_chunk = end / self.chunk_size as u64;
227
228        let mut result = Vec::new();
229
230        for chunk_idx in start_chunk..=end_chunk {
231            let chunk = self.get_chunk(chunk_idx)?;
232
233            // Calculate byte offsets within this chunk
234            let chunk_start = chunk_idx * self.chunk_size as u64;
235            let chunk_end = chunk_start + chunk.len() as u64;
236
237            let data_start = if start > chunk_start {
238                (start - chunk_start) as usize
239            } else {
240                0
241            };
242
243            let data_end = if end < chunk_end {
244                (end - chunk_start + 1) as usize
245            } else {
246                chunk.len()
247            };
248
249            result.extend_from_slice(&chunk[data_start..data_end]);
250        }
251
252        Some(Bytes::from(result))
253    }
254
255    /// Checks if all chunks are cached.
256    ///
257    /// Returns `true` if the entry contains all chunks needed to represent
258    /// the full file.
259    ///
260    /// # Examples
261    ///
262    /// ```rust
263    /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
264    /// # use bytes::Bytes;
265    /// # use http::StatusCode;
266    /// # let metadata = ChunkMetadata {
267    /// #     total_size: 2048,
268    /// #     content_type: "text/plain".to_string(),
269    /// #     etag: None,
270    /// #     last_modified: None,
271    /// #     status: StatusCode::OK,
272    /// #     version: http::Version::HTTP_11,
273    /// #     headers: vec![],
274    /// # };
275    /// let entry = ChunkedEntry::new(metadata, 1024);
276    /// assert!(!entry.is_complete());
277    ///
278    /// entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
279    /// entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
280    /// assert!(entry.is_complete());
281    /// ```
282    pub fn is_complete(&self) -> bool {
283        let total_chunks = self.metadata.total_size.div_ceil(self.chunk_size as u64);
284
285        self.chunks.len() as u64 == total_chunks
286    }
287
288    /// Gets the cache coverage percentage.
289    ///
290    /// Returns a percentage (0-100) indicating how much of the file is cached.
291    ///
292    /// # Examples
293    ///
294    /// ```rust
295    /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
296    /// # use bytes::Bytes;
297    /// # use http::StatusCode;
298    /// # let metadata = ChunkMetadata {
299    /// #     total_size: 4096,
300    /// #     content_type: "text/plain".to_string(),
301    /// #     etag: None,
302    /// #     last_modified: None,
303    /// #     status: StatusCode::OK,
304    /// #     version: http::Version::HTTP_11,
305    /// #     headers: vec![],
306    /// # };
307    /// let entry = ChunkedEntry::new(metadata, 1024);
308    /// assert_eq!(entry.coverage(), 0.0);
309    ///
310    /// entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
311    /// assert_eq!(entry.coverage(), 25.0);
312    ///
313    /// entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
314    /// assert_eq!(entry.coverage(), 50.0);
315    /// ```
316    pub fn coverage(&self) -> f64 {
317        let total_chunks = self.metadata.total_size.div_ceil(self.chunk_size as u64);
318
319        if total_chunks == 0 {
320            return 0.0;
321        }
322
323        (self.chunks.len() as f64 / total_chunks as f64) * 100.0
324    }
325}
326
327/// Chunk cache manager.
328///
329/// Manages multiple chunked entries, providing efficient lookup and storage
330/// of large files split into chunks.
331pub struct ChunkCache {
332    entries: Arc<DashMap<String, Arc<ChunkedEntry>>>,
333    default_chunk_size: usize,
334}
335
336impl ChunkCache {
337    /// Creates a new chunk cache with the given default chunk size.
338    ///
339    /// # Arguments
340    ///
341    /// * `default_chunk_size` - Default size of each chunk in bytes
342    ///
343    /// # Examples
344    ///
345    /// ```rust
346    /// use tower_http_cache::chunks::ChunkCache;
347    ///
348    /// // Create cache with 1MB chunks
349    /// let cache = ChunkCache::new(1024 * 1024);
350    /// ```
351    pub fn new(default_chunk_size: usize) -> Self {
352        Self {
353            entries: Arc::new(DashMap::new()),
354            default_chunk_size,
355        }
356    }
357
358    /// Gets or creates a chunked entry.
359    ///
360    /// If an entry exists for the key, returns it. Otherwise, creates a new
361    /// entry with the given metadata.
362    ///
363    /// # Arguments
364    ///
365    /// * `key` - Cache key
366    /// * `metadata` - File metadata (used only if creating new entry)
367    ///
368    /// # Examples
369    ///
370    /// ```rust
371    /// # use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
372    /// # use http::StatusCode;
373    /// let cache = ChunkCache::new(1024 * 1024);
374    ///
375    /// let metadata = ChunkMetadata {
376    ///     total_size: 10_000_000,
377    ///     content_type: "video/mp4".to_string(),
378    ///     etag: None,
379    ///     last_modified: None,
380    ///     status: StatusCode::OK,
381    ///     version: http::Version::HTTP_11,
382    ///     headers: vec![],
383    /// };
384    ///
385    /// let entry = cache.get_or_create("video.mp4".to_string(), metadata);
386    /// ```
387    pub fn get_or_create(&self, key: String, metadata: ChunkMetadata) -> Arc<ChunkedEntry> {
388        self.entries
389            .entry(key)
390            .or_insert_with(|| Arc::new(ChunkedEntry::new(metadata, self.default_chunk_size)))
391            .clone()
392    }
393
394    /// Gets an entry if it exists.
395    ///
396    /// # Arguments
397    ///
398    /// * `key` - Cache key
399    ///
400    /// # Examples
401    ///
402    /// ```rust
403    /// # use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
404    /// # use http::StatusCode;
405    /// let cache = ChunkCache::new(1024 * 1024);
406    ///
407    /// assert!(cache.get("nonexistent").is_none());
408    ///
409    /// # let metadata = ChunkMetadata {
410    /// #     total_size: 1024,
411    /// #     content_type: "text/plain".to_string(),
412    /// #     etag: None,
413    /// #     last_modified: None,
414    /// #     status: StatusCode::OK,
415    /// #     version: http::Version::HTTP_11,
416    /// #     headers: vec![],
417    /// # };
418    /// cache.get_or_create("exists".to_string(), metadata);
419    /// assert!(cache.get("exists").is_some());
420    /// ```
421    pub fn get(&self, key: &str) -> Option<Arc<ChunkedEntry>> {
422        self.entries.get(key).map(|r| r.value().clone())
423    }
424
425    /// Removes an entry from the cache.
426    ///
427    /// # Arguments
428    ///
429    /// * `key` - Cache key
430    ///
431    /// # Examples
432    ///
433    /// ```rust
434    /// # use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
435    /// # use http::StatusCode;
436    /// let cache = ChunkCache::new(1024 * 1024);
437    ///
438    /// # let metadata = ChunkMetadata {
439    /// #     total_size: 1024,
440    /// #     content_type: "text/plain".to_string(),
441    /// #     etag: None,
442    /// #     last_modified: None,
443    /// #     status: StatusCode::OK,
444    /// #     version: http::Version::HTTP_11,
445    /// #     headers: vec![],
446    /// # };
447    /// cache.get_or_create("temp".to_string(), metadata);
448    /// assert!(cache.get("temp").is_some());
449    ///
450    /// cache.remove("temp");
451    /// assert!(cache.get("temp").is_none());
452    /// ```
453    pub fn remove(&self, key: &str) -> Option<Arc<ChunkedEntry>> {
454        self.entries.remove(key).map(|(_, v)| v)
455    }
456
457    /// Gets cache statistics.
458    ///
459    /// # Examples
460    ///
461    /// ```rust
462    /// # use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
463    /// # use bytes::Bytes;
464    /// # use http::StatusCode;
465    /// let cache = ChunkCache::new(1024);
466    ///
467    /// # let metadata = ChunkMetadata {
468    /// #     total_size: 2048,
469    /// #     content_type: "text/plain".to_string(),
470    /// #     etag: None,
471    /// #     last_modified: None,
472    /// #     status: StatusCode::OK,
473    /// #     version: http::Version::HTTP_11,
474    /// #     headers: vec![],
475    /// # };
476    /// let entry = cache.get_or_create("file1".to_string(), metadata);
477    /// entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
478    ///
479    /// let stats = cache.stats();
480    /// assert_eq!(stats.total_entries, 1);
481    /// assert_eq!(stats.total_chunks, 1);
482    /// assert_eq!(stats.total_bytes, 1024);
483    /// ```
484    pub fn stats(&self) -> ChunkCacheStats {
485        let mut total_entries = 0;
486        let mut total_chunks = 0;
487        let mut total_bytes = 0u64;
488        let mut complete_entries = 0;
489
490        for entry in self.entries.iter() {
491            total_entries += 1;
492            let chunk_entry = entry.value();
493            let chunk_count = chunk_entry.chunks.len();
494            total_chunks += chunk_count;
495
496            for chunk in chunk_entry.chunks.iter() {
497                total_bytes += chunk.value().len() as u64;
498            }
499
500            if chunk_entry.is_complete() {
501                complete_entries += 1;
502            }
503        }
504
505        ChunkCacheStats {
506            total_entries,
507            complete_entries,
508            total_chunks,
509            total_bytes,
510        }
511    }
512}
513
514/// Statistics for chunk cache.
515#[derive(Debug, Clone)]
516#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
517pub struct ChunkCacheStats {
518    /// Total number of cached entries
519    pub total_entries: usize,
520
521    /// Number of entries with all chunks cached
522    pub complete_entries: usize,
523
524    /// Total number of cached chunks across all entries
525    pub total_chunks: usize,
526
527    /// Total bytes stored in cache
528    pub total_bytes: u64,
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    fn test_metadata(size: u64) -> ChunkMetadata {
536        ChunkMetadata {
537            total_size: size,
538            content_type: "application/octet-stream".to_string(),
539            etag: Some("test-etag".to_string()),
540            last_modified: None,
541            status: StatusCode::OK,
542            version: Version::HTTP_11,
543            headers: vec![],
544        }
545    }
546
547    #[test]
548    fn test_chunk_entry_new() {
549        let metadata = test_metadata(1024);
550        let entry = ChunkedEntry::new(metadata, 512);
551
552        assert_eq!(entry.chunk_size, 512);
553        assert_eq!(entry.metadata.total_size, 1024);
554        assert_eq!(entry.chunks.len(), 0);
555    }
556
557    #[test]
558    fn test_add_and_get_chunk() {
559        let metadata = test_metadata(2048);
560        let entry = ChunkedEntry::new(metadata, 1024);
561
562        let chunk_data = Bytes::from(vec![1u8; 1024]);
563        entry.add_chunk(0, chunk_data.clone());
564
565        let retrieved = entry.get_chunk(0);
566        assert!(retrieved.is_some());
567        assert_eq!(retrieved.unwrap(), chunk_data);
568    }
569
570    #[test]
571    fn test_get_nonexistent_chunk() {
572        let metadata = test_metadata(2048);
573        let entry = ChunkedEntry::new(metadata, 1024);
574
575        assert!(entry.get_chunk(0).is_none());
576        assert!(entry.get_chunk(999).is_none());
577    }
578
579    #[test]
580    fn test_get_range_single_chunk() {
581        let metadata = test_metadata(1024);
582        let entry = ChunkedEntry::new(metadata, 1024);
583
584        let chunk_data = Bytes::from((0..1024).map(|i| i as u8).collect::<Vec<u8>>());
585        entry.add_chunk(0, chunk_data);
586
587        // Get first 512 bytes
588        let range = entry.get_range(0, 511);
589        assert!(range.is_some());
590        let data = range.unwrap();
591        assert_eq!(data.len(), 512);
592        assert_eq!(data[0], 0);
593        assert_eq!(data[511], 255);
594    }
595
596    #[test]
597    fn test_get_range_multiple_chunks() {
598        let metadata = test_metadata(3072);
599        let entry = ChunkedEntry::new(metadata, 1024);
600
601        // Add 3 chunks
602        entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
603        entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
604        entry.add_chunk(2, Bytes::from(vec![2u8; 1024]));
605
606        // Get range spanning chunks 0 and 1
607        let range = entry.get_range(512, 1535);
608        assert!(range.is_some());
609        let data = range.unwrap();
610        assert_eq!(data.len(), 1024);
611        assert_eq!(data[0], 0); // From chunk 0
612        assert_eq!(data[512], 1); // From chunk 1
613    }
614
615    #[test]
616    fn test_get_range_missing_chunk() {
617        let metadata = test_metadata(2048);
618        let entry = ChunkedEntry::new(metadata, 1024);
619
620        entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
621        // Chunk 1 is missing
622
623        // Try to get range spanning both chunks
624        let range = entry.get_range(512, 1535);
625        assert!(range.is_none()); // Should fail because chunk 1 is missing
626    }
627
628    #[test]
629    fn test_is_complete_empty() {
630        let metadata = test_metadata(2048);
631        let entry = ChunkedEntry::new(metadata, 1024);
632
633        assert!(!entry.is_complete());
634    }
635
636    #[test]
637    fn test_is_complete_partial() {
638        let metadata = test_metadata(2048);
639        let entry = ChunkedEntry::new(metadata, 1024);
640
641        entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
642        assert!(!entry.is_complete());
643    }
644
645    #[test]
646    fn test_is_complete_full() {
647        let metadata = test_metadata(2048);
648        let entry = ChunkedEntry::new(metadata, 1024);
649
650        entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
651        entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
652        assert!(entry.is_complete());
653    }
654
655    #[test]
656    fn test_coverage() {
657        let metadata = test_metadata(4096);
658        let entry = ChunkedEntry::new(metadata, 1024);
659
660        assert_eq!(entry.coverage(), 0.0);
661
662        entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
663        assert_eq!(entry.coverage(), 25.0);
664
665        entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
666        assert_eq!(entry.coverage(), 50.0);
667
668        entry.add_chunk(2, Bytes::from(vec![2u8; 1024]));
669        assert_eq!(entry.coverage(), 75.0);
670
671        entry.add_chunk(3, Bytes::from(vec![3u8; 1024]));
672        assert_eq!(entry.coverage(), 100.0);
673    }
674
675    #[test]
676    fn test_chunk_cache_new() {
677        let cache = ChunkCache::new(1024 * 1024);
678        let stats = cache.stats();
679
680        assert_eq!(stats.total_entries, 0);
681        assert_eq!(stats.total_chunks, 0);
682        assert_eq!(stats.total_bytes, 0);
683    }
684
685    #[test]
686    fn test_chunk_cache_get_or_create() {
687        let cache = ChunkCache::new(1024);
688        let metadata = test_metadata(2048);
689
690        let entry1 = cache.get_or_create("key1".to_string(), metadata.clone());
691        let entry2 = cache.get_or_create("key1".to_string(), test_metadata(4096));
692
693        // Should return the same entry (original metadata)
694        assert_eq!(entry1.metadata.total_size, entry2.metadata.total_size);
695        assert_eq!(entry1.metadata.total_size, 2048);
696    }
697
698    #[test]
699    fn test_chunk_cache_get() {
700        let cache = ChunkCache::new(1024);
701
702        assert!(cache.get("nonexistent").is_none());
703
704        let metadata = test_metadata(1024);
705        cache.get_or_create("exists".to_string(), metadata);
706
707        assert!(cache.get("exists").is_some());
708    }
709
710    #[test]
711    fn test_chunk_cache_remove() {
712        let cache = ChunkCache::new(1024);
713        let metadata = test_metadata(1024);
714
715        cache.get_or_create("temp".to_string(), metadata);
716        assert!(cache.get("temp").is_some());
717
718        let removed = cache.remove("temp");
719        assert!(removed.is_some());
720        assert!(cache.get("temp").is_none());
721    }
722
723    #[test]
724    fn test_chunk_cache_stats() {
725        let cache = ChunkCache::new(1024);
726
727        let metadata1 = test_metadata(2048);
728        let entry1 = cache.get_or_create("file1".to_string(), metadata1);
729        entry1.add_chunk(0, Bytes::from(vec![0u8; 1024]));
730        entry1.add_chunk(1, Bytes::from(vec![1u8; 1024]));
731
732        let metadata2 = test_metadata(3072);
733        let entry2 = cache.get_or_create("file2".to_string(), metadata2);
734        entry2.add_chunk(0, Bytes::from(vec![2u8; 1024]));
735
736        let stats = cache.stats();
737        assert_eq!(stats.total_entries, 2);
738        assert_eq!(stats.complete_entries, 1); // Only entry1 is complete
739        assert_eq!(stats.total_chunks, 3);
740        assert_eq!(stats.total_bytes, 3072);
741    }
742}