mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 10:10:43 +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)
192 lines
7.5 KiB
Python
192 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
r"""修"大文件下载失败"。
|
||
|
||
现象:
|
||
公网下载 0.19MB 的 APK -> 200 ✓
|
||
公网下载 46KB 的服务器包 -> 200 ✓
|
||
公网下载 19MB 的被控端包 -> 000 ✗
|
||
|
||
原因:
|
||
1. relay 原来是 blob = f.read_bytes() 一次性读进内存再 write 出去。
|
||
19MB 在 460MB 的设备上勉强,但更要命的是——
|
||
2. **Cloudflare 有 100 秒的源站响应上限**。
|
||
设备上行慢(4G,还带丢包重传),19MB 传不完就被掐断。
|
||
小文件几百毫秒就完了,所以只有大文件挂。
|
||
|
||
改法:
|
||
A. 改成**分块流式**发送(64KB 一块),边读边发、及时 flush ——
|
||
至少不会因为攒内存而卡住,也让数据尽早开始流动。
|
||
B. 管理页的下载列表里,每个文件旁边加一个「从 Gitee 下」的链接。
|
||
走 Gitee 的 CDN 比走这条 4G 隧道快得多,大文件应该走那边。
|
||
C. 前端按大小给提示:超过 5MB 的文件,标一句"建议从 Gitee 下"。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay" r"\relay.py")
|
||
|
||
# ---------- A. 流式发送 ----------
|
||
|
||
OLD_SERVE = ''' try:
|
||
blob = target.read_bytes()
|
||
except OSError as exc:
|
||
self._json(500, {"ok": False, "error": f"读取失败: {exc}"})
|
||
return
|
||
if safe.lower().endswith(".apk"):
|
||
content_type = "application/vnd.android.package-archive"
|
||
elif safe.lower().endswith(".zip"):
|
||
content_type = "application/zip"
|
||
elif safe.lower().endswith(".txt"):
|
||
content_type = "text/plain; charset=utf-8"
|
||
else:
|
||
content_type = "application/octet-stream"
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", content_type)
|
||
self.send_header("Content-Length", str(len(blob)))
|
||
# 文件名可能含中文,用 RFC 5987 编码
|
||
quoted = urllib.parse.quote(safe)
|
||
self.send_header(
|
||
"Content-Disposition",
|
||
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)} 字节)")
|
||
return'''
|
||
|
||
NEW_SERVE = ''' try:
|
||
size = target.stat().st_size
|
||
except OSError as exc:
|
||
self._json(500, {"ok": False, "error": f"读不到: {exc}"})
|
||
return
|
||
if safe.lower().endswith(".apk"):
|
||
content_type = "application/vnd.android.package-archive"
|
||
elif safe.lower().endswith(".zip"):
|
||
content_type = "application/zip"
|
||
elif safe.lower().endswith(".txt"):
|
||
content_type = "text/plain; charset=utf-8"
|
||
else:
|
||
content_type = "application/octet-stream"
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", content_type)
|
||
self.send_header("Content-Length", str(size))
|
||
# 文件名可能含中文,用 RFC 5987 编码
|
||
quoted = urllib.parse.quote(safe)
|
||
self.send_header(
|
||
"Content-Disposition",
|
||
f"attachment; filename*=UTF-8''{quoted}")
|
||
self.send_header("Cache-Control", "no-store")
|
||
self.end_headers()
|
||
|
||
# 分块流式发送,不要一次性 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'''
|
||
|
||
# ---------- B/C. 前端加 Gitee 链接与大文件提示 ----------
|
||
|
||
OLD_JS = ''' 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('');'''
|
||
|
||
NEW_JS = ''' const GITEE = 'https://gitee.com/xmz7891/ykxmz/releases';
|
||
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>';
|
||
// 大文件走这条 4G 隧道很慢(Cloudflare 还有 100 秒上限),
|
||
// 超过 5MB 就提示一下,并给个 Gitee 的入口。
|
||
const big = f.size > 5 * 1024 * 1024;
|
||
const hint = big
|
||
? ' <span style="color:#d29922;font-size:12px">'
|
||
+ '(较大,隧道慢,建议走 Gitee)</span>'
|
||
: '';
|
||
const alt = big
|
||
? ' <a href="' + GITEE + '" target="_blank" rel="noopener" '
|
||
+ 'style="color:#8b949e;font-size:12px">从 Gitee 下</a>'
|
||
: '';
|
||
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>'
|
||
+ hint + alt
|
||
+ '</div>';
|
||
}}).join('');'''
|
||
|
||
|
||
def main() -> int:
|
||
text = RELAY.read_text(encoding="utf-8")
|
||
ok = True
|
||
|
||
if "分块流式发送" in text:
|
||
print(" 流式发送已经改过")
|
||
elif OLD_SERVE in text:
|
||
text = text.replace(OLD_SERVE, NEW_SERVE, 1)
|
||
print(" 下载改成 64KB 分块流式发送")
|
||
else:
|
||
print(" [失败] 流式发送锚点没找到")
|
||
ok = False
|
||
|
||
if "从 Gitee 下" in text:
|
||
print(" 前端提示已经加过")
|
||
elif OLD_JS in text:
|
||
text = text.replace(OLD_JS, NEW_JS, 1)
|
||
print(" 前端加了大文件提示 + Gitee 入口")
|
||
else:
|
||
print(" [失败] 前端锚点没找到")
|
||
ok = False
|
||
|
||
if not ok:
|
||
return 1
|
||
|
||
RELAY.write_text(text, encoding="utf-8", newline="\n")
|
||
try:
|
||
ast.parse(text)
|
||
print(" relay.py 语法 OK")
|
||
except SyntaxError as exc:
|
||
print(f" 语法错误: 行 {exc.lineno}: {exc.msg}")
|
||
return 1
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|