Initial internal download continue incomplete download

This commit is contained in:
Nicholas Wallace
2025-04-07 19:04:29 -07:00
parent 7e2ac27eba
commit e11b8cb66c
2 changed files with 85 additions and 42 deletions
@@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import java.io.File
import java.io.FileOutputStream
import java.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -113,7 +112,6 @@ class DownloadItemManager(
val file = File(downloadItemPart.finalDestinationPath)
file.parentFile?.mkdirs()
val fileOutputStream = FileOutputStream(downloadItemPart.finalDestinationPath)
val internalProgressCallback =
object : InternalProgressCallback {
override fun onProgress(totalBytesWritten: Long, progress: Long) {
@@ -131,7 +129,11 @@ class DownloadItemManager(
tag,
"Start internal download to destination path ${downloadItemPart.finalDestinationPath} from ${downloadItemPart.serverUrl}"
)
InternalDownloadManager(fileOutputStream, internalProgressCallback)
InternalDownloadManager(
mainActivity,
downloadItemPart.finalDestinationUri,
internalProgressCallback
)
.download(downloadItemPart.serverUrl)
downloadItemPart.downloadId = 1
currentDownloadItemParts.add(downloadItemPart)
@@ -1,5 +1,7 @@
package com.audiobookshelf.app.managers
import android.content.Context
import android.net.Uri
import android.util.Log
import java.io.*
import java.util.concurrent.TimeUnit
@@ -12,14 +14,14 @@ import okhttp3.*
* @property progressCallback The callback to report download progress.
*/
class InternalDownloadManager(
private val outputStream: FileOutputStream,
private val context: Context,
private val fileUri: Uri,
private val progressCallback: DownloadItemManager.InternalProgressCallback
) : AutoCloseable {
private val tag = "InternalDownloadManager"
private val client: OkHttpClient =
OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).build()
private val writer = BinaryFileWriter(outputStream, progressCallback)
/**
* Downloads a file from the given URL.
@@ -29,69 +31,108 @@ class InternalDownloadManager(
*/
@Throws(IOException::class)
fun download(url: String) {
val request: Request = Request.Builder().url(url).build()
client.newCall(request)
.enqueue(
object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "Download URL $url FAILED", e)
progressCallback.onComplete(true)
}
try {
val existingSize = getInternalFileSize(fileUri)
Log.d(tag, "Existing file size: $existingSize bytes")
override fun onResponse(call: Call, response: Response) {
response.body?.let { responseBody ->
val length: Long = response.header("Content-Length")?.toLongOrNull() ?: 0L
writer.write(responseBody.byteStream(), length)
val request =
Request.Builder()
.url(url)
.apply {
if (existingSize > 0) {
addHeader("Range", "bytes=$existingSize-") // Resume download
}
?: run {
Log.e(tag, "Response doesn't contain a file")
progressCallback.onComplete(true)
}
}
}
)
.build()
client.newCall(request)
.enqueue(
object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "Download failed: $url", e)
progressCallback.onComplete(true)
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
Log.e(tag, "Failed to download: ${response.code}")
progressCallback.onComplete(true)
return
}
response.body?.let { responseBody ->
val totalLength =
(response.header("Content-Length")?.toLongOrNull()
?: 0L) + existingSize
val outputStream = getOutputStream(fileUri, existingSize)
outputStream?.let {
BinaryFileWriter(it, progressCallback, existingSize).use { writer ->
writer.write(responseBody.byteStream(), totalLength)
}
}
?: run {
Log.e(tag, "Failed to open output stream")
progressCallback.onComplete(true)
}
}
?: run {
Log.e(tag, "Response doesn't contain a file")
progressCallback.onComplete(true)
}
}
}
)
} catch (e: Exception) {
Log.e(tag, "Download error", e)
progressCallback.onComplete(true)
}
}
/**
* Closes the download manager and releases resources.
*
* @throws Exception If an error occurs during closing.
*/
@Throws(Exception::class)
override fun close() {
writer.close()
private fun getInternalFileSize(uri: Uri): Long {
val file = File(uri.path!!)
return if (file.exists()) file.length() else 0L
}
private fun getOutputStream(uri: Uri, existingSize: Long): OutputStream? {
val file = File(uri.path!!)
return if (file.exists()) {
FileOutputStream(file, true).apply {
channel.position(existingSize) // Move to the end of the file to append
}
} else {
FileOutputStream(file) // Create a new file if it doesn't exist
}
}
@Throws(Exception::class) override fun close() {}
}
/**
* Writes binary data to an output stream.
*
* @property outputStream The output stream to write the data to.
* @property progressCallback The callback to report write progress.
*/
class BinaryFileWriter(
private val outputStream: OutputStream,
private val progressCallback: DownloadItemManager.InternalProgressCallback
private val progressCallback: DownloadItemManager.InternalProgressCallback,
private val existingSize: Long
) : AutoCloseable {
/**
* Writes data from the input stream to the output stream.
*
* @param inputStream The input stream to read the data from.
* @param length The total length of the data to be written.
* @param totalLength The total length of the data to be written.
* @return The total number of bytes written.
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class)
fun write(inputStream: InputStream, length: Long): Long {
fun write(inputStream: InputStream, totalLength: Long): Long {
BufferedInputStream(inputStream).use { input ->
val dataBuffer = ByteArray(CHUNK_SIZE)
var totalBytes: Long = 0
var totalBytes = existingSize
var readBytes: Int
while (input.read(dataBuffer).also { readBytes = it } != -1) {
totalBytes += readBytes
outputStream.write(dataBuffer, 0, readBytes)
progressCallback.onProgress(totalBytes, (totalBytes * 100L) / length)
progressCallback.onProgress(totalBytes, (totalBytes * 100L) / totalLength)
}
progressCallback.onComplete(false)
return totalBytes