|
23 | 23 | from core.settings import data_root, settings |
24 | 24 | from apps.base.models import FileCodes, UploadChunk |
25 | 25 | from core.utils import get_file_url, sanitize_filename |
26 | | -from fastapi.responses import FileResponse |
| 26 | +from fastapi.responses import FileResponse, StreamingResponse |
27 | 27 |
|
28 | 28 |
|
29 | 29 | class FileStorageInterface: |
@@ -310,20 +310,34 @@ async def get_file_response(self, file_code: FileCodes): |
310 | 310 | }, |
311 | 311 | ExpiresIn=3600, |
312 | 312 | ) |
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, |
| 313 | + |
| 314 | + async def stream_generator(): |
| 315 | + async with aiohttp.ClientSession() as session: |
| 316 | + async with session.get(link) as resp: |
| 317 | + if resp.status != 200: |
| 318 | + raise HTTPException( |
| 319 | + status_code=resp.status, |
| 320 | + detail=f"从S3获取文件失败: {resp.status}" |
| 321 | + ) |
| 322 | + # 设置块大小(例如64KB) |
| 323 | + chunk_size = 65536 |
| 324 | + while True: |
| 325 | + chunk = await resp.content.read(chunk_size) |
| 326 | + if not chunk: |
| 327 | + break |
| 328 | + yield chunk |
| 329 | + |
| 330 | + from fastapi.responses import StreamingResponse |
| 331 | + headers = { |
| 332 | + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' |
| 333 | + } |
| 334 | + return StreamingResponse( |
| 335 | + stream_generator(), |
322 | 336 | media_type="application/octet-stream", |
323 | | - headers={ |
324 | | - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' |
325 | | - }, |
| 337 | + headers=headers |
326 | 338 | ) |
| 339 | + except HTTPException: |
| 340 | + raise |
327 | 341 | except Exception: |
328 | 342 | raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试") |
329 | 343 |
|
@@ -602,20 +616,32 @@ async def get_file_response(self, file_code: FileCodes): |
602 | 616 | link = await asyncio.to_thread( |
603 | 617 | self._get_file_url, await file_code.get_file_path(), filename |
604 | 618 | ) |
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, |
| 619 | + |
| 620 | + async def stream_generator(): |
| 621 | + async with aiohttp.ClientSession() as session: |
| 622 | + async with session.get(link) as resp: |
| 623 | + if resp.status != 200: |
| 624 | + raise HTTPException( |
| 625 | + status_code=resp.status, |
| 626 | + detail=f"从OneDrive获取文件失败: {resp.status}" |
| 627 | + ) |
| 628 | + chunk_size = 65536 |
| 629 | + while True: |
| 630 | + chunk = await resp.content.read(chunk_size) |
| 631 | + if not chunk: |
| 632 | + break |
| 633 | + yield chunk |
| 634 | + |
| 635 | + headers = { |
| 636 | + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' |
| 637 | + } |
| 638 | + return StreamingResponse( |
| 639 | + stream_generator(), |
614 | 640 | media_type="application/octet-stream", |
615 | | - headers={ |
616 | | - "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode("latin-1")}"' |
617 | | - }, |
| 641 | + headers=headers |
618 | 642 | ) |
| 643 | + except HTTPException: |
| 644 | + raise |
619 | 645 | except Exception: |
620 | 646 | raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试") |
621 | 647 |
|
@@ -776,11 +802,35 @@ async def get_file_url(self, file_code: FileCodes): |
776 | 802 | async def get_file_response(self, file_code: FileCodes): |
777 | 803 | try: |
778 | 804 | filename = file_code.prefix + file_code.suffix |
779 | | - content = await self.operator.read(await file_code.get_file_path()) |
| 805 | + # 尝试使用流式读取器 |
| 806 | + try: |
| 807 | + # OpenDAL 可能提供 reader 方法返回一个异步读取器 |
| 808 | + reader = await self.operator.reader(await file_code.get_file_path()) |
| 809 | + except AttributeError: |
| 810 | + # 如果 reader 方法不存在,回退到全量读取(兼容旧版本) |
| 811 | + content = await self.operator.read(await file_code.get_file_path()) |
| 812 | + headers = { |
| 813 | + "Content-Disposition": f'attachment; filename="{filename}"' |
| 814 | + } |
| 815 | + return Response( |
| 816 | + content, headers=headers, media_type="application/octet-stream" |
| 817 | + ) |
| 818 | + |
| 819 | + async def stream_generator(): |
| 820 | + chunk_size = 65536 |
| 821 | + while True: |
| 822 | + chunk = await reader.read(chunk_size) |
| 823 | + if not chunk: |
| 824 | + break |
| 825 | + yield chunk |
| 826 | + |
780 | 827 | headers = { |
781 | | - "Content-Disposition": f'attachment; filename="{filename}"'} |
782 | | - return Response( |
783 | | - content, headers=headers, media_type="application/octet-stream" |
| 828 | + "Content-Disposition": f'attachment; filename="{filename}"' |
| 829 | + } |
| 830 | + return StreamingResponse( |
| 831 | + stream_generator(), |
| 832 | + media_type="application/octet-stream", |
| 833 | + headers=headers |
784 | 834 | ) |
785 | 835 | except Exception as e: |
786 | 836 | logger.info(e) |
@@ -969,26 +1019,32 @@ async def get_file_response(self, file_code: FileCodes): |
969 | 1019 | try: |
970 | 1020 | filename = file_code.prefix + file_code.suffix |
971 | 1021 | url = self._build_url(await file_code.get_file_path()) |
972 | | - async with aiohttp.ClientSession(headers={ |
973 | | - "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 | | - ) |
| 1022 | + |
| 1023 | + async def stream_generator(): |
| 1024 | + async with aiohttp.ClientSession(headers={ |
| 1025 | + "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}" |
| 1026 | + }) as session: |
| 1027 | + async with session.get(url) as resp: |
| 1028 | + if resp.status != 200: |
| 1029 | + raise HTTPException( |
| 1030 | + status_code=resp.status, |
| 1031 | + detail=f"文件获取失败{resp.status}: {await resp.text()}", |
| 1032 | + ) |
| 1033 | + chunk_size = 65536 |
| 1034 | + while True: |
| 1035 | + chunk = await resp.content.read(chunk_size) |
| 1036 | + if not chunk: |
| 1037 | + break |
| 1038 | + yield chunk |
| 1039 | + |
| 1040 | + headers = { |
| 1041 | + "Content-Disposition": f'attachment; filename="{filename.encode("utf-8").decode()}"' |
| 1042 | + } |
| 1043 | + return StreamingResponse( |
| 1044 | + stream_generator(), |
| 1045 | + media_type="application/octet-stream", |
| 1046 | + headers=headers |
| 1047 | + ) |
992 | 1048 | except aiohttp.ClientError as e: |
993 | 1049 | raise HTTPException( |
994 | 1050 | status_code=503, detail=f"WebDAV连接异常: {str(e)}") |
|
0 commit comments