Skip to content

Commit

Permalink
creating symbolic link of downloaded mod files
Browse files Browse the repository at this point in the history
  • Loading branch information
liplum committed Dec 12, 2023
1 parent 413e2d5 commit 2ef8cc0
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 255 deletions.
16 changes: 16 additions & 0 deletions main/src/dsl/FileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

package io.github.liplum.dsl

import org.gradle.api.Task
import java.io.File
import java.io.InputStream
import java.net.URL
import java.nio.file.Files

/**
* Copy data from this input stream to [file].
Expand All @@ -18,6 +20,7 @@ fun InputStream.copyTo(file: File): File {
}
return file
}

/**
* Copy data from this url to [file].
* It will create the parent folder if it doesn't exist.
Expand Down Expand Up @@ -134,4 +137,17 @@ fun findFileInOrder(vararg files: () -> File): File {
else continue
}
return files.last()()
}

fun Task.createSymbolicLinkOrCopyCache(link: File, target: File) {
if (link.exists()) return
try {
Files.createSymbolicLink(link.toPath(), target.toPath())
logger.lifecycle("Created symbolic link: $target -> $link.")
} catch (error: Exception) {
logger.lifecycle("Cannot create symbolic link: $target -> $link, because $error.")
logger.lifecycle("Fallback to copy file.")
target.copyTo(link)
logger.lifecycle("Copied: $target -> $link.")
}
}
File renamed without changes.
2 changes: 1 addition & 1 deletion main/src/extension/run/Modpack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class AddModpackSpec(
* **Not recommended:** Please use any more specific method, such as [java], [json] or [js]
* @param repo like "PlumyGames/mgpp"
*/
fun github(repo: String) = addMod(GitHubMod(repo))
fun github(repo: String) = addMod(GitHubUntypedMod(repo))
/**
* Add a json mod by its repo name on GitHub.
* @param repo like "PlumyGames/mgpp"
Expand Down
218 changes: 32 additions & 186 deletions main/src/extension/run/Mods.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
package io.github.liplum.mindustry

import arc.util.serialization.Jval
import io.github.liplum.dsl.*
import org.gradle.api.GradleException
import org.gradle.api.logging.Logger
import java.io.File
import java.io.Serializable
import java.net.URL
import kotlin.math.absoluteValue

internal
const val infoX = "info.json"
import java.security.MessageDigest

/**
* An abstract mod file.
*/
sealed interface IMod : Serializable

sealed interface IDownloadableMod : IMod {
val fileName: String
fun resolveFile(writeIn: File, logger: Logger? = null)
sealed interface IMod : Serializable {
val fileName4Local: String
fun resolveCacheFile(): File
}

sealed interface IGitHubMod : IDownloadableMod {
fun updateFile(writeIn: File, logger: Logger? = null)
fun isUpdateToDate(modFile: File, logger: Logger? = null): Boolean
sealed interface IGitHubMod : IMod {
override fun resolveCacheFile(): File {
return SharedCache.modsDir.resolve("github").resolve(fileName4Local)
}
}

/**
Expand All @@ -34,25 +27,32 @@ data class LocalMod(
val modFile: File = File(""),
) : IMod {
constructor(path: String) : this(File(path))

override val fileName4Local: String = modFile.name

override fun resolveCacheFile(): File = modFile
}

/**
* A mod from a url.
*/
data class UrlMod(
val url: URL,
) : IDownloadableMod {
) : IMod {
constructor(url: String) : this(URL(url))

override val fileName: String
get() {
val path: String = url.toURI().path
val last = path.substring(path.lastIndexOf('/') + 1)
return if (last.endsWith(".zip")) last else "$last.zip"
}
override val fileName4Local: String = run {
val path: String = url.path
val last = path.substring(path.lastIndexOf('/') + 1)
if (last.endsWith(".zip")) last else "$last.zip"
}

override fun resolveFile(writeIn: File, logger: Logger?) {
url.copyTo(writeIn)
override fun resolveCacheFile(): File {
val urlInBytes = MessageDigest
.getInstance("SHA-1")
.digest(url.toString().toByteArray())
val urlHashed = urlInBytes.toString()
return SharedCache.modsDir.resolve("url").resolve(urlHashed)
}
}

Expand All @@ -68,182 +68,28 @@ data class GihHubModDownloadMeta(
/**
* A mod on GitHub.
*/
data class GitHubMod(
data class GitHubUntypedMod(
/**
* like "PlumyGames/mgpp"
*/
val repo: String,
) : IGitHubMod {
override val fileName = repo.repo2Path() + ".zip"

override fun resolveFile(writeIn: File, logger: Logger?) {
updateGitHubModUpdateToDate(modFile = writeIn, logger = logger)
val jsonText = URL("https://api.github.com/repos/$repo").readText()
val json = Jval.read(jsonText)
val lan = json.getString("language")
if (lan.isJvmMod()) {
importJvmMod(repo, writeIn = writeIn)
} else {
val mainBranch = json.getString("default_branch")
importPlainMod(repo, mainBranch, writeIn)
}
}

override fun updateFile(writeIn: File, logger: Logger?) {
val temp = File.createTempFile(repo.repo2Path(), "zip")
resolveFile(writeIn = temp, logger = logger)
temp.copyTo(writeIn)
}

override fun isUpdateToDate(modFile: File, logger: Logger?): Boolean {
return validateGitHubModUpdateToDate(modFile, logger = logger)
}
override val fileName4Local = repo.repo2Path() + ".zip"
}

internal fun updateGitHubModUpdateToDate(
modFile: File,
newTimestamp: Long = System.currentTimeMillis(),
logger: Logger? = null,
) {
val infoFi = File("$modFile.$infoX")
if (infoFi.isDirectory) {
infoFi.deleteRecursively()
}
val meta = GihHubModDownloadMeta(lastUpdateTimestamp = newTimestamp)
val json = gson.toJson(meta)
try {
infoFi.writeText(json)
} catch (e: Exception) {
logger?.warn("Failed to write into \"info.json\"", e)
}
}

internal
fun validateGitHubModUpdateToDate(
modFile: File,
logger: Logger? = null,
): Boolean {
val infoFi = File("$modFile.$infoX")
if (!modFile.exists()) {
if (infoFi.exists()) infoFi.delete()
return false
}
val meta = tryReadGitHubModInfo(infoFi)
val curTime = System.currentTimeMillis()
// TODO: Configurable out-of-date time
return curTime - meta.lastUpdateTimestamp < R.outOfDataTime.absoluteValue
data class GitHubPlainMod(
val repo: String, val branch: String? = null,
) : IGitHubMod {
val fileNameWithoutExtension = linkString(separator = "-", repo.repo2Path(), branch)
override val fileName4Local = "$fileNameWithoutExtension.zip"
}

internal
fun tryReadGitHubModInfo(infoFi: File, logger: Logger? = null): GihHubModDownloadMeta {
fun writeAndGetDefault(): GihHubModDownloadMeta {
val meta = GihHubModDownloadMeta(lastUpdateTimestamp = System.currentTimeMillis())
val infoContent = gson.toJson(meta)
try {
infoFi.ensureParentDir().writeText(infoContent)
logger?.info("[MGPP] $infoFi is created.")
} catch (e: Exception) {
logger?.warn("Failed to write into \"info.json\"", e)
}
return meta
}
return if (infoFi.isFile) {
try {
val infoContent = infoFi.readText()
gson.fromJson(infoContent)
} catch (e: Exception) {
writeAndGetDefault()
}
} else {
writeAndGetDefault()
}
}

data class GitHubJvmMod(
val repo: String,
val tag: String? = null,
) : IGitHubMod {
val fileNameWithoutExtension = linkString(separator = "-", repo.repo2Path(), tag)
override val fileName = "$fileNameWithoutExtension.jar"

override fun resolveFile(writeIn: File, logger: Logger?) {
updateGitHubModUpdateToDate(modFile = writeIn, logger = logger)
if (tag == null) {
importJvmMod(repo, writeIn = writeIn)
} else {
val releaseJson = URL("https://api.github.com/repos/$repo/releases").readText()
val json = Jval.read(releaseJson)
val releases = json.asArray()
val release = releases.find { it.getString("tag_name") == tag }
?: throw GradleException("Tag<$tag> of $repo not found.")
val url = URL(release.getString("url"))
importJvmMod(url, writeIn)
}
}

override fun updateFile(writeIn: File, logger: Logger?) {
resolveFile(writeIn = writeIn, logger = logger)
}

override fun isUpdateToDate(modFile: File, logger: Logger?): Boolean {
return validateGitHubModUpdateToDate(modFile, logger = logger)
}
}

private
fun String.isJvmMod() = this == "Java" || this == "Kotlin" ||
this == "Groovy" || this == "Scala" ||
this == "Clojure"

private
fun importJvmMod(releaseEntryUrl: URL, writeIn: File) {
val releaseJson = releaseEntryUrl.readText()
val json = Jval.read(releaseJson)
val assets = json["assets"].asArray()
val dexedAsset = assets.find {
it.getString("name").startsWith("dexed") &&
it.getString("name").endsWith(".jar")
}
val asset = dexedAsset ?: assets.find { it.getString("name").endsWith(".jar") }
if (asset != null) {
val url = asset.getString("browser_download_url")
URL(url).copyTo(writeIn)
} else {
throw GradleException("Failed to find the mod.")
}
}

private
fun importJvmMod(repo: String, tag: String = "latest", writeIn: File) {
importJvmMod(releaseEntryUrl = URL("https://api.github.com/repos/$repo/releases/$tag"), writeIn)
}

data class GitHubPlainMod(
val repo: String, val branch: String? = null,
) : IGitHubMod {
val fileNameWithoutExtension = linkString(separator = "-", repo.repo2Path(), branch)
override val fileName = "$fileNameWithoutExtension.zip"

override fun resolveFile(writeIn: File, logger: Logger?) {
updateGitHubModUpdateToDate(modFile = writeIn, logger = logger)
val jsonText = URL("https://api.github.com/repos/$repo").readText()
val json = Jval.read(jsonText)
val branch = if (!branch.isNullOrBlank()) branch
else json.getString("default_branch")
importPlainMod(repo, branch, writeIn)
}

override fun updateFile(writeIn: File, logger: Logger?) {
resolveFile(writeIn = writeIn, logger = logger)
}

override fun isUpdateToDate(modFile: File, logger: Logger?): Boolean {
return validateGitHubModUpdateToDate(modFile, logger = logger)
}
}

internal
fun importPlainMod(repo: String, branch: String, dest: File) {
val url = "https://api.github.com/repos/$repo/zipball/$branch"
URL(url).copyTo(dest)
override val fileName4Local = "$fileNameWithoutExtension.jar"
}
32 changes: 5 additions & 27 deletions main/src/task/ResolveGame.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package io.github.liplum.mindustry

import io.github.liplum.dsl.copyTo
import io.github.liplum.dsl.createSymbolicLinkOrCopyCache
import io.github.liplum.dsl.fileProp
import io.github.liplum.dsl.listProp
import io.github.liplum.dsl.prop
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.nio.file.Files


open class ResolveGame : DefaultTask() {
val location = project.prop<IGameLoc>()
@Input get
val mods = project.listProp<IMod>()
@Input get
val gameFile = project.fileProp()
@OutputFile get

Expand All @@ -37,27 +34,14 @@ open class ResolveGame : DefaultTask() {
val cacheFile = loc.resolveCacheFile()
if (!cacheFile.exists()) {
when (loc) {
is GitHubGameLoc -> loc.downloadTo(cacheFile)
is LocalGameLoc -> loc.downloadTo(cacheFile)
is GitHubGameLoc -> loc.download(cacheFile)
is LocalGameLoc -> if (!cacheFile.isFile) throw GradleException("Local game $cacheFile doesn't exists.")
}
}
createSymbolicLinkOrCopyCache(gameFile, cacheFile)
}

fun createSymbolicLinkOrCopyCache(gameFile: File, cacheFile: File) {
if (!gameFile.exists()) return
try {
Files.createSymbolicLink(gameFile.toPath(), cacheFile.toPath())
logger.lifecycle("Created symbolic link of game: $cacheFile -> $gameFile.")
} catch (error: Exception) {
logger.lifecycle("Cannot create symbolic link of game: $cacheFile -> $gameFile, because $error.")
logger.lifecycle("Fallback to copy file.")
cacheFile.copyTo(gameFile)
logger.lifecycle("Game was copied: $cacheFile -> $gameFile.")
}
createSymbolicLinkOrCopyCache(link = gameFile, target = cacheFile)
}

fun GitHubGameLoc.downloadTo(cacheFile: File) {
fun GitHubGameLoc.download(cacheFile: File) {
logger.lifecycle("Downloading $this -> $cacheFile...")
try {
this.createDownloadLoc().openInputStream().use {
Expand All @@ -70,10 +54,4 @@ open class ResolveGame : DefaultTask() {
throw e
}
}

fun LocalGameLoc.downloadTo(cacheFile: File) {
if (!cacheFile.isFile) {
throw GradleException("Local game $cacheFile doesn't exists.")
}
}
}
Loading

0 comments on commit 2ef8cc0

Please sign in to comment.