Files
Oxicloud/src/infrastructure/services/compression_service.rs
T

517 lines
18 KiB
Rust
Raw Normal View History

2025-03-19 00:44:27 +01:00
use async_trait::async_trait;
use bytes::Bytes;
use flate2::Compression;
use flate2::bufread::GzDecoder;
2026-02-14 01:29:34 +01:00
use flate2::read::GzEncoder as GzEncoderRead;
use futures::{Stream, StreamExt};
use std::io;
use std::io::Read;
use std::sync::Arc;
use tracing::error;
2025-03-19 00:44:27 +01:00
use crate::application::ports::compression_ports::{
2026-02-14 01:29:34 +01:00
CompressionLevel as PortCompressionLevel, CompressionPort,
};
use crate::domain::errors::DomainError;
2025-03-19 00:44:27 +01:00
use crate::infrastructure::services::buffer_pool::BufferPool;
/// Compression level for files
2025-03-19 00:44:27 +01:00
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionLevel {
/// No compression (transfer only)
2025-03-19 00:44:27 +01:00
None = 0,
/// Fast compression with lower ratio
2025-03-19 00:44:27 +01:00
Fast = 1,
/// Balanced compression (default)
2025-03-19 00:44:27 +01:00
Default = 6,
/// Maximum compression (slower)
2025-03-19 00:44:27 +01:00
Best = 9,
}
impl From<CompressionLevel> for Compression {
fn from(level: CompressionLevel) -> Self {
match level {
CompressionLevel::None => Compression::none(),
CompressionLevel::Fast => Compression::fast(),
CompressionLevel::Default => Compression::default(),
CompressionLevel::Best => Compression::best(),
}
}
}
/// Size threshold to decide whether to compress or not
2025-03-19 00:44:27 +01:00
const COMPRESSION_SIZE_THRESHOLD: u64 = 1024 * 50; // 50KB
/// Interface for compression services
2025-03-19 00:44:27 +01:00
#[async_trait]
pub trait CompressionService: Send + Sync {
/// Compresses data in memory
2025-03-19 00:44:27 +01:00
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>>;
2026-02-14 01:29:34 +01:00
/// Decompresses data in memory
2025-03-19 00:44:27 +01:00
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>;
2026-02-14 01:29:34 +01:00
/// Compresses a data stream
2026-02-14 01:29:34 +01:00
fn compress_stream<S>(
&self,
stream: S,
level: CompressionLevel,
) -> impl Stream<Item = io::Result<Bytes>> + Send
2025-03-19 00:44:27 +01:00
where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
2026-02-14 01:29:34 +01:00
/// Decompresses a data stream
2026-02-14 01:29:34 +01:00
fn decompress_stream<S>(
&self,
compressed_stream: S,
) -> impl Stream<Item = io::Result<Bytes>> + Send
2025-03-19 00:44:27 +01:00
where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
2026-02-14 01:29:34 +01:00
/// Determines whether a file should be compressed based on its MIME type and size
2025-03-19 00:44:27 +01:00
fn should_compress(&self, mime_type: &str, size: u64) -> bool;
}
/// Gzip compression service implementation
2025-03-19 00:44:27 +01:00
pub struct GzipCompressionService {
/// Buffer pool for memory optimization
2025-03-19 00:44:27 +01:00
buffer_pool: Option<Arc<BufferPool>>,
}
impl GzipCompressionService {
/// Creates a new service instance
2025-03-19 00:44:27 +01:00
pub fn new() -> Self {
2026-02-14 01:29:34 +01:00
Self { buffer_pool: None }
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
/// Creates a new service instance with buffer pool
2025-03-19 00:44:27 +01:00
pub fn new_with_buffer_pool(buffer_pool: Arc<BufferPool>) -> Self {
Self {
buffer_pool: Some(buffer_pool),
}
}
}
#[async_trait]
impl CompressionService for GzipCompressionService {
/// Compresses data in memory using Gzip
2025-03-19 00:44:27 +01:00
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>> {
// If we have a buffer pool, use a borrowed buffer for compression
2025-03-19 00:44:27 +01:00
if let Some(pool) = &self.buffer_pool {
// Estimate the compression size (approximately 80% of original for typical cases)
2025-03-19 00:44:27 +01:00
let estimated_size = (data.len() as f64 * 0.8) as usize;
2026-02-14 01:29:34 +01:00
// Get a buffer from the pool
2025-03-19 00:44:27 +01:00
let buffer = pool.get_buffer().await;
2026-02-14 01:29:34 +01:00
// Check if the buffer is large enough
2025-03-19 00:44:27 +01:00
if buffer.capacity() >= estimated_size {
// Run compression in a worker thread using the buffer
2025-03-19 00:44:27 +01:00
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
let buffer_clone = buffer_ptr.clone();
2026-02-14 01:29:34 +01:00
// Compress data
// Clone the data to avoid lifetime issues
2025-03-19 00:44:27 +01:00
let data_owned = data.to_vec();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
let result = tokio::task::spawn_blocking(move || {
let mut encoder = GzEncoderRead::new(&data_owned[..], level.into());
2026-02-14 01:29:34 +01:00
// Try to lock the mutex (should not fail since we are in a separate thread)
2025-03-19 00:44:27 +01:00
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
buffer => buffer,
};
2026-02-14 01:29:34 +01:00
// Read directly into the buffer
2025-03-19 00:44:27 +01:00
let read_bytes = encoder.read(buffer_guard.as_mut_slice())?;
buffer_guard.set_used(read_bytes);
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok(()) as io::Result<()>
2026-02-14 01:29:34 +01:00
})
.await;
// Verify result
2025-03-19 00:44:27 +01:00
match result {
Ok(Ok(())) => {
// Get the buffer and convert it to Vec<u8>
2025-03-19 00:44:27 +01:00
let buffer = buffer_ptr.lock().await;
let cloned_buffer = buffer.clone();
drop(buffer); // Release the mutex first
2025-03-19 00:44:27 +01:00
return Ok(cloned_buffer.into_vec());
2026-02-14 01:29:34 +01:00
}
2025-03-19 00:44:27 +01:00
Ok(Err(e)) => {
error!("Compression error with buffer pool: {}", e);
// Fall back to standard implementation
2026-02-14 01:29:34 +01:00
}
2025-03-19 00:44:27 +01:00
Err(e) => {
error!("Compression task error with buffer pool: {}", e);
// Fall back to standard implementation
2025-03-19 00:44:27 +01:00
}
}
}
}
2026-02-14 01:29:34 +01:00
// Standard implementation if there is no buffer pool or the buffer is insufficient
// Clone the data to avoid lifetime issues
2025-03-19 00:44:27 +01:00
let data_owned = data.to_vec();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
tokio::task::spawn_blocking(move || {
let mut encoder = GzEncoderRead::new(&data_owned[..], level.into());
let mut compressed = Vec::new();
encoder.read_to_end(&mut compressed)?;
Ok(compressed)
2026-02-14 01:29:34 +01:00
})
.await
.unwrap_or_else(|e| {
error!("Compression task error: {}", e);
Err(io::Error::other(e.to_string()))
2025-03-19 00:44:27 +01:00
})
}
2026-02-14 01:29:34 +01:00
/// Decompresses data in memory
2025-03-19 00:44:27 +01:00
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>> {
// If we have a buffer pool, use a borrowed buffer for decompression
2025-03-19 00:44:27 +01:00
if let Some(pool) = &self.buffer_pool {
// Estimate the decompression size (approximately 5x of compressed for typical cases)
2025-03-19 00:44:27 +01:00
let estimated_size = compressed_data.len() * 5;
2026-02-14 01:29:34 +01:00
// Get a buffer from the pool
2025-03-19 00:44:27 +01:00
let buffer = pool.get_buffer().await;
2026-02-14 01:29:34 +01:00
// Check if the buffer is large enough
2025-03-19 00:44:27 +01:00
if buffer.capacity() >= estimated_size {
// Clone compressed data to move to the worker
2025-03-19 00:44:27 +01:00
let data = compressed_data.to_vec();
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
let buffer_clone = buffer_ptr.clone();
2026-02-14 01:29:34 +01:00
// Decompress data
2025-03-19 00:44:27 +01:00
let result = tokio::task::spawn_blocking(move || {
let mut decoder = GzDecoder::new(&data[..]);
2026-02-14 01:29:34 +01:00
// Try to lock the mutex
2025-03-19 00:44:27 +01:00
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
buffer => buffer,
};
2026-02-14 01:29:34 +01:00
// Read directly into the buffer
2025-03-19 00:44:27 +01:00
let read_bytes = decoder.read(buffer_guard.as_mut_slice())?;
buffer_guard.set_used(read_bytes);
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Ok(()) as io::Result<()>
2026-02-14 01:29:34 +01:00
})
.await;
// Verify result
2025-03-19 00:44:27 +01:00
match result {
Ok(Ok(())) => {
// Get the buffer and convert it to Vec<u8>
2025-03-19 00:44:27 +01:00
let buffer = buffer_ptr.lock().await;
let cloned_buffer = buffer.clone();
drop(buffer); // Release the mutex first
2025-03-19 00:44:27 +01:00
return Ok(cloned_buffer.into_vec());
2026-02-14 01:29:34 +01:00
}
2025-03-19 00:44:27 +01:00
Ok(Err(e)) => {
error!("Decompression error with buffer pool: {}", e);
// Fall back to standard implementation
2026-02-14 01:29:34 +01:00
}
2025-03-19 00:44:27 +01:00
Err(e) => {
error!("Decompression task error with buffer pool: {}", e);
// Fall back to standard implementation
2025-03-19 00:44:27 +01:00
}
}
}
}
2026-02-14 01:29:34 +01:00
// Standard implementation if there is no buffer pool or the buffer is insufficient
let data = compressed_data.to_vec(); // Clone to move to the worker
2025-03-19 00:44:27 +01:00
tokio::task::spawn_blocking(move || {
let mut decoder = GzDecoder::new(&data[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
2026-02-14 01:29:34 +01:00
})
.await
.unwrap_or_else(|e| {
error!("Decompression task error: {}", e);
Err(io::Error::other(e.to_string()))
2025-03-19 00:44:27 +01:00
})
}
2026-02-14 01:29:34 +01:00
/// Compresses a byte stream
2026-02-14 01:29:34 +01:00
fn compress_stream<S>(
&self,
stream: S,
level: CompressionLevel,
) -> impl Stream<Item = io::Result<Bytes>> + Send
2025-03-19 00:44:27 +01:00
where
2026-02-14 01:29:34 +01:00
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin,
2025-03-19 00:44:27 +01:00
{
// For now, simplify the implementation to avoid complex pinning issues
// This implementation collects all stream data and then compresses it at once
// Future optimization would be to implement true streaming compression
let compression_level = level;
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
Box::pin(async_stream::stream! {
let mut data = Vec::new();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
// Collect all bytes from the stream
let mut stream = Box::pin(stream);
while let Some(result) = stream.next().await {
match result {
Ok(bytes) => {
data.extend_from_slice(&bytes);
},
Err(e) => {
yield Err(e);
return;
}
}
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
// Compress collected data
match CompressionService::compress_data(self, &data, compression_level).await {
2025-03-19 00:44:27 +01:00
Ok(compressed) => {
// Return compressed data as a single chunk
yield Ok(Bytes::from(compressed));
},
Err(e) => {
yield Err(e);
}
}
})
}
2026-02-14 01:29:34 +01:00
/// Decompresses a byte stream
2026-02-14 01:29:34 +01:00
fn decompress_stream<S>(
&self,
compressed_stream: S,
) -> impl Stream<Item = io::Result<Bytes>> + Send
2025-03-19 00:44:27 +01:00
where
2026-02-14 01:29:34 +01:00
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin,
2025-03-19 00:44:27 +01:00
{
// For now, simplify the implementation to avoid complex pinning issues
// This implementation collects all stream data and then decompresses it at once
// Future optimization would be to implement streaming decompression correctly
Box::pin(async_stream::stream! {
let mut compressed_data = Vec::new();
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
// Collect all bytes from the stream
let mut stream = Box::pin(compressed_stream);
while let Some(result) = stream.next().await {
match result {
Ok(bytes) => {
compressed_data.extend_from_slice(&bytes);
},
Err(e) => {
yield Err(e);
return;
}
}
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
// Decompress collected data
match CompressionService::decompress_data(self, &compressed_data).await {
2025-03-19 00:44:27 +01:00
Ok(decompressed) => {
// Return decompressed data as a single chunk
yield Ok(Bytes::from(decompressed));
},
Err(e) => {
yield Err(e);
}
}
})
}
2026-02-14 01:29:34 +01:00
/// Determines whether a file should be compressed based on its MIME type and size
2025-03-19 00:44:27 +01:00
fn should_compress(&self, mime_type: &str, size: u64) -> bool {
// Do not compress very small files (overhead)
2025-03-19 00:44:27 +01:00
if size < COMPRESSION_SIZE_THRESHOLD {
return false;
}
2026-02-14 01:29:34 +01:00
// Do not compress already compressed files
2025-03-19 00:44:27 +01:00
if mime_type.starts_with("image/")
&& !mime_type.contains("svg")
2026-02-14 01:29:34 +01:00
&& !mime_type.contains("bmp")
{
2025-03-19 00:44:27 +01:00
return false;
}
2026-02-14 01:29:34 +01:00
if mime_type.starts_with("audio/")
|| mime_type.starts_with("video/")
2025-03-19 00:44:27 +01:00
|| mime_type.contains("zip")
|| mime_type.contains("gzip")
|| mime_type.contains("compressed")
|| mime_type.contains("7z")
|| mime_type.contains("rar")
|| mime_type.contains("bz2")
|| mime_type.contains("xz")
|| mime_type.contains("jpg")
|| mime_type.contains("jpeg")
|| mime_type.contains("png")
|| mime_type.contains("gif")
|| mime_type.contains("webp")
|| mime_type.contains("mp3")
|| mime_type.contains("mp4")
|| mime_type.contains("ogg")
2026-02-14 01:29:34 +01:00
|| mime_type.contains("webm")
{
2025-03-19 00:44:27 +01:00
return false;
}
2026-02-14 01:29:34 +01:00
// Compress text files, documents, and other compressible types
2025-03-19 00:44:27 +01:00
true
}
}
// ─── Port implementation ─────────────────────────────────────────────────────
/// Convert application-layer CompressionLevel to infrastructure CompressionLevel.
impl From<PortCompressionLevel> for CompressionLevel {
fn from(level: PortCompressionLevel) -> Self {
match level {
PortCompressionLevel::None => CompressionLevel::None,
PortCompressionLevel::Fast => CompressionLevel::Fast,
PortCompressionLevel::Default => CompressionLevel::Default,
PortCompressionLevel::Best => CompressionLevel::Best,
}
}
}
#[async_trait]
impl CompressionPort for GzipCompressionService {
2026-02-14 01:29:34 +01:00
async fn compress_data(
&self,
data: &[u8],
level: PortCompressionLevel,
) -> Result<Vec<u8>, DomainError> {
CompressionService::compress_data(self, data, level.into())
.await
.map_err(DomainError::from)
}
async fn decompress_data(&self, compressed_data: &[u8]) -> Result<Vec<u8>, DomainError> {
2026-02-14 01:29:34 +01:00
CompressionService::decompress_data(self, compressed_data)
.await
.map_err(DomainError::from)
}
fn should_compress(&self, mime_type: &str, size: u64) -> bool {
CompressionService::should_compress(self, mime_type, size)
}
}
2025-03-19 00:44:27 +01:00
#[cfg(test)]
mod tests {
use super::*;
use futures::TryStreamExt;
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
#[tokio::test]
async fn test_compress_decompress_data() {
let service = GzipCompressionService::new();
2026-02-14 01:29:34 +01:00
// Test data
2025-03-19 00:44:27 +01:00
let data = "Hello, world! ".repeat(1000).into_bytes();
2026-02-14 01:29:34 +01:00
// Compress
2026-02-14 01:29:34 +01:00
let compressed =
CompressionService::compress_data(&service, &data, CompressionLevel::Default)
.await
.unwrap();
// Verify that compression reduces the size
2025-03-19 00:44:27 +01:00
assert!(compressed.len() < data.len());
2026-02-14 01:29:34 +01:00
// Decompress
2026-02-14 01:29:34 +01:00
let decompressed = CompressionService::decompress_data(&service, &compressed)
.await
.unwrap();
// Verify that the original data is recovered correctly
2025-03-19 00:44:27 +01:00
assert_eq!(decompressed, data);
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
#[tokio::test]
async fn test_compress_decompress_stream() {
let service = GzipCompressionService::new();
2026-02-14 01:29:34 +01:00
// Create test data
2025-03-19 00:44:27 +01:00
let chunks = vec![
Ok(Bytes::from("Hello, ")),
Ok(Bytes::from("world! ")),
Ok(Bytes::from("This is a test of streaming compression.")),
];
2026-02-14 01:29:34 +01:00
// Convert to stream
2025-03-19 00:44:27 +01:00
let input_stream = futures::stream::iter(chunks);
2026-02-14 01:29:34 +01:00
// Compress the stream
2025-03-19 00:44:27 +01:00
let compressed_stream = service.compress_stream(input_stream, CompressionLevel::Default);
2026-02-14 01:29:34 +01:00
// Collect the compressed bytes
2025-03-19 00:44:27 +01:00
let compressed_bytes = compressed_stream
.try_fold(Vec::new(), |mut acc, chunk| async move {
acc.extend_from_slice(&chunk);
Ok(acc)
2026-02-14 01:29:34 +01:00
})
.await
.unwrap();
// Decompress the data
2026-02-14 01:29:34 +01:00
let decompressed = CompressionService::decompress_data(&service, &compressed_bytes)
.await
.unwrap();
// Verify result
2025-03-19 00:44:27 +01:00
let expected = "Hello, world! This is a test of streaming compression.";
assert_eq!(String::from_utf8(decompressed).unwrap(), expected);
}
2026-02-14 01:29:34 +01:00
2025-03-19 00:44:27 +01:00
#[test]
fn test_should_compress() {
let service = GzipCompressionService::new();
2026-02-14 01:29:34 +01:00
// Cases that should not be compressed
2026-02-14 01:29:34 +01:00
assert!(!CompressionService::should_compress(
&service,
"image/jpeg",
100 * 1024
));
assert!(!CompressionService::should_compress(
&service,
"video/mp4",
10 * 1024 * 1024
));
assert!(!CompressionService::should_compress(
&service,
"application/zip",
5 * 1024 * 1024
));
// Cases that should be compressed
2026-02-14 01:29:34 +01:00
assert!(CompressionService::should_compress(
&service,
"text/html",
100 * 1024
));
assert!(CompressionService::should_compress(
&service,
"application/json",
200 * 1024
));
assert!(CompressionService::should_compress(
&service,
"text/plain",
1024 * 1024
));
// Small files should not be compressed regardless of type
2026-02-14 01:29:34 +01:00
assert!(!CompressionService::should_compress(
&service,
"text/html",
10 * 1024
));
2025-03-19 00:44:27 +01:00
}
2026-02-14 01:29:34 +01:00
}