NapCatQQ/src/onebot/action/stream/UploadFileStreanConcurent.py
手瓜一十雪 890d032794 Add streaming file upload and download actions
Introduces new OneBot actions for streaming file upload and download, including chunked file transfer with memory/disk management and SHA256 verification. Adds CleanStreamTempFile, DownloadFileStream, UploadFileStream, and TestStreamDownload actions, updates action routing and network adapters to support streaming via HTTP and WebSocket, and provides Python test scripts for concurrent upload testing.
2025-09-16 23:24:00 +08:00

202 lines
7.5 KiB
Python

import asyncio
import websockets
import json
import base64
import hashlib
import os
from typing import Optional
class FileUploadTester:
def __init__(self, ws_uri: str, file_path: str):
self.ws_uri = ws_uri
self.file_path = file_path
self.chunk_size = 64 * 1024 # 64KB per chunk
self.stream_id = None
async def connect_and_upload(self):
"""连接到WebSocket并上传文件"""
async with websockets.connect(self.ws_uri) as ws:
print(f"已连接到 {self.ws_uri}")
# 准备文件数据
file_info = self.prepare_file()
if not file_info:
return
print(f"文件信息: {file_info['filename']}, 大小: {file_info['file_size']} bytes, 块数: {file_info['total_chunks']}")
# 生成stream_id
self.stream_id = f"upload_{hash(file_info['filename'] + str(file_info['file_size']))}"
print(f"Stream ID: {self.stream_id}")
# 重置流(如果存在)
await self.reset_stream(ws)
# 开始分块上传
await self.upload_chunks(ws, file_info)
# 完成上传
await self.complete_upload(ws)
# 等待一些响应
await self.listen_for_responses(ws)
def prepare_file(self):
"""准备文件信息"""
if not os.path.exists(self.file_path):
print(f"文件不存在: {self.file_path}")
return None
file_size = os.path.getsize(self.file_path)
filename = os.path.basename(self.file_path)
total_chunks = (file_size + self.chunk_size - 1) // self.chunk_size
# 计算SHA256
sha256_hash = hashlib.sha256()
with open(self.file_path, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256_hash.update(chunk)
expected_sha256 = sha256_hash.hexdigest()
return {
'filename': filename,
'file_size': file_size,
'total_chunks': total_chunks,
'expected_sha256': expected_sha256
}
async def reset_stream(self, ws):
"""重置流"""
req = {
"action": "upload_file_stream",
"params": {
"stream_id": self.stream_id,
"reset": True
},
"echo": "reset"
}
await ws.send(json.dumps(req))
print("发送重置请求...")
async def upload_chunks(self, ws, file_info):
"""上传文件块"""
with open(self.file_path, 'rb') as f:
for chunk_index in range(file_info['total_chunks']):
# 读取块数据
chunk_data = f.read(self.chunk_size)
chunk_base64 = base64.b64encode(chunk_data).decode('utf-8')
# 准备请求
req = {
"action": "upload_file_stream",
"params": {
"stream_id": self.stream_id,
"chunk_data": chunk_base64,
"chunk_index": chunk_index,
"total_chunks": file_info['total_chunks'],
"file_size": file_info['file_size'],
"filename": file_info['filename'],
#"expected_sha256": file_info['expected_sha256']
},
"echo": f"chunk_{chunk_index}"
}
await ws.send(json.dumps(req))
print(f"发送块 {chunk_index + 1}/{file_info['total_chunks']} ({len(chunk_data)} bytes)")
# 等待响应
try:
response = await asyncio.wait_for(ws.recv(), timeout=5.0)
resp_data = json.loads(response)
if resp_data.get('echo') == f"chunk_{chunk_index}":
if resp_data.get('status') == 'ok':
data = resp_data.get('data', {})
print(f" -> 状态: {data.get('status')}, 已接收: {data.get('received_chunks')}")
else:
print(f" -> 错误: {resp_data.get('message')}")
except asyncio.TimeoutError:
print(f" -> 块 {chunk_index} 响应超时")
# 小延迟避免过快发送
await asyncio.sleep(0.1)
async def complete_upload(self, ws):
"""完成上传"""
req = {
"action": "upload_file_stream",
"params": {
"stream_id": self.stream_id,
"is_complete": True
},
"echo": "complete"
}
await ws.send(json.dumps(req))
print("发送完成请求...")
async def verify_stream(self, ws):
"""验证流状态"""
req = {
"action": "upload_file_stream",
"params": {
"stream_id": self.stream_id,
"verify_only": True
},
"echo": "verify"
}
await ws.send(json.dumps(req))
print("发送验证请求...")
async def listen_for_responses(self, ws, duration=10):
"""监听响应"""
print(f"监听响应 {duration} 秒...")
try:
end_time = asyncio.get_event_loop().time() + duration
while asyncio.get_event_loop().time() < end_time:
try:
msg = await asyncio.wait_for(ws.recv(), timeout=1.0)
resp_data = json.loads(msg)
echo = resp_data.get('echo', 'unknown')
if echo == "complete":
if resp_data.get('status') == 'ok':
data = resp_data.get('data', {})
print(f"✅ 上传完成!")
print(f" 文件路径: {data.get('file_path')}")
print(f" 文件大小: {data.get('file_size')} bytes")
print(f" SHA256: {data.get('sha256')}")
print(f" 状态: {data.get('status')}")
else:
print(f"❌ 上传失败: {resp_data.get('message')}")
elif echo == "verify":
if resp_data.get('status') == 'ok':
data = resp_data.get('data', {})
print(f"🔍 验证结果: {data}")
elif echo == "reset":
print(f"🔄 重置完成: {resp_data}")
else:
print(f"📨 收到响应 [{echo}]: {resp_data}")
except asyncio.TimeoutError:
continue
except Exception as e:
print(f"监听出错: {e}")
async def main():
# 配置
WS_URI = "ws://localhost:3001" # 修改为你的WebSocket地址
FILE_PATH = "C:\\Users\\nanaeo\\Pictures\\CatPicture.zip"
print("=== 文件流上传测试 ===")
print(f"WebSocket URI: {WS_URI}")
print(f"文件路径: {FILE_PATH}")
try:
tester = FileUploadTester(WS_URI, FILE_PATH)
await tester.connect_and_upload()
except Exception as e:
print(f"测试出错: {e}")
if __name__ == "__main__":
asyncio.run(main())