download web pages without size validation

This commit is contained in:
AmirHossein Abdolmotallebi 2024-07-15 18:00:53 +03:30
parent 9c459213ba
commit 30e86011e9
2 changed files with 56 additions and 18 deletions

View File

@ -3,14 +3,13 @@ package ir.amirab.downloader.downloaditem
import ir.amirab.downloader.DownloadManager import ir.amirab.downloader.DownloadManager
import ir.amirab.downloader.connection.DownloaderClient import ir.amirab.downloader.connection.DownloaderClient
import ir.amirab.downloader.connection.response.expectSuccess import ir.amirab.downloader.connection.response.expectSuccess
import ir.amirab.downloader.connection.response.isWebPage
import ir.amirab.downloader.destination.SimpleDownloadDestination import ir.amirab.downloader.destination.SimpleDownloadDestination
import ir.amirab.downloader.downloaditem.DownloadItem.Companion.LENGTH_UNKNOWN
import ir.amirab.downloader.exception.DownloadValidationException import ir.amirab.downloader.exception.DownloadValidationException
import ir.amirab.downloader.exception.FileChangedException import ir.amirab.downloader.exception.FileChangedException
import ir.amirab.downloader.exception.TooManyErrorException import ir.amirab.downloader.exception.TooManyErrorException
import ir.amirab.downloader.part.Part import ir.amirab.downloader.part.*
import ir.amirab.downloader.part.PartDownloadStatus
import ir.amirab.downloader.part.PartDownloader
import ir.amirab.downloader.part.awaitIdle
import ir.amirab.downloader.utils.ExceptionUtils import ir.amirab.downloader.utils.ExceptionUtils
import ir.amirab.downloader.utils.printStackIfNOtUsual import ir.amirab.downloader.utils.printStackIfNOtUsual
import ir.amirab.downloader.utils.splitToRange import ir.amirab.downloader.utils.splitToRange
@ -81,6 +80,12 @@ class DownloadJob(
}.orEmpty()) }.orEmpty())
} }
// if strict mode is false part downloader going to download data without any validation of content length
// this is only acceptable when resume is not supported and multiple get requests results multiple result
@Volatile
private var strictDownload = true
private val _status = MutableStateFlow<DownloadJobStatus>(DownloadJobStatus.IDLE) private val _status = MutableStateFlow<DownloadJobStatus>(DownloadJobStatus.IDLE)
val status = _status.asStateFlow() val status = _status.asStateFlow()
fun expectValid(size: Long, parts: List<LongRange>) { fun expectValid(size: Long, parts: List<LongRange>) {
@ -103,6 +108,7 @@ class DownloadJob(
downloadItem.status = DownloadStatus.Added downloadItem.status = DownloadStatus.Added
downloadItem.startTime = null downloadItem.startTime = null
downloadItem.completeTime = null downloadItem.completeTime = null
strictDownload = true
saveState() saveState()
downloadManager.onDownloadItemChange(downloadItem) downloadManager.onDownloadItemChange(downloadItem)
} }
@ -162,6 +168,8 @@ class DownloadJob(
) { ) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
destination.outputSize = downloadItem.contentLength destination.outputSize = downloadItem.contentLength
.takeIf { strictDownload }
?: LENGTH_UNKNOWN
if (!destination.isDownloadedPartsIsValid()) { if (!destination.isDownloadedPartsIsValid()) {
//file deleted or something! //file deleted or something!
parts.forEach { it.resetCurrent() } parts.forEach { it.resetCurrent() }
@ -253,13 +261,21 @@ class DownloadJob(
listOf(Part(0, null, 0)) listOf(Part(0, null, 0))
) )
} else { } else {
setParts(splitToRange( if (supportsConcurrent){
minPartSize = downloadManager.settings.minPartSize, //split parts
maxPartCount = getRequestedPartitionCount().toLong(), setParts(splitToRange(
size = downloadItem.contentLength, minPartSize = downloadManager.settings.minPartSize,
).map { maxPartCount = getRequestedPartitionCount().toLong(),
Part(it.first, it.last) size = downloadItem.contentLength,
}) ).map {
Part(it.first, it.last)
})
}else{
setParts(
listOf(Part(0, (downloadItem.contentLength-1).takeIf { it>=0 }, 0))
)
}
} }
// thisLogger().info("dl_$id parts created $parts") // thisLogger().info("dl_$id parts created $parts")
@ -492,6 +508,7 @@ class DownloadJob(
downloadManager.throttler, downloadManager.throttler,
jobThrottler, jobThrottler,
), ),
strictMode = strictDownload,
partSplitLock = partSplitLock partSplitLock = partSplitLock
).also { partDownloader: PartDownloader -> ).also { partDownloader: PartDownloader ->
partDownloader.onTooManyErrors = { partDownloader.onTooManyErrors = {
@ -526,7 +543,12 @@ class DownloadJob(
//new download //new download
downloadItem.contentLength = totalLength ?: -1 downloadItem.contentLength = totalLength ?: -1
downloadItem.serverETag=newServerETag downloadItem.serverETag=newServerETag
// don't strict if it's a webpage let it download and not a link with resume support
if (response.isWebPage() && !response.resumeSupport){
strictDownload = false
}
} else { } else {
// at the beginning of download
if (totalLength != downloadItem.contentLength) { if (totalLength != downloadItem.contentLength) {
throw FileChangedException.LengthChangedException(downloadItem.contentLength, totalLength ?: -1) throw FileChangedException.LengthChangedException(downloadItem.contentLength, totalLength ?: -1)
} }

View File

@ -34,12 +34,26 @@ import kotlin.math.min
val PART_MAX_TRIES = 10 val PART_MAX_TRIES = 10
const val RetryDelay = 1_000L const val RetryDelay = 1_000L
/**
* @param strictMode
* `false` - this is not the purpose of its app, so we don't strict here
*
* download part without checking for length validation
* this is where we want to only copy data arrived from server
* for example, a web page link that maybe get us different response length
* we only need to download it no matter what is inside
*
* `true` - main purpose of this class
*
* validate download size before trying to write to the filesystem
*/
class PartDownloader( class PartDownloader(
val credentials: IDownloadCredentials, val credentials: IDownloadCredentials,
val getDestWriter: () -> DestWriter, val getDestWriter: () -> DestWriter,
val part: Part, val part: Part,
val client: DownloaderClient, val client: DownloaderClient,
val speedLimiters: List<Throttler>, val speedLimiters: List<Throttler>,
val strictMode:Boolean,
private val partSplitLock: Any, private val partSplitLock: Any,
) { ) {
class ShouldNotHappened(msg: String?) : RuntimeException(msg) class ShouldNotHappened(msg: String?) : RuntimeException(msg)
@ -238,13 +252,15 @@ class PartDownloader(
} }
} }
if (contentLength != partCopy.remainingLength) { if (contentLength != partCopy.remainingLength) {
conn.closeable.close() if (strictMode){
throw ServerPartIsNotTheSameAsWeExpectException( conn.closeable.close()
start = partCopy.current, throw ServerPartIsNotTheSameAsWeExpectException(
end = partCopy.to, start = partCopy.current,
expectedLength = partCopy.remainingLength, end = partCopy.to,
actualLength = contentLength expectedLength = partCopy.remainingLength,
) actualLength = contentLength
)
}
} }
thread = thread { thread = thread {
if (stop || Thread.currentThread().isInterrupted) { if (stop || Thread.currentThread().isInterrupted) {