Spaces:
Running
Running
File size: 4,609 Bytes
929a9dd b91d49e 929a9dd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | """内存结果共用的 HTTP 下载响应,适用于图像字节和 JSON。"""
import hashlib
import secrets
from email.utils import formatdate
from fastapi import Request
from fastapi.responses import FileResponse, Response
class ResultFileResponse(FileResponse):
"""文件响应结束时释放既有下载租约,包括 416 和连接中断路径。"""
async def __call__(self, scope, receive, send):
"""发送文件并保证后台租约释放动作只执行一次。
Args:
scope: ASGI 请求上下文。
receive: ASGI 请求消息接收函数。
send: ASGI 响应消息发送函数。
"""
release = self.background
self.background = None
if scope["method"] == "HEAD":
# Range 只用于 GET;HEAD 应提供完整结果的长度和校验信息。
scope = {**scope, "headers": [(name, value) for name, value in scope["headers"]
if name.lower() != b"range"]}
try:
await super().__call__(scope, receive, send)
finally:
# FileResponse 的范围错误会提前返回,不会运行通常的 background。
if release is not None:
await release()
def result_bytes_response(request: Request, content: bytes, media_type: str,
modified_at: float) -> Response:
"""返回带校验信息、范围读取和 HEAD 支持的不可变结果。
Args:
request: 当前 GET 或 HEAD 请求。
content: 已在任务清理锁内读出的完整结果,发送期间不再依赖文件。
media_type: 结果的 MIME 类型。
modified_at: 任务完成时间,作为稳定的 Last-Modified。
Returns:
完整结果、206 范围结果或 416 越界响应;HEAD 始终不含响应体。
"""
size = len(content)
etag = '"' + hashlib.sha256(content).hexdigest() + '"'
modified = formatdate(modified_at, usegmt=True)
headers = {
"ETag": etag,
"Last-Modified": modified,
"Accept-Ranges": "bytes",
"Cache-Control": "private, max-age=300",
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": (
"Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified"
),
}
status_code = 200
range_header = request.headers.get("range")
# If-Range 不匹配时发送完整的新结果,避免把两个版本拼成一个文件。
if (request.method == "GET" and range_header
and request.headers.get("if-range", etag) in {etag, modified}):
try:
unit, ranges_text = range_header.split("=", 1)
ranges = []
if unit.strip() != "bytes" or len(ranges_text.split(",")) > 100:
raise ValueError("Unsupported range")
for part in ranges_text.split(","):
start_text, end_text = part.strip().split("-", 1)
if not start_text:
suffix = int(end_text)
if suffix <= 0:
raise ValueError("Empty suffix")
start, end = max(0, size - suffix), size - 1
else:
start = int(start_text)
end = min(int(end_text), size - 1) if end_text else size - 1
if start < 0 or start > end or start >= size:
raise ValueError("Unsatisfiable range")
ranges.append((start, end))
except ValueError:
headers["Content-Range"] = f"bytes */{size}"
return Response(status_code=416, headers=headers)
status_code = 206
if len(ranges) == 1:
start, end = ranges[0]
headers["Content-Range"] = f"bytes {start}-{end}/{size}"
content = content[start:end + 1]
else:
boundary = secrets.token_hex(16)
content = b"".join(
(f"--{boundary}\r\nContent-Type: {media_type}\r\n"
f"Content-Range: bytes {start}-{end}/{size}\r\n\r\n").encode()
+ content[start:end + 1] + b"\r\n" for start, end in ranges
) + f"--{boundary}--\r\n".encode()
media_type = f"multipart/byteranges; boundary={boundary}"
headers["Content-Length"] = str(len(content))
return Response(b"" if request.method == "HEAD" else content,
status_code=status_code, media_type=media_type, headers=headers)
|