Use actual file size from server it size does not match metadata

This commit is contained in:
Nicholas Wallace
2026-09-02 13:30:04 -07:00
parent 4b8abe7deb
commit 6560038495
3 changed files with 41 additions and 24 deletions
@@ -4,13 +4,13 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.StatFs import android.os.StatFs
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.fullName
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner import com.audiobookshelf.app.device.FolderScanner
import com.audiobookshelf.app.models.DownloadItem import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.models.DownloadItemPart import com.audiobookshelf.app.models.DownloadItemPart
import com.audiobookshelf.app.plugins.AbsLogger import com.audiobookshelf.app.plugins.AbsLogger
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.anggrayudi.storage.file.fullName
import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
@@ -59,6 +59,7 @@ class DownloadItemManager(
} }
interface InternalProgressCallback { interface InternalProgressCallback {
fun onSizeResolved(totalBytes: Long)
fun onProgress(totalBytesWritten: Long, progress: Long) fun onProgress(totalBytesWritten: Long, progress: Long)
fun onComplete(failed: Boolean) fun onComplete(failed: Boolean)
fun onAuthError() fun onAuthError()
@@ -263,6 +264,21 @@ class DownloadItemManager(
stagingFile, stagingFile,
part.fileSize, part.fileSize,
object : InternalProgressCallback { object : InternalProgressCallback {
override fun onSizeResolved(totalBytes: Long) {
synchronized(this@DownloadItemManager) {
if (part !in currentDownloadItemParts || totalBytes < 0L) return
if (part.fileSize == totalBytes) return
AbsLogger.info(
tag,
"Using server size $totalBytes instead of metadata size ${part.fileSize} for ${part.filename}"
)
part.fileSize = totalBytes
part.lastUpdateTime = System.currentTimeMillis()
persist(item, force = true)
clientEventEmitter.onDownloadItemPartUpdate(part)
}
}
override fun onProgress(totalBytesWritten: Long, progress: Long) { override fun onProgress(totalBytesWritten: Long, progress: Long) {
synchronized(this@DownloadItemManager) { synchronized(this@DownloadItemManager) {
if (part !in currentDownloadItemParts) return if (part !in currentDownloadItemParts) return
@@ -395,7 +411,10 @@ class DownloadItemManager(
if (newAccessToken.isNullOrEmpty()) { if (newAccessToken.isNullOrEmpty()) {
failParkedAuthParts(serverConnectionConfigId) failParkedAuthParts(serverConnectionConfigId)
} else { } else {
AbsLogger.info(tag, "Token refresh succeeded; resuming downloads for $serverConnectionConfigId") AbsLogger.info(
tag,
"Token refresh succeeded; resuming downloads for $serverConnectionConfigId"
)
checkUpdateDownloadQueue() checkUpdateDownloadQueue()
} }
} }
@@ -64,20 +64,6 @@ class InternalDownloadManager(
AbsLogger.info( AbsLogger.info(
tag, tag,
"Starting ${if (existingBytes > 0L) "resumed" else "new"} download for ${destinationFile.name} at byte $existingBytes") "Starting ${if (existingBytes > 0L) "resumed" else "new"} download for ${destinationFile.name} at byte $existingBytes")
if (expectedSize > 0L && existingBytes == expectedSize) {
progressCallback.onProgress(existingBytes, 100L)
AbsLogger.info(tag, "Download completed for ${destinationFile.name} ($existingBytes bytes)")
progressCallback.onComplete(false)
return
}
if (expectedSize > 0L && existingBytes > expectedSize) {
if (!destinationFile.delete()) {
AbsLogger.error(tag, "Could not delete oversized staging file ${destinationFile.name}")
progressCallback.onComplete(true)
return
}
existingBytes = 0L
}
val request = val request =
Request.Builder() Request.Builder()
.url(url) .url(url)
@@ -107,7 +93,8 @@ class InternalDownloadManager(
response.header("Content-Range") response.header("Content-Range")
?.removePrefix("bytes */") ?.removePrefix("bytes */")
?.toLongOrNull() ?.toLongOrNull()
if (serverSize != null && serverSize > 0L && existingBytes == serverSize) { if (serverSize != null) progressCallback.onSizeResolved(serverSize)
if (serverSize != null && existingBytes == serverSize) {
progressCallback.onProgress(existingBytes, 100L) progressCallback.onProgress(existingBytes, 100L)
AbsLogger.info(tag, "Download completed for ${destinationFile.name} ($existingBytes bytes)") AbsLogger.info(tag, "Download completed for ${destinationFile.name} ($existingBytes bytes)")
progressCallback.onComplete(false) progressCallback.onComplete(false)
@@ -140,9 +127,12 @@ class InternalDownloadManager(
val startingBytes = if (append) existingBytes else 0L val startingBytes = if (append) existingBytes else 0L
val responseLength = response.body!!.contentLength() val responseLength = response.body!!.contentLength()
val serverSize =
if (append) contentRangeTotal(response)
else responseLength.takeIf { it >= 0L }
if (serverSize != null) progressCallback.onSizeResolved(serverSize)
val totalLength = val totalLength =
if (expectedSize > 0L) expectedSize serverSize ?: if (expectedSize > 0L) expectedSize else 0L
else if (responseLength >= 0L) startingBytes + responseLength else 0L
FileOutputStream(destinationFile, append).use { output -> FileOutputStream(destinationFile, append).use { output ->
response.body!!.byteStream().use { input -> response.body!!.byteStream().use { input ->
@@ -162,16 +152,17 @@ class InternalDownloadManager(
} }
} }
if (expectedSize > 0L && destinationFile.length() != expectedSize) { val downloadedSize = destinationFile.length()
if (serverSize != null && downloadedSize != serverSize) {
AbsLogger.error( AbsLogger.error(
tag, tag,
"Downloaded size for ${destinationFile.name} was ${destinationFile.length()}, expected $expectedSize" "Downloaded size for ${destinationFile.name} was $downloadedSize, expected server size $serverSize"
) )
progressCallback.onComplete(true) progressCallback.onComplete(true)
} else { } else {
AbsLogger.info( AbsLogger.info(
tag, tag,
"Download completed for ${destinationFile.name} (${destinationFile.length()} bytes)" "Download completed for ${destinationFile.name} ($downloadedSize bytes)"
) )
progressCallback.onComplete(false) progressCallback.onComplete(false)
} }
@@ -192,9 +183,16 @@ class InternalDownloadManager(
match.groupValues[2].toLongOrNull()?.let { it >= offset } == true match.groupValues[2].toLongOrNull()?.let { it >= offset } == true
} }
private fun contentRangeTotal(response: Response): Long? =
CONTENT_RANGE.matchEntire(response.header("Content-Range") ?: "")
?.groupValues
?.get(3)
?.takeUnless { it == "*" }
?.toLongOrNull()
private companion object { private companion object {
const val CHUNK_SIZE = 512 * 1024 // 512 KB const val CHUNK_SIZE = 512 * 1024 // 512 KB
val CONTENT_RANGE = Regex("bytes (\\d+)-(\\d+)/(?:\\d+|\\*)") val CONTENT_RANGE = Regex("bytes (\\d+)-(\\d+)/(\\d+|\\*)")
val client = val client =
OkHttpClient.Builder() OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
@@ -14,7 +14,7 @@ data class DownloadItemPart(
val id: String, val id: String,
val downloadItemId: String, val downloadItemId: String,
val filename: String, val filename: String,
val fileSize: Long, var fileSize: Long,
@JsonIgnore val destinationPath: String, @JsonIgnore val destinationPath: String,
val finalDestinationPath:String, val finalDestinationPath:String,
val serverPath: String, val serverPath: String,