transfer: 新增 list_files/download (流式+进度+Content-Disposition 文件名); CLI --list/--download/--out; 测试 57 项
This commit is contained in:
+63
-1
@@ -11,6 +11,7 @@ import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
import urllib.parse
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
@@ -33,6 +34,7 @@ class ProtocolServer:
|
||||
self.transfer_id = "T-1"
|
||||
self.completed = False
|
||||
self.file_id = "F-42"
|
||||
self.files: list[dict] = [] # ls/下载测试: [{"file_id","file_name","size","data"}]
|
||||
|
||||
def make_handler(self):
|
||||
srv = self
|
||||
@@ -74,6 +76,33 @@ class ProtocolServer:
|
||||
if m:
|
||||
self._json(200, {"received": sorted(srv.received)})
|
||||
return
|
||||
if self.path == "/api/files":
|
||||
# 列表只含元信息, 不含 data (bytes 不可 JSON 序列化)
|
||||
self._json(200, {
|
||||
"files": [
|
||||
{k: v for k, v in f.items() if k != "data"}
|
||||
for f in srv.files
|
||||
]
|
||||
})
|
||||
return
|
||||
m = re.fullmatch(r"/api/files/([^/]+)", self.path)
|
||||
if m:
|
||||
fid = m.group(1)
|
||||
rec = next((f for f in srv.files if f["file_id"] == fid), None)
|
||||
if rec is None:
|
||||
self._json(404, {"error": "文件不存在"})
|
||||
return
|
||||
body = rec["data"]
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/octet-stream")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header(
|
||||
"Content-Disposition",
|
||||
f"attachment; filename*=utf-8''{urllib.parse.quote(rec['file_name'])}",
|
||||
)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return
|
||||
self._json(404, {"error": "not found"})
|
||||
|
||||
def do_PUT(self):
|
||||
@@ -109,6 +138,7 @@ class TransferTestBase(unittest.TestCase):
|
||||
)
|
||||
self._td = tempfile.TemporaryDirectory(prefix='hermes-test-')
|
||||
self.addCleanup(self._td.cleanup)
|
||||
self.tmp = Path(self._td.name)
|
||||
self.chunk_files: dict[int, Path] = {}
|
||||
for i in range(1, CHUNK_COUNT + 1):
|
||||
p = Path(self._td.name) / f"chunk{i}.bin"
|
||||
@@ -174,5 +204,37 @@ class TestProtocolErrors(TransferTestBase):
|
||||
self.assertEqual(payload["status"], "incomplete")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
class TestListDownload(TransferTestBase):
|
||||
"""ls 列表 + 下载 (真实 HTTP 服务器)"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.srv.files = [
|
||||
{"file_id": "F-1", "file_name": "报告.pdf", "size": 6, "data": b"hello"},
|
||||
]
|
||||
|
||||
def test_list_files(self):
|
||||
files = self.client.list_files()
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0]["file_id"], "F-1")
|
||||
self.assertEqual(files[0]["file_name"], "报告.pdf")
|
||||
|
||||
def test_download_to_path(self):
|
||||
dest = self.tmp / "out.bin"
|
||||
got = self.client.download("F-1", dest)
|
||||
self.assertEqual(got, dest)
|
||||
self.assertEqual(dest.read_bytes(), b"hello")
|
||||
|
||||
def test_download_uses_server_filename(self):
|
||||
got = self.client.download("F-1")
|
||||
self.assertEqual(got.name, "报告.pdf")
|
||||
self.assertEqual(got.read_bytes(), b"hello")
|
||||
got.unlink()
|
||||
|
||||
def test_download_unknown_404(self):
|
||||
with self.assertRaises(TransferError):
|
||||
self.client.download("NOPE")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user