mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-19 11:50:45 +00:00
中转站加下载区 + 大文件流式发送
下载区
- 管理页加一张「下载最新版」卡片,就一个按钮
自动指向 /v1/downloads 里最新的被控端整包,不用每次改页面
- GET /v1/downloads 列出成品
- GET /download/<文件> 直接给文件
只允许白名单后缀(.apk/.zip/.txt/.exe/.idsig)
文件名严格校验,挡目录穿越(../ 和 ..%2F 都试过)
- 卡片下面附一个 Gitee 历史版本入口
大文件
- 原来 read_bytes() 一次性读进内存再 write。
19MB 在 460MB 的设备上勉强,更要命的是 Cloudflare
有 100 秒的源站响应上限,数据越晚开始流动越容易被掐。
- 改成 64KB 分块流式发送,边读边发
实测:19MB 公网下载 40 秒完成,字节数完全一致
版本
- 三端分开打包(原来是一个合并 zip)
ykxmz-agent-1.5.0.zip 被控端
ykxmz-server-1.5.0.zip 服务器端
ykxmz-watch-1.5.0.apk 控制端(直接给 APK)
This commit is contained in:
+44
-30
@@ -1010,9 +1010,9 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._json(404, {"ok": False, "error": "没有这个文件"})
|
||||
return
|
||||
try:
|
||||
blob = target.read_bytes()
|
||||
size = target.stat().st_size
|
||||
except OSError as exc:
|
||||
self._json(500, {"ok": False, "error": f"读取失败: {exc}"})
|
||||
self._json(500, {"ok": False, "error": f"读不到: {exc}"})
|
||||
return
|
||||
if safe.lower().endswith(".apk"):
|
||||
content_type = "application/vnd.android.package-archive"
|
||||
@@ -1024,7 +1024,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
content_type = "application/octet-stream"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(blob)))
|
||||
self.send_header("Content-Length", str(size))
|
||||
# 文件名可能含中文,用 RFC 5987 编码
|
||||
quoted = urllib.parse.quote(safe)
|
||||
self.send_header(
|
||||
@@ -1032,8 +1032,28 @@ class Handler(BaseHTTPRequestHandler):
|
||||
f"attachment; filename*=UTF-8''{quoted}")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(blob)
|
||||
log(f"下载:{safe}({len(blob)} 字节)")
|
||||
|
||||
# 分块流式发送,不要一次性 read_bytes 再 write ——
|
||||
# 大文件(19MB)那样会先攒满内存、数据也迟迟不开始流动;
|
||||
# 而 Cloudflare 有 100 秒的源站响应上限,越晚开始发越容易被掐。
|
||||
sent = 0
|
||||
try:
|
||||
with target.open("rb") as stream:
|
||||
while True:
|
||||
chunk = stream.read(65536)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
sent += len(chunk)
|
||||
self.wfile.flush()
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
# 客户端/边缘主动断开(比如大文件超时),正常现象
|
||||
log(f"下载中断:{safe}(已发 {sent}/{size} 字节)")
|
||||
return
|
||||
except OSError as exc:
|
||||
log(f"下载出错:{safe}(已发 {sent}/{size} 字节){exc}")
|
||||
return
|
||||
log(f"下载:{safe}({sent} 字节)")
|
||||
return
|
||||
|
||||
if path in ("/v1/health", "/health"):
|
||||
@@ -1602,7 +1622,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
</div>
|
||||
|
||||
<div class=panel id=dlPanel>
|
||||
<div class=label>下载 —— 最新版本</div>
|
||||
<div class=label>下载最新版</div>
|
||||
<div id=dlBody class=tip>正在读…</div>
|
||||
</div>
|
||||
|
||||
@@ -1830,13 +1850,7 @@ if (netBtn) netBtn.onclick = async () => {{
|
||||
}}
|
||||
}};
|
||||
|
||||
// ---------------- 下载区 ----------------
|
||||
function humanSize(bytes) {{
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
||||
}}
|
||||
|
||||
// ---------------- 下载(就一个链接)----------------
|
||||
(async () => {{
|
||||
const body = document.getElementById('dlBody');
|
||||
if (!body) return;
|
||||
@@ -1845,25 +1859,25 @@ function humanSize(bytes) {{
|
||||
const j = await r.json();
|
||||
const files = (j && j.files) || [];
|
||||
if (!files.length) {{
|
||||
body.innerHTML = '<span style="color:#8b949e">还没有上传成品。'
|
||||
+ '把 APK / 整包放到服务器的 downloads 目录就会出现在这里。</span>';
|
||||
body.innerHTML = '<span style="color:#8b949e">服务器上还没有成品。</span>';
|
||||
return;
|
||||
}}
|
||||
body.innerHTML = files.map(f => {{
|
||||
const isApk = f.name.toLowerCase().endsWith('.apk');
|
||||
const tag = isApk
|
||||
? '<span class="badge on">手表 APK</span>'
|
||||
: '<span class="badge off">整包</span>';
|
||||
return '<div style="display:flex;align-items:center;gap:10px;'
|
||||
+ 'margin:6px 0;flex-wrap:wrap">'
|
||||
+ tag
|
||||
+ '<a href="/download/' + encodeURIComponent(f.name) + '" '
|
||||
+ 'style="color:#79c0ff;text-decoration:none;font-weight:500">'
|
||||
+ f.name + '</a>'
|
||||
+ '<span style="color:#6e7681;font-size:12px">'
|
||||
+ humanSize(f.size) + '</span>'
|
||||
+ '</div>';
|
||||
}}).join('');
|
||||
// 优先挑被控端整包;没有就挑第一个 zip;再没有就第一个
|
||||
const pick = files.find(f => /agent/i.test(f.name))
|
||||
|| files.find(f => /\.zip$/i.test(f.name))
|
||||
|| files[0];
|
||||
const mb = (pick.size / 1024 / 1024).toFixed(2);
|
||||
body.innerHTML =
|
||||
'<a href="/download/' + encodeURIComponent(pick.name) + '" '
|
||||
+ 'style="display:inline-block;padding:10px 20px;border-radius:6px;'
|
||||
+ 'background:#238636;color:#fff;text-decoration:none;'
|
||||
+ 'font-weight:600;font-size:15px">'
|
||||
+ '⬇ 下载 ' + pick.name + '</a>'
|
||||
+ '<div style="color:#6e7681;font-size:12px;margin-top:8px">'
|
||||
+ mb + ' MB · 也可以去 '
|
||||
+ '<a href="https://gitee.com/xmz7891/ykxmz/releases" target="_blank" '
|
||||
+ 'rel="noopener" style="color:#79c0ff">Gitee 历史版本</a>'
|
||||
+ '</div>';
|
||||
}} catch (e) {{
|
||||
body.innerHTML = '<span style="color:#f85149">读不到:' + e + '</span>';
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user