fix(#104): enforce storage quota on uploads & fix usage tracking
Backend: - Add QuotaExceeded error kind mapped to HTTP 507 Insufficient Storage - Add check_storage_quota() and get_user_storage_info() to StorageUsagePort - Enforce quota in upload_file_with_cache, upload_file_with_thumbnails (AuthUser extractor) - Enforce quota in chunked upload create_upload handler - Add update_user_storage_usage_by_username() for username-based lookup - Fix extract_username_from_path() to handle subfolders (take first segment only) - Fix maybe_update_storage_usage() to use username-based lookup instead of passing username to get_user_by_id (which always failed silently) Frontend: - Parse and display quota error messages on upload failure (507 / QuotaExceeded) - Stop remaining uploads when quota is exceeded - Call refreshUserData() after uploads to update storage usage display - Bump service worker cache to v9
This commit is contained in:
@@ -20,6 +20,7 @@ use std::sync::Arc;
|
||||
use crate::application::ports::chunked_upload_ports::DEFAULT_CHUNK_SIZE;
|
||||
use crate::common::di::AppState;
|
||||
use crate::domain::errors::ErrorKind;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
/// Request body for creating an upload session
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -75,6 +76,7 @@ impl ChunkedUploadHandler {
|
||||
/// ```
|
||||
pub async fn create_upload(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Json(request): Json<CreateUploadRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let chunked_service = &state.core.chunked_upload_service;
|
||||
@@ -100,6 +102,30 @@ impl ChunkedUploadHandler {
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// ── Quota enforcement ────────────────────────────────────
|
||||
if let Some(storage_svc) = state.storage_usage_service.as_ref() {
|
||||
if let Err(err) = storage_svc
|
||||
.check_storage_quota(&auth_user.id, request.total_size)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"⛔ CHUNKED UPLOAD REJECTED (quota): user={}, file={}, size={} — {}",
|
||||
auth_user.username,
|
||||
request.filename,
|
||||
request.total_size,
|
||||
err.message
|
||||
);
|
||||
return (
|
||||
StatusCode::INSUFFICIENT_STORAGE,
|
||||
Json(serde_json::json!({
|
||||
"error": err.message,
|
||||
"error_type": "QuotaExceeded"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate chunk size if provided
|
||||
let chunk_size = request.chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE);
|
||||
if chunk_size < 1024 * 1024 {
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::collections::HashMap;
|
||||
use crate::application::ports::compression_ports::{CompressionLevel, CompressionPort};
|
||||
use crate::application::ports::file_ports::OptimizedFileContent;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::OptionalUserId;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, OptionalUserId};
|
||||
|
||||
/**
|
||||
* Type aliases for dependency injection state.
|
||||
@@ -121,6 +121,7 @@ impl FileHandler {
|
||||
/// optimal tier and handles deduplication internally.
|
||||
pub async fn upload_file_with_cache(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
mut multipart: Multipart,
|
||||
) -> impl IntoResponse {
|
||||
let upload_service = &state.applications.file_upload_service;
|
||||
@@ -167,6 +168,23 @@ impl FileHandler {
|
||||
};
|
||||
}
|
||||
|
||||
// ── Quota enforcement ────────────────────────────────────
|
||||
if let Some(storage_svc) = state.storage_usage_service.as_ref() {
|
||||
if let Err(err) = storage_svc
|
||||
.check_storage_quota(&auth_user.id, total_size as u64)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"⛔ UPLOAD REJECTED (quota): user={}, file={}, size={} — {}",
|
||||
auth_user.username,
|
||||
filename,
|
||||
total_size,
|
||||
err
|
||||
);
|
||||
return Self::quota_error_response(err).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate to smart_upload (handles write-behind, dedup, streaming)
|
||||
match upload_service
|
||||
.smart_upload(
|
||||
@@ -531,10 +549,12 @@ impl FileHandler {
|
||||
/// a background task to generate all thumbnail sizes.
|
||||
pub async fn upload_file_with_thumbnails(
|
||||
State(state): State<GlobalState>,
|
||||
auth_user: AuthUser,
|
||||
multipart: Multipart,
|
||||
) -> impl IntoResponse {
|
||||
// Use the smart upload handler
|
||||
let response = Self::upload_file_with_cache(State(state.clone()), multipart).await;
|
||||
let response =
|
||||
Self::upload_file_with_cache(State(state.clone()), auth_user, multipart).await;
|
||||
|
||||
// Try to extract file info for thumbnail generation
|
||||
if let Ok(body_bytes) =
|
||||
@@ -827,6 +847,7 @@ impl FileHandler {
|
||||
fn domain_error_response(err: crate::common::errors::DomainError) -> Response<Body> {
|
||||
let status = match err.kind {
|
||||
crate::common::errors::ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
crate::common::errors::ErrorKind::QuotaExceeded => StatusCode::INSUFFICIENT_STORAGE,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
Response::builder()
|
||||
@@ -838,6 +859,21 @@ impl FileHandler {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Build a quota-specific error response with 507 status and structured body.
|
||||
fn quota_error_response(err: crate::common::errors::DomainError) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INSUFFICIENT_STORAGE)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"error": err.message,
|
||||
"error_type": "QuotaExceeded"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Build response for cached/small files with optional compression.
|
||||
async fn build_cached_response(
|
||||
content: Bytes,
|
||||
|
||||
Reference in New Issue
Block a user