use super::types::{StorageError, StorageResult};
#[derive(Debug, Clone)]
pub struct UploadPolicy {
max_file_size: Option<u64>,
allowed_mime_types: Option<Vec<String>>,
max_total_storage: Option<u64>,
max_uploads_per_window: Option<usize>,
rate_limit_window_secs: Option<u64>,
}
impl Default for UploadPolicy {
fn default() -> Self {
Self {
max_file_size: Some(10 * 1024 * 1024), allowed_mime_types: None, max_total_storage: Some(1024 * 1024 * 1024), max_uploads_per_window: Some(100), rate_limit_window_secs: Some(3600), }
}
}
impl UploadPolicy {
#[must_use]
pub fn builder() -> PolicyBuilder {
PolicyBuilder::new()
}
#[must_use]
pub const fn unrestricted() -> Self {
Self {
max_file_size: None,
allowed_mime_types: None,
max_total_storage: None,
max_uploads_per_window: None,
rate_limit_window_secs: None,
}
}
#[must_use]
pub fn restrictive() -> Self {
Self {
max_file_size: Some(1024 * 1024), allowed_mime_types: Some(vec![
"image/jpeg".to_string(),
"image/png".to_string(),
"image/gif".to_string(),
]),
max_total_storage: Some(10 * 1024 * 1024), max_uploads_per_window: Some(10), rate_limit_window_secs: Some(3600), }
}
pub fn allows_upload(
&self,
file_size: u64,
mime_type: &str,
current_storage_used: u64,
) -> StorageResult<()> {
if let Some(max_size) = self.max_file_size {
if file_size > max_size {
return Err(StorageError::FileSizeExceeded {
actual: file_size,
limit: max_size,
});
}
}
if let Some(allowed_types) = &self.allowed_mime_types {
if !allowed_types.iter().any(|t| t == mime_type) {
return Err(StorageError::InvalidMimeType {
expected: allowed_types.clone(),
actual: mime_type.to_string(),
});
}
}
if let Some(max_storage) = self.max_total_storage {
if current_storage_used + file_size > max_storage {
return Err(StorageError::QuotaExceeded);
}
}
Ok(())
}
#[must_use]
pub const fn max_file_size(&self) -> Option<u64> {
self.max_file_size
}
#[must_use]
pub fn allowed_mime_types(&self) -> Option<&[String]> {
self.allowed_mime_types.as_deref()
}
#[must_use]
pub const fn max_total_storage(&self) -> Option<u64> {
self.max_total_storage
}
#[must_use]
pub const fn rate_limit(&self) -> Option<(usize, u64)> {
match (self.max_uploads_per_window, self.rate_limit_window_secs) {
(Some(max_uploads), Some(window_secs)) => Some((max_uploads, window_secs)),
_ => None,
}
}
}
#[derive(Debug, Default)]
pub struct PolicyBuilder {
max_file_size: Option<u64>,
allowed_mime_types: Option<Vec<String>>,
max_total_storage: Option<u64>,
max_uploads_per_window: Option<usize>,
rate_limit_window_secs: Option<u64>,
}
impl PolicyBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn max_file_size(mut self, size: u64) -> Self {
self.max_file_size = Some(size);
self
}
#[must_use]
pub fn allowed_mime_types<I, S>(mut self, types: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_mime_types = Some(types.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub const fn max_total_storage(mut self, size: u64) -> Self {
self.max_total_storage = Some(size);
self
}
#[must_use]
pub const fn rate_limit(mut self, max_uploads: usize, window_secs: u64) -> Self {
self.max_uploads_per_window = Some(max_uploads);
self.rate_limit_window_secs = Some(window_secs);
self
}
#[must_use]
pub fn build(self) -> UploadPolicy {
UploadPolicy {
max_file_size: self.max_file_size,
allowed_mime_types: self.allowed_mime_types,
max_total_storage: self.max_total_storage,
max_uploads_per_window: self.max_uploads_per_window,
rate_limit_window_secs: self.rate_limit_window_secs,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_policy() {
let policy = UploadPolicy::default();
assert!(policy.max_file_size().is_some());
assert!(policy.max_total_storage().is_some());
assert!(policy.rate_limit().is_some());
}
#[test]
fn test_unrestricted_policy() {
let policy = UploadPolicy::unrestricted();
assert!(policy.max_file_size().is_none());
assert!(policy.allowed_mime_types().is_none());
assert!(policy.max_total_storage().is_none());
assert!(policy.rate_limit().is_none());
}
#[test]
fn test_restrictive_policy() {
let policy = UploadPolicy::restrictive();
assert_eq!(policy.max_file_size(), Some(1024 * 1024));
assert!(policy.allowed_mime_types().is_some());
}
#[test]
fn test_allows_upload_success() {
let policy = PolicyBuilder::new()
.max_file_size(10 * 1024 * 1024)
.allowed_mime_types(vec!["image/jpeg"])
.max_total_storage(100 * 1024 * 1024)
.build();
let result = policy.allows_upload(5 * 1024 * 1024, "image/jpeg", 50 * 1024 * 1024);
assert!(result.is_ok());
}
#[test]
fn test_allows_upload_file_too_large() {
let policy = PolicyBuilder::new()
.max_file_size(10 * 1024 * 1024)
.build();
let result = policy.allows_upload(20 * 1024 * 1024, "image/jpeg", 0);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
StorageError::FileSizeExceeded { .. }
));
}
#[test]
fn test_allows_upload_invalid_mime() {
let policy = PolicyBuilder::new()
.allowed_mime_types(vec!["image/jpeg", "image/png"])
.build();
let result = policy.allows_upload(1024, "application/pdf", 0);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
StorageError::InvalidMimeType { .. }
));
}
#[test]
fn test_allows_upload_quota_exceeded() {
let policy = PolicyBuilder::new()
.max_total_storage(100 * 1024 * 1024)
.build();
let result = policy.allows_upload(10 * 1024 * 1024, "image/jpeg", 95 * 1024 * 1024);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
StorageError::QuotaExceeded
));
}
#[test]
fn test_policy_builder() {
let policy = PolicyBuilder::new()
.max_file_size(5 * 1024 * 1024)
.allowed_mime_types(vec!["image/jpeg"])
.max_total_storage(50 * 1024 * 1024)
.rate_limit(100, 3600)
.build();
assert_eq!(policy.max_file_size(), Some(5 * 1024 * 1024));
assert!(policy.allowed_mime_types().is_some());
assert_eq!(policy.max_total_storage(), Some(50 * 1024 * 1024));
assert_eq!(policy.rate_limit(), Some((100, 3600)));
}
#[test]
fn test_rate_limit_getters() {
let policy = PolicyBuilder::new().rate_limit(50, 1800).build();
let (max_uploads, window_secs) = policy.rate_limit().unwrap();
assert_eq!(max_uploads, 50);
assert_eq!(window_secs, 1800);
}
}