22from datetime import datetime , timedelta , timezone
33from typing import Annotated
44
5- from fastapi import APIRouter , Form , UploadFile
5+ from fastapi import APIRouter , Form , HTTPException , UploadFile , status
6+ from sqlmodel import select
67
78from app .converter .bytes import ByteSize
89from app .deps import S3Dep , SessionDep
10+ from app .models .config import Config
911from app .models .files import File , FileOut
1012from app .settings import settings
1113from app .tasks .clean_file import delete_expired_file
1719).total_bytes ()
1820
1921
22+ async def _get_current_storage_used (session : SessionDep , s3 : S3Dep ) -> int :
23+ """Sum the sizes (ContentLength) of currently active (non-expired) files."""
24+ now = datetime .now (timezone .utc ).replace (tzinfo = None )
25+ query = select (File ).where (
26+ File .expires_at > now , File .download_count < File .expire_after_n_download
27+ )
28+ result = await session .exec (query )
29+ files = result .all ()
30+
31+ total = 0
32+ for f in files :
33+ try :
34+ resp = await s3 .head_object (Bucket = settings .RUSTFS_BUCKET_NAME , Key = f .key )
35+ total += int (resp .get ("ContentLength" , 0 ) or 0 )
36+ except Exception :
37+ # If the object is missing or head fails for any reason, ignore and continue
38+ continue
39+ return total
40+
41+
2042@router .post ("/upload" )
2143async def upload_file (
2244 file : UploadFile ,
@@ -31,6 +53,28 @@ async def upload_file(
3153 filename = uuid .uuid7 () # type: ignore
3254
3355 key = uuid .uuid7 ()
56+
57+ # Load the singleton config and determine current usage
58+ config_q = select (Config )
59+ config_result = await session .exec (config_q )
60+ config = config_result .first ()
61+ if not config :
62+ raise HTTPException (
63+ status_code = status .HTTP_503_SERVICE_UNAVAILABLE ,
64+ detail = "Configuration not found" ,
65+ )
66+
67+ total_limit = config .total_storage_limit
68+ current_used = 0
69+ if total_limit is not None :
70+ current_used = await _get_current_storage_used (session , s3 )
71+ # Quick fail: no space at all left
72+ if current_used >= total_limit :
73+ raise HTTPException (
74+ status_code = status .HTTP_413_REQUEST_ENTITY_TOO_LARGE ,
75+ detail = "Storage quota exceeded" ,
76+ )
77+
3478 resp = await s3 .create_multipart_upload (
3579 Bucket = settings .RUSTFS_BUCKET_NAME ,
3680 Key = str (key ),
@@ -39,13 +83,25 @@ async def upload_file(
3983 upload_id = resp ["UploadId" ]
4084 parts = []
4185 part_number = 1
86+ uploaded_size = 0
4287
4388 try :
4489 while True :
4590 chunk = await file .read (CHUNK_SIZE )
4691 if not chunk :
4792 break
4893
94+ # Enforce total storage limit incrementally
95+ if (
96+ total_limit is not None
97+ and current_used + uploaded_size + len (chunk ) > total_limit
98+ ):
99+ # This will be caught by the outer except block which aborts the multipart upload
100+ raise HTTPException (
101+ status_code = status .HTTP_413_REQUEST_ENTITY_TOO_LARGE ,
102+ detail = "Storage quota exceeded" ,
103+ )
104+
49105 part = await s3 .upload_part (
50106 Bucket = settings .RUSTFS_BUCKET_NAME ,
51107 Key = str (key ),
@@ -56,6 +112,7 @@ async def upload_file(
56112
57113 parts .append ({"PartNumber" : part_number , "ETag" : part ["ETag" ]})
58114 part_number += 1
115+ uploaded_size += len (chunk )
59116
60117 await s3 .complete_multipart_upload (
61118 Bucket = settings .RUSTFS_BUCKET_NAME ,
@@ -71,7 +128,6 @@ async def upload_file(
71128 UploadId = upload_id ,
72129 )
73130 raise
74-
75131 now = datetime .now (timezone .utc ).replace (tzinfo = None )
76132 file_obj = File (
77133 filename = str (filename ),
0 commit comments