Files
Viper_VRTE/requests/download.vp
T
2026-09-18 20:01:14 +08:00

437 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import t, c
from stdint import *
import string
import stdio
import stdlib
import requests
import requests.https as rhttps
import vthreading as vt
import w32.win32file as wf
import w32.win32base as wb
import w32.win32sync as wsync
# ============================================================
# requests/download.vp — 大文件切片下载(Range + 串行/多线程)
#
# 用法:
# import requests.download as rd
# rc: INT = rd.download("https://host/big.bin", "big.bin", threads=4,
# chunk_size=262144, verify=True, ca_file="ca.crt")
# if rc != 0:
# print(rd.last_error())
#
# 策略:
# threads == 1 串行 Range 切片(顺序拉取并写入文件)
# threads > 1 多线程:文件按线程数均分为若干带(band),每线程一个
# 连接顺序下载自己带的切片,写入同一文件的对应偏移。
#
# 说明:
# - 通过 Content-Range 探测总大小与服务器 Range 支持;
# - http:// 走 requests(纯 socket),https:// 走 requests.https(openssl);
# - 每片请求不带 Accept-Encoding,响应为原始字节,直接落盘。
# ============================================================
DL_MAX_THREADS: t.CDefine = 8
DL_DEFAULT_CHUNK: t.CDefine = 262144
DL_REDIRECT_MAX: t.CDefine = 5
_dl_err_store: t.CArray[t.CChar, 512]
def last_error() -> str:
"""返回最近一次下载失败原因文本(无则空串)。"""
return _dl_err_store
def _dl_set_err(text: str):
if text is None:
return
_dl_err_store[0] = 0
stdio.snprintf(_dl_err_store, 512, "%s", text)
def _dl_strdup(s: str) -> str:
"""stdlib 堆上复制字符串(调用方 free),None 失败。"""
if s is None:
return None
n: t.CSizeT = string.strlen(s)
out: str = stdlib.malloc(n + 1)
if out is None:
return None
if n > 0:
string.memcpy(out, s, n)
out[n] = 0
return out
# ============================================================
# 单切片拉取(含重定向跟随,≤5 跳;http/https 按 scheme 分发)
# ============================================================
def _dl_slice(url: str, pos: t.CSizeT, want: INT, verify: bool, ca_file: str,
dst: str, dst_cap: INT, out_total: t.CSizeT | t.CPtr) -> INT:
"""拉取 [pos, pos+want) 一片,返回体字节数写入 dst(dst 内正文起始需
调用方用 _dl_body_off 定位);-2 重定向超限;-3 网络/服务器失败。"""
if url is None or dst is None or out_total is None:
return -1
cur: str = _dl_strdup(url)
if cur is None:
return -1
hops: INT = 0
rc: INT = 0
while True:
is_https: INT = 1 if (string.strncmp(cur, "https://", 8) == 0) else 0
st: INT = 0
tot: t.CSizeT = 0
loc: t.CArray[t.CChar, 512]
if is_https != 0:
rc = rhttps._DlOnce(cur, pos, want, verify, ca_file, dst, dst_cap,
c.Addr(st), loc, 512, c.Addr(tot))
else:
rc = requests._DlOnce(cur, pos, want, dst, dst_cap,
c.Addr(st), loc, 512, c.Addr(tot))
if rc == -2 and loc[0] != 0:
hops += 1
if hops > DL_REDIRECT_MAX:
_dl_set_err("too many redirects")
stdlib.free(cur)
return -2
new_url: str = stdlib.malloc(2048)
if new_url is None:
stdlib.free(cur)
return -1
if requests._ResolveRedirectUrl(cur, loc, new_url, 2048) != 0:
stdlib.free(new_url)
stdlib.free(cur)
_dl_set_err("bad redirect location")
return -3
stdlib.free(cur)
cur = new_url
continue
break
stdlib.free(cur)
if rc < 0:
return rc
c.Set(c.Deref(out_total), tot)
return rc
def _dl_body_off(dst: str, dst_cap: INT) -> INT:
"""定位响应体(头结束处)在 dst 中的偏移,找不到返回 -1。"""
return requests._FindBodyStart(dst, dst_cap)
# ============================================================
# 文件写入(按偏移落盘,支持多线程并发写不同区段)
# ============================================================
def _dl_write_at(path: str, src: str, length: INT, offset: t.CSizeT) -> INT:
"""打开文件(不存在则创建),Seek 到 offset 写 length 字节。0 成功;-1 失败。"""
if path is None or src is None or length <= 0:
return -1
h: wb.HANDLE = wf.CreateFileA(
path, wf.GENERIC_WRITE,
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE | wf.FILE_SHARE_DELETE,
None, wf.OPEN_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, None)
if h == wb.INVALID_HANDLE_VALUE:
return -1
li: wb.LARGE_INTEGER = wb.LARGE_INTEGER()
li.QuadPart = t.CLongLong(offset)
ok_seek: BOOL = wf.SetFilePointerEx(h, c.Addr(li), None, wf.FILE_BEGIN)
if ok_seek == 0:
wb.CloseHandle(h)
return -1
written: ULONG = 0
ok_w: BOOL = wf.WriteFile(h, src, t.CUnsignedLong(length), c.Addr(written), None)
wb.CloseHandle(h)
if ok_w == 0:
return -1
if t.CInt(written) != length:
return -1
return 0
def _dl_truncate(path: str) -> INT:
"""CREATE_ALWAYS 建(截断)空文件。0 成功;-1 失败。"""
h: wb.HANDLE = wf.CreateFileA(
path, wf.GENERIC_WRITE,
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE | wf.FILE_SHARE_DELETE,
None, wf.CREATE_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, None)
if h == wb.INVALID_HANDLE_VALUE:
return -1
wb.CloseHandle(h)
return 0
# ============================================================
# 探测:总大小 / Range 支持(首片 bytes=0-0)
# ============================================================
def _dl_probe(url: str, verify: bool, ca_file: str,
out_total: t.CSizeT | t.CPtr) -> INT:
"""探测服务器总大小与 Range 支持。
返回 0=支持 Range 且 out_total 有效;1=服务器返回 200(不支持 Range,
退化为整段下载,out_total=Content-Length);-3 失败。"""
c.Set(c.Deref(out_total), 0)
buf: str = stdlib.malloc(8192)
if buf is None:
_dl_set_err("out of memory")
return -3
tot: t.CSizeT = 0
rc: INT = _dl_slice(url, 0, 0, verify, ca_file, buf, 8192, c.Addr(tot))
stdlib.free(buf)
if rc < 0:
_dl_set_err("probe failed")
return -3
if tot > 0:
c.Set(c.Deref(out_total), tot)
return 0
_dl_set_err("cannot determine size")
return -3
# ============================================================
# 下载一个区间带([b_start, b_end)),逐片拉取并落盘
# ============================================================
def _dl_download_band(path: str, url: str, is_https: INT, verify: bool, ca_file: str,
b_start: t.CSizeT, b_end: t.CSizeT, chunk: INT,
buf: str, buf_cap: INT,
p_done: t.CSizeT | t.CPtr) -> INT:
"""0 成功;-1 网络/服务器;-2 写入失败。p_done 累计已写字节。"""
pos: t.CSizeT = b_start
while pos < b_end:
want: INT = chunk
if b_end - pos < t.CSizeT(want):
want = INT(b_end - pos)
tot: t.CSizeT = 0
rc: INT = _dl_slice(url, pos, want, verify, ca_file, buf, buf_cap, c.Addr(tot))
if rc <= 0:
_dl_set_err("slice fetch failed")
return -1
bo: INT = _dl_body_off(buf, buf_cap)
if bo < 0:
_dl_set_err("bad slice response")
return -1
got: INT = rc
if bo + got > buf_cap:
got = buf_cap - bo
if got <= 0:
_dl_set_err("slice empty")
return -1
if _dl_write_at(path, buf + bo, got, pos) != 0:
_dl_set_err("write file failed")
return -2
c.Set(c.Deref(p_done), c.Deref(p_done) + t.CSizeT(got))
pos += t.CSizeT(got)
if got < want:
# 服务端提前结束(正常收尾时仅末片 < want)
if pos >= b_end:
break
_dl_set_err("short slice")
return -1
return 0
# ============================================================
# 多线程 worker
# ============================================================
@t.Object
class DlJob:
path: str
url: str
is_https: INT
verify: INT
ca_file: str
start: t.CSizeT
end: t.CSizeT
chunk: INT
buf: str
buf_cap: INT
done: t.CSizeT
status: INT
def _dl_worker(arg: VOIDPTR) -> t.CPtr:
if arg is None:
return None
job: DlJob | t.CPtr = arg
rc: INT = _dl_download_band(
job.path, job.url, job.is_https,
True if job.verify != 0 else False,
job.ca_file,
job.start, job.end, job.chunk, job.buf, job.buf_cap,
c.Addr(job.done))
if rc != 0:
job.status = rc
return None
# ============================================================
# 公共 API
# ============================================================
def download(url: str, path: str, threads: INT = 1,
chunk_size: INT = DL_DEFAULT_CHUNK,
verify: bool = True, ca_file: str = None) -> INT:
"""大文件切片下载。
Args:
url: 支持 http:// 与 https://(Range 分段拉取)
path: 保存路径(不存在自动创建;已存在则截断重建)
threads: 1=串行切片;2~8=多线程并行分带下载
chunk_size: 单片大小(字节,默认 256KB)
verify / ca_file: https 证书校验参数(对齐 requests.https)
Returns:
0 成功;-1 参数错误;-2 文件创建失败;-3 服务器不支持/下载失败;
-4 写入失败。last_error() 可查原因文本。
"""
_dl_err_store[0] = 0
if url is None or path is None or url[0] == 0 or path[0] == 0:
_dl_set_err("url/path empty")
return -1
if chunk_size < 1024:
chunk_size = DL_DEFAULT_CHUNK
if threads < 1:
threads = 1
if threads > DL_MAX_THREADS:
threads = DL_MAX_THREADS
if string.strncmp(url, "http://", 7) != 0 \
and string.strncmp(url, "https://", 8) != 0:
_dl_set_err("unsupported scheme")
return -1
# 探测总大小与 Range 支持
total: t.CSizeT = 0
prc: INT = _dl_probe(url, verify, ca_file, c.Addr(total))
if prc < 0:
return -3
if total == 0:
_dl_set_err("cannot determine size")
return -3
# 建立(截断)目标文件
if _dl_truncate(path) != 0:
_dl_set_err("create file failed")
return -2
stdio.printf("[download] %s -> %s total=%zu bytes threads=%d chunk=%d\n",
url, path, total, threads, chunk_size)
slab: INT = chunk_size + 8192
if threads == 1:
# ---- 串行切片 ----
buf: str = stdlib.malloc(t.CSizeT(slab))
if buf is None:
_dl_set_err("out of memory")
return -3
done: t.CSizeT = 0
rc: INT = _dl_download_band(path, url, 0, verify, ca_file,
0, total, chunk_size, buf, slab, c.Addr(done))
stdlib.free(buf)
if rc != 0:
return -3 if rc == -1 else -4
stdio.printf("[download] done: %zu/%zu bytes\n", done, total)
return 0
# ---- 多线程并行分带 ----
band: t.CSizeT = (total + t.CSizeT(threads) - 1) / t.CSizeT(threads)
ctxs: t.CArray[VOIDPTR, DL_MAX_THREADS]
handles: t.CArray[VOIDPTR, DL_MAX_THREADS]
threads_ok: INT = 0
i: INT = 0
while i < threads:
b_start: t.CSizeT = t.CSizeT(i) * band
if b_start >= total:
break
b_end: t.CSizeT = b_start + band
if b_end > total:
b_end = total
job: DlJob | t.CPtr = stdlib.calloc(1, DlJob.__sizeof__())
if job is None:
break
job.path = _dl_strdup(path)
job.url = _dl_strdup(url)
if job.path is None or job.url is None:
stdlib.free(job.path)
stdlib.free(job.url)
stdlib.free(t.CPtr(t.CUInt64T(job)))
break
job.is_https = 1 if (string.strncmp(url, "https://", 8) == 0) else 0
job.verify = 1 if verify else 0
job.ca_file = _dl_strdup(ca_file) if ca_file is not None else None
job.start = b_start
job.end = b_end
job.chunk = chunk_size
job.buf_cap = slab
job.buf = stdlib.malloc(t.CSizeT(slab))
if job.buf is None:
stdlib.free(job.path)
stdlib.free(job.url)
if job.ca_file is not None:
stdlib.free(job.ca_file)
stdlib.free(t.CPtr(t.CUInt64T(job)))
break
job.done = 0
job.status = 0
ctxs[i] = t.CPtr(t.CUInt64T(job))
th: rawptr[vt.Thread] = vt.Thread(_dl_worker, ctxs[i])
if th is None:
stdlib.free(job.buf)
stdlib.free(job.path)
stdlib.free(job.url)
if job.ca_file is not None:
stdlib.free(job.ca_file)
stdlib.free(t.CPtr(t.CUInt64T(job)))
break
th.start()
handles[i] = th.handle
threads_ok += 1
i += 1
if threads_ok == 0:
_dl_set_err("thread create failed")
return -3
# 等待全部线程结束
j: INT = 0
while j < threads_ok:
wsync.WaitForSingleObject(handles[j], 0xFFFFFFFF)
wb.CloseHandle(handles[j])
j += 1
# 汇总结果
total_done: t.CSizeT = 0
failed_job: INT = 0
k: INT = 0
while k < threads_ok:
jobk: DlJob | t.CPtr = ctxs[k]
total_done += jobk.done
if jobk.status != 0:
failed_job = 1
k += 1
# 释放线程上下文
m: INT = 0
while m < threads_ok:
jobm: DlJob | t.CPtr = ctxs[m]
stdlib.free(jobm.path)
stdlib.free(jobm.url)
if jobm.ca_file is not None:
stdlib.free(jobm.ca_file)
stdlib.free(jobm.buf)
stdlib.free(t.CPtr(t.CUInt64T(jobm)))
m += 1
if failed_job != 0 or total_done != total:
stdio.printf("[download] incomplete: %zu/%zu bytes\n", total_done, total)
_dl_set_err("download incomplete")
return -3
stdio.printf("[download] done: %zu/%zu bytes\n", total_done, total)
return 0