Skip to content

Commit 9120486

Browse files
authored
Merge pull request #454 from jerryliang122/proxy-download
优化启用下载代理
2 parents f5a5e03 + ade5799 commit 9120486

File tree

1 file changed

+196
-49
lines changed

1 file changed

+196
-49
lines changed

core/storage.py

Lines changed: 196 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from core.settings import data_root, settings
2424
from apps.base.models import FileCodes, UploadChunk
2525
from core.utils import get_file_url, sanitize_filename
26-
from fastapi.responses import FileResponse
26+
from fastapi.responses import FileResponse, StreamingResponse
2727

2828

2929
class 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

Comments
 (0)