mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-10-16 16:04:32 +02:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
73d21a475a | ||
|
7568770078 | ||
|
3d1d6db9b4 | ||
|
c6e304bc7f | ||
|
92d6f68f1d | ||
|
07af6046c1 | ||
|
89bdfd9c98 | ||
|
f4dd4fa866 | ||
|
6db8422c87 | ||
|
7d6346c088 | ||
|
aff921f67e | ||
|
afb574d82d | ||
|
8635906b1c | ||
|
bf95f03a18 | ||
|
bca2d758e1 | ||
|
46771ce870 | ||
|
b143f67acd |
100
build.gradle.kts
100
build.gradle.kts
@@ -1,9 +1,9 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.guardsquare:proguard-gradle:7.0.0") {
|
||||
classpath("com.guardsquare:proguard-gradle:7.1.0") {
|
||||
exclude("com.android.tools.build")
|
||||
}
|
||||
}
|
||||
@@ -12,30 +12,30 @@ buildscript {
|
||||
plugins {
|
||||
java
|
||||
application
|
||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||
id("com.palantir.git-version") version "0.12.3"
|
||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||
id("com.palantir.git-version") version "0.13.0"
|
||||
id("com.github.breadmoirai.github-release") version "2.2.12"
|
||||
kotlin("jvm") version "1.4.21"
|
||||
id("com.github.jk1.dependency-license-report") version "1.16"
|
||||
kotlin("jvm") version "1.6.10"
|
||||
id("com.github.jk1.dependency-license-report") version "2.0"
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
val shrinkClasspath: Configuration by configurations.creating
|
||||
|
||||
dependencies {
|
||||
implementation("commons-cli:commons-cli:1.4")
|
||||
shrinkClasspath("commons-cli:commons-cli:1.4")
|
||||
implementation("com.moandjiezana.toml:toml4j:0.7.2")
|
||||
implementation("com.google.code.gson:gson:2.8.1")
|
||||
implementation("com.squareup.okio:okio:2.9.0")
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
dependencies {
|
||||
implementation("commons-cli:commons-cli:1.5.0")
|
||||
implementation("com.moandjiezana.toml:toml4j:0.7.2")
|
||||
implementation("com.google.code.gson:gson:2.8.9")
|
||||
implementation("com.squareup.okio:okio:3.0.0")
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
implementation("com.squareup.okhttp3:okhttp:4.9.3")
|
||||
implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.14")
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -59,41 +59,47 @@ licenseReport {
|
||||
filters = arrayOf<com.github.jk1.license.filter.DependencyFilter>(com.github.jk1.license.filter.LicenseBundleNormalizer())
|
||||
}
|
||||
|
||||
// TODO: build relocated jar for minecraft launcher lib, non-relocated jar for packwiz-installer
|
||||
//tasks.register<com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation>("relocateShadowJar") {
|
||||
// target = tasks.shadowJar.get()
|
||||
// prefix = "link.infra.packwiz.deps"
|
||||
//}
|
||||
|
||||
// Commons CLI and Minimal JSON are already included in packwiz-installer-bootstrap
|
||||
tasks.shadowJar {
|
||||
dependencies {
|
||||
exclude(dependency("commons-cli:commons-cli:1.4"))
|
||||
exclude(dependency("com.eclipsesource.minimal-json:minimal-json:0.9.5"))
|
||||
// TODO: exclude meta inf files
|
||||
}
|
||||
exclude("**/*.kotlin_metadata")
|
||||
exclude("**/*.kotlin_builtins")
|
||||
exclude("META-INF/maven/**/*")
|
||||
exclude("META-INF/proguard/**/*")
|
||||
//dependsOn(tasks.named("relocateShadowJar"))
|
||||
|
||||
// Relocate Commons CLI, so that it doesn't clash with old packwiz-installer-bootstrap (that shades it)
|
||||
relocate("org.apache.commons.cli", "link.infra.packwiz.installer.deps.commons-cli")
|
||||
|
||||
// from Commons CLI
|
||||
exclude("META-INF/LICENSE.txt")
|
||||
exclude("META-INF/NOTICE.txt")
|
||||
}
|
||||
|
||||
tasks.register<proguard.gradle.ProGuardTask>("shrinkJar") {
|
||||
injars(tasks.shadowJar)
|
||||
libraryjars(files(shrinkClasspath.files))
|
||||
outjars("build/libs/" + tasks.shadowJar.get().outputs.files.first().name.removeSuffix(".jar") + "-shrink.jar")
|
||||
if (System.getProperty("java.version").startsWith("1.")) {
|
||||
libraryjars("${System.getProperty("java.home")}/lib/rt.jar")
|
||||
libraryjars("${System.getProperty("java.home")}/lib/jce.jar")
|
||||
} else {
|
||||
throw RuntimeException("Compiling with Java 9+ not supported!")
|
||||
// Use jmods for Java 9+
|
||||
val mods = listOf("java.base", "java.logging", "java.desktop", "java.sql")
|
||||
for (mod in mods) {
|
||||
libraryjars(mapOf(
|
||||
"jarfilter" to "!**.jar",
|
||||
"filter" to "!module-info.class"
|
||||
), "${System.getProperty("java.home")}/jmods/$mod.jmod")
|
||||
}
|
||||
}
|
||||
|
||||
keep("class link.infra.packwiz.installer.** { *; }")
|
||||
dontoptimize()
|
||||
dontobfuscate()
|
||||
|
||||
// Used by Okio and OkHttp
|
||||
dontwarn("org.codehaus.mojo.animal_sniffer.*")
|
||||
dontwarn("okhttp3.internal.platform.**")
|
||||
dontwarn("org.conscrypt.**")
|
||||
dontwarn("org.bouncycastle.**")
|
||||
dontwarn("org.openjsse.**")
|
||||
}
|
||||
|
||||
// Used for vscode launch.json
|
||||
@@ -126,12 +132,38 @@ if (project.hasProperty("github.token")) {
|
||||
tasks.compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable", "-Xallow-result-return-type", "-Xopt-in=kotlin.io.path.ExperimentalPathApi")
|
||||
}
|
||||
}
|
||||
tasks.compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable", "-Xallow-result-return-type", "-Xopt-in=kotlin.io.path.ExperimentalPathApi")
|
||||
}
|
||||
}
|
||||
|
||||
if (project.hasProperty("bunnycdn.token")) {
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("maven") {
|
||||
groupId = "link.infra.packwiz"
|
||||
artifactId = "packwiz-installer"
|
||||
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
url = uri("https://storage.bunnycdn.com/comp-maven")
|
||||
credentials(HttpHeaderCredentials::class) {
|
||||
name = "AccessKey"
|
||||
value = findProperty("bunnycdn.token") as String?
|
||||
}
|
||||
authentication {
|
||||
create<HttpHeaderAuthentication>("header")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -8,8 +8,6 @@ public class RequiresBootstrap {
|
||||
public static void main(String[] args) {
|
||||
// Very small CLI implementation, because Commons CLI complains on unexpected
|
||||
// options
|
||||
// Also so that Commons CLI can be excluded from the shaded JAR, as it is
|
||||
// included in the bootstrap
|
||||
if (Arrays.stream(args).map(str -> {
|
||||
if (str == null) return "";
|
||||
if (str.startsWith("--")) {
|
||||
|
5
src/main/kotlin/link/infra/packwiz/installer/DevMain.kt
Normal file
5
src/main/kotlin/link/infra/packwiz/installer/DevMain.kt
Normal file
@@ -0,0 +1,5 @@
|
||||
package link.infra.packwiz.installer
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Main(args)
|
||||
}
|
@@ -6,19 +6,20 @@ import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||
import link.infra.packwiz.installer.target.Side
|
||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||
import link.infra.packwiz.installer.util.Log
|
||||
import okio.Buffer
|
||||
import okio.HashingSink
|
||||
import okio.buffer
|
||||
import okio.*
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import kotlin.io.use
|
||||
|
||||
internal class DownloadTask private constructor(val metadata: IndexFile.File, defaultFormat: String, private val downloadSide: UpdateManager.Options.Side) : IOptionDetails {
|
||||
internal class DownloadTask private constructor(val metadata: IndexFile.File, defaultFormat: String, private val downloadSide: Side) : IOptionDetails {
|
||||
var cachedFile: ManifestFile.File? = null
|
||||
|
||||
private var err: Exception? = null
|
||||
@@ -26,7 +27,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
|
||||
fun failed() = err != null
|
||||
|
||||
private var alreadyUpToDate = false
|
||||
var alreadyUpToDate = false
|
||||
private var metadataRequired = true
|
||||
private var invalidated = false
|
||||
// If file is new or isOptional changed to true, the option needs to be presented again
|
||||
@@ -123,27 +124,77 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file in the destination location is already valid
|
||||
* Must be done after metadata retrieval
|
||||
*/
|
||||
fun validateExistingFile(packFolder: String) {
|
||||
if (!alreadyUpToDate) {
|
||||
try {
|
||||
// TODO: only do this for files that didn't exist before or have been modified since last full update?
|
||||
val destPath = Paths.get(packFolder, metadata.destURI.toString())
|
||||
FileSystem.SYSTEM.source(destPath.toOkioPath()).buffer().use { src ->
|
||||
val hash: Hash
|
||||
val fileHashFormat: String
|
||||
val linkedFile = metadata.linkedFile
|
||||
|
||||
if (linkedFile != null) {
|
||||
hash = linkedFile.hash
|
||||
fileHashFormat = linkedFile.download!!.hashFormat!!
|
||||
} else {
|
||||
hash = metadata.getHashObj()
|
||||
fileHashFormat = metadata.hashFormat!!
|
||||
}
|
||||
|
||||
val fileSource = getHasher(fileHashFormat).getHashingSource(src)
|
||||
fileSource.buffer().readAll(blackholeSink())
|
||||
if (fileSource.hashIsEqual(hash)) {
|
||||
alreadyUpToDate = true
|
||||
|
||||
// Update the manifest file
|
||||
cachedFile = (cachedFile ?: ManifestFile.File()).also {
|
||||
try {
|
||||
it.hash = metadata.getHashObj()
|
||||
} catch (e: Exception) {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
it.isOptional = isOptional
|
||||
it.cachedLocation = metadata.destURI.toString()
|
||||
metadata.linkedFile?.let { linked ->
|
||||
try {
|
||||
it.linkedFileHash = linked.hash
|
||||
} catch (e: Exception) {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// Ignore exceptions; if the file doesn't exist we'll be downloading it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun download(packFolder: String, indexUri: SpaceSafeURI) {
|
||||
if (err != null) return
|
||||
|
||||
// TODO: is this necessary if we overwrite?
|
||||
// Ensure it is removed
|
||||
// Ensure wrong-side or optional false files are removed
|
||||
cachedFile?.let {
|
||||
if (!it.optionValue || !correctSide()) {
|
||||
if (it.cachedLocation == null) return
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(packFolder, it.cachedLocation))
|
||||
} catch (e: IOException) {
|
||||
Log.warn("Failed to delete file before downloading", e)
|
||||
if ((it.isOptional && !it.optionValue) || !correctSide()) {
|
||||
if (it.cachedLocation != null) {
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(packFolder, it.cachedLocation))
|
||||
} catch (e: IOException) {
|
||||
Log.warn("Failed to delete file before downloading", e)
|
||||
}
|
||||
}
|
||||
it.cachedLocation = null
|
||||
}
|
||||
}
|
||||
if (alreadyUpToDate) return
|
||||
|
||||
// TODO: should I be validating JSON properly, or this fine!!!!!!!??
|
||||
assert(metadata.destURI != null)
|
||||
val destPath = Paths.get(packFolder, metadata.destURI.toString())
|
||||
|
||||
// Don't update files marked with preserve if they already exist on disk
|
||||
@@ -163,10 +214,10 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
|
||||
if (linkedFile != null) {
|
||||
hash = linkedFile.hash
|
||||
fileHashFormat = Objects.requireNonNull(Objects.requireNonNull(linkedFile.download)!!.hashFormat)!!
|
||||
fileHashFormat = linkedFile.download!!.hashFormat!!
|
||||
} else {
|
||||
hash = metadata.getHashObj()
|
||||
fileHashFormat = Objects.requireNonNull(metadata.hashFormat)!!
|
||||
fileHashFormat = metadata.hashFormat!!
|
||||
}
|
||||
|
||||
val src = metadata.getSource(indexUri)
|
||||
@@ -183,7 +234,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
// isDirectory follows symlinks, but createDirectories doesn't
|
||||
try {
|
||||
Files.createDirectories(destPath.parent)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
} catch (e: java.nio.file.FileAlreadyExistsException) {
|
||||
if (!Files.isDirectory(destPath.parent)) {
|
||||
throw e
|
||||
}
|
||||
@@ -196,7 +247,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
println("Calculated: " + fileSource.hash)
|
||||
println("Expected: $hash")
|
||||
// Attempt to get the SHA256 hash
|
||||
val sha256 = HashingSink.sha256(okio.blackholeSink())
|
||||
val sha256 = HashingSink.sha256(blackholeSink())
|
||||
data.readAll(sha256)
|
||||
println("SHA256 hash value: " + sha256.hash)
|
||||
err = Exception("Hash invalid!")
|
||||
@@ -241,7 +292,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createTasksFromIndex(index: IndexFile, defaultFormat: String, downloadSide: UpdateManager.Options.Side): List<DownloadTask> {
|
||||
fun createTasksFromIndex(index: IndexFile, defaultFormat: String, downloadSide: Side): List<DownloadTask> {
|
||||
val tasks = ArrayList<DownloadTask>()
|
||||
for (file in Objects.requireNonNull(index.files)) {
|
||||
tasks.add(DownloadTask(file, defaultFormat, downloadSide))
|
||||
|
@@ -3,6 +3,7 @@
|
||||
package link.infra.packwiz.installer
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.target.Side
|
||||
import link.infra.packwiz.installer.ui.cli.CLIHandler
|
||||
import link.infra.packwiz.installer.ui.gui.GUIHandler
|
||||
import link.infra.packwiz.installer.util.Log
|
||||
@@ -68,7 +69,7 @@ class Main(args: Array<String>) {
|
||||
val uOptions = try {
|
||||
UpdateManager.Options.construct(
|
||||
downloadURI = SpaceSafeURI(unparsedArgs[0]),
|
||||
side = cmd.getOptionValue("side")?.let((UpdateManager.Options.Side)::from),
|
||||
side = cmd.getOptionValue("side")?.let((Side)::from),
|
||||
packFolder = cmd.getOptionValue("pack-folder"),
|
||||
manifestFile = cmd.getOptionValue("meta-file")
|
||||
)
|
||||
|
@@ -3,33 +3,29 @@ package link.infra.packwiz.installer
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonIOException
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.moandjiezana.toml.Toml
|
||||
import link.infra.packwiz.installer.DownloadTask.Companion.createTasksFromIndex
|
||||
import link.infra.packwiz.installer.metadata.IndexFile
|
||||
import link.infra.packwiz.installer.metadata.ManifestFile
|
||||
import link.infra.packwiz.installer.metadata.PackFile
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.metadata.curseforge.resolveCfMetadata
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||
import link.infra.packwiz.installer.target.Side
|
||||
import link.infra.packwiz.installer.ui.IUserInterface
|
||||
import link.infra.packwiz.installer.ui.IUserInterface.CancellationResult
|
||||
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||
import link.infra.packwiz.installer.util.Log
|
||||
import link.infra.packwiz.installer.util.ifletOrErr
|
||||
import link.infra.packwiz.installer.util.ifletOrWarn
|
||||
import okio.buffer
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.io.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletionService
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ExecutorCompletionService
|
||||
@@ -57,56 +53,6 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
Options(downloadURI, manifestFile ?: "packwiz.json", packFolder ?: ".", side ?: Side.CLIENT)
|
||||
}
|
||||
|
||||
enum class Side {
|
||||
@SerializedName("client")
|
||||
CLIENT("client"),
|
||||
@SerializedName("server")
|
||||
SERVER("server"),
|
||||
@SerializedName("both")
|
||||
@Suppress("unused")
|
||||
BOTH("both", arrayOf(CLIENT, SERVER));
|
||||
|
||||
private val sideName: String
|
||||
private val depSides: Array<Side>?
|
||||
|
||||
constructor(sideName: String) {
|
||||
this.sideName = sideName.toLowerCase()
|
||||
depSides = null
|
||||
}
|
||||
|
||||
constructor(sideName: String, depSides: Array<Side>) {
|
||||
this.sideName = sideName.toLowerCase()
|
||||
this.depSides = depSides
|
||||
}
|
||||
|
||||
override fun toString() = sideName
|
||||
|
||||
fun hasSide(tSide: Side): Boolean {
|
||||
if (this == tSide) {
|
||||
return true
|
||||
}
|
||||
if (depSides != null) {
|
||||
for (depSide in depSides) {
|
||||
if (depSide == tSide) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(name: String): Side? {
|
||||
val lowerName = name.toLowerCase()
|
||||
for (side in values()) {
|
||||
if (side.sideName == lowerName) {
|
||||
return side
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
@@ -140,7 +86,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
}
|
||||
val pf = packFileSource.buffer().use {
|
||||
try {
|
||||
Toml().read(it.inputStream()).to(PackFile::class.java)
|
||||
Toml().read(InputStreamReader(it.inputStream(), "UTF-8")).to(PackFile::class.java)
|
||||
} catch (e: IllegalStateException) {
|
||||
ui.showErrorAndExit("Failed to parse pack.toml", e)
|
||||
}
|
||||
@@ -180,8 +126,11 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
}
|
||||
|
||||
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
|
||||
Log.info("Modpack is already up to date!")
|
||||
// todo: --force?
|
||||
ui.submitProgress(InstallProgress("Modpack is already up to date!", 1, 1))
|
||||
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
||||
ui.awaitOptionalButton(false)
|
||||
}
|
||||
if (!ui.optionsButtonPressed) {
|
||||
return
|
||||
}
|
||||
@@ -194,7 +143,8 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
handleCancellation()
|
||||
}
|
||||
try {
|
||||
ifletOrWarn(pf.index, "No index file found") { index ->
|
||||
// TODO: switch to OkHttp for better redirect handling
|
||||
ui.ifletOrErr(pf.index, "No index file found, or the pack file is empty; note that Java doesn't automatically follow redirects from HTTP to HTTPS (and may cause this error)") { index ->
|
||||
ui.ifletOrErr(index.hashFormat, index.hash, "Pack has no hash or hashFormat for index") { hashFormat, hash ->
|
||||
ui.ifletOrErr(getNewLoc(opts.downloadURI, index.file), "Pack has invalid index file: " + index.file) { newLoc ->
|
||||
processIndex(
|
||||
@@ -237,10 +187,17 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
|
||||
private fun processIndex(indexUri: SpaceSafeURI, indexHash: Hash, hashFormat: String, manifest: ManifestFile, invalidatedUris: List<SpaceSafeURI>) {
|
||||
if (manifest.indexFileHash == indexHash && invalidatedUris.isEmpty()) {
|
||||
Log.info("Modpack files are already up to date!")
|
||||
ui.submitProgress(InstallProgress("Modpack files are already up to date!", 1, 1))
|
||||
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
||||
ui.awaitOptionalButton(false)
|
||||
}
|
||||
if (!ui.optionsButtonPressed) {
|
||||
return
|
||||
}
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
return
|
||||
}
|
||||
}
|
||||
manifest.indexFileHash = indexHash
|
||||
|
||||
@@ -252,7 +209,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
}
|
||||
|
||||
val indexFile = try {
|
||||
Toml().read(indexFileSource.buffer().inputStream()).to(IndexFile::class.java)
|
||||
Toml().read(InputStreamReader(indexFileSource.buffer().inputStream(), "UTF-8")).to(IndexFile::class.java)
|
||||
} catch (e: IllegalStateException) {
|
||||
ui.showErrorAndExit("Failed to parse index file", e)
|
||||
}
|
||||
@@ -359,8 +316,20 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
// TODO: task failed function?
|
||||
val nonFailedFirstTasks = tasks.filter { t -> !t.failed() }.toList()
|
||||
val optionTasks = nonFailedFirstTasks.filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).toList()
|
||||
val optionsChanged = optionTasks.any(DownloadTask::isNewOptional)
|
||||
if (optionTasks.isNotEmpty() && !optionsChanged) {
|
||||
if (!ui.optionsButtonPressed) {
|
||||
// TODO: this is so ugly
|
||||
ui.submitProgress(InstallProgress("Reconfigure optional mods?", 0,1))
|
||||
ui.awaitOptionalButton(true)
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// If options changed, present all options again
|
||||
if (ui.optionsButtonPressed || optionTasks.any(DownloadTask::isNewOptional)) {
|
||||
if (ui.optionsButtonPressed || optionsChanged) {
|
||||
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
|
||||
if (ui.showOptions(ArrayList(optionTasks))) {
|
||||
cancelled = true
|
||||
@@ -370,6 +339,38 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
// TODO: keep this enabled? then apply changes after download process?
|
||||
ui.disableOptionsButton(optionTasks.isNotEmpty())
|
||||
|
||||
ui.submitProgress(InstallProgress("Validating existing files..."))
|
||||
|
||||
// Validate existing files
|
||||
for (downloadTask in nonFailedFirstTasks.filter(DownloadTask::correctSide)) {
|
||||
downloadTask.validateExistingFile(opts.packFolder)
|
||||
}
|
||||
|
||||
// Resolve CurseForge metadata
|
||||
val cfFiles = nonFailedFirstTasks.asSequence().filter { !it.alreadyUpToDate }
|
||||
.filter(DownloadTask::correctSide)
|
||||
.map { it.metadata }
|
||||
.filter { it.linkedFile != null }
|
||||
.filter { it.linkedFile?.download?.mode == "metadata:curseforge" }.toList()
|
||||
if (cfFiles.isNotEmpty()) {
|
||||
ui.submitProgress(InstallProgress("Resolving CurseForge metadata..."))
|
||||
val resolveFailures = resolveCfMetadata(cfFiles)
|
||||
if (resolveFailures.isNotEmpty()) {
|
||||
errorsOccurred = true
|
||||
when (ui.showExceptions(resolveFailures, cfFiles.size, true)) {
|
||||
ExceptionListResult.CONTINUE -> {}
|
||||
ExceptionListResult.CANCEL -> {
|
||||
cancelled = true
|
||||
return
|
||||
}
|
||||
ExceptionListResult.IGNORE -> {
|
||||
cancelledStartGame = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: different thread pool type?
|
||||
val threadPool = Executors.newFixedThreadPool(10)
|
||||
val completionService: CompletionService<DownloadTask> = ExecutorCompletionService(threadPool)
|
||||
@@ -445,4 +446,5 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -9,6 +9,7 @@ import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.file.Paths
|
||||
|
||||
class IndexFile {
|
||||
@@ -43,7 +44,7 @@ class IndexFile {
|
||||
linkedFileURI = getNewLoc(indexUri, file)
|
||||
val src = getFileSource(linkedFileURI!!)
|
||||
val fileStream = getHasher(hashFormat!!).getHashingSource(src)
|
||||
linkedFile = Toml().read(fileStream.buffer().inputStream()).to(ModFile::class.java)
|
||||
linkedFile = Toml().read(InputStreamReader(fileStream.buffer().inputStream(), "UTF-8")).to(ModFile::class.java)
|
||||
if (!fileStream.hashIsEqual(fileHash)) {
|
||||
throw Exception("Invalid mod file hash")
|
||||
}
|
||||
@@ -78,9 +79,9 @@ class IndexFile {
|
||||
get() {
|
||||
if (metafile) {
|
||||
return linkedFile?.name ?: linkedFile?.filename ?:
|
||||
file?.run { Paths.get(path).fileName.toString() } ?: "Invalid file"
|
||||
file?.run { Paths.get(path ?: return "Invalid file").fileName.toString() } ?: "Invalid file"
|
||||
}
|
||||
return file?.run { Paths.get(path).fileName.toString() } ?: "Invalid file"
|
||||
return file?.run { Paths.get(path ?: return "Invalid file").fileName.toString() } ?: "Invalid file"
|
||||
}
|
||||
|
||||
// TODO: URIs are bad
|
||||
|
@@ -1,15 +1,15 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import link.infra.packwiz.installer.UpdateManager
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.target.Side
|
||||
|
||||
class ManifestFile {
|
||||
var packFileHash: Hash? = null
|
||||
var indexFileHash: Hash? = null
|
||||
var cachedFiles: MutableMap<SpaceSafeURI, File> = HashMap()
|
||||
// If the side changes, EVERYTHING invalidates. FUN!!!
|
||||
var cachedSide = UpdateManager.Options.Side.CLIENT
|
||||
var cachedSide = Side.CLIENT
|
||||
|
||||
// TODO: switch to Kotlin-friendly JSON/TOML libs?
|
||||
class File {
|
||||
|
@@ -1,17 +1,20 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import link.infra.packwiz.installer.UpdateManager
|
||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateData
|
||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateDeserializer
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||
import link.infra.packwiz.installer.target.Side
|
||||
import okio.Source
|
||||
|
||||
class ModFile {
|
||||
var name: String? = null
|
||||
var filename: String? = null
|
||||
var side: UpdateManager.Options.Side? = null
|
||||
var side: Side? = null
|
||||
var download: Download? = null
|
||||
|
||||
class Download {
|
||||
@@ -19,11 +22,16 @@ class ModFile {
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
var mode: String? = null
|
||||
}
|
||||
|
||||
var update: Map<String, Any>? = null
|
||||
@JsonAdapter(UpdateDeserializer::class)
|
||||
var update: Map<String, UpdateData>? = null
|
||||
var option: Option? = null
|
||||
|
||||
@Transient
|
||||
val resolvedUpdateData = mutableMapOf<String, SpaceSafeURI>()
|
||||
|
||||
class Option {
|
||||
var optional = false
|
||||
var description: String? = null
|
||||
@@ -34,11 +42,20 @@ class ModFile {
|
||||
@Throws(Exception::class)
|
||||
fun getSource(baseLoc: SpaceSafeURI?): Source {
|
||||
download?.let {
|
||||
if (it.url == null) {
|
||||
throw Exception("Metadata file doesn't have a download URI")
|
||||
if (it.mode == null || it.mode == "" || it.mode == "url") {
|
||||
if (it.url == null) {
|
||||
throw Exception("Metadata file doesn't have a download URI")
|
||||
}
|
||||
val newLoc = getNewLoc(baseLoc, it.url) ?: throw Exception("Metadata file URI is invalid")
|
||||
return getFileSource(newLoc)
|
||||
} else if (it.mode == "metadata:curseforge") {
|
||||
if (!resolvedUpdateData.contains("curseforge")) {
|
||||
throw Exception("Metadata file specifies CurseForge mode, but is missing metadata")
|
||||
}
|
||||
return getFileSource(resolvedUpdateData["curseforge"]!!)
|
||||
} else {
|
||||
throw Exception("Unsupported download mode " + it.mode)
|
||||
}
|
||||
val newLoc = getNewLoc(baseLoc, it.url) ?: throw Exception("Metadata file URI is invalid")
|
||||
return getFileSource(newLoc)
|
||||
} ?: throw Exception("Metadata file doesn't have download")
|
||||
}
|
||||
|
||||
|
@@ -14,6 +14,4 @@ class PackFile {
|
||||
}
|
||||
|
||||
var versions: Map<String, String>? = null
|
||||
var client: Map<String, Any>? = null
|
||||
var server: Map<String, Any>? = null
|
||||
}
|
@@ -32,7 +32,7 @@ class SpaceSafeURI : Comparable<SpaceSafeURI>, Serializable {
|
||||
)
|
||||
}
|
||||
|
||||
val path: String get() = u.path.replace("%20", " ")
|
||||
val path: String? get() = u.path?.replace("%20", " ")
|
||||
|
||||
override fun toString(): String = u.toString().replace("%20", " ")
|
||||
|
||||
@@ -52,9 +52,9 @@ class SpaceSafeURI : Comparable<SpaceSafeURI>, Serializable {
|
||||
|
||||
override fun compareTo(other: SpaceSafeURI): Int = u.compareTo(other.u)
|
||||
|
||||
val scheme: String get() = u.scheme
|
||||
val authority: String get() = u.authority
|
||||
val host: String get() = u.host
|
||||
val scheme: String? get() = u.scheme
|
||||
val authority: String? get() = u.authority
|
||||
val host: String? get() = u.host
|
||||
|
||||
@Throws(MalformedURLException::class)
|
||||
fun toURL(): URL = u.toURL()
|
||||
|
@@ -0,0 +1,132 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonIOException
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import link.infra.packwiz.installer.metadata.IndexFile
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
private class GetFilesRequest(val fileIds: List<Int>)
|
||||
private class GetModsRequest(val modIds: List<Int>)
|
||||
|
||||
private class GetFilesResponse {
|
||||
class CfFile {
|
||||
var id = 0
|
||||
var modId = 0
|
||||
var downloadUrl: SpaceSafeURI? = null
|
||||
}
|
||||
val data = mutableListOf<CfFile>()
|
||||
}
|
||||
|
||||
private class GetModsResponse {
|
||||
class CfMod {
|
||||
var id = 0
|
||||
var name = ""
|
||||
var links: CfLinks? = null
|
||||
}
|
||||
class CfLinks {
|
||||
var websiteUrl = ""
|
||||
}
|
||||
val data = mutableListOf<CfMod>()
|
||||
}
|
||||
|
||||
private const val APIServer = "api.curseforge.com"
|
||||
// If you fork/derive from packwiz, I request that you obtain your own API key.
|
||||
private val APIKey = "JDJhJDEwJHNBWVhqblU1N0EzSmpzcmJYM3JVdk92UWk2NHBLS3BnQ2VpbGc1TUM1UGNKL0RYTmlGWWxh".decodeBase64()!!
|
||||
.string(StandardCharsets.UTF_8)
|
||||
|
||||
private val clientHolder = ClientHolder()
|
||||
|
||||
// TODO: switch to PackwizPath stuff and OkHttp in old code
|
||||
|
||||
@Throws(JsonSyntaxException::class, JsonIOException::class)
|
||||
fun resolveCfMetadata(mods: List<IndexFile.File>): List<ExceptionDetails> {
|
||||
val failures = mutableListOf<ExceptionDetails>()
|
||||
val fileIdMap = mutableMapOf<Int, IndexFile.File>()
|
||||
|
||||
for (mod in mods) {
|
||||
if (mod.linkedFile!!.update == null) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name ?: mod.linkedFile!!.filename!!, Exception("Failed to resolve CurseForge metadata: no update section")))
|
||||
continue
|
||||
}
|
||||
if (!mod.linkedFile!!.update!!.contains("curseforge")) {
|
||||
failures.add(ExceptionDetails(mod.linkedFile!!.name ?: mod.linkedFile!!.filename!!, Exception("Failed to resolve CurseForge metadata: no CurseForge update section")))
|
||||
continue
|
||||
}
|
||||
fileIdMap[(mod.linkedFile!!.update!!["curseforge"] as CurseForgeUpdateData).fileId] = mod
|
||||
}
|
||||
|
||||
val reqData = GetFilesRequest(fileIdMap.keys.toList())
|
||||
val req = Request.Builder()
|
||||
.url("https://${APIServer}/v1/mods/files")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", "packwiz-installer")
|
||||
.header("X-API-Key", APIKey)
|
||||
.post(Gson().toJson(reqData, GetFilesRequest::class.java).toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
val res = clientHolder.okHttpClient.newCall(req).execute()
|
||||
if (!res.isSuccessful || res.body == null) {
|
||||
res.closeQuietly()
|
||||
failures.add(ExceptionDetails("Other", Exception("Failed to resolve CurseForge metadata for file data: error code ${res.code}")))
|
||||
return failures
|
||||
}
|
||||
|
||||
val resData = Gson().fromJson(res.body!!.charStream(), GetFilesResponse::class.java)
|
||||
res.closeQuietly()
|
||||
|
||||
val manualDownloadMods = mutableMapOf<Int, Pair<IndexFile.File, Int>>()
|
||||
for (file in resData.data) {
|
||||
if (!fileIdMap.contains(file.id)) {
|
||||
failures.add(ExceptionDetails(file.id.toString(),
|
||||
Exception("Failed to find file from result: ID ${file.id}, Project ID ${file.modId}")))
|
||||
continue
|
||||
}
|
||||
if (file.downloadUrl == null) {
|
||||
manualDownloadMods[file.modId] = Pair(fileIdMap[file.id]!!, file.id)
|
||||
continue
|
||||
}
|
||||
fileIdMap[file.id]!!.linkedFile!!.resolvedUpdateData["curseforge"] = file.downloadUrl!!
|
||||
}
|
||||
|
||||
if (manualDownloadMods.isNotEmpty()) {
|
||||
val reqModsData = GetModsRequest(manualDownloadMods.keys.toList())
|
||||
val reqMods = Request.Builder()
|
||||
.url("https://${APIServer}/v1/mods")
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", "packwiz-installer")
|
||||
.header("X-API-Key", APIKey)
|
||||
.post(Gson().toJson(reqModsData, GetModsRequest::class.java).toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
val resMods = clientHolder.okHttpClient.newCall(reqMods).execute()
|
||||
if (!resMods.isSuccessful || resMods.body == null) {
|
||||
resMods.closeQuietly()
|
||||
failures.add(ExceptionDetails("Other", Exception("Failed to resolve CurseForge metadata for mod data: error code ${resMods.code}")))
|
||||
return failures
|
||||
}
|
||||
|
||||
val resModsData = Gson().fromJson(resMods.body!!.charStream(), GetModsResponse::class.java)
|
||||
resMods.closeQuietly()
|
||||
|
||||
for (mod in resModsData.data) {
|
||||
if (!manualDownloadMods.contains(mod.id)) {
|
||||
failures.add(ExceptionDetails(mod.name,
|
||||
Exception("Failed to find project from result: ID ${mod.id}")))
|
||||
continue
|
||||
}
|
||||
|
||||
val modFile = manualDownloadMods[mod.id]!!
|
||||
failures.add(ExceptionDetails(mod.name, Exception("This mod is excluded from the CurseForge API and must be downloaded manually.\n" +
|
||||
"Please go to ${mod.links?.websiteUrl}/files/${modFile.second} and save this file to ${modFile.first.destURI}")))
|
||||
}
|
||||
}
|
||||
|
||||
return failures
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class CurseForgeUpdateData: UpdateData {
|
||||
@SerializedName("file-id")
|
||||
var fileId = 0
|
||||
@SerializedName("project-id")
|
||||
var projectId = 0
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
interface UpdateData
|
@@ -0,0 +1,22 @@
|
||||
package link.infra.packwiz.installer.metadata.curseforge
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class UpdateDeserializer: JsonDeserializer<Map<String, UpdateData>> {
|
||||
override fun deserialize(
|
||||
json: JsonElement?,
|
||||
typeOfT: Type?,
|
||||
context: JsonDeserializationContext?
|
||||
): Map<String, UpdateData> {
|
||||
val out = mutableMapOf<String, UpdateData>()
|
||||
for ((k, v) in json!!.asJsonObject.entrySet()) {
|
||||
if (k == "curseforge") {
|
||||
out[k] = context!!.deserialize(v, CurseForgeUpdateData::class.java)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package link.infra.packwiz.installer.request
|
||||
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
|
||||
sealed class RequestException: Exception {
|
||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
/**
|
||||
* Internal errors that should not be shown to the user when the code is correct
|
||||
*/
|
||||
sealed class Internal: RequestException {
|
||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
class UnsinkableBase: Internal("Base associated with this path is not a SinkableBase")
|
||||
|
||||
sealed class HTTP: Internal {
|
||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
class NoResponseBody: HTTP("HTTP response in onResponse must have a response body")
|
||||
class RequestFailed(cause: IOException): HTTP("HTTP request failed; may have been cancelled", cause)
|
||||
class IllegalState(cause: IllegalStateException): HTTP("Internal fatal HTTP request error", cause)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors indicating that the request is malformed
|
||||
*/
|
||||
sealed class Validation: RequestException {
|
||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
// TODO: move out of RequestException?
|
||||
class PathContainsNUL(path: String): Validation("Invalid path; contains NUL bytes: ${path.replace("\u0000", "")}")
|
||||
class PathContainsVolumeLetter(path: String): Validation("Invalid path; contains volume letter: $path")
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors relating to the response from the server
|
||||
*/
|
||||
sealed class Response: RequestException {
|
||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
// TODO: fancier way of displaying this?
|
||||
sealed class HTTP: Response {
|
||||
val response: okhttp3.Response
|
||||
|
||||
constructor(response: okhttp3.Response, message: String, cause: Throwable) : super(message, cause) {
|
||||
this.response = response
|
||||
}
|
||||
constructor(response: okhttp3.Response, message: String) : super(message) {
|
||||
this.response = response
|
||||
}
|
||||
|
||||
class ErrorCode(res: okhttp3.Response): HTTP(res, "Non-successful error code from HTTP request: ${res.code}")
|
||||
}
|
||||
|
||||
sealed class File: RequestException {
|
||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
class FileNotFound(file: String): File("File path not found: $file")
|
||||
class Other(cause: Throwable): File("Failed to read file", cause)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
package link.infra.packwiz.installer.request.handlers
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.concurrent.read
|
||||
@@ -21,7 +20,7 @@ class RequestHandlerGithub : RequestHandlerZip(true) {
|
||||
private val zipUriMap: MutableMap<String, SpaceSafeURI> = HashMap()
|
||||
private val zipUriLock = ReentrantReadWriteLock()
|
||||
private fun getRepoName(loc: SpaceSafeURI): String? {
|
||||
val matcher = repoMatcherPattern.matcher(loc.path)
|
||||
val matcher = repoMatcherPattern.matcher(loc.path ?: return null)
|
||||
return if (matcher.matches()) {
|
||||
matcher.group(1)
|
||||
} else {
|
||||
@@ -47,7 +46,7 @@ class RequestHandlerGithub : RequestHandlerZip(true) {
|
||||
}
|
||||
|
||||
private fun getBranch(loc: SpaceSafeURI): String? {
|
||||
val matcher = branchMatcherPattern.matcher(loc.path)
|
||||
val matcher = branchMatcherPattern.matcher(loc.path ?: return null)
|
||||
return if (matcher.matches()) {
|
||||
matcher.group(1)
|
||||
} else {
|
||||
@@ -66,6 +65,6 @@ class RequestHandlerGithub : RequestHandlerZip(true) {
|
||||
return false
|
||||
}
|
||||
// TODO: more match testing?
|
||||
return "github.com" == loc.host && branchMatcherPattern.matcher(loc.path).matches()
|
||||
return "github.com" == loc.host && branchMatcherPattern.matcher(loc.path ?: return false).matches()
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package link.infra.packwiz.installer.target
|
||||
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import java.nio.file.Path
|
||||
|
||||
data class CachedTarget(
|
||||
/**
|
||||
* @see Target.name
|
||||
*/
|
||||
val name: String,
|
||||
/**
|
||||
* The location where the target was last downloaded to.
|
||||
* This is used for removing old files when the destination path changes.
|
||||
* This shouldn't be set to the .disabled path (that is manually appended and checked)
|
||||
*/
|
||||
val cachedLocation: Path,
|
||||
val enabled: Boolean,
|
||||
val hash: Hash,
|
||||
/**
|
||||
* For detecting when a target transitions non-optional -> optional and showing the option selection screen
|
||||
*/
|
||||
val isOptional: Boolean
|
||||
)
|
@@ -0,0 +1,95 @@
|
||||
package link.infra.packwiz.installer.target
|
||||
|
||||
import java.io.IOException
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import kotlin.io.path.relativeTo
|
||||
|
||||
data class CachedTargetStatus(val target: CachedTarget, var isValid: Boolean, var markDisabled: Boolean)
|
||||
|
||||
fun validate(targets: List<CachedTarget>, baseDir: Path) = runCatching {
|
||||
val results = targets.map {
|
||||
CachedTargetStatus(it, isValid = false, markDisabled = false)
|
||||
}
|
||||
val tree = buildTree(results, baseDir)
|
||||
|
||||
// Efficient file exists checking using directory listing, several orders of magnitude faster than Files.exists calls
|
||||
Files.walkFileTree(baseDir, setOf(FileVisitOption.FOLLOW_LINKS), Int.MAX_VALUE, object : FileVisitor<Path> {
|
||||
var currentNode: PathNode<CachedTargetStatus> = tree
|
||||
|
||||
override fun preVisitDirectory(dir: Path?, attrs: BasicFileAttributes?): FileVisitResult {
|
||||
if (dir == null) {
|
||||
return FileVisitResult.SKIP_SUBTREE
|
||||
}
|
||||
val subdirNode = currentNode.subdirs[dir.getName(dir.nameCount - 1)]
|
||||
return if (subdirNode != null) {
|
||||
currentNode = subdirNode
|
||||
FileVisitResult.CONTINUE
|
||||
} else {
|
||||
FileVisitResult.SKIP_SUBTREE
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitFile(file: Path?, attrs: BasicFileAttributes?): FileVisitResult {
|
||||
if (file == null) {
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
// TODO: these are relative paths to baseDir
|
||||
// TODO: strip the .disabled for lookup
|
||||
val target = currentNode.files[file.getName(file.nameCount - 1)]
|
||||
if (target != null) {
|
||||
val disabledFile = file.endsWith(".disabled")
|
||||
// If a .disabled file and the actual file both exist, mark as invalid if the target is disabled
|
||||
if ((disabledFile )) {
|
||||
|
||||
}
|
||||
}
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun visitFileFailed(file: Path?, exc: IOException?): FileVisitResult {
|
||||
if (exc != null) {
|
||||
throw exc
|
||||
}
|
||||
throw IOException("visitFileFailed called with no exception")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun postVisitDirectory(dir: Path?, exc: IOException?): FileVisitResult {
|
||||
if (exc != null) {
|
||||
throw exc
|
||||
} else {
|
||||
val parent = currentNode.parent
|
||||
if (parent != null) {
|
||||
currentNode = parent
|
||||
} else {
|
||||
throw IOException("Invalid visitor tree structure")
|
||||
}
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fun buildTree(targets: List<CachedTargetStatus>, baseDir: Path): PathNode<CachedTargetStatus> {
|
||||
val root = PathNode<CachedTargetStatus>()
|
||||
for (target in targets) {
|
||||
val relPath = target.target.cachedLocation.relativeTo(baseDir)
|
||||
var node = root
|
||||
// Traverse all the directory components, except for the last one
|
||||
for (i in 0 until (relPath.nameCount - 1)) {
|
||||
node = node.createSubdir(relPath.getName(i))
|
||||
}
|
||||
node.files[relPath.getName(relPath.nameCount - 1)] = target
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
data class PathNode<T>(val subdirs: MutableMap<Path, PathNode<T>>, val files: MutableMap<Path, T>, val parent: PathNode<T>?) {
|
||||
constructor() : this(mutableMapOf(), mutableMapOf(), null)
|
||||
|
||||
fun createSubdir(nextComponent: Path) = subdirs.getOrPut(nextComponent, { PathNode(mutableMapOf(), mutableMapOf(), this) })
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package link.infra.packwiz.installer.target
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okio.FileSystem
|
||||
|
||||
class ClientHolder {
|
||||
// TODO: timeouts?
|
||||
// TODO: a button to increase timeouts temporarily when retrying?
|
||||
val okHttpClient by lazy { OkHttpClient.Builder().build() }
|
||||
|
||||
val fileSystem = FileSystem.SYSTEM
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package link.infra.packwiz.installer.target
|
||||
|
||||
enum class OverwriteMode {
|
||||
/**
|
||||
* Overwrite the destination with the source file, if the source file has changed.
|
||||
*/
|
||||
IF_SRC_CHANGED,
|
||||
|
||||
/**
|
||||
* Never overwrite the destination; if it exists, it should not be written to.
|
||||
*/
|
||||
NEVER
|
||||
}
|
54
src/main/kotlin/link/infra/packwiz/installer/target/Side.kt
Normal file
54
src/main/kotlin/link/infra/packwiz/installer/target/Side.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
package link.infra.packwiz.installer.target
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
enum class Side {
|
||||
@SerializedName("client")
|
||||
CLIENT("client"),
|
||||
@SerializedName("server")
|
||||
SERVER("server"),
|
||||
@SerializedName("both")
|
||||
@Suppress("unused")
|
||||
BOTH("both", arrayOf(CLIENT, SERVER));
|
||||
|
||||
private val sideName: String
|
||||
private val depSides: Array<Side>?
|
||||
|
||||
constructor(sideName: String) {
|
||||
this.sideName = sideName.toLowerCase()
|
||||
depSides = null
|
||||
}
|
||||
|
||||
constructor(sideName: String, depSides: Array<Side>) {
|
||||
this.sideName = sideName.toLowerCase()
|
||||
this.depSides = depSides
|
||||
}
|
||||
|
||||
override fun toString() = sideName
|
||||
|
||||
fun hasSide(tSide: Side): Boolean {
|
||||
if (this == tSide) {
|
||||
return true
|
||||
}
|
||||
if (depSides != null) {
|
||||
for (depSide in depSides) {
|
||||
if (depSide == tSide) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(name: String): Side? {
|
||||
val lowerName = name.toLowerCase()
|
||||
for (side in values()) {
|
||||
if (side.sideName == lowerName) {
|
||||
return side
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package link.infra.packwiz.installer.target
|
||||
|
||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||
|
||||
// TODO: rename to avoid conflicting with @Target
|
||||
interface Target {
|
||||
val src: PackwizPath
|
||||
val dest: PackwizPath
|
||||
val validityToken: ValidityToken
|
||||
|
||||
/**
|
||||
* Token interface for types used to compare Target identity. Implementations should use equals to indicate that
|
||||
* these tokens represent the same file; used to preserve optional target choices between file renames.
|
||||
*/
|
||||
interface IdentityToken
|
||||
|
||||
/**
|
||||
* Default implementation of IdentityToken that assumes files are not renamed; so optional choices do not need to
|
||||
* be preserved across renames.
|
||||
*/
|
||||
@JvmInline
|
||||
value class PathIdentityToken(val path: String): IdentityToken
|
||||
|
||||
val ident: IdentityToken
|
||||
get() = PathIdentityToken(dest.path)
|
||||
|
||||
/**
|
||||
* A user-friendly name; defaults to the destination path of the file.
|
||||
*/
|
||||
val name: String
|
||||
get() = dest.path
|
||||
val side: Side
|
||||
get() = Side.BOTH
|
||||
val overwriteMode: OverwriteMode
|
||||
get() = OverwriteMode.IF_SRC_CHANGED
|
||||
|
||||
interface Optional: Target {
|
||||
val optional: Boolean
|
||||
val optionalDefaultValue: Boolean
|
||||
}
|
||||
|
||||
interface OptionalDescribed: Optional {
|
||||
val optionalDescription: String
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package link.infra.packwiz.installer.target
|
||||
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
|
||||
/**
|
||||
* A token used to determine if the source or destination file is changed, and ensure that the destination file is valid.
|
||||
*/
|
||||
interface ValidityToken {
|
||||
// TODO: functions to allow validating this from file metadata, from file, or during the download process
|
||||
|
||||
/**
|
||||
* Default implementation of ValidityToken based on a single hash.
|
||||
*/
|
||||
@JvmInline
|
||||
value class HashValidityToken(val hash: Hash): ValidityToken
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package link.infra.packwiz.installer.target.path
|
||||
|
||||
import link.infra.packwiz.installer.request.RequestException
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import okio.*
|
||||
|
||||
data class FilePathBase(private val path: Path): PackwizPath.Base, PackwizPath.SinkableBase {
|
||||
override fun source(path: String, clientHolder: ClientHolder): BufferedSource {
|
||||
val resolved = this.path.resolve(path, true)
|
||||
try {
|
||||
return clientHolder.fileSystem.source(resolved).buffer()
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw RequestException.Response.File.FileNotFound(resolved.toString())
|
||||
} catch (e: IOException) {
|
||||
throw RequestException.Response.File.Other(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sink(path: String, clientHolder: ClientHolder): BufferedSink {
|
||||
val resolved = this.path.resolve(path, true)
|
||||
try {
|
||||
return clientHolder.fileSystem.sink(resolved).buffer()
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw RequestException.Response.File.FileNotFound(resolved.toString())
|
||||
} catch (e: IOException) {
|
||||
throw RequestException.Response.File.Other(e)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package link.infra.packwiz.installer.target.path
|
||||
|
||||
import link.infra.packwiz.installer.request.RequestException
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okio.BufferedSource
|
||||
import okio.IOException
|
||||
|
||||
data class HttpUrlBase(private val url: HttpUrl): PackwizPath.Base {
|
||||
private fun resolve(path: String) = url.newBuilder().addPathSegments(path).build()
|
||||
|
||||
override fun source(path: String, clientHolder: ClientHolder): BufferedSource {
|
||||
val req = Request.Builder()
|
||||
.url(resolve(path))
|
||||
.header("Accept", "application/octet-stream")
|
||||
.header("User-Agent", "packwiz-installer")
|
||||
.get()
|
||||
.build()
|
||||
try {
|
||||
val res = clientHolder.okHttpClient.newCall(req).execute()
|
||||
// Can't use .use since it would close the response body before returning it to the caller
|
||||
try {
|
||||
if (!res.isSuccessful) {
|
||||
throw RequestException.Response.HTTP.ErrorCode(res)
|
||||
}
|
||||
|
||||
val body = res.body ?: throw RequestException.Internal.HTTP.NoResponseBody()
|
||||
return body.source()
|
||||
} catch (e: Exception) {
|
||||
// If an exception is thrown, close the response and rethrow
|
||||
res.close()
|
||||
throw e
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw RequestException.Internal.HTTP.RequestFailed(e)
|
||||
} catch (e: IllegalStateException) {
|
||||
throw RequestException.Internal.HTTP.IllegalState(e)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
package link.infra.packwiz.installer.target.path
|
||||
|
||||
import link.infra.packwiz.installer.request.RequestException
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
import okio.BufferedSink
|
||||
import okio.BufferedSource
|
||||
|
||||
class PackwizPath(path: String, base: Base) {
|
||||
val path: String
|
||||
val base: Base
|
||||
|
||||
init {
|
||||
this.base = base
|
||||
|
||||
// Check for NUL bytes
|
||||
if (path.contains('\u0000')) { throw RequestException.Validation.PathContainsNUL(path) }
|
||||
// Normalise separator, to prevent differences between Unix/Windows
|
||||
val pathNorm = path.replace('\\', '/')
|
||||
// Split, create new lists for output
|
||||
val split = pathNorm.split('/')
|
||||
val canonicalised = mutableListOf<String>()
|
||||
|
||||
// Backward pass: collapse ".." components, remove "." components and empty components (except an empty component at the end; indicating a folder)
|
||||
var parentComponentCount = 0
|
||||
var first = true
|
||||
for (component in split.asReversed()) {
|
||||
if (first) {
|
||||
first = false
|
||||
if (component == "") {
|
||||
canonicalised += component
|
||||
}
|
||||
}
|
||||
// URL-encoded . is normalised
|
||||
val componentNorm = component.replace("%2e", ".")
|
||||
if (componentNorm == "." || componentNorm == "") {
|
||||
// Do nothing
|
||||
} else if (componentNorm == "..") {
|
||||
parentComponentCount++
|
||||
} else if (parentComponentCount > 0) {
|
||||
parentComponentCount--
|
||||
} else {
|
||||
canonicalised += componentNorm
|
||||
// Don't allow volume letters (allows traversal to the root on Windows)
|
||||
if (componentNorm[0] in 'a'..'z' || componentNorm[0] in 'A'..'Z') {
|
||||
if (componentNorm[1] == ':') {
|
||||
throw RequestException.Validation.PathContainsVolumeLetter(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join path
|
||||
this.path = canonicalised.asReversed().joinToString("/")
|
||||
}
|
||||
|
||||
val folder: Boolean get() = path.endsWith("/")
|
||||
|
||||
fun resolve(path: String): PackwizPath {
|
||||
return if (path.startsWith('/') || path.startsWith('\\')) {
|
||||
// Absolute (but still relative to base of pack)
|
||||
PackwizPath(path, base)
|
||||
} else if (folder) {
|
||||
// File in folder; append
|
||||
PackwizPath(this.path + path, base)
|
||||
} else {
|
||||
// File in parent folder; append with parent component
|
||||
PackwizPath(this.path + "/../" + path, base)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a BufferedSource for this path
|
||||
* @throws RequestException When resolving the file failed
|
||||
*/
|
||||
fun source(clientHolder: ClientHolder): BufferedSource = base.source(path, clientHolder)
|
||||
|
||||
/**
|
||||
* Obtain a BufferedSink for this path
|
||||
* @throws RequestException.Internal.UnsinkableBase When the base of this path does not have a sink
|
||||
* @throws RequestException When resolving the file failed
|
||||
*/
|
||||
fun sink(clientHolder: ClientHolder): BufferedSink =
|
||||
if (base is SinkableBase) { base.sink(path, clientHolder) } else { throw RequestException.Internal.UnsinkableBase() }
|
||||
|
||||
interface Base {
|
||||
/**
|
||||
* Resolve the given (canonical) path against the base, and get a BufferedSource for this file.
|
||||
* @throws RequestException
|
||||
*/
|
||||
fun source(path: String, clientHolder: ClientHolder): BufferedSource
|
||||
|
||||
operator fun div(path: String) = PackwizPath(path, this)
|
||||
}
|
||||
|
||||
interface SinkableBase: Base {
|
||||
/**
|
||||
* Resolve the given (canonical) path against the base, and get a BufferedSink for this file.
|
||||
* @throws RequestException
|
||||
*/
|
||||
fun sink(path: String, clientHolder: ClientHolder): BufferedSink
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as PackwizPath
|
||||
|
||||
if (path != other.path) return false
|
||||
if (base != other.base) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = path.hashCode()
|
||||
result = 31 * result + base.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "base=$base; $path"
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
package link.infra.packwiz.installer.task
|
||||
|
||||
data class CacheKey<T>(val key: String, val version: Int)
|
@@ -0,0 +1,22 @@
|
||||
package link.infra.packwiz.installer.task
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class CacheManager {
|
||||
class CacheValue<T> {
|
||||
operator fun getValue(thisVal: Any?, property: KProperty<*>): T {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
operator fun setValue(thisVal: Any?, property: KProperty<*>, value: T) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
operator fun <T> get(cacheKey: CacheKey<T>): CacheValue<T> {
|
||||
return CacheValue()
|
||||
}
|
||||
|
||||
|
||||
}
|
20
src/main/kotlin/link/infra/packwiz/installer/task/Task.kt
Normal file
20
src/main/kotlin/link/infra/packwiz/installer/task/Task.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package link.infra.packwiz.installer.task
|
||||
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
// TODO: task processing on 1 background thread; actual resolving of values calls out to a thread group
|
||||
// TODO: progress bar is updated from each of these tasks
|
||||
// TODO: have everything be lazy so there's no need to determine task ordering upfront? a bit like rust async - task results must be queried to occur
|
||||
|
||||
abstract class Task<T>(protected val ctx: TaskContext): TaskInput<T> {
|
||||
// TODO: lazy wrapper for fallible results
|
||||
// TODO: multithreaded fanout subclass/helper
|
||||
|
||||
protected fun <T> wasUpdated(value: KMutableProperty0<T>, newValue: T): Boolean {
|
||||
if (value.get() == newValue) {
|
||||
return false
|
||||
}
|
||||
value.set(newValue)
|
||||
return true
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package link.infra.packwiz.installer.task
|
||||
|
||||
/**
|
||||
* An object for storing results where result and upToDate are calculated simultaneously
|
||||
*/
|
||||
data class TaskCombinedResult<T>(val result: T, val upToDate: Boolean)
|
@@ -0,0 +1,12 @@
|
||||
package link.infra.packwiz.installer.task
|
||||
|
||||
import link.infra.packwiz.installer.target.ClientHolder
|
||||
|
||||
class TaskContext {
|
||||
// TODO: thread pools, protocol roots
|
||||
// TODO: cache management
|
||||
|
||||
val cache = CacheManager()
|
||||
|
||||
val clients = ClientHolder()
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package link.infra.packwiz.installer.task
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface TaskInput<T> {
|
||||
/**
|
||||
* The value of this task input. May be lazily evaluated; must be threadsafe.
|
||||
*/
|
||||
val value: T
|
||||
|
||||
/**
|
||||
* True if the effective value of this input has changed since the task was last run.
|
||||
* Doesn't require evaluation of the input value; should use cached data if possible.
|
||||
* May be lazily evaluated; must be threadsafe.
|
||||
*/
|
||||
val upToDate: Boolean
|
||||
|
||||
operator fun getValue(thisVal: Any?, property: KProperty<*>): T = value
|
||||
|
||||
companion object {
|
||||
fun <T> raw(value: T): TaskInput<T> {
|
||||
return object: TaskInput<T> {
|
||||
override val value = value
|
||||
override val upToDate: Boolean
|
||||
get() = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package link.infra.packwiz.installer.task.formats.packwizv1
|
||||
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||
|
||||
data class PackwizV1PackFile(val name: String, val indexPath: PackwizPath, val indexHash: Hash)
|
@@ -0,0 +1,48 @@
|
||||
package link.infra.packwiz.installer.task.formats.packwizv1
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.moandjiezana.toml.Toml
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils
|
||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||
import link.infra.packwiz.installer.task.CacheKey
|
||||
import link.infra.packwiz.installer.task.Task
|
||||
import link.infra.packwiz.installer.task.TaskCombinedResult
|
||||
import link.infra.packwiz.installer.task.TaskContext
|
||||
import java.io.InputStreamReader
|
||||
|
||||
class PackwizV1PackTomlTask(ctx: TaskContext, val path: PackwizPath): Task<PackwizV1PackFile>(ctx) {
|
||||
// TODO: make hierarchically defined by caller? - then changing the pack format type doesn't leave junk in the cache
|
||||
private var cache by ctx.cache[CacheKey<Hash>("packwiz.v1.packtoml.hash", 1)]
|
||||
|
||||
private class PackFile {
|
||||
var name: String? = null
|
||||
var index: IndexFileLoc? = null
|
||||
|
||||
class IndexFileLoc {
|
||||
var file: String? = null
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
}
|
||||
|
||||
var versions: Map<String, String>? = null
|
||||
}
|
||||
|
||||
private val internalResult by lazy {
|
||||
// TODO: query, parse JSON
|
||||
val packFile = Toml().read(InputStreamReader(path.source(ctx.clients).inputStream(), "UTF-8")).to(PackFile::class.java)
|
||||
|
||||
val resolved = PackwizV1PackFile(packFile.name ?: throw RuntimeException("Name required"), // TODO: better exception handling
|
||||
path.resolve(packFile.index?.file ?: throw RuntimeException("File required")),
|
||||
HashUtils.getHash(packFile.index?.hashFormat ?: throw RuntimeException("Hash format required"),
|
||||
packFile.index?.hash ?: throw RuntimeException("Hash required"))
|
||||
)
|
||||
val hash = HashUtils.getHash("sha256", "whatever was just read")
|
||||
|
||||
TaskCombinedResult(resolved, wasUpdated(::cache, hash))
|
||||
}
|
||||
|
||||
override val value by internalResult::result
|
||||
override val upToDate by internalResult::upToDate
|
||||
}
|
@@ -23,6 +23,8 @@ interface IUserInterface {
|
||||
|
||||
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
||||
|
||||
fun awaitOptionalButton(showCancel: Boolean)
|
||||
|
||||
enum class ExceptionListResult {
|
||||
CONTINUE, CANCEL, IGNORE
|
||||
}
|
||||
|
@@ -59,4 +59,8 @@ class CLIHandler : IUserInterface {
|
||||
}
|
||||
return ExceptionListResult.CANCEL
|
||||
}
|
||||
|
||||
override fun awaitOptionalButton(showCancel: Boolean) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
@@ -125,7 +125,7 @@ class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFutu
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
addActionListener {
|
||||
try {
|
||||
Desktop.getDesktop().browse(URI("https://github.com/comp500/packwiz-installer/issues/new"))
|
||||
Desktop.getDesktop().browse(URI("https://github.com/packwiz/packwiz-installer/issues/new"))
|
||||
} catch (e: IOException) {
|
||||
// lol the button just won't work i guess
|
||||
} catch (e: URISyntaxException) {}
|
||||
|
@@ -8,6 +8,7 @@ import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||
import link.infra.packwiz.installer.util.Log
|
||||
import java.awt.EventQueue
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.UIManager
|
||||
@@ -18,8 +19,21 @@ class GUIHandler : IUserInterface {
|
||||
|
||||
@Volatile
|
||||
override var optionsButtonPressed = false
|
||||
set(value) {
|
||||
optionalSelectedLatch.countDown()
|
||||
field = value
|
||||
}
|
||||
@Volatile
|
||||
override var cancelButtonPressed = false
|
||||
set(value) {
|
||||
optionalSelectedLatch.countDown()
|
||||
field = value
|
||||
}
|
||||
var okButtonPressed = false
|
||||
set(value) {
|
||||
optionalSelectedLatch.countDown()
|
||||
field = value
|
||||
}
|
||||
@Volatile
|
||||
override var firstInstall = false
|
||||
|
||||
@@ -42,8 +56,12 @@ class GUIHandler : IUserInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private val visibleCountdownLatch = CountDownLatch(1)
|
||||
private val optionalSelectedLatch = CountDownLatch(1)
|
||||
|
||||
override fun show() = EventQueue.invokeLater {
|
||||
frmPackwizlauncher.isVisible = true
|
||||
visibleCountdownLatch.countDown()
|
||||
}
|
||||
|
||||
override fun dispose() = EventQueue.invokeAndWait {
|
||||
@@ -147,4 +165,15 @@ class GUIHandler : IUserInterface {
|
||||
}
|
||||
return future.get()
|
||||
}
|
||||
|
||||
override fun awaitOptionalButton(showCancel: Boolean) {
|
||||
EventQueue.invokeAndWait {
|
||||
frmPackwizlauncher.showOk(!showCancel)
|
||||
}
|
||||
visibleCountdownLatch.await()
|
||||
optionalSelectedLatch.await()
|
||||
EventQueue.invokeLater {
|
||||
frmPackwizlauncher.hideOk()
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,6 +12,9 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
||||
private var lblProgresslabel: JLabel
|
||||
private var progressBar: JProgressBar
|
||||
private var btnOptions: JButton
|
||||
private val btnCancel: JButton
|
||||
private val btnOk: JButton
|
||||
private val buttonsPanel: JPanel
|
||||
|
||||
init {
|
||||
setBounds(100, 100, 493, 95)
|
||||
@@ -35,7 +38,7 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
||||
}, BorderLayout.CENTER)
|
||||
|
||||
// Buttons
|
||||
add(JPanel().apply {
|
||||
buttonsPanel = JPanel().apply {
|
||||
border = EmptyBorder(0, 5, 0, 5)
|
||||
layout = GridBagLayout()
|
||||
|
||||
@@ -49,20 +52,28 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
||||
}
|
||||
}
|
||||
add(btnOptions, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridx = 1
|
||||
gridy = 0
|
||||
})
|
||||
|
||||
add(JButton("Cancel").apply {
|
||||
btnCancel = JButton("Cancel").apply {
|
||||
addActionListener {
|
||||
isEnabled = false
|
||||
handler.cancelButtonPressed = true
|
||||
}
|
||||
}, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
}
|
||||
add(btnCancel, GridBagConstraints().apply {
|
||||
gridx = 1
|
||||
gridy = 1
|
||||
})
|
||||
}, BorderLayout.EAST)
|
||||
}
|
||||
|
||||
btnOk = JButton("Continue").apply {
|
||||
addActionListener {
|
||||
handler.okButtonPressed = true
|
||||
}
|
||||
}
|
||||
add(buttonsPanel, BorderLayout.EAST)
|
||||
}
|
||||
|
||||
fun displayProgress(progress: InstallProgress) {
|
||||
@@ -83,4 +94,31 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
||||
isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
fun showOk(hideCancel: Boolean) {
|
||||
if (hideCancel) {
|
||||
buttonsPanel.add(btnOk, GridBagConstraints().apply {
|
||||
gridx = 1
|
||||
gridy = 1
|
||||
})
|
||||
buttonsPanel.remove(btnCancel)
|
||||
} else {
|
||||
buttonsPanel.add(btnOk, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridy = 1
|
||||
})
|
||||
}
|
||||
buttonsPanel.revalidate()
|
||||
}
|
||||
|
||||
fun hideOk() {
|
||||
buttonsPanel.remove(btnOk)
|
||||
if (!buttonsPanel.components.contains(btnCancel)) {
|
||||
buttonsPanel.add(btnCancel, GridBagConstraints().apply {
|
||||
gridx = 1
|
||||
gridy = 1
|
||||
})
|
||||
}
|
||||
buttonsPanel.revalidate()
|
||||
}
|
||||
}
|
@@ -1,14 +1,22 @@
|
||||
# Licenses
|
||||
|
||||
packwiz-installer itself is under the MIT license, except for Murmur2Lib and bundled dependencies as follows:
|
||||
packwiz-installer itself is under the MIT license ([Source](https://github.com/packwiz/packwiz-installer)), except for Murmur2Lib and bundled dependencies as follows:
|
||||
|
||||
- Murmur2Lib: Apache 2.0 ([Source](https://github.com/prasanthj/hasher/blob/master/src/main/java/hasher/Murmur2.java))
|
||||
- Google Gson 2.8.1: Apache 2.0 ([Source](https://github.com/google/gson))
|
||||
- Okio 2.9.0: Apache 2.0 ([Source](https://github.com/square/okio/))
|
||||
- Commons CLI 1.4: Apache 2.0 ([Source](http://commons.apache.org/proper/commons-cli/))
|
||||
- Copyright 2014 Prasanth Jayachandran
|
||||
- Google Gson 2.8.9: Apache 2.0 ([Source](https://github.com/google/gson))
|
||||
- Copyright 2008 Google Inc.
|
||||
- Okio 3.0.0: Apache 2.0 ([Source](https://github.com/square/okio/))
|
||||
- Copyright 2013 Square, Inc.
|
||||
- Commons CLI 1.5: Apache 2.0 ([Source](http://commons.apache.org/proper/commons-cli/))
|
||||
- Jetbrains Annotations 13.0: Apache 2.0 ([Source](https://github.com/JetBrains/java-annotations))
|
||||
- Kotlin Standard Library 1.4.21: Apache 2.0 ([Source](https://github.com/JetBrains/kotlin))
|
||||
- Copyright 2000-2016 JetBrains s.r.o.
|
||||
- Kotlin Standard Library 1.6.10: Apache 2.0 ([Source](https://github.com/JetBrains/kotlin))
|
||||
- Copyright 2010-2020 JetBrains s.r.o and respective authors and developers
|
||||
- toml4j 0.7.2: MIT ([Source](https://github.com/mwanji/toml4j))
|
||||
- Copyright (c) 2013-2015 Moandji Ezana
|
||||
- kotlin-result 1.1.14: ISC ([Source](https://github.com/michaelbull/kotlin-result))
|
||||
- Copyright (c) 2017-2022 Michael Bull (https://www.michael-bull.com)
|
||||
|
||||
## Associated notices
|
||||
|
||||
@@ -22,10 +30,6 @@ The Apache Software Foundation (http://www.apache.org/).
|
||||
## Full license texts
|
||||
|
||||
### MIT
|
||||
MIT License
|
||||
|
||||
Copyright (c)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
@@ -248,3 +252,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
### ISC
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
Reference in New Issue
Block a user