From ba8802ad75b5322b8aa2a8314f3fb51985ee578b Mon Sep 17 00:00:00 2001 From: jerryliang Date: Fri, 16 Jan 2026 15:45:12 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E4=B8=BA=E6=89=80=E6=9C=89?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=90=8E=E7=AB=AF=E5=AE=9E=E7=8E=B0=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将文件下载方式从全量读取改为流式传输,减少内存占用并提高大文件下载性能 支持S3、OneDrive、OpenDAL和WebDAV存储后端的流式下载 添加错误处理和HTTP异常捕获 --- core/storage.py | 154 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 105 insertions(+), 49 deletions(-) diff --git a/core/storage.py b/core/storage.py index 0e820bbc..a4597977 100644 --- a/core/storage.py +++ b/core/storage.py @@ -23,7 +23,7 @@ from core.settings import data_root, settings from apps.base.models import FileCodes, UploadChunk from core.utils import get_file_url, sanitize_filename -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, StreamingResponse class FileStorageInterface: @@ -310,20 +310,34 @@ async def get_file_response(self, file_code: FileCodes): }, ExpiresIn=3600, ) - tmp = io.BytesIO() - async with aiohttp.ClientSession() as session: - async with session.get(link) as resp: - tmp.write(await resp.read()) - tmp.seek(0) - content = tmp.read() - tmp.close() - return Response( - content, + + async def stream_generator(): + async with aiohttp.ClientSession() as session: + async with session.get(link) as resp: + if resp.status != 200: + raise HTTPException( + status_code=resp.status, + detail=f"从S3获取文件失败: {resp.status}" + ) + # 设置块大小(例如64KB) + chunk_size = 65536 + while True: + chunk = await resp.content.read(chunk_size) + if not chunk: + break + yield chunk + + from fastapi.responses import StreamingResponse + headers = { + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' + } + return StreamingResponse( + stream_generator(), media_type="application/octet-stream", - headers={ - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' - }, + headers=headers ) + except HTTPException: + raise except Exception: raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试") @@ -602,20 +616,32 @@ async def get_file_response(self, file_code: FileCodes): link = await asyncio.to_thread( self._get_file_url, await file_code.get_file_path(), filename ) - tmp = io.BytesIO() - async with aiohttp.ClientSession() as session: - async with session.get(link) as resp: - tmp.write(await resp.read()) - tmp.seek(0) - content = tmp.read() - tmp.close() - return Response( - content, + + async def stream_generator(): + async with aiohttp.ClientSession() as session: + async with session.get(link) as resp: + if resp.status != 200: + raise HTTPException( + status_code=resp.status, + detail=f"从OneDrive获取文件失败: {resp.status}" + ) + chunk_size = 65536 + while True: + chunk = await resp.content.read(chunk_size) + if not chunk: + break + yield chunk + + headers = { + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' + } + return StreamingResponse( + stream_generator(), media_type="application/octet-stream", - headers={ - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' - }, + headers=headers ) + except HTTPException: + raise except Exception: raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试") @@ -776,11 +802,35 @@ async def get_file_url(self, file_code: FileCodes): async def get_file_response(self, file_code: FileCodes): try: filename = file_code.prefix + file_code.suffix - content = await self.operator.read(await file_code.get_file_path()) + # 尝试使用流式读取器 + try: + # OpenDAL 可能提供 reader 方法返回一个异步读取器 + reader = await self.operator.reader(await file_code.get_file_path()) + except AttributeError: + # 如果 reader 方法不存在,回退到全量读取(兼容旧版本) + content = await self.operator.read(await file_code.get_file_path()) + headers = { + "Content-Disposition": f'attachment; filename="{filename}"' + } + return Response( + content, headers=headers, media_type="application/octet-stream" + ) + + async def stream_generator(): + chunk_size = 65536 + while True: + chunk = await reader.read(chunk_size) + if not chunk: + break + yield chunk + headers = { - "Content-Disposition": f'attachment; filename="{filename}"'} - return Response( - content, headers=headers, media_type="application/octet-stream" + "Content-Disposition": f'attachment; filename="{filename}"' + } + return StreamingResponse( + stream_generator(), + media_type="application/octet-stream", + headers=headers ) except Exception as e: logger.info(e) @@ -969,26 +1019,32 @@ async def get_file_response(self, file_code: FileCodes): try: filename = file_code.prefix + file_code.suffix url = self._build_url(await file_code.get_file_path()) - async with aiohttp.ClientSession(headers={ - "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}" - }) as session: - async with session.get(url) as resp: - if resp.status != 200: - raise HTTPException( - status_code=resp.status, - detail=f"文件获取失败{resp.status}: {await resp.text()}", - ) - # 读取内容到内存 - content = await resp.read() - return Response( - content=content, - media_type=resp.headers.get( - "Content-Type", "application/octet-stream" - ), - headers={ - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode()}"' - }, - ) + + async def stream_generator(): + async with aiohttp.ClientSession(headers={ + "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}" + }) as session: + async with session.get(url) as resp: + if resp.status != 200: + raise HTTPException( + status_code=resp.status, + detail=f"文件获取失败{resp.status}: {await resp.text()}", + ) + chunk_size = 65536 + while True: + chunk = await resp.content.read(chunk_size) + if not chunk: + break + yield chunk + + headers = { + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode()}"' + } + return StreamingResponse( + stream_generator(), + media_type="application/octet-stream", + headers=headers + ) except aiohttp.ClientError as e: raise HTTPException( status_code=503, detail=f"WebDAV连接异常: {str(e)}") From 34e36944bd149a81c4ffa44dc0eaf5262dce169d Mon Sep 17 00:00:00 2001 From: jerryliang Date: Fri, 16 Jan 2026 16:44:06 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E5=9C=A8=E6=89=80=E6=9C=89?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=AE=9E=E7=8E=B0=E4=B8=AD=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?Content-Length=E5=93=8D=E5=BA=94=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/storage.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/core/storage.py b/core/storage.py index a4597977..707ff91f 100644 --- a/core/storage.py +++ b/core/storage.py @@ -147,7 +147,7 @@ async def get_file_response(self, file_code: FileCodes): return FileResponse( file_path, media_type="application/octet-stream", - headers={"Content-Disposition": content_disposition}, + headers={"Content-Disposition": content_disposition, "Content-Length": str(file_code.size)}, filename=filename # 保留原始文件名以备某些场景使用 ) @@ -329,7 +329,8 @@ async def stream_generator(): from fastapi.responses import StreamingResponse headers = { - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"', + "Content-Length": str(file_code.size) } return StreamingResponse( stream_generator(), @@ -633,7 +634,8 @@ async def stream_generator(): yield chunk headers = { - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"', + "Content-Length": str(file_code.size) } return StreamingResponse( stream_generator(), @@ -810,7 +812,8 @@ async def get_file_response(self, file_code: FileCodes): # 如果 reader 方法不存在,回退到全量读取(兼容旧版本) content = await self.operator.read(await file_code.get_file_path()) headers = { - "Content-Disposition": f'attachment; filename="{filename}"' + "Content-Disposition": f'attachment; filename="{filename}"', + "Content-Length": str(file_code.size) } return Response( content, headers=headers, media_type="application/octet-stream" @@ -825,7 +828,8 @@ async def stream_generator(): yield chunk headers = { - "Content-Disposition": f'attachment; filename="{filename}"' + "Content-Disposition": f'attachment; filename="{filename}"', + "Content-Length": str(file_code.size) } return StreamingResponse( stream_generator(), @@ -1038,7 +1042,8 @@ async def stream_generator(): yield chunk headers = { - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode()}"' + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode()}"', + "Content-Length": str(file_code.size) } return StreamingResponse( stream_generator(), From 442ef5b0e07eea49f0e48ce7f48626aeeefc6704 Mon Sep 17 00:00:00 2001 From: jerryliang Date: Fri, 16 Jan 2026 16:53:39 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8B=E8=BD=BD=E5=93=8D=E5=BA=94=E4=B8=AD=E7=9A=84?= =?UTF-8?q?Content-Length=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为所有存储后端添加文件大小获取逻辑,优先从实际存储系统获取文件大小,失败时回退到数据库记录的大小 --- core/storage.py | 76 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/core/storage.py b/core/storage.py index 707ff91f..803a1ff0 100644 --- a/core/storage.py +++ b/core/storage.py @@ -144,10 +144,19 @@ async def get_file_response(self, file_code: FileCodes): filename = f"{file_code.prefix}{file_code.suffix}" encoded_filename = quote(filename, safe='') content_disposition = f"attachment; filename*=UTF-8''{encoded_filename}" + + # 优先使用文件系统大小 + content_length = file_code.size # 默认使用数据库中的大小 + try: + content_length = file_path.stat().st_size + except Exception: + # 如果获取文件大小失败,继续使用默认大小 + pass + return FileResponse( file_path, media_type="application/octet-stream", - headers={"Content-Disposition": content_disposition, "Content-Length": str(file_code.size)}, + headers={"Content-Disposition": content_disposition, "Content-Length": str(content_length)}, filename=filename # 保留原始文件名以备某些场景使用 ) @@ -296,12 +305,29 @@ async def delete_file(self, file_code: FileCodes): async def get_file_response(self, file_code: FileCodes): try: filename = file_code.prefix + file_code.suffix + content_length = file_code.size # 默认使用数据库中的大小 + async with self.session.client( "s3", endpoint_url=self.endpoint_url, region_name=self.region_name, config=Config(signature_version=self.signature_version), ) as s3: + # 首先尝试获取文件大小(HEAD请求) + try: + head_response = await s3.head_object( + Bucket=self.bucket_name, + Key=await file_code.get_file_path() + ) + # 从HEAD响应中获取Content-Length + if 'ContentLength' in head_response: + content_length = head_response['ContentLength'] + elif 'Content-Length' in head_response['ResponseMetadata']['HTTPHeaders']: + content_length = int(head_response['ResponseMetadata']['HTTPHeaders']['Content-Length']) + except Exception: + # 如果HEAD请求失败,继续使用默认大小 + pass + link = await s3.generate_presigned_url( "get_object", Params={ @@ -330,7 +356,7 @@ async def stream_generator(): from fastapi.responses import StreamingResponse headers = { "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"', - "Content-Length": str(file_code.size) + "Content-Length": str(content_length) } return StreamingResponse( stream_generator(), @@ -618,6 +644,18 @@ async def get_file_response(self, file_code: FileCodes): self._get_file_url, await file_code.get_file_path(), filename ) + content_length = file_code.size # 默认使用数据库中的大小 + + # 尝试发送HEAD请求获取Content-Length + try: + async with aiohttp.ClientSession() as session: + async with session.head(link) as resp: + if resp.status == 200 and 'Content-Length' in resp.headers: + content_length = int(resp.headers['Content-Length']) + except Exception: + # 如果HEAD请求失败,继续使用默认大小 + pass + async def stream_generator(): async with aiohttp.ClientSession() as session: async with session.get(link) as resp: @@ -635,7 +673,7 @@ async def stream_generator(): headers = { "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"', - "Content-Length": str(file_code.size) + "Content-Length": str(content_length) } return StreamingResponse( stream_generator(), @@ -804,6 +842,19 @@ async def get_file_url(self, file_code: FileCodes): async def get_file_response(self, file_code: FileCodes): try: filename = file_code.prefix + file_code.suffix + content_length = file_code.size # 默认使用数据库中的大小 + + # 尝试获取文件大小 + try: + stat_result = await self.operator.stat(await file_code.get_file_path()) + if hasattr(stat_result, 'content_length') and stat_result.content_length: + content_length = stat_result.content_length + elif hasattr(stat_result, 'size') and stat_result.size: + content_length = stat_result.size + except Exception: + # 如果获取大小失败,继续使用默认大小 + pass + # 尝试使用流式读取器 try: # OpenDAL 可能提供 reader 方法返回一个异步读取器 @@ -813,7 +864,7 @@ async def get_file_response(self, file_code: FileCodes): content = await self.operator.read(await file_code.get_file_path()) headers = { "Content-Disposition": f'attachment; filename="{filename}"', - "Content-Length": str(file_code.size) + "Content-Length": str(content_length) } return Response( content, headers=headers, media_type="application/octet-stream" @@ -829,7 +880,7 @@ async def stream_generator(): headers = { "Content-Disposition": f'attachment; filename="{filename}"', - "Content-Length": str(file_code.size) + "Content-Length": str(content_length) } return StreamingResponse( stream_generator(), @@ -1023,6 +1074,19 @@ async def get_file_response(self, file_code: FileCodes): try: filename = file_code.prefix + file_code.suffix url = self._build_url(await file_code.get_file_path()) + content_length = file_code.size # 默认使用数据库中的大小 + + # 尝试发送HEAD请求获取Content-Length + try: + async with aiohttp.ClientSession(headers={ + "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}" + }) as session: + async with session.head(url) as resp: + if resp.status == 200 and 'Content-Length' in resp.headers: + content_length = int(resp.headers['Content-Length']) + except Exception: + # 如果HEAD请求失败,继续使用默认大小 + pass async def stream_generator(): async with aiohttp.ClientSession(headers={ @@ -1043,7 +1107,7 @@ async def stream_generator(): headers = { "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode()}"', - "Content-Length": str(file_code.size) + "Content-Length": str(content_length) } return StreamingResponse( stream_generator(), From ff1943d60c6f9624448fe689059a8552e7685d1b Mon Sep 17 00:00:00 2001 From: jerryliang Date: Fri, 16 Jan 2026 17:18:14 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E6=A0=87=E5=87=86?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E7=BC=96=E7=A0=81=E4=B8=8B=E8=BD=BD=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/storage.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/storage.py b/core/storage.py index 803a1ff0..17d354bc 100644 --- a/core/storage.py +++ b/core/storage.py @@ -354,8 +354,9 @@ async def stream_generator(): yield chunk from fastapi.responses import StreamingResponse + encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"', + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", "Content-Length": str(content_length) } return StreamingResponse( @@ -671,8 +672,9 @@ async def stream_generator(): break yield chunk + encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"', + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", "Content-Length": str(content_length) } return StreamingResponse( @@ -862,8 +864,9 @@ async def get_file_response(self, file_code: FileCodes): except AttributeError: # 如果 reader 方法不存在,回退到全量读取(兼容旧版本) content = await self.operator.read(await file_code.get_file_path()) + encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f'attachment; filename="{filename}"', + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", "Content-Length": str(content_length) } return Response( @@ -878,8 +881,9 @@ async def stream_generator(): break yield chunk + encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f'attachment; filename="{filename}"', + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", "Content-Length": str(content_length) } return StreamingResponse( @@ -1105,8 +1109,9 @@ async def stream_generator(): break yield chunk + encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode()}"', + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", "Content-Length": str(content_length) } return StreamingResponse( From 6b728abbeb5e652b96d677d59ef352045d916282 Mon Sep 17 00:00:00 2001 From: jerryliang Date: Fri, 16 Jan 2026 17:33:22 +0800 Subject: [PATCH 5/6] =?UTF-8?q?perf:=20=E5=A4=8D=E7=94=A8ClientSession?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD=E6=80=A7?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/storage.py | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/core/storage.py b/core/storage.py index 17d354bc..f04d1d89 100644 --- a/core/storage.py +++ b/core/storage.py @@ -337,8 +337,11 @@ async def get_file_response(self, file_code: FileCodes): ExpiresIn=3600, ) + # 创建ClientSession并传递给生成器复用 + session = aiohttp.ClientSession() + async def stream_generator(): - async with aiohttp.ClientSession() as session: + try: async with session.get(link) as resp: if resp.status != 200: raise HTTPException( @@ -352,6 +355,8 @@ async def stream_generator(): if not chunk: break yield chunk + finally: + await session.close() from fastapi.responses import StreamingResponse encoded_filename = quote(filename, safe='') @@ -647,18 +652,20 @@ async def get_file_response(self, file_code: FileCodes): content_length = file_code.size # 默认使用数据库中的大小 + # 创建ClientSession并复用 + session = aiohttp.ClientSession() + # 尝试发送HEAD请求获取Content-Length try: - async with aiohttp.ClientSession() as session: - async with session.head(link) as resp: - if resp.status == 200 and 'Content-Length' in resp.headers: - content_length = int(resp.headers['Content-Length']) + async with session.head(link) as resp: + if resp.status == 200 and 'Content-Length' in resp.headers: + content_length = int(resp.headers['Content-Length']) except Exception: # 如果HEAD请求失败,继续使用默认大小 pass async def stream_generator(): - async with aiohttp.ClientSession() as session: + try: async with session.get(link) as resp: if resp.status != 200: raise HTTPException( @@ -671,6 +678,8 @@ async def stream_generator(): if not chunk: break yield chunk + finally: + await session.close() encoded_filename = quote(filename, safe='') headers = { @@ -1080,22 +1089,22 @@ async def get_file_response(self, file_code: FileCodes): url = self._build_url(await file_code.get_file_path()) content_length = file_code.size # 默认使用数据库中的大小 + # 创建ClientSession并复用(包含认证头) + session = aiohttp.ClientSession(headers={ + "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}" + }) + # 尝试发送HEAD请求获取Content-Length try: - async with aiohttp.ClientSession(headers={ - "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}" - }) as session: - async with session.head(url) as resp: - if resp.status == 200 and 'Content-Length' in resp.headers: - content_length = int(resp.headers['Content-Length']) + async with session.head(url) as resp: + if resp.status == 200 and 'Content-Length' in resp.headers: + content_length = int(resp.headers['Content-Length']) except Exception: # 如果HEAD请求失败,继续使用默认大小 pass async def stream_generator(): - async with aiohttp.ClientSession(headers={ - "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}" - }) as session: + try: async with session.get(url) as resp: if resp.status != 200: raise HTTPException( @@ -1108,6 +1117,8 @@ async def stream_generator(): if not chunk: break yield chunk + finally: + await session.close() encoded_filename = quote(filename, safe='') headers = { From ade5799db4aa27b9f6a66334a2740efadbed4287 Mon Sep 17 00:00:00 2001 From: jerryliang Date: Fri, 16 Jan 2026 17:55:12 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E4=BB=85=E5=9C=A8=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E5=88=B0=E6=96=87=E4=BB=B6=E5=A4=A7=E5=B0=8F=E6=97=B6=E6=89=8D?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=20Content-Length=20=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/storage.py | 52 +++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/core/storage.py b/core/storage.py index f04d1d89..4c6b7334 100644 --- a/core/storage.py +++ b/core/storage.py @@ -145,18 +145,19 @@ async def get_file_response(self, file_code: FileCodes): encoded_filename = quote(filename, safe='') content_disposition = f"attachment; filename*=UTF-8''{encoded_filename}" - # 优先使用文件系统大小 - content_length = file_code.size # 默认使用数据库中的大小 + # 尝试获取文件系统大小,如果成功则设置 Content-Length + headers = {"Content-Disposition": content_disposition} try: content_length = file_path.stat().st_size + headers["Content-Length"] = str(content_length) except Exception: - # 如果获取文件大小失败,继续使用默认大小 + # 如果获取文件大小失败,则不提供 Content-Length pass return FileResponse( file_path, media_type="application/octet-stream", - headers={"Content-Disposition": content_disposition, "Content-Length": str(content_length)}, + headers=headers, filename=filename # 保留原始文件名以备某些场景使用 ) @@ -305,7 +306,7 @@ async def delete_file(self, file_code: FileCodes): async def get_file_response(self, file_code: FileCodes): try: filename = file_code.prefix + file_code.suffix - content_length = file_code.size # 默认使用数据库中的大小 + content_length = None # 初始化为 None,表示未知大小 async with self.session.client( "s3", @@ -313,7 +314,7 @@ async def get_file_response(self, file_code: FileCodes): region_name=self.region_name, config=Config(signature_version=self.signature_version), ) as s3: - # 首先尝试获取文件大小(HEAD请求) + # 尝试获取文件大小(HEAD请求) try: head_response = await s3.head_object( Bucket=self.bucket_name, @@ -325,7 +326,7 @@ async def get_file_response(self, file_code: FileCodes): elif 'Content-Length' in head_response['ResponseMetadata']['HTTPHeaders']: content_length = int(head_response['ResponseMetadata']['HTTPHeaders']['Content-Length']) except Exception: - # 如果HEAD请求失败,继续使用默认大小 + # 如果HEAD请求失败,则不提供 Content-Length pass link = await s3.generate_presigned_url( @@ -361,9 +362,10 @@ async def stream_generator(): from fastapi.responses import StreamingResponse encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", - "Content-Length": str(content_length) + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" } + if content_length is not None: + headers["Content-Length"] = str(content_length) return StreamingResponse( stream_generator(), media_type="application/octet-stream", @@ -650,7 +652,7 @@ async def get_file_response(self, file_code: FileCodes): self._get_file_url, await file_code.get_file_path(), filename ) - content_length = file_code.size # 默认使用数据库中的大小 + content_length = None # 初始化为 None,表示未知大小 # 创建ClientSession并复用 session = aiohttp.ClientSession() @@ -661,7 +663,7 @@ async def get_file_response(self, file_code: FileCodes): if resp.status == 200 and 'Content-Length' in resp.headers: content_length = int(resp.headers['Content-Length']) except Exception: - # 如果HEAD请求失败,继续使用默认大小 + # 如果HEAD请求失败,则不提供 Content-Length pass async def stream_generator(): @@ -683,9 +685,10 @@ async def stream_generator(): encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", - "Content-Length": str(content_length) + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" } + if content_length is not None: + headers["Content-Length"] = str(content_length) return StreamingResponse( stream_generator(), media_type="application/octet-stream", @@ -853,7 +856,7 @@ async def get_file_url(self, file_code: FileCodes): async def get_file_response(self, file_code: FileCodes): try: filename = file_code.prefix + file_code.suffix - content_length = file_code.size # 默认使用数据库中的大小 + content_length = None # 初始化为 None,表示未知大小 # 尝试获取文件大小 try: @@ -863,7 +866,7 @@ async def get_file_response(self, file_code: FileCodes): elif hasattr(stat_result, 'size') and stat_result.size: content_length = stat_result.size except Exception: - # 如果获取大小失败,继续使用默认大小 + # 如果获取大小失败,则不提供 Content-Length pass # 尝试使用流式读取器 @@ -875,9 +878,10 @@ async def get_file_response(self, file_code: FileCodes): content = await self.operator.read(await file_code.get_file_path()) encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", - "Content-Length": str(content_length) + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" } + if content_length is not None: + headers["Content-Length"] = str(content_length) return Response( content, headers=headers, media_type="application/octet-stream" ) @@ -892,9 +896,10 @@ async def stream_generator(): encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", - "Content-Length": str(content_length) + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" } + if content_length is not None: + headers["Content-Length"] = str(content_length) return StreamingResponse( stream_generator(), media_type="application/octet-stream", @@ -1087,7 +1092,7 @@ async def get_file_response(self, file_code: FileCodes): try: filename = file_code.prefix + file_code.suffix url = self._build_url(await file_code.get_file_path()) - content_length = file_code.size # 默认使用数据库中的大小 + content_length = None # 初始化为 None,表示未知大小 # 创建ClientSession并复用(包含认证头) session = aiohttp.ClientSession(headers={ @@ -1100,7 +1105,7 @@ async def get_file_response(self, file_code: FileCodes): if resp.status == 200 and 'Content-Length' in resp.headers: content_length = int(resp.headers['Content-Length']) except Exception: - # 如果HEAD请求失败,继续使用默认大小 + # 如果HEAD请求失败,则不提供 Content-Length pass async def stream_generator(): @@ -1122,9 +1127,10 @@ async def stream_generator(): encoded_filename = quote(filename, safe='') headers = { - "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", - "Content-Length": str(content_length) + "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" } + if content_length is not None: + headers["Content-Length"] = str(content_length) return StreamingResponse( stream_generator(), media_type="application/octet-stream",