如何使用 Ktor 和 Kotlin 下载带有进度指示器的大文件?

Posted

技术标签:

【中文标题】如何使用 Ktor 和 Kotlin 下载带有进度指示器的大文件?【英文标题】:How can I download a large file with Ktor and Kotlin with a progress indicator? 【发布时间】:2021-03-12 21:59:11 【问题描述】:

我花了太多时间试图解决这个问题。因此,我在下面发布的代码在下载文件方面确实有效,但问题是,流程具有非常意外的行为。 response.content.readAvailable() 方法调用似乎会阻塞,直到它完全完成下载整个文件,此时会发生发射进度,所以你最终要等待很长时间才能下载文件,然后在一瞬间你得到所有进度更新。所以我想知道是否有办法做到这一点,我一次读取一定数量的字节,然后发出一个进度,然后重复直到文件完成下载?或者可能是一种挂钩 readAvailable() 方法并以这种方式更新进度的方法?对此的任何帮助将不胜感激。

这是我找到并修改的代码,但仍然无法正常工作:

suspend fun HttpClient.downloadFile(
    output: File,
    downloadUrl: String,
    md5Hash: String,
) = flow 
    try 
        val response = get<HttpResponse>  url(downloadUrl) 
        val data = ByteArray(response.contentLength()?.toInt() ?: 0)
        val contentLn = response.contentLength()?.toInt() ?: 0
        var offset = 0
        var bytesRemaining = contentLn
        do 
            val chunkSize = min(maxChunkSize, bytesRemaining)
            logger?.d  "Read Available:" 
            val result = response.content.readAvailable(data, offset, length = chunkSize)
            val progress = ((offset / contentLn.toDouble()) * 100).toInt()
            emit(DownloadResult.Progress(progress))
            logger?.d  "logged progress: $progress" 
            // delay(6000L) this was to test my assumption that the readAvalible was blocking. 
            offset += chunkSize
            bytesRemaining -= chunkSize
         while (result != -1)

        if (response.status.isSuccess()) 
            if (data.md5().hex == md5Hash) 
                output.write(data)
                emit(DownloadResult.Success)
             else 
                emit(DownloadResult.ErrorCorruptFile)
            
         else 
            emit(DownloadResult.ErrorBadResponseCode(response.status.value))
        
     catch (e: TimeoutCancellationException) 
        emit(DownloadResult.ErrorRequestTimeout("Connection timed out", e))
    

【问题讨论】:

【参考方案1】:

最后经过一段愚蠢的时间后,我解决了这个问题。您需要使用的是this。这使您可以在下载时访问字节通道。

一个非常粗略的实现(我还没有完成)是这样的:

    get<HttpStatement>(url = downloadUrl).execute 
        var offset = 0
        val byteBufferSize = 1024 * 100
        val channel = it.receive<ByteReadChannel>()
        val contentLen = it.contentLength()?.toInt() ?: 0
        val data = ByteArray(contentLen)
        do 
            val currentRead = channel.readAvailable(data, offset, byteBufferSize)
            val progress = if(contentLen == 0) 0 else ( offset / contentLen.toDouble() ) * 100
            logger?.d  "progress: $progress" 
            offset += currentRead
         while (currentRead >= 0)

    

此解决方案不能做两件事。 1.) 我在 HttpClient 的上下文中,所以这就是我访问 get() 的方式。 2.) 我正在创建一个大小为1024 * 100 的字节缓冲区,以便不让readAvailable 方法阻塞太久,尽管这可能不是必需的......关于它的一个好处是它决定了如何您会经常发布进度更新。

【讨论】:

以上是关于如何使用 Ktor 和 Kotlin 下载带有进度指示器的大文件?的主要内容,如果未能解决你的问题,请参考以下文章

在带有 Postgresql 数据库的 docker 容器中运行简单的 Kotlin Ktor 应用程序

Kotlin ktor 暴露 DSL 插入参考

在 Ktor 中使用 kotlin Reified 进行通用 api 调用

Kotlin Ktor 客户端 mltiplatform gradle 配置

如何在 Ktor (Kotlin) 中管道的各个部分之间传递数据

如何使用 ktor kotlin 通过 POST 发送 JSON 字符串?