mirror of
https://github.com/f-droid/fdroidclient.git
synced 2025-12-23 23:27:44 -05:00
[download] verify already downloaded files
This commit is contained in:
@@ -25,9 +25,12 @@ import io.ktor.client.plugins.ResponseException
|
||||
import io.ktor.http.HttpStatusCode.Companion.NotFound
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mu.KotlinLogging
|
||||
import org.fdroid.fdroid.toHex
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
/**
|
||||
* Download files over HTTP, with support for proxies, `.onion` addresses, HTTP Basic Auth, etc.
|
||||
@@ -62,10 +65,23 @@ public class HttpDownloaderV2 constructor(
|
||||
var resumable = false
|
||||
val fileLength = outputFile.length()
|
||||
if (fileLength > (request.indexFile.size ?: -1)) {
|
||||
// file was larger than expected, so delete and re-download
|
||||
if (!outputFile.delete()) log.warn { "Warning: outputFile not deleted" }
|
||||
} else if (fileLength == request.indexFile.size && outputFile.isFile) {
|
||||
log.debug { "Already have outputFile, not downloading: ${outputFile.name}" }
|
||||
return // already have it!
|
||||
if (request.indexFile.sha256 == null) {
|
||||
// no way to check file, so we trust that what we have is legit (v1 only)
|
||||
return
|
||||
} else {
|
||||
if (hashFile(outputFile) == request.indexFile.sha256) {
|
||||
// hash matched, so we already have the good file, don't download again
|
||||
return
|
||||
} else {
|
||||
log.warn { "Hash mismatch for ${request.indexFile}" }
|
||||
// delete file and continue
|
||||
if (!outputFile.delete()) log.warn { "Warning: outputFile not deleted" }
|
||||
}
|
||||
}
|
||||
} else if (fileLength > 0) {
|
||||
resumable = true
|
||||
}
|
||||
@@ -90,4 +106,21 @@ public class HttpDownloaderV2 constructor(
|
||||
override fun close() {
|
||||
}
|
||||
|
||||
private fun hashFile(file: File): String {
|
||||
val messageDigest: MessageDigest = try {
|
||||
MessageDigest.getInstance("SHA-256")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
file.inputStream().use { inputStream ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var bytes = inputStream.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
messageDigest.update(buffer, 0, bytes)
|
||||
bytes = inputStream.read(buffer)
|
||||
}
|
||||
}
|
||||
return messageDigest.digest().toHex()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.fdroid.runSuspend
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.BindException
|
||||
import java.net.ServerSocket
|
||||
@@ -33,6 +34,7 @@ import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
@@ -214,6 +216,53 @@ internal class HttpDownloaderTest {
|
||||
assertContentEquals(firstBytes + secondBytes, file.readBytes())
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests re-using an already downloaded file with hash verification.
|
||||
* This can fail if the hashing doesn't take the already downloaded bytes into account.
|
||||
*/
|
||||
@Test
|
||||
fun testCompleteResumeWithHashSuccess() = runSuspend {
|
||||
val sha256 = "efabb260da949061c88173c19f369b4aa0eaa82003c7c2dec08b5dfe75525368"
|
||||
val file = File(folder.newFolder(), sha256).apply { createNewFile() }
|
||||
val bytes = ("These are the first bytes that were already downloaded." +
|
||||
"These are the last bytes that still need to be downloaded.").encodeToByteArray()
|
||||
file.writeBytes(bytes)
|
||||
// specifying the sha256 hash forces its validation
|
||||
val indexFile = getIndexFile("foo/bar", sha256, bytes.size.toLong())
|
||||
val downloadRequest = DownloadRequest(indexFile, mirrors)
|
||||
|
||||
val httpManager = HttpManager(userAgent, null)
|
||||
val httpDownloader = HttpDownloaderV2(httpManager, downloadRequest, file)
|
||||
// this throws if the hash doesn't match while downloading
|
||||
httpDownloader.download()
|
||||
|
||||
assertContentEquals(bytes, file.readBytes())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCompleteResumeWithHashFailure() = runSuspend {
|
||||
val sha256 = "efabb260da949061c88173c19f369b4aa0eaa82003c7c2dec08b5dfe75525368"
|
||||
val file = File(folder.newFolder(), sha256).apply { createNewFile() }
|
||||
val bytes = ("These are the first bytes that were already downloaded." +
|
||||
"These are the last bytes that still need to be downloaded.").encodeToByteArray()
|
||||
file.writeBytes(bytes)
|
||||
// specifying the sha256 hash forces its validation
|
||||
val indexFile = getIndexFile("foo/bar", sha256.replaceFirst('e', 'f'), bytes.size.toLong())
|
||||
val downloadRequest = DownloadRequest(indexFile, mirrors)
|
||||
val mockEngine = MockEngine.config {
|
||||
reuseHandlers = false
|
||||
addHandler {
|
||||
respond("", OK)
|
||||
}
|
||||
}
|
||||
val httpManager = HttpManager(userAgent, null, httpClientEngineFactory = mockEngine)
|
||||
val httpDownloader = HttpDownloaderV2(httpManager, downloadRequest, file)
|
||||
val e = assertFailsWith<IOException> {
|
||||
httpDownloader.download()
|
||||
}
|
||||
assertEquals("Hash not matching", e.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResumeError() = runSuspend {
|
||||
val file = folder.newFile()
|
||||
|
||||
Reference in New Issue
Block a user