background_download
原生后台下载、上传与持久任务中心。
background_download 是 iOS 原生后台文件传输与持久任务中心。它既能下载,也能把本地文件复制到宿主持有的安全 spool 后后台上传;任务离开 MiniApp 页面、结束本次 Python 运行,甚至 App 因系统事件重启后,仍可由同一个 MiniApp 查询和控制。
诚实的恢复边界:可恢复的是宿主的 URLSession 文件任务,不是 Python 调用栈、局部变量、Timer 或回调。新运行通过保存的task_id、background_download.list()或miniapp_runtime.durable_tasks()重新连接。旧运行会在记录中显示origin_run_state="interrupted",宿主不会伪装继续执行旧 Python 代码。
声明式 miniapp.json 通常声明:
{
"capabilities": ["network", "file_system"],
"allowedNetworkHosts": ["downloads.example.com", "uploads.example.com"]
}
从系统分享页启动时,miniapp_runtime.launch_request() 返回的文件拥有本次运行的只读 host grant。upload() 会在该授权仍有效时立即复制到宿主 spool,因此无需长期持有外部文件 URL;联网能力和目标 host 仍必须通过检查。普通工作区文件和下载目标继续要求 file_system。
#能力概览
| 能力 | 行为 |
|---|---|
| 后台下载 | 原生 background URLSessionDownloadTask,完成后校验并原子写入目标 |
| 后台上传 | 只接受文件;先复制到宿主持有、禁止备份且受文件保护的 spool |
| 暂停与恢复 | 下载使用持久化 resumeData;服务端不支持时明确失败为 resume_unavailable |
| 重试 | 复用不可变能力租约与持久请求元数据;敏感 header 必须重新提供 |
| 任务中心 | 同 MiniApp 可 status() / list();其他 MiniApp 即使猜中 ID 也只能得到 unknown |
| 完整性 | 可选 SHA-256;下载在落盘前校验,上传在 spool 提交前校验 |
| 响应元数据 | 返回 HTTP status、安全响应 header、收发字节数;不保存响应体和 Cookie |
状态机:
running ── pause() ──> pausing ── resumeData ──> paused ── resume() ──> running
│ └── 无 resumeData ──> failed(resume_unavailable)
├── 收完 ──> finalizing(校验/原子提交)──> completed
├── 错误 ──> failed ── retry() ──> running
└── cancel() ──> cancelled
#快速开始:下载、暂停与恢复
import os
import time
import background_download
destination = os.path.join(os.path.expanduser("~/Documents"), "package.zip")
task_id = background_download.download(
"https://downloads.example.com/package.zip",
destination,
headers={"Accept": "application/zip"},
sha256="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
)
while True:
task = background_download.status(task_id)
print(task["state"], task["progress"], task["bytes_received"])
if task["state"] in ("completed", "failed", "cancelled"):
break
time.sleep(0.5)
# pause() 是请求;轮询到 paused 后才代表 resumeData 已安全持久化。
# background_download.pause(task_id)
# background_download.resume(task_id)
#分享文件直接后台上传
import background_download
import miniapp_runtime
request = miniapp_runtime.launch_request(required=True)
item = request.first
task_id = background_download.upload(
"https://uploads.example.com/import",
item.path,
method="POST",
headers={"Authorization": "Bearer current-secret"},
)
print("后台任务:", task_id)
upload() 返回前,文件已复制到宿主 spool。之后分享授权失效、原文件移动或本次 MiniApp 退出,都不会让已提交上传失去源文件。成功或取消会删除 spool;失败时保留它供 retry() 使用,终态记录过期时再统一清理。
#重新连接持久任务
import miniapp_runtime
import background_download
for task in miniapp_runtime.durable_tasks(limit=50):
print(
task["task_id"],
task["kind"],
task["state"],
task["origin_run_state"],
)
failed = background_download.list(state="failed")
for task in failed:
if task["can_resume"]:
background_download.resume(task["task_id"])
elif task["can_retry"] and not task["retry_requires_headers"]:
background_download.retry(task["task_id"])
如果 retry_requires_headers 非空,表示这些自定义或敏感值从未写入 App 的任务记录:
background_download.retry(
task_id,
headers={"Authorization": "Bearer refreshed-secret"},
)
#API 参考
#download()
download(
url,
destination_path,
task_id=None,
*,
method="GET",
headers=None,
sha256=None,
) -> str
- 保留原有
download(url, destination_path, task_id)写法。 method支持GET、HEAD、POST、PUT、PATCH、DELETE;模块不接受内存 request body。headers最多 64 项、总计 32 KiB;禁止Host、Content-Length等 URLSession 管理字段。sha256是可选的 64 位十六进制摘要。校验失败不会替换已有目标文件。- 目标必须位于当前 MiniApp 可写范围。写入使用同目录 staging 与原子替换,并拒绝符号链接目标。
#upload()
upload(
url,
source_path,
task_id=None,
*,
method="POST",
headers=None,
sha256=None,
) -> str
method支持POST、PUT、PATCH。- 只上传常规文件,不接受目录、符号链接或内存
bytes。 source_path在原始 capability lease 下复验并复制;spool 由宿主持有。- iOS 后台上传要求 file-backed body,因此这是正确的系统级实现,不是前台 async 请求伪装。
#status() 与 list()
status(task_id) -> dict
list(*, state=None, kind=None, limit=100) -> list[dict]
status() 含最多 32 条 events;list() 返回紧凑摘要。任务只对相同 owner_app_id 可见。终态记录保留约 30 天,最多保留 256 条;同时活动任务最多 64 条。
省略 task_id 会生成安全的 UUID,通常应采用这种方式。若业务必须自定义 ID,请使用足够唯一的值;宿主不会允许另一个 MiniApp 以同名 ID 覆盖现存记录。
主要字段:
| 字段 | 说明 |
|---|---|
task_id / provider / kind | 逻辑任务 ID;任务中心 provider 为 background_transfer;类型为 download 或 upload |
state / progress | 当前状态;含校验/原子提交中的 finalizing;进度为 0.0–1.0,未知长度时可保持 0 |
owner_app_id | 任务所属 MiniApp;宿主任务为 None |
origin_session_id | 创建任务的运行票据,不会因重试或重启变化 |
origin_run_state | current、interrupted 或 host;interrupted 明确表示旧 Python 栈已结束 |
path | 下载目标;上传不会暴露宿主 spool 路径 |
can_pause / can_resume / can_retry | 当前可用控制能力 |
retry_requires_headers | 重试前必须重新提供的自定义或敏感 header 名称;仅少量内容协商字段会持久化 |
http_status / response_headers | 最终响应元数据;Cookie、认证 challenge 等敏感字段被过滤 |
bytes_received / bytes_sent | 已收/发字节;也提供对应 expected 字段 |
expected_sha256 / sha256 | 期望值与实际校验值(启用校验时) |
attempt / retry_count / resume_count | 持久化执行统计 |
created_at / updated_at / last_retry_at | Unix 时间戳 |
error_code / error | 稳定错误码与去敏后的说明 |
events | 有序、有界的状态历史,仅 status() 返回 |
#pause() / resume() / retry() / cancel()
pause(task_id) -> dict
resume(task_id) -> dict
retry(task_id, *, headers=None) -> dict
cancel(task_id) -> None
pause()仅支持下载,并先进入pausing。只有生成并持久化非空resumeData后才进入paused。resume()仅接受有可用 resumeData 的paused/failed下载;失效时抛出resume_unavailable并更新任务状态。retry()发起全新 attempt。上传失败会复用宿主 spool;下载失败或取消后重新开始。已取消上传释放了 spool,若要再次上传需创建新任务。敏感 header 不落盘,必须按提示重新提供。cancel()保持旧 API 的幂等语义:未知、跨 MiniApp、已终止或已经进入不可中断原子提交的 ID 是 no-op。
#安全与系统行为
- 每个重定向都在创建任务时的不可变 capability lease 下重新验证。
- HTTPS 到 HTTP 的降级重定向会被拒绝;跨 scheme/host/port 的重定向只保留少量内容协商字段,其余自定义或凭据 header 全部移除。
- 持久 JSON 只保存少量可移植内容协商 request header,不含其他自定义/敏感值,不含上传 spool 路径的公开返回,也不保存响应体。
- 为了能够在重启后重试,请求 URL(包括 query)会持久化。不要把长期凭据放进 URL;把凭据放在敏感 header 中,并在重试时重新提供。
- HTTP 非 2xx 明确为
http_error;安全响应 header 和 status 仍可诊断。 - iOS 决定后台调度时机、蜂窝网络和电量策略;“后台”不等于保证立即运行。
- 模拟器可以验证 API 和状态机,但挂起、系统杀进程后恢复、网络切换与大文件必须在真机验收。
常见稳定错误码:
| 错误码 | 含义与处理 |
|---|---|
network_denied / redirect_denied | 初始地址或重定向不符合原始网络授权;修改声明/host,不要盲目重试 |
http_error / network_error | 服务端非 2xx 或网络失败;查看 http_status,再决定 retry() |
checksum_mismatch | 文件与期望 SHA-256 不一致;下载不会覆盖旧目标,上传不会启动 |
resume_unavailable | 服务端/系统没有可用 resumeData;改用 retry() 重新下载 |
sensitive_headers_required | 自定义或敏感 header 未落盘;按 retry_requires_headers 重新提供 |
upload_spool_unavailable / finalization_missing | 宿主持有的恢复文件已不存在;需要重新创建任务 |
task_missing | 重启时找不到原 URLSession 任务;记录诚实进入失败态 |
persistence_failed / insufficient_storage | 任务记录或 spool 无法安全落盘;释放空间后重试 |
reconciliation_pending | 冷启动正在与 iOS 后台会话重连;稍后再次调用控制 API |
duplicate_task / too_many_tasks | ID 冲突或达到 64 个活动任务上限 |
#AppUI 使用建议
在按钮回调里创建和控制任务;不要在 body() 里发起副作用。把 task_id 存入业务状态,用 0.5 秒或更慢的 Timer 轮询;页面重建时也可从 miniapp_runtime.durable_tasks() 恢复展示。
#相关文档
| 文档 | 用途 |
|---|---|
| network | 前台 REST、流式响应和普通文件请求 |
| background | 有限后台执行时间与 BGTask 调度,不是文件传输 |
| MiniApp 原生能力 | capability、host allowlist、外部输入与运行隔离 |
| storage | 保存业务侧 task_id 和 UI 偏好 |