上传、下载与进度¶
小文件上传¶
小文件可以直接使用 Multipart ByteArray:
@Multipart
@POST("/v1/avatar")
fun uploadAvatar(
@Part(value = "file", fileName = "avatar.jpg", contentType = "image/jpeg") bytes: ByteArray,
@IdempotencyKey key: String,
): NetworkCall<AvatarDto>
这种方式适合头像、表单附件等有明确内存上限的正文。
大文件上传与断点续传¶
大文件使用 network-transfer。平台 Engine 从文件流式读取,默认以 4 MiB 分片发送;持久状态同时记录
bytesUploaded(网络上已经发送)和 committedBytes(服务端已经确认),失败恢复只使用后者:
val store = PersistentUploadStore(filesDir.resolve("network/uploads.json").path)
val repository = DefaultNetworkUploadRepository(store)
val id = repository.enqueue(
UploadRequest(
id = UploadId("video-draft-42"),
url = "https://uploads.example.com/v1/files",
source = filesDir.resolve("draft.mp4").path,
totalBytes = filesDir.resolve("draft.mp4").length(),
expectedSha256 = manifest.sha256,
method = UploadHttpMethod.PUT,
protocol = UploadProtocol.CONTENT_RANGE,
),
)
repository.observe(id).collect(::renderUpload)
分片请求包含 Content-Range、Upload-Id、Upload-Offset、Upload-Length、Upload-Complete,配置摘要时
还会携带 Upload-Checksum: sha256 …。
服务端可以返回 2xx 确认当前分片;返回 308 时必须携带 Upload-Offset(或 Range)。同一 Upload-Id
与偏移的重复分片必须幂等,否则应用被终止后无法安全重放最后一个未确认分片。
pause、resume、cancel、retry 与 recover 都会持久化,源文件长度发生变化时任务会以
SOURCE_CHANGED 失败,不会继续发送错误偏移。
小文件下载¶
val result = api.download(id)
.responseLimit(20L * 1024 * 1024)
.progress { progress -> render(progress) }
.await()
当前 @Streaming 仍返回内存 ByteArray,只是跳过 JSON 解码并提供字节进度,不等于无限大文件落盘流。
大文件下载与断点续传¶
大文件使用 network-transfer,正文会直接写入 .part 文件,不进入内存:
val store = PersistentDownloadStore(filesDir.resolve("network/downloads.json").path)
val repository = DefaultNetworkDownloadRepository(store)
val id = repository.enqueue(
DownloadRequest(
id = DownloadId("offline-map-v4"),
url = "https://downloads.example.com/map-v4.zip",
destination = filesDir.resolve("map-v4.zip").path,
expectedSha256 = manifest.sha256,
expectedBytes = manifest.bytes,
),
)
repository.observe(id).collect(::renderDownload)
模块使用 Range 与 If-Range 恢复传输,校验长度和可选 SHA-256,通过同目录 .part 文件完成后再发布。
暂停、取消和失败是三个独立状态;进程重启后使用 recover(autoResume = true) 恢复未完成任务。下载和重定向默认
拒绝私有/回环地址;仅受信任的内网场景可以显式设置 allowPrivateAddresses = true。
Android 后台任务¶
需要跨进程后台执行时,将 Repository 设置为 autoStart = false,安装对应 Registry,并通过 WorkManager
Scheduler 入队。上传和下载 Worker 都遵守计量网络/漫游约束,维护前台通知,并提供暂停、恢复和取消操作。
val repository = DefaultNetworkDownloadRepository(store, autoStart = false)
DownloadWorkerRegistry.install(repository, repository)
repository.enqueue(request)
DownloadWorkScheduler(applicationContext).enqueue(request)
val uploads = DefaultNetworkUploadRepository(uploadStore, autoStart = false)
UploadWorkerRegistry.install(uploads, uploads)
uploads.enqueue(uploadRequest)
UploadWorkScheduler(applicationContext).enqueue(uploadRequest)
Android 13 及以上由宿主在入队前申请 POST_NOTIFICATIONS;模块已经合并前台服务权限与不可导出的通知操作
Receiver。
iOS 后台 NSURLSession¶
向 Engine 传入稳定的后台 Session 前缀;实际 identifier 会追加任务 ID,因此上传和下载任务不会共享同一个
delegate。后台上传始终使用文件任务,应用重启后会重新绑定系统中同一 taskDescription,下载也会接管已存在任务:
val downloads = DefaultNetworkDownloadRepository(
downloadStore,
engine = UrlSessionDownloadEngine("com.example.network.download"),
)
val uploads = DefaultNetworkUploadRepository(
uploadStore,
engine = UrlSessionUploadEngine("com.example.network.upload"),
)
宿主收到系统回调后先保存 completion handler,再用相同前缀重建 Repository/Engine,并执行 recover(true):
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
IosBackgroundTransferEvents.shared.registerCompletionHandler(
identifier: identifier,
completionHandler: completionHandler
)
TransferRuntime.shared.recoverPersistedTasks()
}
iOS 后台重定向由系统接管,因此后台任务拒绝 Authorization、Cookie 等敏感 Header。需要鉴权时使用短期、
限定对象与域名的签名 HTTPS URL;前台 Engine 仍按白名单检查重定向并在跨 Origin 时剥离凭据。
进度模型¶
data class NetworkTransferProgress(
val bytesTransferred: Long,
val totalBytes: Long?,
val direction: NetworkTransferDirection,
)
当服务端没有 Content-Length 时 totalBytes 为 null,此时 UI 应显示不确定进度,而不是除以零。
fun render(progress: NetworkTransferProgress) {
val ratio = progress.totalBytes
?.takeIf { it > 0L }
?.let { progress.bytesTransferred.toDouble() / it }
if (ratio == null) showIndeterminate() else showPercent(ratio)
}
取消与重试¶
- 取消收集/协程会取消平台任务。
- 进度回调异常不会中断传输。
- 普通
NetworkCall<ByteArray>上传重试必须提供幂等键;network-transfer使用稳定Upload-Id与服务端确认偏移。 NetworkCall<ByteArray>重试会重新开始;network-transfer会从安全的.part文件断点续传。
Base URL 与 CDN¶
服务端返回签名 CDN 地址时使用 @Url:
动态 URL 只接受 HTTPS 且禁止用户信息和 fragment。跨域地址还必须加入 NetworkOriginPolicy.allowedHosts;
Authorization、Cookie 等敏感 Header 不会被转发到其他 Origin。对于固定上传/下载域名,更推荐命名 Client:
这样 TLS、超时、Header 与限流策略可以和主 API 分开配置。
HarmonyOS¶
HAR Bridge 提供 uploadFile、downloadFile、cancelTransfer 和原有 httpRequest:
{
"transferId": "video-42",
"url": "https://uploads.example.com/v1/files",
"sourcePath": "/data/storage/el2/base/files/video.mp4",
"totalBytes": 73400320,
"offset": 0,
"chunkSizeBytes": 4194304,
"contentType": "video/mp4"
}
上传通过 RCP UploadFromStream 分片读取,不把完整文件放入内存;回调连续返回 progress,只有服务端确认后才
推进 committedBytes。下载写入应用私有目录中的分段临时文件,使用 Range/If-Range 恢复,校验成功后再发布目标文件,不向业务暴露半文件。
文件路径仅允许 filesDir 或 cacheDir,地址仍必须是公网 HTTPS。页面销毁时调用 cancelTransfer 或
cancelAll;真正离开应用进程继续执行的 Harmony 任务应由宿主把同一协议接到系统 request.agent。