2323from core .settings import data_root , settings
2424from apps .base .models import FileCodes , UploadChunk
2525from core .utils import get_file_url , sanitize_filename
26- from fastapi .responses import FileResponse
26+ from fastapi .responses import FileResponse , StreamingResponse
2727
2828
2929class FileStorageInterface :
@@ -144,10 +144,20 @@ async def get_file_response(self, file_code: FileCodes):
144144 filename = f"{ file_code .prefix } { file_code .suffix } "
145145 encoded_filename = quote (filename , safe = '' )
146146 content_disposition = f"attachment; filename*=UTF-8''{ encoded_filename } "
147+
148+ # 尝试获取文件系统大小,如果成功则设置 Content-Length
149+ headers = {"Content-Disposition" : content_disposition }
150+ try :
151+ content_length = file_path .stat ().st_size
152+ headers ["Content-Length" ] = str (content_length )
153+ except Exception :
154+ # 如果获取文件大小失败,则不提供 Content-Length
155+ pass
156+
147157 return FileResponse (
148158 file_path ,
149159 media_type = "application/octet-stream" ,
150- headers = { "Content-Disposition" : content_disposition } ,
160+ headers = headers ,
151161 filename = filename # 保留原始文件名以备某些场景使用
152162 )
153163
@@ -296,12 +306,29 @@ async def delete_file(self, file_code: FileCodes):
296306 async def get_file_response (self , file_code : FileCodes ):
297307 try :
298308 filename = file_code .prefix + file_code .suffix
309+ content_length = None # 初始化为 None,表示未知大小
310+
299311 async with self .session .client (
300312 "s3" ,
301313 endpoint_url = self .endpoint_url ,
302314 region_name = self .region_name ,
303315 config = Config (signature_version = self .signature_version ),
304316 ) as s3 :
317+ # 尝试获取文件大小(HEAD请求)
318+ try :
319+ head_response = await s3 .head_object (
320+ Bucket = self .bucket_name ,
321+ Key = await file_code .get_file_path ()
322+ )
323+ # 从HEAD响应中获取Content-Length
324+ if 'ContentLength' in head_response :
325+ content_length = head_response ['ContentLength' ]
326+ elif 'Content-Length' in head_response ['ResponseMetadata' ]['HTTPHeaders' ]:
327+ content_length = int (head_response ['ResponseMetadata' ]['HTTPHeaders' ]['Content-Length' ])
328+ except Exception :
329+ # 如果HEAD请求失败,则不提供 Content-Length
330+ pass
331+
305332 link = await s3 .generate_presigned_url (
306333 "get_object" ,
307334 Params = {
@@ -310,20 +337,42 @@ async def get_file_response(self, file_code: FileCodes):
310337 },
311338 ExpiresIn = 3600 ,
312339 )
313- tmp = io .BytesIO ()
314- async with aiohttp .ClientSession () as session :
315- async with session .get (link ) as resp :
316- tmp .write (await resp .read ())
317- tmp .seek (0 )
318- content = tmp .read ()
319- tmp .close ()
320- return Response (
321- content ,
340+
341+ # 创建ClientSession并传递给生成器复用
342+ session = aiohttp .ClientSession ()
343+
344+ async def stream_generator ():
345+ try :
346+ async with session .get (link ) as resp :
347+ if resp .status != 200 :
348+ raise HTTPException (
349+ status_code = resp .status ,
350+ detail = f"从S3获取文件失败: { resp .status } "
351+ )
352+ # 设置块大小(例如64KB)
353+ chunk_size = 65536
354+ while True :
355+ chunk = await resp .content .read (chunk_size )
356+ if not chunk :
357+ break
358+ yield chunk
359+ finally :
360+ await session .close ()
361+
362+ from fastapi .responses import StreamingResponse
363+ encoded_filename = quote (filename , safe = '' )
364+ headers = {
365+ "Content-Disposition" : f"attachment; filename*=UTF-8''{ encoded_filename } "
366+ }
367+ if content_length is not None :
368+ headers ["Content-Length" ] = str (content_length )
369+ return StreamingResponse (
370+ stream_generator (),
322371 media_type = "application/octet-stream" ,
323- headers = {
324- "Content-Disposition" : f'attachment; filename="{ filename .encode ("utf-8" ).decode ("latin-1" )} "'
325- },
372+ headers = headers
326373 )
374+ except HTTPException :
375+ raise
327376 except Exception :
328377 raise HTTPException (status_code = 503 , detail = "服务代理下载异常,请稍后再试" )
329378
@@ -602,20 +651,51 @@ async def get_file_response(self, file_code: FileCodes):
602651 link = await asyncio .to_thread (
603652 self ._get_file_url , await file_code .get_file_path (), filename
604653 )
605- tmp = io .BytesIO ()
606- async with aiohttp .ClientSession () as session :
607- async with session .get (link ) as resp :
608- tmp .write (await resp .read ())
609- tmp .seek (0 )
610- content = tmp .read ()
611- tmp .close ()
612- return Response (
613- content ,
654+
655+ content_length = None # 初始化为 None,表示未知大小
656+
657+ # 创建ClientSession并复用
658+ session = aiohttp .ClientSession ()
659+
660+ # 尝试发送HEAD请求获取Content-Length
661+ try :
662+ async with session .head (link ) as resp :
663+ if resp .status == 200 and 'Content-Length' in resp .headers :
664+ content_length = int (resp .headers ['Content-Length' ])
665+ except Exception :
666+ # 如果HEAD请求失败,则不提供 Content-Length
667+ pass
668+
669+ async def stream_generator ():
670+ try :
671+ async with session .get (link ) as resp :
672+ if resp .status != 200 :
673+ raise HTTPException (
674+ status_code = resp .status ,
675+ detail = f"从OneDrive获取文件失败: { resp .status } "
676+ )
677+ chunk_size = 65536
678+ while True :
679+ chunk = await resp .content .read (chunk_size )
680+ if not chunk :
681+ break
682+ yield chunk
683+ finally :
684+ await session .close ()
685+
686+ encoded_filename = quote (filename , safe = '' )
687+ headers = {
688+ "Content-Disposition" : f"attachment; filename*=UTF-8''{ encoded_filename } "
689+ }
690+ if content_length is not None :
691+ headers ["Content-Length" ] = str (content_length )
692+ return StreamingResponse (
693+ stream_generator (),
614694 media_type = "application/octet-stream" ,
615- headers = {
616- "Content-Disposition" : f'attachment; filename="{ filename .encode ("utf-8" ).decode ("latin-1" )} "'
617- },
695+ headers = headers
618696 )
697+ except HTTPException :
698+ raise
619699 except Exception :
620700 raise HTTPException (status_code = 503 , detail = "服务代理下载异常,请稍后再试" )
621701
@@ -776,11 +856,54 @@ async def get_file_url(self, file_code: FileCodes):
776856 async def get_file_response (self , file_code : FileCodes ):
777857 try :
778858 filename = file_code .prefix + file_code .suffix
779- content = await self .operator .read (await file_code .get_file_path ())
859+ content_length = None # 初始化为 None,表示未知大小
860+
861+ # 尝试获取文件大小
862+ try :
863+ stat_result = await self .operator .stat (await file_code .get_file_path ())
864+ if hasattr (stat_result , 'content_length' ) and stat_result .content_length :
865+ content_length = stat_result .content_length
866+ elif hasattr (stat_result , 'size' ) and stat_result .size :
867+ content_length = stat_result .size
868+ except Exception :
869+ # 如果获取大小失败,则不提供 Content-Length
870+ pass
871+
872+ # 尝试使用流式读取器
873+ try :
874+ # OpenDAL 可能提供 reader 方法返回一个异步读取器
875+ reader = await self .operator .reader (await file_code .get_file_path ())
876+ except AttributeError :
877+ # 如果 reader 方法不存在,回退到全量读取(兼容旧版本)
878+ content = await self .operator .read (await file_code .get_file_path ())
879+ encoded_filename = quote (filename , safe = '' )
880+ headers = {
881+ "Content-Disposition" : f"attachment; filename*=UTF-8''{ encoded_filename } "
882+ }
883+ if content_length is not None :
884+ headers ["Content-Length" ] = str (content_length )
885+ return Response (
886+ content , headers = headers , media_type = "application/octet-stream"
887+ )
888+
889+ async def stream_generator ():
890+ chunk_size = 65536
891+ while True :
892+ chunk = await reader .read (chunk_size )
893+ if not chunk :
894+ break
895+ yield chunk
896+
897+ encoded_filename = quote (filename , safe = '' )
780898 headers = {
781- "Content-Disposition" : f'attachment; filename="{ filename } "' }
782- return Response (
783- content , headers = headers , media_type = "application/octet-stream"
899+ "Content-Disposition" : f"attachment; filename*=UTF-8''{ encoded_filename } "
900+ }
901+ if content_length is not None :
902+ headers ["Content-Length" ] = str (content_length )
903+ return StreamingResponse (
904+ stream_generator (),
905+ media_type = "application/octet-stream" ,
906+ headers = headers
784907 )
785908 except Exception as e :
786909 logger .info (e )
@@ -969,26 +1092,50 @@ async def get_file_response(self, file_code: FileCodes):
9691092 try :
9701093 filename = file_code .prefix + file_code .suffix
9711094 url = self ._build_url (await file_code .get_file_path ())
972- async with aiohttp .ClientSession (headers = {
1095+ content_length = None # 初始化为 None,表示未知大小
1096+
1097+ # 创建ClientSession并复用(包含认证头)
1098+ session = aiohttp .ClientSession (headers = {
9731099 "Authorization" : f"Basic { base64 .b64encode (f'{ settings .webdav_username } :{ settings .webdav_password } ' .encode ()).decode ()} "
974- }) as session :
975- async with session .get (url ) as resp :
976- if resp .status != 200 :
977- raise HTTPException (
978- status_code = resp .status ,
979- detail = f"文件获取失败{ resp .status } : { await resp .text ()} " ,
980- )
981- # 读取内容到内存
982- content = await resp .read ()
983- return Response (
984- content = content ,
985- media_type = resp .headers .get (
986- "Content-Type" , "application/octet-stream"
987- ),
988- headers = {
989- "Content-Disposition" : f'attachment; filename="{ filename .encode ("utf-8" ).decode ()} "'
990- },
991- )
1100+ })
1101+
1102+ # 尝试发送HEAD请求获取Content-Length
1103+ try :
1104+ async with session .head (url ) as resp :
1105+ if resp .status == 200 and 'Content-Length' in resp .headers :
1106+ content_length = int (resp .headers ['Content-Length' ])
1107+ except Exception :
1108+ # 如果HEAD请求失败,则不提供 Content-Length
1109+ pass
1110+
1111+ async def stream_generator ():
1112+ try :
1113+ async with session .get (url ) as resp :
1114+ if resp .status != 200 :
1115+ raise HTTPException (
1116+ status_code = resp .status ,
1117+ detail = f"文件获取失败{ resp .status } : { await resp .text ()} " ,
1118+ )
1119+ chunk_size = 65536
1120+ while True :
1121+ chunk = await resp .content .read (chunk_size )
1122+ if not chunk :
1123+ break
1124+ yield chunk
1125+ finally :
1126+ await session .close ()
1127+
1128+ encoded_filename = quote (filename , safe = '' )
1129+ headers = {
1130+ "Content-Disposition" : f"attachment; filename*=UTF-8''{ encoded_filename } "
1131+ }
1132+ if content_length is not None :
1133+ headers ["Content-Length" ] = str (content_length )
1134+ return StreamingResponse (
1135+ stream_generator (),
1136+ media_type = "application/octet-stream" ,
1137+ headers = headers
1138+ )
9921139 except aiohttp .ClientError as e :
9931140 raise HTTPException (
9941141 status_code = 503 , detail = f"WebDAV连接异常: { str (e )} " )
0 commit comments