Skip to content

Commit 9f14142

Browse files
committed
fix: Far more reliable downloads
fix: Account for program stopping mid download by writing to partial file, then renaming it fix: Set a default maximum of 4 concurrent download tasks fix: Don't check last changed headers for updates by default, reduces number of http calls when caching greatly chore: better rclone command not found error message
1 parent 58886bc commit 9f14142

File tree

4 files changed

+47
-24
lines changed

4 files changed

+47
-24
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
kotlin.code.style=official
22
group=com.mineinabyss
3-
version=3.2.0-alpha.4
3+
version=3.2.0-alpha.5
44
idofrontVersion=0.25.6

keepup-api/src/main/kotlin/com/mineinabyss/keepup/api/KeepupDownloader.kt

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import com.mineinabyss.keepup.downloads.parsing.DownloadSource
77
import com.mineinabyss.keepup.similarfiles.SimilarFileChecker
88
import com.mineinabyss.keepup.type_checker.FileTypeChecker.SYSTEM_SUPPORTS_FILE
99
import io.ktor.client.*
10-
import kotlinx.coroutines.CoroutineScope
11-
import kotlinx.coroutines.Dispatchers
12-
import kotlinx.coroutines.ExperimentalCoroutinesApi
10+
import kotlinx.coroutines.*
1311
import kotlinx.coroutines.channels.ReceiveChannel
1412
import kotlinx.coroutines.channels.produce
15-
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.sync.Semaphore
14+
import kotlinx.coroutines.sync.withPermit
1615
import java.nio.file.Path
1716
import kotlin.io.path.absolute
1817
import kotlin.io.path.createDirectories
@@ -22,13 +21,17 @@ class KeepupDownloader(
2221
val http: HttpClient,
2322
val config: KeepupDownloaderConfig,
2423
val githubConfig: GithubConfig,
24+
val downloadDispatcher: CoroutineDispatcher = Dispatchers.IO,
25+
val maxConcurrentDownloads: Int = 4,
2526
) {
27+
private val concurrentDownloads = Semaphore(maxConcurrentDownloads)
28+
2629
@OptIn(ExperimentalCoroutinesApi::class)
2730
fun download(
2831
vararg sources: DownloadSource,
2932
dest: Path,
3033
scope: CoroutineScope,
31-
): ReceiveChannel<DownloadResult> = scope.produce(Dispatchers.IO) {
34+
): ReceiveChannel<DownloadResult> = scope.produce {
3235
SYSTEM_SUPPORTS_FILE // check if system supports file command
3336
val similarFileChecker = if (config.ignoreSimilar) SimilarFileChecker(dest) else null
3437
val downloader = DownloadParser(
@@ -37,16 +40,16 @@ class KeepupDownloader(
3740
githubConfig = githubConfig,
3841
similarFileChecker = similarFileChecker,
3942
)
40-
4143
sources.map { source ->
42-
val downloadPathForKey = (config.downloadCache / source.keyInConfig).absolute()
43-
downloadPathForKey.createDirectories()
44-
launch {
45-
downloader
46-
.download(source, downloadPathForKey)
47-
.forEach { channel.send(it) }
44+
launch(downloadDispatcher) {
45+
concurrentDownloads.withPermit {
46+
val downloadPathForKey = (config.downloadCache / source.keyInConfig).absolute()
47+
downloadPathForKey.createDirectories()
48+
downloader.download(source, downloadPathForKey)
49+
.forEach { channel.send(it) }
50+
}
4851
}
49-
}
52+
}.joinAll()
5053
}
5154
}
5255

keepup-api/src/main/kotlin/com/mineinabyss/keepup/downloads/http/HttpDownloader.kt

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,43 @@ class HttpDownloader(
1919
val source: DownloadSource,
2020
val targetDir: Path,
2121
val fileName: String = source.query.substringAfterLast("/"),
22+
val overrideWhenHeadersChange: Boolean = false,
2223
) : Downloader {
23-
override suspend fun download(): List<DownloadResult> {
24-
val cacheFile = targetDir.resolve("$fileName.cache")
25-
val targetFile = targetDir.resolve(fileName)
24+
suspend fun getCacheString(query: String): String {
2625
val headers = client.head(source.query)
2726
val length = headers.contentLength()
2827
val lastModified = headers.lastModified()
28+
return "Last-Modified: $lastModified, Content-Length: $length"
29+
}
2930

30-
val cache = "Last-Modified: $lastModified, Content-Length: $length"
31-
if (targetFile.exists() && cacheFile.exists() && cacheFile.readText() == cache)
31+
override suspend fun download(): List<DownloadResult> {
32+
val cacheFile = targetDir.resolve("$fileName.cache")
33+
val targetFile = targetDir.resolve(fileName)
34+
val partial = targetDir.resolve("$fileName.partial")
35+
val cacheString = if (overrideWhenHeadersChange) getCacheString(source.query) else null
36+
37+
// Check if target already exists and skip if it does, check last modified headers if overrideWhenHeadersChange is true
38+
if (targetFile.exists() && (cacheString == null || (cacheFile.exists() && cacheFile.readText() == cacheString)))
3239
return listOf(DownloadResult.SkippedBecauseCached(targetFile, source.keyInConfig))
3340

34-
client.get(source.query) {
41+
// Write to partial file, then move it to target once download is complete
42+
partial.deleteIfExists()
43+
client.prepareGet {
44+
url(source.query)
3545
timeout {
3646
requestTimeoutMillis = 30.seconds.inWholeMilliseconds
3747
}
48+
}.execute {
49+
it.bodyAsChannel()
50+
.copyAndClose(partial.toFile().writeChannel())
3851
}
39-
.bodyAsChannel()
40-
.copyAndClose(targetFile.toFile().writeChannel())
52+
53+
targetFile.deleteIfExists()
54+
partial.moveTo(targetFile)
4155

4256
// Only mark as cached after download is complete
4357
cacheFile.deleteIfExists()
44-
cacheFile.createFile().writeText(cache)
58+
cacheFile.createFile().writeText(cacheString ?: getCacheString(source.query))
4559

4660
return listOf(DownloadResult.Downloaded(targetFile, source.keyInConfig))
4761
}

keepup-api/src/main/kotlin/com/mineinabyss/keepup/downloads/rclone/RcloneDownloader.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.mineinabyss.keepup.downloads.rclone
22

3+
import com.lordcodes.turtle.ShellCommandNotFoundException
34
import com.mineinabyss.keepup.downloads.DownloadResult
45
import com.mineinabyss.keepup.downloads.Downloader
56
import com.mineinabyss.keepup.downloads.parsing.DownloadSource
@@ -11,7 +12,12 @@ class RcloneDownloader(
1112
val targetDir: Path,
1213
) : Downloader {
1314
override suspend fun download(): List<DownloadResult> {
14-
val downloadPath = Rclone.sync(source.query, targetDir)
15+
val downloadPath = runCatching { Rclone.sync(source.query, targetDir) }
16+
.onFailure {
17+
if (it is ShellCommandNotFoundException)
18+
return listOf(DownloadResult.Failure("rclone command not found", source.keyInConfig))
19+
}
20+
.getOrThrow()
1521
return listOf(DownloadResult.Downloaded(downloadPath, source.keyInConfig, overrideInfoMsg = MSG.rclone))
1622
}
1723
}

0 commit comments

Comments
 (0)