Skip to content

Commit

Permalink
Rewrite to use ktor and coroutines, cleanup
Browse files Browse the repository at this point in the history
- Big performance improvements, both when downloading and caching values
- Cache more smartly based on last edited date and content size
- Lots of internal cleanup
- Use shadowJar's minimized dist
- Pretty up that console and add a progress bar!
  • Loading branch information
0ffz committed Feb 12, 2024
1 parent 1777f67 commit 4badb2d
Show file tree
Hide file tree
Showing 25 changed files with 458 additions and 196 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- name: Run gradle build and publish
run: >
gradle distZip
gradle shadowDistZip
-PmineinabyssMavenUsername=${{ secrets.MAVEN_PUBLISH_USERNAME }} -PmineinabyssMavenPassword=${{ secrets.MAVEN_PUBLISH_PASSWORD }}
- name: Get version from gradle
Expand All @@ -40,4 +40,4 @@ jobs:
prerelease: false
automatic_release_tag: v${{ steps.extract_version.outputs.version }}
files: |
./build/distributions/keepup*.zip
./build/distributions/keepup-shadow*.zip
20 changes: 18 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.9.20"
kotlin("plugin.serialization") version "1.9.20"
kotlin("jvm") version "1.9.21"
kotlin("plugin.serialization") version "1.9.21"
id("com.github.johnrengelman.shadow") version "8.1.1"
application
}

Expand All @@ -24,7 +25,12 @@ dependencies {
implementation("com.github.ajalt.clikt:clikt:3.5.0")
implementation("com.lordcodes.turtle:turtle:0.8.0")
implementation("com.jayway.jsonpath:json-path:2.7.0")
implementation("io.ktor:ktor-client-core:2.3.8")
implementation("io.ktor:ktor-client-cio:2.3.8")
implementation("com.github.ajalt.mordant:mordant:2.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC2")
implementation("com.sealwu:kscript-tools:1.0.22")
implementation("io.ktor:ktor-client-cio-jvm:2.3.8")
testImplementation(kotlin("test"))
}

Expand All @@ -35,3 +41,13 @@ tasks.test {
application {
mainClass.set("MainKt")
}

kotlin {
jvmToolchain(17)
}

tasks {
shadowJar {
minimize()
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
kotlin.code.style=official
version=1.2.3
version=2.0.0-beta.1
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
52 changes: 0 additions & 52 deletions src/main/kotlin/GithubDownload.kt

This file was deleted.

97 changes: 79 additions & 18 deletions src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,39 @@ import com.github.ajalt.clikt.parameters.types.choice
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.inputStream
import com.github.ajalt.clikt.parameters.types.path
import com.github.ajalt.mordant.animation.progressAnimation
import com.github.ajalt.mordant.rendering.TextColors.brightGreen
import com.github.ajalt.mordant.rendering.TextColors.yellow
import com.github.ajalt.mordant.terminal.Terminal
import com.jayway.jsonpath.JsonPath
import config.GithubConfig
import downloading.DownloadParser
import downloading.DownloadResult
import downloading.Source
import helpers.*
import kotlin.io.path.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.io.path.absolute
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes

val keepup by lazy { Keepup() }
val t by lazy { Terminal() }

class Keepup : CliktCommand() {
init {
context {
autoEnvvarPrefix = "KEEPUP"
}
}

// === Arguments ===
val input by argument(help = "Path to the file").inputStream()

Expand All @@ -43,6 +64,9 @@ class Keepup : CliktCommand() {
val ignoreSimilar by option(help = "Don't create symlinks for files with matching characters before the first number")
.flag(default = true)

val failAllDownloads by option(help = "Don't actually download anything, useful for testing")
.flag(default = false)

val overrideGithubRelease by option(help = "Force downloading the latest version of files from GitHub")
.enum<GithubReleaseOverride>()
.default(GithubReleaseOverride.NONE)
Expand All @@ -53,38 +77,75 @@ class Keepup : CliktCommand() {

val githubAuthToken: String? by option(help = "Used to access private repos or get a higher rate limit")

val githubConfig by lazy {
GithubConfig(
githubAuthToken = githubAuthToken,
overrideGithubRelease = overrideGithubRelease,
cacheExpirationTime = cacheExpirationTime,
)
}

val progress = t.progressAnimation {
text("Keepup!")
percentage()
progressBar()
completed()
timeRemaining()
}
override fun run() {
if (overrideGithubRelease != GithubReleaseOverride.NONE)
echo("Overriding GitHub release versions to $overrideGithubRelease")
t.println("${yellow("[!]")} Overriding GitHub release versions to $overrideGithubRelease")

val jsonInput = if (fileType == "hocon") {
println("Converting HOCON to JSON")
t.println("Converting HOCON to JSON")
renderHocon(input)
} else input

println("Parsing input")
t.println("Parsing input")
val parsed = JsonPath.parse(jsonInput)
val items = parsed.read<Map<String, Any?>>(jsonPath)
val strings = getLeafStrings(items)

println("Clearing symlinks")
t.println("Clearing symlinks")
clearSymlinks(dest)

println("Creating new symlinks")
strings.forEach { (key, source) ->
val isolatedPath = (downloadPath / key).absolute()
isolatedPath.createDirectories()
val files = dest.listDirectoryEntries().filter { it.isRegularFile() }
download(source, isolatedPath).forEach download@{ item ->
if (ignoreSimilar && files.any { similar(item.name, it.name) }) {
println("Skipping ${item.name} because it is similar to an existing file")
return@download
progress.updateTotal(strings.size.toLong())
progress.start()

runBlocking(Dispatchers.IO) {
val channel = Channel<DownloadResult>()
launch {
HttpClient(CIO).use { client ->
val similarFileChecker = if (ignoreSimilar) SimilarFileChecker(dest) else null
val downloader = DownloadParser(
client,
githubConfig,
similarFileChecker
)

strings.map { (key, downloadQuery) ->
val downloadPathForKey = (downloadPath / key).absolute()
downloadPathForKey.createDirectories()
launch {
downloader.download(Source(key, downloadQuery), downloadPathForKey)
.forEach { channel.send(it) }
progress.advance(1)
}
}.joinAll()
}
linkToDest(dest, isolatedPath, item)
channel.close()
}
for (result in channel) {
if (result is DownloadResult.HasFiles) {
linkToDest(dest, result)
}
result.printToConsole()
}

progress.clear()
progress.stop()
t.println(brightGreen("Keepup done!"))
}
println("Keepup done!")
}
}

fun main(args: Array<String>) = Keepup().main(args)
fun main(args: Array<String>) = keepup.main(args)
35 changes: 35 additions & 0 deletions src/main/kotlin/SimilarFileChecker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import downloading.DownloadResult
import java.nio.file.Path
import kotlin.io.path.isRegularFile
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name

class SimilarFileChecker(
val dest: Path
) {
val names = dest
.listDirectoryEntries()
.filter { it.isRegularFile() }
.map { it.name }

fun findSimilarFileTo(key: String): String? {
return names.firstOrNull { similar(it, key) }
}

fun filterSimilarFiles(results: List<DownloadResult>): List<DownloadResult> {
return results.map {
if (it is DownloadResult.HasFiles) {
val similarFile = this.findSimilarFileTo(it.file.name)
if (similarFile != null)
return@map DownloadResult.SkippedBecauseSimilar(it.keyInConfig, similarFile)
}
it
}
}

/** Removes everything between the first digit and ext of the file */
fun String.removeVersion() = "${takeWhile { !it.isDigit() }}.${takeLastWhile { it != '.' }}"

/** Checks if two strings are similar with their versions removed */
fun similar(a: String, b: String): Boolean = a.removeVersion() == b.removeVersion()
}
18 changes: 13 additions & 5 deletions src/main/kotlin/commands/CachedCommand.kt
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
package commands

import evalBash
import java.nio.file.LinkOption
import java.nio.file.Path
import java.time.LocalDateTime
import kotlin.io.path.*
import kotlin.time.Duration
import kotlin.time.toJavaDuration

class CachedCommand(val command: String, val path: Path, val expiration: Duration? = null) {
fun getFromCacheOrEval(): String {
class Result(
val wasCached: Boolean,
val result: String,
)

fun getFromCacheOrEval(): Result {
val expirationPath = path.parent / (path.name + ".expiration")
// get current time
val time = LocalDateTime.now()
val expiryDate = expirationPath.takeIf { it.exists() }?.readText()?.let { LocalDateTime.parse(it) }

if (path.exists() && (expiryDate == null || time < expiryDate)) {
return path.readText()
return Result(true, path.readText())
}

val evaluated = command.evalBash(env = mapOf()).getOrThrow()
val evaluated = command.evalBash(env = mapOf())
.onFailure {
throw IllegalStateException("Failed to evaluate command $command, result:\n${this.stderr.joinToString("\n")}")
}
.getOrThrow()
path.deleteIfExists()
path.createParentDirectories().createFile().writeText(evaluated)
// write expiration date
if (expiration != null) {
expirationPath.deleteIfExists()
expirationPath.createFile().writeText((time + expiration.toJavaDuration()).toString())
}
return evaluated
return Result(false, evaluated)
}
}
12 changes: 0 additions & 12 deletions src/main/kotlin/commands/DownloadedItem.kt

This file was deleted.

28 changes: 6 additions & 22 deletions src/main/kotlin/commands/Rclone.kt
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
package commands

import com.lordcodes.turtle.ShellRunException
import com.lordcodes.turtle.shellRun
import kotlinx.serialization.json.Json
import java.nio.file.Path
import kotlin.io.path.div

object Rclone {
val rclone = "rclone"
private val json = Json { ignoreUnknownKeys = true }

fun sync(source: String, targetDir: Path): DownloadedItem? {
return runCatching {
shellRun(rclone, listOf("sync", source, targetDir.toString()))
}
.map {
val name = source.substringAfterLast("/")
DownloadedItem("$targetDir/$name", name)
}
.onSuccess { println("Downloaded $source") }
.onFailure {
if (it is ShellRunException) System.err.println(
"Error downloading $source\n${
it.message?.prependIndent(
" "
)
}"
)
}
.getOrNull()
fun sync(source: String, targetDir: Path): Path {
shellRun(rclone, listOf("sync", source, targetDir.toString()))
// TODO support multiple item downloads via rclone
val name = source.substringAfterLast("/")
return targetDir / name
}
}

0 comments on commit 4badb2d

Please sign in to comment.