Strip all 413 #[allow(dead_code)] annotations from 139 files and remove the actual dead code they were suppressing: unused struct fields (and their constructor sites), unused methods/functions, and entire dead structs. Key removals: - trading_engine compliance: ~50 dead structs/fields across audit, reporting, SOX modules - trading_service: dead execution engine fields, broker routing, paper trading methods - ml_training_service: dead TLS validation (~340 lines), GPU state, monitoring fields - backtesting_service: dead model cache, TLS validation, TradeSignal fields - risk: dead VaR engine fields, safety coordinator fields, position tracker fields - adaptive-strategy: dead ensemble methods, regime detection, sizing functions 147 files changed, -4264 net lines. Workspace compiles with 0 errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
666 lines
21 KiB
Rust
666 lines
21 KiB
Rust
//! Model Storage Management
|
|
//!
|
|
//! This module handles storage and retrieval of trained model artifacts,
|
|
//! supporting both local filesystem and AWS S3 storage for HFT deployments.
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::{Context, Result};
|
|
use async_trait::async_trait;
|
|
// Using storage crate's object_store backend instead of AWS SDK
|
|
use storage::Storage;
|
|
use tokio::fs;
|
|
use tracing::{debug, info};
|
|
use uuid::Uuid;
|
|
|
|
use config::manager::ConfigManager;
|
|
use config::S3Config;
|
|
|
|
/// Local storage configuration for ML models
|
|
#[derive(Debug, Clone)]
|
|
pub struct StorageConfig {
|
|
pub storage_type: String,
|
|
pub local_base_path: Option<PathBuf>,
|
|
pub enable_compression: bool,
|
|
}
|
|
|
|
impl Default for StorageConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
storage_type: "local".to_string(),
|
|
local_base_path: Some(PathBuf::from("./models")),
|
|
enable_compression: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<config::storage_config::StorageConfig> for StorageConfig {
|
|
fn from(config_storage: config::storage_config::StorageConfig) -> Self {
|
|
Self {
|
|
storage_type: config_storage.storage_type,
|
|
local_base_path: config_storage.local_base_path,
|
|
enable_compression: config_storage.enable_compression,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trait for model storage operations
|
|
#[async_trait]
|
|
pub trait ModelStorage: Send + Sync + std::fmt::Debug {
|
|
/// Store a model artifact
|
|
async fn store_model(&self, job_id: Uuid, model_data: &[u8]) -> Result<String>;
|
|
|
|
/// Retrieve a model artifact
|
|
async fn retrieve_model(&self, artifact_path: &str) -> Result<Vec<u8>>;
|
|
|
|
/// Delete a model artifact
|
|
async fn delete_model(&self, artifact_path: &str) -> Result<bool>;
|
|
|
|
/// Check if a model artifact exists
|
|
async fn model_exists(&self, artifact_path: &str) -> Result<bool>;
|
|
|
|
/// List all models for a specific job (for versioning)
|
|
async fn list_job_models(&self, job_id: Uuid) -> Result<Vec<String>>;
|
|
|
|
/// Get storage usage statistics
|
|
async fn get_storage_stats(&self) -> Result<StorageStats>;
|
|
}
|
|
|
|
/// Storage statistics
|
|
#[derive(Debug, Clone)]
|
|
pub struct StorageStats {
|
|
pub total_models: u64,
|
|
pub total_size_bytes: u64,
|
|
pub average_model_size_bytes: u64,
|
|
pub storage_type: String,
|
|
}
|
|
|
|
/// Model storage manager that delegates to the appropriate storage backend
|
|
#[derive(Debug)]
|
|
pub struct ModelStorageManager {
|
|
backend: Box<dyn ModelStorage>,
|
|
config: StorageConfig,
|
|
}
|
|
|
|
impl ModelStorageManager {
|
|
/// Create a new model storage manager
|
|
pub async fn new(config: StorageConfig) -> Result<Self> {
|
|
let backend: Box<dyn ModelStorage> = match config.storage_type.as_str() {
|
|
"local" => {
|
|
let local_storage = LocalModelStorage::new(config.clone()).await?;
|
|
Box::new(local_storage)
|
|
},
|
|
"s3" => {
|
|
return Err(anyhow::anyhow!(
|
|
"S3 storage requires secure configuration. Use new_with_config_loader() instead."
|
|
));
|
|
},
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"Unsupported storage type: {}. Supported types: 'local', 's3'",
|
|
config.storage_type
|
|
));
|
|
},
|
|
};
|
|
|
|
info!("Initialized {} model storage", config.storage_type);
|
|
|
|
Ok(Self { backend, config })
|
|
}
|
|
|
|
/// Create a new model storage manager with secure configuration loading
|
|
pub async fn new_with_config_manager(
|
|
config: StorageConfig,
|
|
config_manager: Arc<ConfigManager>,
|
|
) -> Result<Self> {
|
|
let backend: Box<dyn ModelStorage> = match config.storage_type.as_str() {
|
|
"local" => {
|
|
let local_storage = LocalModelStorage::new(config.clone()).await?;
|
|
Box::new(local_storage)
|
|
},
|
|
"s3" => {
|
|
let s3_storage =
|
|
S3ModelStorage::new_with_config(config.clone(), config_manager).await?;
|
|
Box::new(s3_storage)
|
|
},
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"Unsupported storage type: {}. Supported types: 'local', 's3'",
|
|
config.storage_type
|
|
));
|
|
},
|
|
};
|
|
|
|
info!(
|
|
"Initialized {} model storage with secure configuration",
|
|
config.storage_type
|
|
);
|
|
|
|
Ok(Self { backend, config })
|
|
}
|
|
|
|
/// Store a model with optional compression
|
|
pub async fn store_model(&self, job_id: Uuid, model_data: &[u8]) -> Result<String> {
|
|
let data_to_store = if self.config.enable_compression {
|
|
self.compress_model_data(model_data)?
|
|
} else {
|
|
model_data.to_vec()
|
|
};
|
|
|
|
let artifact_path = self.backend.store_model(job_id, &data_to_store).await?;
|
|
|
|
info!("Stored model for job {} at {}", job_id, artifact_path);
|
|
Ok(artifact_path)
|
|
}
|
|
|
|
/// Retrieve a model with automatic decompression
|
|
pub async fn retrieve_model(&self, artifact_path: &str) -> Result<Vec<u8>> {
|
|
let stored_data = self.backend.retrieve_model(artifact_path).await?;
|
|
|
|
let model_data = if self.config.enable_compression {
|
|
self.decompress_model_data(&stored_data)?
|
|
} else {
|
|
stored_data
|
|
};
|
|
|
|
debug!("Retrieved model from {}", artifact_path);
|
|
Ok(model_data)
|
|
}
|
|
|
|
/// Delete a model artifact
|
|
pub async fn delete_model(&self, artifact_path: &str) -> Result<bool> {
|
|
let deleted = self.backend.delete_model(artifact_path).await?;
|
|
if deleted {
|
|
info!("Deleted model artifact: {}", artifact_path);
|
|
}
|
|
Ok(deleted)
|
|
}
|
|
|
|
/// Check if a model exists
|
|
pub async fn model_exists(&self, artifact_path: &str) -> Result<bool> {
|
|
self.backend.model_exists(artifact_path).await
|
|
}
|
|
|
|
/// List models for a job
|
|
pub async fn list_job_models(&self, job_id: Uuid) -> Result<Vec<String>> {
|
|
self.backend.list_job_models(job_id).await
|
|
}
|
|
|
|
/// Get storage statistics
|
|
pub async fn get_storage_stats(&self) -> Result<StorageStats> {
|
|
self.backend.get_storage_stats().await
|
|
}
|
|
|
|
/// Compress model data using gzip
|
|
fn compress_model_data(&self, data: &[u8]) -> Result<Vec<u8>> {
|
|
use flate2::write::GzEncoder;
|
|
use flate2::Compression;
|
|
use std::io::Write;
|
|
|
|
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
|
encoder
|
|
.write_all(data)
|
|
.context("Failed to compress model data")?;
|
|
let compressed = encoder.finish().context("Failed to finalize compression")?;
|
|
|
|
debug!(
|
|
"Compressed model from {} to {} bytes ({:.1}% reduction)",
|
|
data.len(),
|
|
compressed.len(),
|
|
(1.0 - compressed.len() as f64 / data.len() as f64) * 100.0
|
|
);
|
|
|
|
Ok(compressed)
|
|
}
|
|
|
|
/// Decompress model data
|
|
fn decompress_model_data(&self, compressed_data: &[u8]) -> Result<Vec<u8>> {
|
|
use flate2::read::GzDecoder;
|
|
use std::io::Read;
|
|
|
|
let mut decoder = GzDecoder::new(compressed_data);
|
|
let mut decompressed = Vec::new();
|
|
decoder
|
|
.read_to_end(&mut decompressed)
|
|
.context("Failed to decompress model data")?;
|
|
|
|
debug!(
|
|
"Decompressed model from {} to {} bytes",
|
|
compressed_data.len(),
|
|
decompressed.len()
|
|
);
|
|
|
|
Ok(decompressed)
|
|
}
|
|
}
|
|
|
|
/// Local filesystem storage implementation
|
|
#[derive(Debug)]
|
|
pub struct LocalModelStorage {
|
|
base_path: PathBuf,
|
|
}
|
|
|
|
impl LocalModelStorage {
|
|
/// Create a new local storage instance
|
|
pub async fn new(config: StorageConfig) -> Result<Self> {
|
|
let base_path = config
|
|
.local_base_path
|
|
.ok_or_else(|| anyhow::anyhow!("local_base_path required for local storage"))?;
|
|
|
|
// Create base directory if it doesn't exist
|
|
fs::create_dir_all(&base_path)
|
|
.await
|
|
.context("Failed to create storage directory")?;
|
|
|
|
info!(
|
|
"Initialized local model storage at: {}",
|
|
base_path.display()
|
|
);
|
|
|
|
Ok(Self { base_path })
|
|
}
|
|
|
|
/// Generate directory path for job models
|
|
fn get_job_directory(&self, job_id: Uuid) -> PathBuf {
|
|
self.base_path.join("jobs").join(job_id.to_string())
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ModelStorage for LocalModelStorage {
|
|
async fn store_model(&self, job_id: Uuid, model_data: &[u8]) -> Result<String> {
|
|
// Use timestamped path for versioning support
|
|
let timestamp = chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0);
|
|
let filename = format!("{}_{}.bin", job_id, timestamp);
|
|
let model_path = self.base_path.join("models").join(filename);
|
|
|
|
// Create parent directory if it doesn't exist
|
|
if let Some(parent) = model_path.parent() {
|
|
fs::create_dir_all(parent)
|
|
.await
|
|
.context("Failed to create model directory")?;
|
|
}
|
|
|
|
// Write model data to file
|
|
fs::write(&model_path, model_data)
|
|
.await
|
|
.context("Failed to write model file")?;
|
|
|
|
// Return relative path from base
|
|
let relative_path = model_path
|
|
.strip_prefix(&self.base_path)
|
|
.map_err(|_| anyhow::anyhow!("Failed to get relative path"))?;
|
|
|
|
Ok(relative_path.to_string_lossy().to_string())
|
|
}
|
|
|
|
async fn retrieve_model(&self, artifact_path: &str) -> Result<Vec<u8>> {
|
|
let full_path = self.base_path.join(artifact_path);
|
|
|
|
fs::read(&full_path)
|
|
.await
|
|
.context("Failed to read model file")
|
|
}
|
|
|
|
async fn delete_model(&self, artifact_path: &str) -> Result<bool> {
|
|
let full_path = self.base_path.join(artifact_path);
|
|
|
|
match fs::remove_file(&full_path).await {
|
|
Ok(()) => Ok(true),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
|
Err(e) => Err(e).context("Failed to delete model file"),
|
|
}
|
|
}
|
|
|
|
async fn model_exists(&self, artifact_path: &str) -> Result<bool> {
|
|
let full_path = self.base_path.join(artifact_path);
|
|
Ok(full_path.exists())
|
|
}
|
|
|
|
async fn list_job_models(&self, job_id: Uuid) -> Result<Vec<String>> {
|
|
let job_dir = self.get_job_directory(job_id);
|
|
|
|
if !job_dir.exists() {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let mut models = Vec::new();
|
|
let mut entries = fs::read_dir(&job_dir)
|
|
.await
|
|
.context("Failed to read job directory")?;
|
|
|
|
while let Some(entry) = entries
|
|
.next_entry()
|
|
.await
|
|
.context("Failed to read directory entry")?
|
|
{
|
|
if entry.file_type().await?.is_file() {
|
|
if let Some(_path_str) = entry.path().to_str() {
|
|
if let Ok(relative) = entry.path().strip_prefix(&self.base_path) {
|
|
models.push(relative.to_string_lossy().to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(models)
|
|
}
|
|
|
|
async fn get_storage_stats(&self) -> Result<StorageStats> {
|
|
let mut total_models = 0u64;
|
|
let mut total_size = 0u64;
|
|
|
|
let models_dir = self.base_path.join("models");
|
|
if models_dir.exists() {
|
|
let mut entries = fs::read_dir(&models_dir)
|
|
.await
|
|
.context("Failed to read models directory")?;
|
|
|
|
while let Some(entry) = entries
|
|
.next_entry()
|
|
.await
|
|
.context("Failed to read directory entry")?
|
|
{
|
|
if entry.file_type().await?.is_file() {
|
|
total_models += 1;
|
|
if let Ok(metadata) = entry.metadata().await {
|
|
total_size += metadata.len();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let average_size = if total_models > 0 {
|
|
total_size / total_models
|
|
} else {
|
|
0
|
|
};
|
|
|
|
Ok(StorageStats {
|
|
total_models,
|
|
total_size_bytes: total_size,
|
|
average_model_size_bytes: average_size,
|
|
storage_type: "local".to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// S3 storage implementation using storage crate backend
|
|
#[derive(Debug)]
|
|
pub struct S3ModelStorage {
|
|
storage_backend: storage::ObjectStoreBackend,
|
|
bucket_name: String,
|
|
}
|
|
|
|
impl S3ModelStorage {
|
|
/// Create a new S3 storage instance using secure configuration
|
|
pub async fn new_with_config(
|
|
_config: StorageConfig,
|
|
config_manager: Arc<ConfigManager>,
|
|
) -> Result<Self> {
|
|
// Retrieve S3 credentials from environment or use defaults
|
|
let s3_config = config::S3Config::default();
|
|
|
|
info!(
|
|
"Initializing S3 storage with bucket: {}, region: {}",
|
|
s3_config.bucket_name, s3_config.region
|
|
);
|
|
|
|
// Use storage crate's object store backend
|
|
let storage_backend =
|
|
storage::ObjectStoreBackend::new(s3_config.clone(), Some(config_manager))
|
|
.await
|
|
.context("Failed to create S3 object store")?;
|
|
|
|
info!(
|
|
"Successfully connected to S3 bucket: {}",
|
|
s3_config.bucket_name
|
|
);
|
|
|
|
Ok(Self {
|
|
storage_backend,
|
|
bucket_name: s3_config.bucket_name,
|
|
})
|
|
}
|
|
|
|
/// Create a new S3 storage instance using environment variables
|
|
pub async fn from_env() -> Result<Self> {
|
|
let bucket_name = std::env::var("S3_MODEL_STORAGE_BUCKET")
|
|
.unwrap_or_else(|_| "foxhunt-models".to_string());
|
|
|
|
info!(
|
|
"Initializing S3 storage from environment with bucket: {}",
|
|
bucket_name
|
|
);
|
|
|
|
// Use storage crate's environment-aware S3 creation
|
|
let s3_config = S3Config {
|
|
bucket_name: bucket_name.clone(),
|
|
region: std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string()),
|
|
..Default::default()
|
|
};
|
|
let storage_backend = storage::ObjectStoreBackend::new(s3_config, None)
|
|
.await
|
|
.context("Failed to create S3 object store from environment")?;
|
|
|
|
info!("Successfully connected to S3 bucket: {}", bucket_name);
|
|
|
|
Ok(Self {
|
|
storage_backend,
|
|
bucket_name,
|
|
})
|
|
}
|
|
|
|
/// Generate S3 key for a model
|
|
fn get_model_key(&self, job_id: Uuid) -> String {
|
|
format!("models/{}.bin", job_id)
|
|
}
|
|
|
|
/// Generate S3 key prefix for job models
|
|
fn get_job_key_prefix(&self, job_id: Uuid) -> String {
|
|
format!("jobs/{}/", job_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ModelStorage for S3ModelStorage {
|
|
async fn store_model(&self, job_id: Uuid, model_data: &[u8]) -> Result<String> {
|
|
let key = self.get_model_key(job_id);
|
|
|
|
debug!("Storing model to S3: s3://{}/{}", self.bucket_name, key);
|
|
|
|
// Use storage crate's store operation
|
|
self.storage_backend
|
|
.store(&key, model_data)
|
|
.await
|
|
.context("Failed to upload model to S3")?;
|
|
|
|
info!(
|
|
"Successfully stored model to S3: s3://{}/{}",
|
|
self.bucket_name, key
|
|
);
|
|
Ok(key)
|
|
}
|
|
|
|
async fn retrieve_model(&self, artifact_path: &str) -> Result<Vec<u8>> {
|
|
debug!(
|
|
"Retrieving model from S3: s3://{}/{}",
|
|
self.bucket_name, artifact_path
|
|
);
|
|
|
|
let model_data = self
|
|
.storage_backend
|
|
.retrieve(artifact_path)
|
|
.await
|
|
.context("Failed to retrieve model from S3")?;
|
|
|
|
debug!("Retrieved {} bytes from S3", model_data.len());
|
|
Ok(model_data)
|
|
}
|
|
|
|
async fn delete_model(&self, artifact_path: &str) -> Result<bool> {
|
|
debug!(
|
|
"Deleting model from S3: s3://{}/{}",
|
|
self.bucket_name, artifact_path
|
|
);
|
|
|
|
match self.storage_backend.delete(artifact_path).await {
|
|
Ok(deleted) => {
|
|
if deleted {
|
|
info!("Successfully deleted model from S3: {}", artifact_path);
|
|
} else {
|
|
debug!("Model not found in S3: {}", artifact_path);
|
|
}
|
|
Ok(deleted)
|
|
},
|
|
Err(e) => Err(anyhow::anyhow!("Failed to delete model from S3: {}", e)),
|
|
}
|
|
}
|
|
|
|
async fn model_exists(&self, artifact_path: &str) -> Result<bool> {
|
|
match self.storage_backend.exists(artifact_path).await {
|
|
Ok(exists) => Ok(exists),
|
|
Err(e) => Err(anyhow::anyhow!(
|
|
"Failed to check if model exists in S3: {}",
|
|
e
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn list_job_models(&self, job_id: Uuid) -> Result<Vec<String>> {
|
|
let prefix = self.get_job_key_prefix(job_id);
|
|
|
|
debug!("Listing models for job {} with prefix: {}", job_id, prefix);
|
|
|
|
let models = self
|
|
.storage_backend
|
|
.list(&prefix)
|
|
.await
|
|
.context("Failed to list objects in S3")?;
|
|
|
|
debug!("Found {} models for job {}", models.len(), job_id);
|
|
Ok(models)
|
|
}
|
|
|
|
async fn get_storage_stats(&self) -> Result<StorageStats> {
|
|
debug!(
|
|
"Getting S3 storage statistics for bucket: {}",
|
|
self.bucket_name
|
|
);
|
|
|
|
let mut total_models = 0u64;
|
|
let mut total_size = 0u64;
|
|
|
|
// Count all objects in the models/ prefix
|
|
let models = self
|
|
.storage_backend
|
|
.list("models/")
|
|
.await
|
|
.context("Failed to list objects for statistics")?;
|
|
|
|
for model_path in models {
|
|
total_models += 1;
|
|
// Get metadata to determine size
|
|
if let Ok(metadata) = self.storage_backend.metadata(&model_path).await {
|
|
total_size += metadata.size;
|
|
}
|
|
}
|
|
|
|
let average_size = if total_models > 0 {
|
|
total_size / total_models
|
|
} else {
|
|
0
|
|
};
|
|
|
|
Ok(StorageStats {
|
|
total_models,
|
|
total_size_bytes: total_size,
|
|
average_model_size_bytes: average_size,
|
|
storage_type: format!("s3:{}", self.bucket_name),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::TempDir;
|
|
|
|
async fn create_test_local_storage() -> Result<(LocalModelStorage, TempDir)> {
|
|
let temp_dir = TempDir::new()?;
|
|
let config = StorageConfig {
|
|
storage_type: "local".to_string(),
|
|
local_base_path: Some(temp_dir.path().to_path_buf()),
|
|
enable_compression: false,
|
|
};
|
|
|
|
let storage = LocalModelStorage::new(config).await?;
|
|
Ok((storage, temp_dir))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_local_storage_store_and_retrieve() {
|
|
let (storage, _temp_dir) = create_test_local_storage().await.unwrap();
|
|
|
|
let job_id = Uuid::new_v4();
|
|
let model_data = b"test model data";
|
|
|
|
// Store model
|
|
let artifact_path = storage.store_model(job_id, model_data).await.unwrap();
|
|
assert!(!artifact_path.is_empty());
|
|
|
|
// Check existence
|
|
assert!(storage.model_exists(&artifact_path).await.unwrap());
|
|
|
|
// Retrieve model
|
|
let retrieved_data = storage.retrieve_model(&artifact_path).await.unwrap();
|
|
assert_eq!(retrieved_data, model_data);
|
|
|
|
// Delete model
|
|
assert!(storage.delete_model(&artifact_path).await.unwrap());
|
|
assert!(!storage.model_exists(&artifact_path).await.unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_storage_manager_with_compression() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let config = StorageConfig {
|
|
storage_type: "local".to_string(),
|
|
local_base_path: Some(temp_dir.path().to_path_buf()),
|
|
enable_compression: true,
|
|
};
|
|
|
|
let manager = ModelStorageManager::new(config).await.unwrap();
|
|
|
|
let job_id = Uuid::new_v4();
|
|
let model_data = b"test model data that should be compressed";
|
|
|
|
// Store with compression
|
|
let artifact_path = manager.store_model(job_id, model_data).await.unwrap();
|
|
|
|
// Retrieve with decompression
|
|
let retrieved_data = manager.retrieve_model(&artifact_path).await.unwrap();
|
|
assert_eq!(retrieved_data, model_data);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_storage_stats() {
|
|
let (storage, _temp_dir) = create_test_local_storage().await.unwrap();
|
|
|
|
// Initially empty
|
|
let stats = storage.get_storage_stats().await.unwrap();
|
|
assert_eq!(stats.total_models, 0);
|
|
|
|
// Store a model
|
|
let job_id = Uuid::new_v4();
|
|
let model_data = b"test model data";
|
|
storage.store_model(job_id, model_data).await.unwrap();
|
|
|
|
// Check stats
|
|
let stats = storage.get_storage_stats().await.unwrap();
|
|
assert_eq!(stats.total_models, 1);
|
|
assert!(stats.total_size_bytes > 0);
|
|
}
|
|
}
|