use super::*;
use std::time::{ SystemTime, Instant };
use std::sync::{ Arc, RwLock };
use core::sync::atomic::{ AtomicU64, AtomicUsize, Ordering };
use std::collections::HashMap;
use std::hash::{ Hash, Hasher };
use std::collections::hash_map::DefaultHasher;
use bytes::{ Bytes, BytesMut };
#[ derive( Debug ) ]
pub struct MediaCache
{
config : MediaProcessingConfig,
entries : Arc< RwLock< HashMap< String, CachedMediaEntry > > >,
total_size_bytes : AtomicUsize,
stats : Arc< MediaCacheStats >,
}
#[ derive( Debug, Clone ) ]
struct CachedMediaEntry
{
data : Bytes,
metadata : CachedMediaMetadata,
last_accessed : SystemTime,
size_bytes : usize,
}
#[ derive( Debug, Clone ) ]
pub struct CachedMediaMetadata
{
pub mime_type : String,
pub original_size : usize,
pub is_compressed : bool,
pub compression_ratio : f64,
#[ allow(dead_code) ]
content_hash : String,
}
#[ derive( Debug ) ]
pub struct MediaCacheStats
{
pub hits : AtomicU64,
pub misses : AtomicU64,
pub evictions : AtomicU64,
pub total_compressed_bytes : AtomicU64,
pub total_compression_time_us : AtomicU64,
}
impl Default for MediaCacheStats
{
fn default() -> Self
{
Self {
hits : AtomicU64::new( 0 ),
misses : AtomicU64::new( 0 ),
evictions : AtomicU64::new( 0 ),
total_compressed_bytes : AtomicU64::new( 0 ),
total_compression_time_us : AtomicU64::new( 0 ),
}
}
}
impl MediaCache
{
#[ inline ]
#[ must_use ]
pub fn new( config : MediaProcessingConfig ) -> Self
{
Self {
config,
entries : Arc::new( RwLock::new( HashMap::new() ) ),
total_size_bytes : AtomicUsize::new( 0 ),
stats : Arc::new( MediaCacheStats::default() ),
}
}
#[ inline ]
pub fn get( &self, key : &str ) -> Option< ( Bytes, CachedMediaMetadata ) >
{
let mut entries = self.entries.write().unwrap();
if let Some( entry ) = entries.get_mut( key )
{
let age = SystemTime::now()
.duration_since( entry.last_accessed )
.unwrap_or_default()
.as_secs();
if age <= self.config.cache_ttl_seconds
{
entry.last_accessed = SystemTime::now();
self.stats.hits.fetch_add( 1, Ordering::Relaxed );
return Some( ( entry.data.clone(), entry.metadata.clone() ) );
}
let entry = entries.remove( key ).unwrap();
self.total_size_bytes.fetch_sub( entry.size_bytes, Ordering::Relaxed );
}
self.stats.misses.fetch_add( 1, Ordering::Relaxed );
None
}
#[ inline ]
pub fn put( &self, key : String, data : Bytes, mime_type : String ) -> Result< (), crate::error::Error >
{
let original_size = data.len();
let start_time = Instant::now();
let ( final_data, is_compressed, compression_ratio ) = if self.config.enable_compression && self.should_compress( &mime_type )
{
match self.compress_data( &data )
{
Ok( compressed ) =>
{
let ratio = compressed.len() as f64 / original_size as f64;
if ratio < 0.95 {
( compressed, true, ratio )
}
else
{
( data, false, 1.0 )
}
},
Err( _ ) => ( data, false, 1.0 ),
}
} else {
( data, false, 1.0 )
};
let content_hash = self.calculate_hash( &final_data );
let entry_size = final_data.len();
let current_size = self.total_size_bytes.load( Ordering::Relaxed );
if current_size + entry_size > self.config.max_cache_size_bytes
{
self.evict_lru_entries( entry_size );
}
let metadata = CachedMediaMetadata {
mime_type,
original_size,
is_compressed,
compression_ratio,
content_hash,
};
let entry = CachedMediaEntry {
data : final_data,
metadata,
last_accessed : SystemTime::now(),
size_bytes : entry_size,
};
{
let mut entries = self.entries.write().unwrap();
entries.insert( key, entry );
}
self.total_size_bytes.fetch_add( entry_size, Ordering::Relaxed );
if is_compressed
{
self.stats.total_compressed_bytes.fetch_add( entry_size as u64, Ordering::Relaxed );
let compression_time_us = start_time.elapsed().as_micros() as u64;
self.stats.total_compression_time_us.fetch_add( compression_time_us, Ordering::Relaxed );
}
Ok( () )
}
#[ inline ]
fn should_compress( &self, mime_type : &str ) -> bool
{
!matches!( mime_type,
"image/jpeg" | "image/jpg" | "image/webp" |
"video/mp4" | "video/webm" | "video/h264" |
"audio/mp3" | "audio/aac" | "audio/ogg" |
"application/zip" | "application/gzip" | "application/brotli"
)
}
#[ inline ]
fn compress_data( &self, data : &Bytes ) -> Result< Bytes, std::io::Error >
{
let mut compressed = BytesMut::new();
let mut last_byte = None;
let mut count = 0u8;
for &byte in data.iter()
{
if last_byte == Some( byte ) && count < 255
{
count += 1;
} else {
if let Some( last ) = last_byte
{
if count > 0
{
compressed.extend_from_slice( &[ 0xFF, count, last ] ); } else {
compressed.extend_from_slice( &[ last ] );
}
}
last_byte = Some( byte );
count = 0;
}
}
if let Some( last ) = last_byte
{
if count > 0
{
compressed.extend_from_slice( &[ 0xFF, count, last ] );
} else {
compressed.extend_from_slice( &[ last ] );
}
}
Ok( compressed.freeze() )
}
fn calculate_hash( &self, data : &Bytes ) -> String
{
let mut hasher = DefaultHasher::new();
data.hash( &mut hasher );
format!( "{:x}", hasher.finish() )
}
fn evict_lru_entries( &self, required_space : usize )
{
let mut entries = self.entries.write().unwrap();
let mut entries_by_access : Vec< _ > = entries.iter()
.map( | ( key, entry ) | ( key.clone(), entry.last_accessed, entry.size_bytes ) )
.collect();
entries_by_access.sort_by_key( | ( _, timestamp, _ ) | *timestamp );
let mut freed_space = 0;
let mut evicted_count = 0;
for ( key, _, size ) in entries_by_access
{
if freed_space >= required_space
{
break;
}
entries.remove( &key );
freed_space += size;
evicted_count += 1;
}
self.total_size_bytes.fetch_sub( freed_space, Ordering::Relaxed );
self.stats.evictions.fetch_add( evicted_count, Ordering::Relaxed );
}
#[ inline ]
#[ must_use ]
pub fn get_stats( &self ) -> MediaCacheStatsReport
{
let hits = self.stats.hits.load( Ordering::Relaxed );
let misses = self.stats.misses.load( Ordering::Relaxed );
let total_requests = hits + misses;
let hit_rate = if total_requests > 0 { hits as f64 / total_requests as f64 * 100.0 } else { 0.0 };
MediaCacheStatsReport {
hits,
misses,
hit_rate,
evictions : self.stats.evictions.load( Ordering::Relaxed ),
total_size_bytes : self.total_size_bytes.load( Ordering::Relaxed ),
total_compressed_bytes : self.stats.total_compressed_bytes.load( Ordering::Relaxed ),
avg_compression_time_us :
{
let total_time = self.stats.total_compression_time_us.load( Ordering::Relaxed );
let total_compressed = self.stats.total_compressed_bytes.load( Ordering::Relaxed );
if total_compressed > 0
{
total_time / total_compressed
}
else
{
0
}
},
}
}
#[ inline ]
pub fn clear( &self )
{
let mut entries = self.entries.write().unwrap();
entries.clear();
self.total_size_bytes.store( 0, Ordering::Relaxed );
}
}
#[ derive( Debug ) ]
pub struct ThumbnailGenerator
{
config : ThumbnailConfig,
}
impl ThumbnailGenerator
{
pub fn new( config : ThumbnailConfig ) -> Self
{
Self { config }
}
pub async fn generate_thumbnail( &self, _image_data : &Bytes, mime_type : &str ) -> Result< Bytes, crate::error::Error >
{
if !self.config.enabled
{
return Err( crate::error::Error::ApiError( "Thumbnail generation disabled".to_string() ) );
}
let thumbnail_data = self.create_placeholder_thumbnail( mime_type );
Ok( thumbnail_data )
}
fn create_placeholder_thumbnail( &self, _mime_type : &str ) -> Bytes
{
let mut thumbnail = BytesMut::new();
match self.config.format
{
ThumbnailFormat::Jpeg => {
thumbnail.extend_from_slice( &[ 0xFF, 0xD8, 0xFF, 0xE0 ] ); },
ThumbnailFormat::Png => {
thumbnail.extend_from_slice( &[ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ] ); },
ThumbnailFormat::WebP => {
thumbnail.extend_from_slice( b"RIFF" );
thumbnail.extend_from_slice( &[ 0x00, 0x00, 0x00, 0x00 ] ); thumbnail.extend_from_slice( b"WEBP" );
},
}
let data_size = ( self.config.width * self.config.height * 3 ) / 10; thumbnail.resize( data_size as usize, 0x80 );
thumbnail.freeze()
}
}