mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-10-16 16:04:32 +02:00
Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
73d21a475a | ||
|
7568770078 | ||
|
3d1d6db9b4 | ||
|
c6e304bc7f | ||
|
92d6f68f1d | ||
|
07af6046c1 | ||
|
89bdfd9c98 | ||
|
f4dd4fa866 | ||
|
6db8422c87 | ||
|
7d6346c088 | ||
|
aff921f67e | ||
|
afb574d82d | ||
|
8635906b1c | ||
|
bf95f03a18 | ||
|
bca2d758e1 | ||
|
46771ce870 | ||
|
b143f67acd | ||
|
03b0f1b09b | ||
|
6c6a0100fd | ||
|
6d47c0d61f | ||
|
226e754547 | ||
|
2c02703101 | ||
|
81a60cc759 | ||
|
92afa93fd7 | ||
|
0858c90079 | ||
|
1d4c94f5b6 | ||
|
74ddca5d54 | ||
|
0df48d19a9 | ||
|
f5b22f37a4 | ||
|
f52cd19ad4 | ||
|
60887a4312 | ||
|
a368268038 | ||
|
8beded7b41 |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019
|
Copyright (c) 2021 comp500
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
125
build.gradle.kts
125
build.gradle.kts
@@ -1,28 +1,41 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.guardsquare:proguard-gradle:7.1.0") {
|
||||||
|
exclude("com.android.tools.build")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
application
|
application
|
||||||
id("com.github.johnrengelman.shadow") version "5.0.0"
|
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||||
id("com.palantir.git-version") version "0.11.0"
|
id("com.palantir.git-version") version "0.13.0"
|
||||||
id("com.github.breadmoirai.github-release") version "2.2.9"
|
id("com.github.breadmoirai.github-release") version "2.2.12"
|
||||||
kotlin("jvm") version "1.3.61"
|
kotlin("jvm") version "1.6.10"
|
||||||
|
id("com.github.jk1.dependency-license-report") version "2.0"
|
||||||
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
repositories {
|
||||||
implementation("commons-cli:commons-cli:1.4")
|
mavenCentral()
|
||||||
implementation("com.moandjiezana.toml:toml4j:0.7.2")
|
|
||||||
// TODO: Implement tests
|
|
||||||
//testImplementation "junit:junit:4.12"
|
|
||||||
implementation("com.google.code.gson:gson:2.8.1")
|
|
||||||
implementation("com.squareup.okio:okio:2.2.2")
|
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
dependencies {
|
||||||
jcenter()
|
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 {
|
application {
|
||||||
@@ -39,17 +52,59 @@ tasks.jar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commons CLI and Minimal JSON are already included in packwiz-installer-bootstrap
|
licenseReport {
|
||||||
|
renderers = arrayOf<com.github.jk1.license.render.ReportRenderer>(
|
||||||
|
com.github.jk1.license.render.InventoryMarkdownReportRenderer("licenses.md", "packwiz-installer")
|
||||||
|
)
|
||||||
|
filters = arrayOf<com.github.jk1.license.filter.DependencyFilter>(com.github.jk1.license.filter.LicenseBundleNormalizer())
|
||||||
|
}
|
||||||
|
|
||||||
tasks.shadowJar {
|
tasks.shadowJar {
|
||||||
dependencies {
|
exclude("**/*.kotlin_metadata")
|
||||||
exclude(dependency("commons-cli:commons-cli:1.4"))
|
exclude("**/*.kotlin_builtins")
|
||||||
exclude(dependency("com.eclipsesource.minimal-json:minimal-json:0.9.5"))
|
exclude("META-INF/maven/**/*")
|
||||||
|
exclude("META-INF/proguard/**/*")
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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 {
|
||||||
|
// 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
|
// Used for vscode launch.json
|
||||||
tasks.register<Copy>("copyJar") {
|
tasks.register<Copy>("copyJar") {
|
||||||
from(tasks.shadowJar)
|
from(tasks.named("shrinkJar"))
|
||||||
rename("packwiz-installer-(.*)\\.jar", "packwiz-installer.jar")
|
rename("packwiz-installer-(.*)\\.jar", "packwiz-installer.jar")
|
||||||
into("build/libs/")
|
into("build/libs/")
|
||||||
}
|
}
|
||||||
@@ -77,12 +132,38 @@ if (project.hasProperty("github.token")) {
|
|||||||
tasks.compileKotlin {
|
tasks.compileKotlin {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
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 {
|
tasks.compileTestKotlin {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@@ -8,8 +8,6 @@ public class RequiresBootstrap {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// Very small CLI implementation, because Commons CLI complains on unexpected
|
// Very small CLI implementation, because Commons CLI complains on unexpected
|
||||||
// options
|
// 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 (Arrays.stream(args).map(str -> {
|
||||||
if (str == null) return "";
|
if (str == null) return "";
|
||||||
if (str.startsWith("--")) {
|
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,18 +6,20 @@ import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
|||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
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.getHash
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||||
import link.infra.packwiz.installer.ui.ExceptionDetails
|
import link.infra.packwiz.installer.target.Side
|
||||||
import link.infra.packwiz.installer.ui.IOptionDetails
|
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||||
import okio.Buffer
|
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||||
import okio.HashingSink
|
import link.infra.packwiz.installer.util.Log
|
||||||
import okio.buffer
|
import okio.*
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
import java.util.*
|
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
|
var cachedFile: ManifestFile.File? = null
|
||||||
|
|
||||||
private var err: Exception? = null
|
private var err: Exception? = null
|
||||||
@@ -25,7 +27,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
|||||||
|
|
||||||
fun failed() = err != null
|
fun failed() = err != null
|
||||||
|
|
||||||
private var alreadyUpToDate = false
|
var alreadyUpToDate = false
|
||||||
private var metadataRequired = true
|
private var metadataRequired = true
|
||||||
private var invalidated = false
|
private var invalidated = false
|
||||||
// If file is new or isOptional changed to true, the option needs to be presented again
|
// If file is new or isOptional changed to true, the option needs to be presented again
|
||||||
@@ -122,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) {
|
fun download(packFolder: String, indexUri: SpaceSafeURI) {
|
||||||
if (err != null) return
|
if (err != null) return
|
||||||
|
|
||||||
// Ensure it is removed
|
// Ensure wrong-side or optional false files are removed
|
||||||
cachedFile?.let {
|
cachedFile?.let {
|
||||||
if (!it.optionValue || !correctSide()) {
|
if ((it.isOptional && !it.optionValue) || !correctSide()) {
|
||||||
if (it.cachedLocation == null) return
|
if (it.cachedLocation != null) {
|
||||||
|
try {
|
||||||
try {
|
Files.deleteIfExists(Paths.get(packFolder, it.cachedLocation))
|
||||||
Files.deleteIfExists(Paths.get(packFolder, it.cachedLocation))
|
} catch (e: IOException) {
|
||||||
} catch (e: IOException) {
|
Log.warn("Failed to delete file before downloading", e)
|
||||||
// TODO: how much of a problem is this? use log4j/other log library to show warning?
|
}
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
it.cachedLocation = null
|
it.cachedLocation = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (alreadyUpToDate) return
|
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())
|
val destPath = Paths.get(packFolder, metadata.destURI.toString())
|
||||||
|
|
||||||
// Don't update files marked with preserve if they already exist on disk
|
// Don't update files marked with preserve if they already exist on disk
|
||||||
@@ -152,6 +204,9 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: if already exists and has correct hash, ignore?
|
||||||
|
// TODO: add .disabled support?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val hash: Hash
|
val hash: Hash
|
||||||
val fileHashFormat: String
|
val fileHashFormat: String
|
||||||
@@ -159,10 +214,10 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
|||||||
|
|
||||||
if (linkedFile != null) {
|
if (linkedFile != null) {
|
||||||
hash = linkedFile.hash
|
hash = linkedFile.hash
|
||||||
fileHashFormat = Objects.requireNonNull(Objects.requireNonNull(linkedFile.download)!!.hashFormat)!!
|
fileHashFormat = linkedFile.download!!.hashFormat!!
|
||||||
} else {
|
} else {
|
||||||
hash = metadata.getHashObj()
|
hash = metadata.getHashObj()
|
||||||
fileHashFormat = Objects.requireNonNull(metadata.hashFormat)!!
|
fileHashFormat = metadata.hashFormat!!
|
||||||
}
|
}
|
||||||
|
|
||||||
val src = metadata.getSource(indexUri)
|
val src = metadata.getSource(indexUri)
|
||||||
@@ -176,16 +231,23 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fileSource.hashIsEqual(hash)) {
|
if (fileSource.hashIsEqual(hash)) {
|
||||||
Files.createDirectories(destPath.parent)
|
// isDirectory follows symlinks, but createDirectories doesn't
|
||||||
|
try {
|
||||||
|
Files.createDirectories(destPath.parent)
|
||||||
|
} catch (e: java.nio.file.FileAlreadyExistsException) {
|
||||||
|
if (!Files.isDirectory(destPath.parent)) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING)
|
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING)
|
||||||
data.clear()
|
data.clear()
|
||||||
} else {
|
} else {
|
||||||
// TODO: no more PRINTLN!!!!!!!!!
|
// TODO: move println to something visible in the error window
|
||||||
println("Invalid hash for " + metadata.destURI.toString())
|
println("Invalid hash for " + metadata.destURI.toString())
|
||||||
println("Calculated: " + fileSource.hash)
|
println("Calculated: " + fileSource.hash)
|
||||||
println("Expected: $hash")
|
println("Expected: $hash")
|
||||||
// Attempt to get the SHA256 hash
|
// Attempt to get the SHA256 hash
|
||||||
val sha256 = HashingSink.sha256(okio.blackholeSink())
|
val sha256 = HashingSink.sha256(blackholeSink())
|
||||||
data.readAll(sha256)
|
data.readAll(sha256)
|
||||||
println("SHA256 hash value: " + sha256.hash)
|
println("SHA256 hash value: " + sha256.hash)
|
||||||
err = Exception("Hash invalid!")
|
err = Exception("Hash invalid!")
|
||||||
@@ -230,7 +292,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@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>()
|
val tasks = ArrayList<DownloadTask>()
|
||||||
for (file in Objects.requireNonNull(index.files)) {
|
for (file in Objects.requireNonNull(index.files)) {
|
||||||
tasks.add(DownloadTask(file, defaultFormat, downloadSide))
|
tasks.add(DownloadTask(file, defaultFormat, downloadSide))
|
||||||
|
@@ -3,9 +3,10 @@
|
|||||||
package link.infra.packwiz.installer
|
package link.infra.packwiz.installer
|
||||||
|
|
||||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||||
import link.infra.packwiz.installer.ui.CLIHandler
|
import link.infra.packwiz.installer.target.Side
|
||||||
import link.infra.packwiz.installer.ui.InputStateHandler
|
import link.infra.packwiz.installer.ui.cli.CLIHandler
|
||||||
import link.infra.packwiz.installer.ui.InstallWindow
|
import link.infra.packwiz.installer.ui.gui.GUIHandler
|
||||||
|
import link.infra.packwiz.installer.util.Log
|
||||||
import org.apache.commons.cli.DefaultParser
|
import org.apache.commons.cli.DefaultParser
|
||||||
import org.apache.commons.cli.Options
|
import org.apache.commons.cli.Options
|
||||||
import org.apache.commons.cli.ParseException
|
import org.apache.commons.cli.ParseException
|
||||||
@@ -19,7 +20,7 @@ import kotlin.system.exitProcess
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class Main(args: Array<String>) {
|
class Main(args: Array<String>) {
|
||||||
// Don't attempt to start a GUI if we are headless
|
// Don't attempt to start a GUI if we are headless
|
||||||
var guiEnabled = !GraphicsEnvironment.isHeadless()
|
private var guiEnabled = !GraphicsEnvironment.isHeadless()
|
||||||
|
|
||||||
private fun startup(args: Array<String>) {
|
private fun startup(args: Array<String>) {
|
||||||
val options = Options()
|
val options = Options()
|
||||||
@@ -30,7 +31,7 @@ class Main(args: Array<String>) {
|
|||||||
val cmd = try {
|
val cmd = try {
|
||||||
parser.parse(options, args)
|
parser.parse(options, args)
|
||||||
} catch (e: ParseException) {
|
} catch (e: ParseException) {
|
||||||
e.printStackTrace()
|
Log.fatal("Failed to parse command line arguments", e)
|
||||||
if (guiEnabled) {
|
if (guiEnabled) {
|
||||||
EventQueue.invokeAndWait {
|
EventQueue.invokeAndWait {
|
||||||
try {
|
try {
|
||||||
@@ -38,7 +39,8 @@ class Main(args: Array<String>) {
|
|||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
// Ignore the exceptions, just continue using the ugly L&F
|
// Ignore the exceptions, just continue using the ugly L&F
|
||||||
}
|
}
|
||||||
JOptionPane.showMessageDialog(null, e.message, "packwiz-installer", JOptionPane.ERROR_MESSAGE)
|
JOptionPane.showMessageDialog(null, "Failed to parse command line arguments: $e",
|
||||||
|
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
@@ -48,46 +50,41 @@ class Main(args: Array<String>) {
|
|||||||
guiEnabled = false
|
guiEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val ui = if (guiEnabled) InstallWindow() else CLIHandler()
|
val ui = if (guiEnabled) GUIHandler() else CLIHandler()
|
||||||
|
|
||||||
val unparsedArgs = cmd.args
|
val unparsedArgs = cmd.args
|
||||||
if (unparsedArgs.size > 1) {
|
if (unparsedArgs.size > 1) {
|
||||||
ui.handleExceptionAndExit(RuntimeException("Too many arguments specified!"))
|
ui.showErrorAndExit("Too many arguments specified!")
|
||||||
} else if (unparsedArgs.isEmpty()) {
|
} else if (unparsedArgs.isEmpty()) {
|
||||||
ui.handleExceptionAndExit(RuntimeException("URI to install from must be specified!"))
|
ui.showErrorAndExit("pack.toml URI to install from must be specified!")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.getOptionValue("title")?.also(ui::setTitle)
|
val title = cmd.getOptionValue("title")
|
||||||
|
if (title != null) {
|
||||||
val inputStateHandler = InputStateHandler()
|
ui.title = title
|
||||||
ui.show(inputStateHandler)
|
|
||||||
|
|
||||||
val uOptions = UpdateManager.Options().apply {
|
|
||||||
side = cmd.getOptionValue("side")?.let((UpdateManager.Options.Side)::from) ?: side
|
|
||||||
packFolder = cmd.getOptionValue("pack-folder") ?: packFolder
|
|
||||||
manifestFile = cmd.getOptionValue("meta-file") ?: manifestFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
ui.show()
|
||||||
uOptions.downloadURI = SpaceSafeURI(unparsedArgs[0])
|
|
||||||
|
val uOptions = try {
|
||||||
|
UpdateManager.Options.construct(
|
||||||
|
downloadURI = SpaceSafeURI(unparsedArgs[0]),
|
||||||
|
side = cmd.getOptionValue("side")?.let((Side)::from),
|
||||||
|
packFolder = cmd.getOptionValue("pack-folder"),
|
||||||
|
manifestFile = cmd.getOptionValue("meta-file")
|
||||||
|
)
|
||||||
} catch (e: URISyntaxException) {
|
} catch (e: URISyntaxException) {
|
||||||
// TODO: better error message?
|
ui.showErrorAndExit("Failed to read pack.toml URI", e)
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start update process!
|
// Start update process!
|
||||||
// TODO: start in SwingWorker?
|
|
||||||
try {
|
try {
|
||||||
ui.executeManager {
|
UpdateManager(uOptions, ui)
|
||||||
try {
|
} catch (e: Exception) {
|
||||||
UpdateManager(uOptions, ui, inputStateHandler)
|
ui.showErrorAndExit("Update process failed", e)
|
||||||
} catch (e: Exception) { // TODO: better error message?
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) { // TODO: better error message?
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
}
|
}
|
||||||
|
println("Finished successfully!")
|
||||||
|
ui.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -118,17 +115,17 @@ class Main(args: Array<String>) {
|
|||||||
try {
|
try {
|
||||||
startup(args)
|
startup(args)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.fatal("Error from main", e)
|
||||||
if (guiEnabled) {
|
if (guiEnabled) {
|
||||||
EventQueue.invokeLater {
|
EventQueue.invokeLater {
|
||||||
JOptionPane.showMessageDialog(null,
|
JOptionPane.showMessageDialog(null,
|
||||||
"A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
|
"A fatal error occurred: \n$e",
|
||||||
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
|
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
// In case the EventQueue is broken, exit after 1 minute
|
||||||
|
Thread.sleep(60 * 1000.toLong())
|
||||||
}
|
}
|
||||||
// In case the EventQueue is broken, exit after 1 minute
|
|
||||||
Thread.sleep(60 * 1000.toLong())
|
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,38 +3,36 @@ package link.infra.packwiz.installer
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonIOException
|
import com.google.gson.JsonIOException
|
||||||
import com.google.gson.JsonSyntaxException
|
import com.google.gson.JsonSyntaxException
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.moandjiezana.toml.Toml
|
import com.moandjiezana.toml.Toml
|
||||||
import link.infra.packwiz.installer.DownloadTask.Companion.createTasksFromIndex
|
import link.infra.packwiz.installer.DownloadTask.Companion.createTasksFromIndex
|
||||||
import link.infra.packwiz.installer.metadata.IndexFile
|
import link.infra.packwiz.installer.metadata.IndexFile
|
||||||
import link.infra.packwiz.installer.metadata.ManifestFile
|
import link.infra.packwiz.installer.metadata.ManifestFile
|
||||||
import link.infra.packwiz.installer.metadata.PackFile
|
import link.infra.packwiz.installer.metadata.PackFile
|
||||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
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.Hash
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
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
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface.CancellationResult
|
import link.infra.packwiz.installer.ui.IUserInterface.CancellationResult
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
||||||
import link.infra.packwiz.installer.ui.InputStateHandler
|
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||||
import link.infra.packwiz.installer.ui.InstallProgress
|
import link.infra.packwiz.installer.util.Log
|
||||||
|
import link.infra.packwiz.installer.util.ifletOrErr
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import java.io.FileNotFoundException
|
import java.io.*
|
||||||
import java.io.FileReader
|
|
||||||
import java.io.FileWriter
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CompletionService
|
import java.util.concurrent.CompletionService
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.ExecutorCompletionService
|
import java.util.concurrent.ExecutorCompletionService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class UpdateManager internal constructor(private val opts: Options, val ui: IUserInterface, private val stateHandler: InputStateHandler) {
|
class UpdateManager internal constructor(private val opts: Options, val ui: IUserInterface) {
|
||||||
private var cancelled = false
|
private var cancelled = false
|
||||||
private var cancelledStartGame = false
|
private var cancelledStartGame = false
|
||||||
private var errorsOccurred = false
|
private var errorsOccurred = false
|
||||||
@@ -44,61 +42,17 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Options(
|
data class Options(
|
||||||
var downloadURI: SpaceSafeURI? = null,
|
val downloadURI: SpaceSafeURI,
|
||||||
var manifestFile: String = "packwiz.json", // TODO: make configurable
|
val manifestFile: String,
|
||||||
var packFolder: String = ".",
|
val packFolder: String,
|
||||||
var side: Side = Side.CLIENT
|
val side: Side
|
||||||
) {
|
) {
|
||||||
enum class Side {
|
// Horrible workaround for default params not working cleanly with nullable values
|
||||||
@SerializedName("client")
|
companion object {
|
||||||
CLIENT("client"),
|
fun construct(downloadURI: SpaceSafeURI, manifestFile: String?, packFolder: String?, side: Side?) =
|
||||||
@SerializedName("server")
|
Options(downloadURI, manifestFile ?: "packwiz.json", packFolder ?: ".", side ?: Side.CLIENT)
|
||||||
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() {
|
private fun start() {
|
||||||
@@ -110,39 +64,35 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
gson.fromJson(FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
|
gson.fromJson(FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
|
||||||
ManifestFile::class.java)
|
ManifestFile::class.java)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
|
ui.firstInstall = true
|
||||||
ManifestFile()
|
ManifestFile()
|
||||||
} catch (e: JsonSyntaxException) {
|
} catch (e: JsonSyntaxException) {
|
||||||
ui.handleExceptionAndExit(e)
|
ui.showErrorAndExit("Invalid local manifest file, try deleting ${opts.manifestFile}", e)
|
||||||
return
|
|
||||||
} catch (e: JsonIOException) {
|
} catch (e: JsonIOException) {
|
||||||
ui.handleExceptionAndExit(e)
|
ui.showErrorAndExit("Failed to read local manifest file, try deleting ${opts.manifestFile}", e)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateHandler.cancelButton) {
|
if (ui.cancelButtonPressed) {
|
||||||
showCancellationDialog()
|
showCancellationDialog()
|
||||||
handleCancellation()
|
handleCancellation()
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.submitProgress(InstallProgress("Loading pack file..."))
|
ui.submitProgress(InstallProgress("Loading pack file..."))
|
||||||
val packFileSource = try {
|
val packFileSource = try {
|
||||||
val src = getFileSource(opts.downloadURI!!)
|
val src = getFileSource(opts.downloadURI)
|
||||||
getHasher("sha256").getHashingSource(src)
|
getHasher("sha256").getHashingSource(src)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// TODO: run cancellation window?
|
ui.showErrorAndExit("Failed to download pack.toml", e)
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
val pf = packFileSource.buffer().use {
|
val pf = packFileSource.buffer().use {
|
||||||
try {
|
try {
|
||||||
Toml().read(it.inputStream()).to(PackFile::class.java)
|
Toml().read(InputStreamReader(it.inputStream(), "UTF-8")).to(PackFile::class.java)
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
ui.handleExceptionAndExit(e)
|
ui.showErrorAndExit("Failed to parse pack.toml", e)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateHandler.cancelButton) {
|
if (ui.cancelButtonPressed) {
|
||||||
showCancellationDialog()
|
showCancellationDialog()
|
||||||
handleCancellation()
|
handleCancellation()
|
||||||
}
|
}
|
||||||
@@ -170,40 +120,45 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (invalid) {
|
if (invalid) {
|
||||||
println("File $fileUri invalidated, marked for redownloading")
|
Log.info("File $fileUri invalidated, marked for redownloading")
|
||||||
invalidatedUris.add(fileUri)
|
invalidatedUris.add(fileUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
|
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
|
||||||
println("Modpack is already up to date!")
|
|
||||||
// todo: --force?
|
// todo: --force?
|
||||||
if (!stateHandler.optionsButton) {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println("Modpack name: " + pf.name)
|
Log.info("Modpack name: ${pf.name}")
|
||||||
|
|
||||||
if (stateHandler.cancelButton) {
|
if (ui.cancelButtonPressed) {
|
||||||
showCancellationDialog()
|
showCancellationDialog()
|
||||||
handleCancellation()
|
handleCancellation()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val index = pf.index!!
|
// TODO: switch to OkHttp for better redirect handling
|
||||||
getNewLoc(opts.downloadURI, index.file)?.let { newLoc ->
|
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 ->
|
||||||
index.hashFormat?.let { hashFormat ->
|
ui.ifletOrErr(index.hashFormat, index.hash, "Pack has no hash or hashFormat for index") { hashFormat, hash ->
|
||||||
processIndex(
|
ui.ifletOrErr(getNewLoc(opts.downloadURI, index.file), "Pack has invalid index file: " + index.file) { newLoc ->
|
||||||
newLoc,
|
processIndex(
|
||||||
getHash(index.hashFormat!!, index.hash!!),
|
newLoc,
|
||||||
hashFormat,
|
getHash(hashFormat, hash),
|
||||||
manifest,
|
hashFormat,
|
||||||
invalidatedUris
|
manifest,
|
||||||
)
|
invalidatedUris
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e1: Exception) {
|
} catch (e1: Exception) {
|
||||||
ui.handleExceptionAndExit(e1)
|
ui.showErrorAndExit("Failed to process index file", e1)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancellation()
|
handleCancellation()
|
||||||
@@ -222,8 +177,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
try {
|
try {
|
||||||
FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString()).use { writer -> gson.toJson(manifest, writer) }
|
FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString()).use { writer -> gson.toJson(manifest, writer) }
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// TODO: add message?
|
ui.showErrorAndExit("Failed to save local manifest file", e)
|
||||||
ui.handleException(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,8 +187,15 @@ 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>) {
|
private fun processIndex(indexUri: SpaceSafeURI, indexHash: Hash, hashFormat: String, manifest: ManifestFile, invalidatedUris: List<SpaceSafeURI>) {
|
||||||
if (manifest.indexFileHash == indexHash && invalidatedUris.isEmpty()) {
|
if (manifest.indexFileHash == indexHash && invalidatedUris.isEmpty()) {
|
||||||
println("Modpack files are already up to date!")
|
ui.submitProgress(InstallProgress("Modpack files are already up to date!", 1, 1))
|
||||||
if (!stateHandler.optionsButton) {
|
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
||||||
|
ui.awaitOptionalButton(false)
|
||||||
|
}
|
||||||
|
if (!ui.optionsButtonPressed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ui.cancelButtonPressed) {
|
||||||
|
showCancellationDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,21 +205,19 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
val src = getFileSource(indexUri)
|
val src = getFileSource(indexUri)
|
||||||
getHasher(hashFormat).getHashingSource(src)
|
getHasher(hashFormat).getHashingSource(src)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// TODO: run cancellation window?
|
ui.showErrorAndExit("Failed to download index file", e)
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val indexFile = try {
|
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) {
|
} catch (e: IllegalStateException) {
|
||||||
ui.handleExceptionAndExit(e)
|
ui.showErrorAndExit("Failed to parse index file", e)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (!indexFileSource.hashIsEqual(indexHash)) {
|
if (!indexFileSource.hashIsEqual(indexHash)) {
|
||||||
ui.handleExceptionAndExit(RuntimeException("Your index hash is invalid! Please run packwiz refresh on the pack again"))
|
ui.showErrorAndExit("Your index file hash is invalid! The pack developer should packwiz refresh on the pack again")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (stateHandler.cancelButton) {
|
|
||||||
|
if (ui.cancelButtonPressed) {
|
||||||
showCancellationDialog()
|
showCancellationDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -275,8 +234,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
try {
|
try {
|
||||||
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
|
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// TODO: should this be shown to the user in some way?
|
Log.warn("Failed to delete optional disabled file", e)
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
// Set to null, as it doesn't exist anymore
|
// Set to null, as it doesn't exist anymore
|
||||||
file.cachedLocation = null
|
file.cachedLocation = null
|
||||||
@@ -286,8 +244,8 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
if (!alreadyDeleted) {
|
if (!alreadyDeleted) {
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
|
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
|
||||||
} catch (e: IOException) { // TODO: should this be shown to the user in some way?
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
Log.warn("Failed to delete file removed from index", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
it.remove()
|
it.remove()
|
||||||
@@ -295,7 +253,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateHandler.cancelButton) {
|
if (ui.cancelButtonPressed) {
|
||||||
showCancellationDialog()
|
showCancellationDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -303,14 +261,14 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
|
|
||||||
// TODO: progress bar?
|
// TODO: progress bar?
|
||||||
if (indexFile.files.isEmpty()) {
|
if (indexFile.files.isEmpty()) {
|
||||||
println("Warning: Index is empty!")
|
Log.warn("Index is empty!")
|
||||||
}
|
}
|
||||||
val tasks = createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side)
|
val tasks = createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side)
|
||||||
// If the side changes, invalidate EVERYTHING just in case
|
// If the side changes, invalidate EVERYTHING just in case
|
||||||
// Might not be needed, but done just to be safe
|
// Might not be needed, but done just to be safe
|
||||||
val invalidateAll = opts.side != manifest.cachedSide
|
val invalidateAll = opts.side != manifest.cachedSide
|
||||||
if (invalidateAll) {
|
if (invalidateAll) {
|
||||||
println("Side changed, invalidating all mods")
|
Log.info("Side changed, invalidating all mods")
|
||||||
}
|
}
|
||||||
tasks.forEach{ f ->
|
tasks.forEach{ f ->
|
||||||
// TODO: should linkedfile be checked as well? should this be done in the download section?
|
// TODO: should linkedfile be checked as well? should this be done in the download section?
|
||||||
@@ -326,7 +284,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
f.updateFromCache(file)
|
f.updateFromCache(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateHandler.cancelButton) {
|
if (ui.cancelButtonPressed) {
|
||||||
showCancellationDialog()
|
showCancellationDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -337,17 +295,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
val failedTaskDetails = tasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
val failedTaskDetails = tasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
||||||
if (failedTaskDetails.isNotEmpty()) {
|
if (failedTaskDetails.isNotEmpty()) {
|
||||||
errorsOccurred = true
|
errorsOccurred = true
|
||||||
val exceptionListResult: ExceptionListResult
|
when (ui.showExceptions(failedTaskDetails, tasks.size, true)) {
|
||||||
exceptionListResult = try {
|
|
||||||
ui.showExceptions(failedTaskDetails, tasks.size, true).get()
|
|
||||||
} catch (e: InterruptedException) { // Interrupted means cancelled???
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
return
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (exceptionListResult) {
|
|
||||||
ExceptionListResult.CONTINUE -> {}
|
ExceptionListResult.CONTINUE -> {}
|
||||||
ExceptionListResult.CANCEL -> {
|
ExceptionListResult.CANCEL -> {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
@@ -360,7 +308,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateHandler.cancelButton) {
|
if (ui.cancelButtonPressed) {
|
||||||
showCancellationDialog()
|
showCancellationDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -368,24 +316,60 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
// TODO: task failed function?
|
// TODO: task failed function?
|
||||||
val nonFailedFirstTasks = tasks.filter { t -> !t.failed() }.toList()
|
val nonFailedFirstTasks = tasks.filter { t -> !t.failed() }.toList()
|
||||||
val optionTasks = nonFailedFirstTasks.filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).toList()
|
val optionTasks = nonFailedFirstTasks.filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).toList()
|
||||||
// If options changed, present all options again
|
val optionsChanged = optionTasks.any(DownloadTask::isNewOptional)
|
||||||
if (stateHandler.optionsButton || optionTasks.any(DownloadTask::isNewOptional)) {
|
if (optionTasks.isNotEmpty() && !optionsChanged) {
|
||||||
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
|
if (!ui.optionsButtonPressed) {
|
||||||
val cancelledResult = ui.showOptions(ArrayList(optionTasks))
|
// TODO: this is so ugly
|
||||||
try {
|
ui.submitProgress(InstallProgress("Reconfigure optional mods?", 0,1))
|
||||||
if (cancelledResult.get()) {
|
ui.awaitOptionalButton(true)
|
||||||
cancelled = true
|
if (ui.cancelButtonPressed) {
|
||||||
// TODO: Should the UI be closed somehow??
|
showCancellationDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
// Interrupted means cancelled???
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.disableOptionsButton()
|
// If options changed, present all options again
|
||||||
|
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
|
||||||
|
handleCancellation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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?
|
// TODO: different thread pool type?
|
||||||
val threadPool = Executors.newFixedThreadPool(10)
|
val threadPool = Executors.newFixedThreadPool(10)
|
||||||
@@ -397,18 +381,15 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (i in tasks.indices) {
|
for (i in tasks.indices) {
|
||||||
var task: DownloadTask?
|
val task: DownloadTask = try {
|
||||||
task = try {
|
|
||||||
completionService.take().get()
|
completionService.take().get()
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
ui.handleException(e)
|
ui.showErrorAndExit("Interrupted when consuming download tasks", e)
|
||||||
null
|
|
||||||
} catch (e: ExecutionException) {
|
} catch (e: ExecutionException) {
|
||||||
ui.handleException(e)
|
ui.showErrorAndExit("Failed to execute download task", e)
|
||||||
null
|
|
||||||
}
|
}
|
||||||
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
|
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
|
||||||
task?.cachedFile?.let { file ->
|
task.cachedFile?.let { file ->
|
||||||
if (task.failed()) {
|
if (task.failed()) {
|
||||||
val oldFile = file.revert
|
val oldFile = file.revert
|
||||||
if (oldFile != null) {
|
if (oldFile != null) {
|
||||||
@@ -419,21 +400,15 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var progress: String
|
val exDetails = task.exceptionDetails
|
||||||
if (task != null) {
|
val progress = if (exDetails != null) {
|
||||||
val exDetails = task.exceptionDetails
|
"Failed to download ${exDetails.name}: ${exDetails.exception.message}"
|
||||||
if (exDetails != null) {
|
|
||||||
progress = "Failed to download ${exDetails.name}: ${exDetails.exception.message}"
|
|
||||||
exDetails.exception.printStackTrace()
|
|
||||||
} else {
|
|
||||||
progress = "Downloaded ${task.name}"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
progress = "Failed to download, unknown reason"
|
"Downloaded ${task.name}"
|
||||||
}
|
}
|
||||||
ui.submitProgress(InstallProgress(progress, i + 1, tasks.size))
|
ui.submitProgress(InstallProgress(progress, i + 1, tasks.size))
|
||||||
|
|
||||||
if (stateHandler.cancelButton) { // Stop all tasks, don't launch the game (it's in an invalid state!)
|
if (ui.cancelButtonPressed) { // Stop all tasks, don't launch the game (it's in an invalid state!)
|
||||||
threadPool.shutdown()
|
threadPool.shutdown()
|
||||||
cancelled = true
|
cancelled = true
|
||||||
return
|
return
|
||||||
@@ -446,18 +421,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
val failedTasks2ElectricBoogaloo = nonFailedFirstTasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
val failedTasks2ElectricBoogaloo = nonFailedFirstTasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
||||||
if (failedTasks2ElectricBoogaloo.isNotEmpty()) {
|
if (failedTasks2ElectricBoogaloo.isNotEmpty()) {
|
||||||
errorsOccurred = true
|
errorsOccurred = true
|
||||||
val exceptionListResult: ExceptionListResult
|
when (ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size, false)) {
|
||||||
exceptionListResult = try {
|
|
||||||
ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size, false).get()
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
// Interrupted means cancelled???
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
return
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (exceptionListResult) {
|
|
||||||
ExceptionListResult.CONTINUE -> {}
|
ExceptionListResult.CONTINUE -> {}
|
||||||
ExceptionListResult.CANCEL -> cancelled = true
|
ExceptionListResult.CANCEL -> cancelled = true
|
||||||
ExceptionListResult.IGNORE -> cancelledStartGame = true
|
ExceptionListResult.IGNORE -> cancelledStartGame = true
|
||||||
@@ -466,23 +430,13 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showCancellationDialog() {
|
private fun showCancellationDialog() {
|
||||||
val cancellationResult: CancellationResult
|
when (ui.showCancellationDialog()) {
|
||||||
cancellationResult = try {
|
|
||||||
ui.showCancellationDialog().get()
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
// Interrupted means cancelled???
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
return
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
ui.handleExceptionAndExit(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (cancellationResult) {
|
|
||||||
CancellationResult.QUIT -> cancelled = true
|
CancellationResult.QUIT -> cancelled = true
|
||||||
CancellationResult.CONTINUE -> cancelledStartGame = true
|
CancellationResult.CONTINUE -> cancelledStartGame = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move to UI?
|
||||||
private fun handleCancellation() {
|
private fun handleCancellation() {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
println("Update cancelled by user!")
|
println("Update cancelled by user!")
|
||||||
@@ -492,4 +446,5 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -9,6 +9,7 @@ import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
|||||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||||
import okio.Source
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
import java.io.InputStreamReader
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
||||||
class IndexFile {
|
class IndexFile {
|
||||||
@@ -43,7 +44,7 @@ class IndexFile {
|
|||||||
linkedFileURI = getNewLoc(indexUri, file)
|
linkedFileURI = getNewLoc(indexUri, file)
|
||||||
val src = getFileSource(linkedFileURI!!)
|
val src = getFileSource(linkedFileURI!!)
|
||||||
val fileStream = getHasher(hashFormat!!).getHashingSource(src)
|
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)) {
|
if (!fileStream.hashIsEqual(fileHash)) {
|
||||||
throw Exception("Invalid mod file hash")
|
throw Exception("Invalid mod file hash")
|
||||||
}
|
}
|
||||||
@@ -78,9 +79,9 @@ class IndexFile {
|
|||||||
get() {
|
get() {
|
||||||
if (metafile) {
|
if (metafile) {
|
||||||
return linkedFile?.name ?: linkedFile?.filename ?:
|
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
|
// TODO: URIs are bad
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
package link.infra.packwiz.installer.metadata
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter
|
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.metadata.hash.Hash
|
||||||
|
import link.infra.packwiz.installer.target.Side
|
||||||
|
|
||||||
class ManifestFile {
|
class ManifestFile {
|
||||||
var packFileHash: Hash? = null
|
var packFileHash: Hash? = null
|
||||||
var indexFileHash: Hash? = null
|
var indexFileHash: Hash? = null
|
||||||
var cachedFiles: MutableMap<SpaceSafeURI, File> = HashMap()
|
var cachedFiles: MutableMap<SpaceSafeURI, File> = HashMap()
|
||||||
// If the side changes, EVERYTHING invalidates. FUN!!!
|
// 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?
|
// TODO: switch to Kotlin-friendly JSON/TOML libs?
|
||||||
class File {
|
class File {
|
||||||
|
@@ -1,17 +1,20 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
package link.infra.packwiz.installer.metadata
|
||||||
|
|
||||||
|
import com.google.gson.annotations.JsonAdapter
|
||||||
import com.google.gson.annotations.SerializedName
|
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.Hash
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||||
|
import link.infra.packwiz.installer.target.Side
|
||||||
import okio.Source
|
import okio.Source
|
||||||
|
|
||||||
class ModFile {
|
class ModFile {
|
||||||
var name: String? = null
|
var name: String? = null
|
||||||
var filename: String? = null
|
var filename: String? = null
|
||||||
var side: UpdateManager.Options.Side? = null
|
var side: Side? = null
|
||||||
var download: Download? = null
|
var download: Download? = null
|
||||||
|
|
||||||
class Download {
|
class Download {
|
||||||
@@ -19,11 +22,16 @@ class ModFile {
|
|||||||
@SerializedName("hash-format")
|
@SerializedName("hash-format")
|
||||||
var hashFormat: String? = null
|
var hashFormat: String? = null
|
||||||
var hash: 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
|
var option: Option? = null
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val resolvedUpdateData = mutableMapOf<String, SpaceSafeURI>()
|
||||||
|
|
||||||
class Option {
|
class Option {
|
||||||
var optional = false
|
var optional = false
|
||||||
var description: String? = null
|
var description: String? = null
|
||||||
@@ -34,11 +42,20 @@ class ModFile {
|
|||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getSource(baseLoc: SpaceSafeURI?): Source {
|
fun getSource(baseLoc: SpaceSafeURI?): Source {
|
||||||
download?.let {
|
download?.let {
|
||||||
if (it.url == null) {
|
if (it.mode == null || it.mode == "" || it.mode == "url") {
|
||||||
throw Exception("Metadata file doesn't have a download URI")
|
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")
|
} ?: throw Exception("Metadata file doesn't have download")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,6 +14,4 @@ class PackFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var versions: Map<String, String>? = null
|
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", " ")
|
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)
|
override fun compareTo(other: SpaceSafeURI): Int = u.compareTo(other.u)
|
||||||
|
|
||||||
val scheme: String get() = u.scheme
|
val scheme: String? get() = u.scheme
|
||||||
val authority: String get() = u.authority
|
val authority: String? get() = u.authority
|
||||||
val host: String get() = u.host
|
val host: String? get() = u.host
|
||||||
|
|
||||||
@Throws(MalformedURLException::class)
|
@Throws(MalformedURLException::class)
|
||||||
fun toURL(): URL = u.toURL()
|
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
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,8 @@ object HashUtils {
|
|||||||
private val hashTypeConversion: Map<String, IHasher> = mapOf(
|
private val hashTypeConversion: Map<String, IHasher> = mapOf(
|
||||||
"sha256" to HashingSourceHasher("sha256"),
|
"sha256" to HashingSourceHasher("sha256"),
|
||||||
"sha512" to HashingSourceHasher("sha512"),
|
"sha512" to HashingSourceHasher("sha512"),
|
||||||
"murmur2" to Murmur2Hasher()
|
"murmur2" to Murmur2Hasher(),
|
||||||
|
"sha1" to HashingSourceHasher("sha1")
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@@ -5,7 +5,7 @@ import okio.Source
|
|||||||
|
|
||||||
class HashingSourceHasher internal constructor(private val type: String) : IHasher {
|
class HashingSourceHasher internal constructor(private val type: String) : IHasher {
|
||||||
// i love naming things
|
// i love naming things
|
||||||
private inner class HashingSourceGeneralHashingSource internal constructor(val delegateHashing: HashingSource) : GeneralHashingSource(delegateHashing) {
|
private inner class HashingSourceGeneralHashingSource(val delegateHashing: HashingSource) : GeneralHashingSource(delegateHashing) {
|
||||||
override val hash: Hash by lazy(LazyThreadSafetyMode.NONE) {
|
override val hash: Hash by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
HashingSourceHash(delegateHashing.hash.hex())
|
HashingSourceHash(delegateHashing.hash.hex())
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@ class HashingSourceHasher internal constructor(private val type: String) : IHash
|
|||||||
"md5" -> return HashingSourceGeneralHashingSource(HashingSource.md5(delegate))
|
"md5" -> return HashingSourceGeneralHashingSource(HashingSource.md5(delegate))
|
||||||
"sha256" -> return HashingSourceGeneralHashingSource(HashingSource.sha256(delegate))
|
"sha256" -> return HashingSourceGeneralHashingSource(HashingSource.sha256(delegate))
|
||||||
"sha512" -> return HashingSourceGeneralHashingSource(HashingSource.sha512(delegate))
|
"sha512" -> return HashingSourceGeneralHashingSource(HashingSource.sha512(delegate))
|
||||||
|
"sha1" -> return HashingSourceGeneralHashingSource(HashingSource.sha1(delegate))
|
||||||
}
|
}
|
||||||
throw RuntimeException("Invalid hash type provided")
|
throw RuntimeException("Invalid hash type provided")
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ object HandlerManager {
|
|||||||
RequestHandlerFile()
|
RequestHandlerFile()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: get rid of nullable stuff here
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getNewLoc(base: SpaceSafeURI?, loc: SpaceSafeURI?): SpaceSafeURI? {
|
fun getNewLoc(base: SpaceSafeURI?, loc: SpaceSafeURI?): SpaceSafeURI? {
|
||||||
if (loc == null) {
|
if (loc == null) {
|
||||||
@@ -32,6 +33,8 @@ object HandlerManager {
|
|||||||
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
|
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
|
||||||
// Caching system? Copy from already downloaded files?
|
// Caching system? Copy from already downloaded files?
|
||||||
|
|
||||||
|
// TODO: change to use something more idiomatic than exceptions?
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getFileSource(loc: SpaceSafeURI): Source {
|
fun getFileSource(loc: SpaceSafeURI): Source {
|
||||||
|
@@ -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
|
package link.infra.packwiz.installer.request.handlers
|
||||||
|
|
||||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import kotlin.concurrent.read
|
import kotlin.concurrent.read
|
||||||
@@ -21,7 +20,7 @@ class RequestHandlerGithub : RequestHandlerZip(true) {
|
|||||||
private val zipUriMap: MutableMap<String, SpaceSafeURI> = HashMap()
|
private val zipUriMap: MutableMap<String, SpaceSafeURI> = HashMap()
|
||||||
private val zipUriLock = ReentrantReadWriteLock()
|
private val zipUriLock = ReentrantReadWriteLock()
|
||||||
private fun getRepoName(loc: SpaceSafeURI): String? {
|
private fun getRepoName(loc: SpaceSafeURI): String? {
|
||||||
val matcher = repoMatcherPattern.matcher(loc.path)
|
val matcher = repoMatcherPattern.matcher(loc.path ?: return null)
|
||||||
return if (matcher.matches()) {
|
return if (matcher.matches()) {
|
||||||
matcher.group(1)
|
matcher.group(1)
|
||||||
} else {
|
} else {
|
||||||
@@ -47,7 +46,7 @@ class RequestHandlerGithub : RequestHandlerZip(true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getBranch(loc: SpaceSafeURI): String? {
|
private fun getBranch(loc: SpaceSafeURI): String? {
|
||||||
val matcher = branchMatcherPattern.matcher(loc.path)
|
val matcher = branchMatcherPattern.matcher(loc.path ?: return null)
|
||||||
return if (matcher.matches()) {
|
return if (matcher.matches()) {
|
||||||
matcher.group(1)
|
matcher.group(1)
|
||||||
} else {
|
} else {
|
||||||
@@ -66,6 +65,6 @@ class RequestHandlerGithub : RequestHandlerZip(true) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// TODO: more match testing?
|
// 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -25,7 +25,7 @@ abstract class RequestHandlerZip(private val modeHasFolder: Boolean) : RequestHa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ZipReader internal constructor(zip: Source) {
|
private inner class ZipReader(zip: Source) {
|
||||||
private val zis = ZipInputStream(zip.buffer().inputStream())
|
private val zis = ZipInputStream(zip.buffer().inputStream())
|
||||||
private val readFiles: MutableMap<SpaceSafeURI, Buffer> = HashMap()
|
private val readFiles: MutableMap<SpaceSafeURI, Buffer> = HashMap()
|
||||||
// Write lock implies access to ZipInputStream - only 1 thread must read at a time!
|
// Write lock implies access to ZipInputStream - only 1 thread must read at a time!
|
||||||
|
@@ -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
|
||||||
|
}
|
@@ -1,47 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import java.util.concurrent.Future
|
|
||||||
|
|
||||||
class CLIHandler : IUserInterface {
|
|
||||||
override fun handleException(e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun show(handler: InputStateHandler) {}
|
|
||||||
override fun submitProgress(progress: InstallProgress) {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
if (progress.hasProgress) {
|
|
||||||
sb.append('(')
|
|
||||||
sb.append(progress.progress)
|
|
||||||
sb.append('/')
|
|
||||||
sb.append(progress.progressTotal)
|
|
||||||
sb.append(") ")
|
|
||||||
}
|
|
||||||
sb.append(progress.message)
|
|
||||||
println(sb.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun executeManager(task: () -> Unit) {
|
|
||||||
task()
|
|
||||||
println("Finished successfully!")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showOptions(options: List<IOptionDetails>): Future<Boolean> {
|
|
||||||
for (opt in options) {
|
|
||||||
opt.optionValue = true
|
|
||||||
// TODO: implement option choice in the CLI?
|
|
||||||
println("Warning: accepting option " + opt.name + " as option choosing is not implemented in the CLI")
|
|
||||||
}
|
|
||||||
return CompletableFuture<Boolean>().apply {
|
|
||||||
complete(false) // Can't be cancelled!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
|
|
||||||
val future = CompletableFuture<ExceptionListResult>()
|
|
||||||
future.complete(ExceptionListResult.CANCEL)
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,35 +1,29 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture
|
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||||
import java.util.concurrent.Future
|
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||||
import kotlin.system.exitProcess
|
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||||
|
|
||||||
interface IUserInterface {
|
interface IUserInterface {
|
||||||
fun show(handler: InputStateHandler)
|
fun show()
|
||||||
fun handleException(e: Exception)
|
fun dispose()
|
||||||
@JvmDefault
|
|
||||||
fun handleExceptionAndExit(e: Exception) {
|
|
||||||
handleException(e)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmDefault
|
fun showErrorAndExit(message: String): Nothing {
|
||||||
fun setTitle(title: String) {}
|
showErrorAndExit(message, null)
|
||||||
|
}
|
||||||
|
fun showErrorAndExit(message: String, e: Exception?): Nothing
|
||||||
|
|
||||||
|
var title: String
|
||||||
fun submitProgress(progress: InstallProgress)
|
fun submitProgress(progress: InstallProgress)
|
||||||
fun executeManager(task: () -> Unit)
|
|
||||||
// Return true if the installation was cancelled!
|
// Return true if the installation was cancelled!
|
||||||
fun showOptions(options: List<IOptionDetails>): Future<Boolean>
|
fun showOptions(options: List<IOptionDetails>): Boolean
|
||||||
|
|
||||||
fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult>
|
fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult
|
||||||
@JvmDefault
|
fun disableOptionsButton(hasOptions: Boolean) {}
|
||||||
fun disableOptionsButton() {}
|
|
||||||
|
|
||||||
@JvmDefault
|
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
||||||
fun showCancellationDialog(): Future<CancellationResult> {
|
|
||||||
return CompletableFuture<CancellationResult>().apply {
|
fun awaitOptionalButton(showCancel: Boolean)
|
||||||
complete(CancellationResult.QUIT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class ExceptionListResult {
|
enum class ExceptionListResult {
|
||||||
CONTINUE, CANCEL, IGNORE
|
CONTINUE, CANCEL, IGNORE
|
||||||
@@ -38,4 +32,9 @@ interface IUserInterface {
|
|||||||
enum class CancellationResult {
|
enum class CancellationResult {
|
||||||
QUIT, CONTINUE
|
QUIT, CONTINUE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var optionsButtonPressed: Boolean
|
||||||
|
var cancelButtonPressed: Boolean
|
||||||
|
|
||||||
|
var firstInstall: Boolean
|
||||||
}
|
}
|
@@ -1,21 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
|
||||||
|
|
||||||
class InputStateHandler {
|
|
||||||
// TODO: convert to coroutines/locks?
|
|
||||||
@get:Synchronized
|
|
||||||
var optionsButton = false
|
|
||||||
private set
|
|
||||||
@get:Synchronized
|
|
||||||
var cancelButton = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun pressCancelButton() {
|
|
||||||
cancelButton = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun pressOptionsButton() {
|
|
||||||
optionsButton = true
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,221 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
|
||||||
import java.awt.*
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import java.util.concurrent.Future
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import javax.swing.*
|
|
||||||
import javax.swing.border.EmptyBorder
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class InstallWindow : IUserInterface {
|
|
||||||
private lateinit var frmPackwizlauncher: JFrame
|
|
||||||
private lateinit var lblProgresslabel: JLabel
|
|
||||||
private lateinit var progressBar: JProgressBar
|
|
||||||
private lateinit var btnOptions: JButton
|
|
||||||
|
|
||||||
private var inputStateHandler: InputStateHandler? = null
|
|
||||||
private var title = "Updating modpack..."
|
|
||||||
private var worker: SwingWorkerButWithPublicPublish<Unit, InstallProgress>? = null
|
|
||||||
private val aboutToCrash = AtomicBoolean()
|
|
||||||
|
|
||||||
// TODO: separate JFrame junk from IUserInterface junk?
|
|
||||||
|
|
||||||
init {
|
|
||||||
EventQueue.invokeAndWait {
|
|
||||||
frmPackwizlauncher = JFrame().apply {
|
|
||||||
title = this@InstallWindow.title
|
|
||||||
setBounds(100, 100, 493, 95)
|
|
||||||
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
|
|
||||||
setLocationRelativeTo(null)
|
|
||||||
|
|
||||||
// Progress bar and loading text
|
|
||||||
add(JPanel().apply {
|
|
||||||
border = EmptyBorder(10, 10, 10, 10)
|
|
||||||
layout = BorderLayout(0, 0)
|
|
||||||
|
|
||||||
progressBar = JProgressBar().apply {
|
|
||||||
isIndeterminate = true
|
|
||||||
}
|
|
||||||
add(progressBar, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
lblProgresslabel = JLabel("Loading...")
|
|
||||||
add(lblProgresslabel, BorderLayout.SOUTH)
|
|
||||||
}, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
add(JPanel().apply {
|
|
||||||
border = EmptyBorder(0, 5, 0, 5)
|
|
||||||
layout = GridBagLayout()
|
|
||||||
|
|
||||||
btnOptions = JButton("Optional mods...").apply {
|
|
||||||
alignmentX = Component.CENTER_ALIGNMENT
|
|
||||||
|
|
||||||
addActionListener {
|
|
||||||
text = "Loading..."
|
|
||||||
isEnabled = false
|
|
||||||
inputStateHandler?.pressOptionsButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add(btnOptions, GridBagConstraints().apply {
|
|
||||||
gridx = 0
|
|
||||||
gridy = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
add(JButton("Cancel").apply {
|
|
||||||
addActionListener {
|
|
||||||
isEnabled = false
|
|
||||||
inputStateHandler?.pressCancelButton()
|
|
||||||
}
|
|
||||||
}, GridBagConstraints().apply {
|
|
||||||
gridx = 0
|
|
||||||
gridy = 1
|
|
||||||
})
|
|
||||||
}, BorderLayout.EAST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun show(handler: InputStateHandler) {
|
|
||||||
inputStateHandler = handler
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
|
|
||||||
frmPackwizlauncher.isVisible = true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleException(e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
JOptionPane.showMessageDialog(null,
|
|
||||||
"An error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
|
|
||||||
title, JOptionPane.ERROR_MESSAGE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleExceptionAndExit(e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
// TODO: Fix this mess
|
|
||||||
// Used to prevent the done() handler of SwingWorker executing if the invokeLater hasn't happened yet
|
|
||||||
aboutToCrash.set(true)
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
JOptionPane.showMessageDialog(null,
|
|
||||||
"A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
|
|
||||||
title, JOptionPane.ERROR_MESSAGE)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
// Pause forever, so it blocks while we wait for System.exit to take effect
|
|
||||||
try {
|
|
||||||
Thread.currentThread().join()
|
|
||||||
} catch (ex: InterruptedException) { // no u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setTitle(title: String) {
|
|
||||||
this.title = title
|
|
||||||
frmPackwizlauncher.let { frame ->
|
|
||||||
EventQueue.invokeLater { frame.title = title }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun submitProgress(progress: InstallProgress) {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
if (progress.hasProgress) {
|
|
||||||
sb.append('(')
|
|
||||||
sb.append(progress.progress)
|
|
||||||
sb.append('/')
|
|
||||||
sb.append(progress.progressTotal)
|
|
||||||
sb.append(") ")
|
|
||||||
}
|
|
||||||
sb.append(progress.message)
|
|
||||||
// TODO: better logging library?
|
|
||||||
println(sb.toString())
|
|
||||||
worker?.publishPublic(progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun executeManager(task: Function0<Unit>) {
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
// TODO: rewrite this stupidity to use channels??!!!
|
|
||||||
worker = object : SwingWorkerButWithPublicPublish<Unit, InstallProgress>() {
|
|
||||||
override fun doInBackground() {
|
|
||||||
task.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun process(chunks: List<InstallProgress>) {
|
|
||||||
// Only process last chunk
|
|
||||||
if (chunks.isNotEmpty()) {
|
|
||||||
val (message, hasProgress, progress, progressTotal) = chunks[chunks.size - 1]
|
|
||||||
if (hasProgress) {
|
|
||||||
progressBar.isIndeterminate = false
|
|
||||||
progressBar.value = progress
|
|
||||||
progressBar.maximum = progressTotal
|
|
||||||
} else {
|
|
||||||
progressBar.isIndeterminate = true
|
|
||||||
progressBar.value = 0
|
|
||||||
}
|
|
||||||
lblProgresslabel.text = message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun done() {
|
|
||||||
if (aboutToCrash.get()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: a better way to do this?
|
|
||||||
frmPackwizlauncher.dispose()
|
|
||||||
println("Finished successfully!")
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
it.execute()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showOptions(options: List<IOptionDetails>): Future<Boolean> {
|
|
||||||
val future = CompletableFuture<Boolean>()
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
OptionsSelectWindow(options, future, frmPackwizlauncher).apply {
|
|
||||||
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
|
|
||||||
isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
|
|
||||||
val future = CompletableFuture<ExceptionListResult>()
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
|
|
||||||
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
|
|
||||||
isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun disableOptionsButton() {
|
|
||||||
btnOptions.apply {
|
|
||||||
text = "Optional mods..."
|
|
||||||
isEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showCancellationDialog(): Future<IUserInterface.CancellationResult> {
|
|
||||||
val future = CompletableFuture<IUserInterface.CancellationResult>()
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
val buttons = arrayOf("Quit", "Ignore")
|
|
||||||
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
|
|
||||||
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
|
|
||||||
"Cancelled installation",
|
|
||||||
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0])
|
|
||||||
future.complete(if (result == 0) IUserInterface.CancellationResult.QUIT else IUserInterface.CancellationResult.CONTINUE)
|
|
||||||
}
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
|
||||||
|
|
||||||
import javax.swing.SwingWorker
|
|
||||||
|
|
||||||
// Q: AAA WHAT HAVE YOU DONE THIS IS DISGUSTING
|
|
||||||
// A: it just makes things easier, so i can easily have one interface for CLI/GUI
|
|
||||||
// if someone has a better way to do this please PR it
|
|
||||||
abstract class SwingWorkerButWithPublicPublish<T, V> : SwingWorker<T, V>() {
|
|
||||||
@SafeVarargs
|
|
||||||
fun publishPublic(vararg chunks: V) {
|
|
||||||
publish(*chunks)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,66 @@
|
|||||||
|
package link.infra.packwiz.installer.ui.cli
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
||||||
|
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||||
|
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||||
|
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||||
|
import link.infra.packwiz.installer.util.Log
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class CLIHandler : IUserInterface {
|
||||||
|
@Volatile
|
||||||
|
override var optionsButtonPressed = false
|
||||||
|
@Volatile
|
||||||
|
override var cancelButtonPressed = false
|
||||||
|
@Volatile
|
||||||
|
override var firstInstall = false
|
||||||
|
|
||||||
|
override var title: String = ""
|
||||||
|
|
||||||
|
override fun showErrorAndExit(message: String, e: Exception?): Nothing {
|
||||||
|
if (e != null) {
|
||||||
|
Log.fatal(message, e)
|
||||||
|
} else {
|
||||||
|
Log.fatal(message)
|
||||||
|
}
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun show() {}
|
||||||
|
override fun dispose() {}
|
||||||
|
override fun submitProgress(progress: InstallProgress) {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
if (progress.hasProgress) {
|
||||||
|
sb.append('(')
|
||||||
|
sb.append(progress.progress)
|
||||||
|
sb.append('/')
|
||||||
|
sb.append(progress.progressTotal)
|
||||||
|
sb.append(") ")
|
||||||
|
}
|
||||||
|
sb.append(progress.message)
|
||||||
|
println(sb.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showOptions(options: List<IOptionDetails>): Boolean {
|
||||||
|
for (opt in options) {
|
||||||
|
opt.optionValue = true
|
||||||
|
// TODO: implement option choice in the CLI?
|
||||||
|
Log.warn("Accepting option ${opt.name} as option choosing is not implemented in the CLI")
|
||||||
|
}
|
||||||
|
return false // Can't be cancelled!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult {
|
||||||
|
println("Failed to download modpack, the following errors were encountered:")
|
||||||
|
for (ex in exceptions) {
|
||||||
|
print(ex.name + ": ")
|
||||||
|
ex.exception.printStackTrace()
|
||||||
|
}
|
||||||
|
return ExceptionListResult.CANCEL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun awaitOptionalButton(showCancel: Boolean) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui.data
|
||||||
|
|
||||||
data class ExceptionDetails(
|
data class ExceptionDetails(
|
||||||
val name: String,
|
val name: String,
|
@@ -1,4 +1,4 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui.data
|
||||||
|
|
||||||
interface IOptionDetails {
|
interface IOptionDetails {
|
||||||
val name: String
|
val name: String
|
@@ -1,4 +1,4 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui.data
|
||||||
|
|
||||||
data class InstallProgress(
|
data class InstallProgress(
|
||||||
val message: String,
|
val message: String,
|
@@ -1,5 +1,7 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui.gui
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface
|
||||||
|
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
@@ -16,7 +18,7 @@ import javax.swing.border.EmptyBorder
|
|||||||
class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFuture<IUserInterface.ExceptionListResult>, numTotal: Int, allowsIgnore: Boolean, parentWindow: JFrame?) : JDialog(parentWindow, "Failed file downloads", true) {
|
class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFuture<IUserInterface.ExceptionListResult>, numTotal: Int, allowsIgnore: Boolean, parentWindow: JFrame?) : JDialog(parentWindow, "Failed file downloads", true) {
|
||||||
private val lblExceptionStacktrace: JTextArea
|
private val lblExceptionStacktrace: JTextArea
|
||||||
|
|
||||||
private class ExceptionListModel internal constructor(private val details: List<ExceptionDetails>) : AbstractListModel<String>() {
|
private class ExceptionListModel(private val details: List<ExceptionDetails>) : AbstractListModel<String>() {
|
||||||
override fun getSize() = details.size
|
override fun getSize() = details.size
|
||||||
override fun getElementAt(index: Int) = details[index].name
|
override fun getElementAt(index: Int) = details[index].name
|
||||||
fun getExceptionAt(index: Int) = details[index].exception
|
fun getExceptionAt(index: Int) = details[index].exception
|
||||||
@@ -123,7 +125,7 @@ class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFutu
|
|||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
addActionListener {
|
addActionListener {
|
||||||
try {
|
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) {
|
} catch (e: IOException) {
|
||||||
// lol the button just won't work i guess
|
// lol the button just won't work i guess
|
||||||
} catch (e: URISyntaxException) {}
|
} catch (e: URISyntaxException) {}
|
@@ -0,0 +1,179 @@
|
|||||||
|
package link.infra.packwiz.installer.ui.gui
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
||||||
|
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||||
|
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||||
|
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
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class GUIHandler : IUserInterface {
|
||||||
|
private lateinit var frmPackwizlauncher: InstallWindow
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
override var title = "packwiz-installer"
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
EventQueue.invokeLater { frmPackwizlauncher.title = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
EventQueue.invokeAndWait {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.warn("Failed to set look and feel", e)
|
||||||
|
}
|
||||||
|
frmPackwizlauncher = InstallWindow(this).apply {
|
||||||
|
title = this@GUIHandler.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
frmPackwizlauncher.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showErrorAndExit(message: String, e: Exception?): Nothing {
|
||||||
|
val buttons = arrayOf("Quit", if (firstInstall) "Continue without installing" else "Continue without updating")
|
||||||
|
if (e != null) {
|
||||||
|
Log.fatal(message, e)
|
||||||
|
EventQueue.invokeAndWait {
|
||||||
|
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
|
||||||
|
"$message: $e",
|
||||||
|
title,
|
||||||
|
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, buttons, buttons[0])
|
||||||
|
if (result == 1) {
|
||||||
|
Log.info("User selected to continue without installing/updating, exiting with code 0...")
|
||||||
|
exitProcess(0)
|
||||||
|
} else {
|
||||||
|
Log.info("User selected to quit, exiting with code 1...")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.fatal(message)
|
||||||
|
EventQueue.invokeAndWait {
|
||||||
|
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, buttons, buttons[0])
|
||||||
|
if (result == 1) {
|
||||||
|
Log.info("User selected to continue without installing/updating, exiting with code 0...")
|
||||||
|
exitProcess(0)
|
||||||
|
} else {
|
||||||
|
Log.info("User selected to quit, exiting with code 1...")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun submitProgress(progress: InstallProgress) {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
if (progress.hasProgress) {
|
||||||
|
sb.append('(')
|
||||||
|
sb.append(progress.progress)
|
||||||
|
sb.append('/')
|
||||||
|
sb.append(progress.progressTotal)
|
||||||
|
sb.append(") ")
|
||||||
|
}
|
||||||
|
sb.append(progress.message)
|
||||||
|
Log.info(sb.toString())
|
||||||
|
EventQueue.invokeLater {
|
||||||
|
frmPackwizlauncher.displayProgress(progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showOptions(options: List<IOptionDetails>): Boolean {
|
||||||
|
val future = CompletableFuture<Boolean>()
|
||||||
|
EventQueue.invokeAndWait {
|
||||||
|
if (options.isEmpty()) {
|
||||||
|
JOptionPane.showMessageDialog(null,
|
||||||
|
"This modpack has no optional mods!",
|
||||||
|
"Optional mods", JOptionPane.INFORMATION_MESSAGE)
|
||||||
|
future.complete(false)
|
||||||
|
} else {
|
||||||
|
OptionsSelectWindow(options, future, frmPackwizlauncher).apply {
|
||||||
|
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return future.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult {
|
||||||
|
val future = CompletableFuture<ExceptionListResult>()
|
||||||
|
EventQueue.invokeLater {
|
||||||
|
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
|
||||||
|
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return future.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableOptionsButton(hasOptions: Boolean) = EventQueue.invokeLater {
|
||||||
|
frmPackwizlauncher.disableOptionsButton(hasOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showCancellationDialog(): IUserInterface.CancellationResult {
|
||||||
|
val future = CompletableFuture<IUserInterface.CancellationResult>()
|
||||||
|
EventQueue.invokeLater {
|
||||||
|
val buttons = arrayOf("Quit", "Ignore")
|
||||||
|
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
|
||||||
|
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
|
||||||
|
"Cancelled installation",
|
||||||
|
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0])
|
||||||
|
future.complete(if (result == 0) IUserInterface.CancellationResult.QUIT else IUserInterface.CancellationResult.CONTINUE)
|
||||||
|
}
|
||||||
|
return future.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun awaitOptionalButton(showCancel: Boolean) {
|
||||||
|
EventQueue.invokeAndWait {
|
||||||
|
frmPackwizlauncher.showOk(!showCancel)
|
||||||
|
}
|
||||||
|
visibleCountdownLatch.await()
|
||||||
|
optionalSelectedLatch.await()
|
||||||
|
EventQueue.invokeLater {
|
||||||
|
frmPackwizlauncher.hideOk()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,124 @@
|
|||||||
|
package link.infra.packwiz.installer.ui.gui
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Component
|
||||||
|
import java.awt.GridBagConstraints
|
||||||
|
import java.awt.GridBagLayout
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.border.EmptyBorder
|
||||||
|
|
||||||
|
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)
|
||||||
|
// Works better with tiling window managers - there isn't any reason to change window size currently anyway
|
||||||
|
isResizable = false
|
||||||
|
defaultCloseOperation = EXIT_ON_CLOSE
|
||||||
|
setLocationRelativeTo(null)
|
||||||
|
|
||||||
|
// Progress bar and loading text
|
||||||
|
add(JPanel().apply {
|
||||||
|
border = EmptyBorder(10, 10, 10, 10)
|
||||||
|
layout = BorderLayout(0, 0)
|
||||||
|
|
||||||
|
progressBar = JProgressBar().apply {
|
||||||
|
isIndeterminate = true
|
||||||
|
}
|
||||||
|
add(progressBar, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
lblProgresslabel = JLabel("Loading...")
|
||||||
|
add(lblProgresslabel, BorderLayout.SOUTH)
|
||||||
|
}, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
buttonsPanel = JPanel().apply {
|
||||||
|
border = EmptyBorder(0, 5, 0, 5)
|
||||||
|
layout = GridBagLayout()
|
||||||
|
|
||||||
|
btnOptions = JButton("Optional mods...").apply {
|
||||||
|
alignmentX = Component.CENTER_ALIGNMENT
|
||||||
|
|
||||||
|
addActionListener {
|
||||||
|
text = "Loading..."
|
||||||
|
isEnabled = false
|
||||||
|
handler.optionsButtonPressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(btnOptions, GridBagConstraints().apply {
|
||||||
|
gridx = 1
|
||||||
|
gridy = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
btnCancel = JButton("Cancel").apply {
|
||||||
|
addActionListener {
|
||||||
|
isEnabled = false
|
||||||
|
handler.cancelButtonPressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(btnCancel, GridBagConstraints().apply {
|
||||||
|
gridx = 1
|
||||||
|
gridy = 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
btnOk = JButton("Continue").apply {
|
||||||
|
addActionListener {
|
||||||
|
handler.okButtonPressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(buttonsPanel, BorderLayout.EAST)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayProgress(progress: InstallProgress) {
|
||||||
|
if (progress.hasProgress) {
|
||||||
|
progressBar.isIndeterminate = false
|
||||||
|
progressBar.value = progress.progress
|
||||||
|
progressBar.maximum = progress.progressTotal
|
||||||
|
} else {
|
||||||
|
progressBar.isIndeterminate = true
|
||||||
|
progressBar.value = 0
|
||||||
|
}
|
||||||
|
lblProgresslabel.text = progress.message
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableOptionsButton(hasOptions: Boolean) {
|
||||||
|
btnOptions.apply {
|
||||||
|
text = if (hasOptions) { "Optional mods..." } else { "No optional mods" }
|
||||||
|
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,4 +1,6 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui.gui
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||||
|
|
||||||
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
|
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
|
||||||
internal class OptionTempHandler(private val opt: IOptionDetails) : IOptionDetails {
|
internal class OptionTempHandler(private val opt: IOptionDetails) : IOptionDetails {
|
@@ -1,5 +1,6 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui.gui
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.FlowLayout
|
import java.awt.FlowLayout
|
||||||
import java.awt.event.ActionEvent
|
import java.awt.event.ActionEvent
|
||||||
@@ -18,7 +19,7 @@ class OptionsSelectWindow internal constructor(optList: List<IOptionDetails>, fu
|
|||||||
private val tableModel: OptionTableModel
|
private val tableModel: OptionTableModel
|
||||||
private val future: CompletableFuture<Boolean>
|
private val future: CompletableFuture<Boolean>
|
||||||
|
|
||||||
private class OptionTableModel internal constructor(givenOpts: List<IOptionDetails>) : TableModel {
|
private class OptionTableModel(givenOpts: List<IOptionDetails>) : TableModel {
|
||||||
private val opts: List<OptionTempHandler>
|
private val opts: List<OptionTempHandler>
|
||||||
|
|
||||||
init {
|
init {
|
38
src/main/kotlin/link/infra/packwiz/installer/util/Exts.kt
Normal file
38
src/main/kotlin/link/infra/packwiz/installer/util/Exts.kt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package link.infra.packwiz.installer.util
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface
|
||||||
|
|
||||||
|
inline fun <T> iflet(value: T?, whenNotNull: (T) -> Unit) {
|
||||||
|
if (value != null) {
|
||||||
|
whenNotNull(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, U> IUserInterface.ifletOrErr(value: T?, message: String, whenNotNull: (T) -> U): U =
|
||||||
|
if (value != null) {
|
||||||
|
whenNotNull(value)
|
||||||
|
} else {
|
||||||
|
this.showErrorAndExit(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, U, V> IUserInterface.ifletOrErr(value: T?, value2: U?, message: String, whenNotNull: (T, U) -> V): V =
|
||||||
|
if (value != null && value2 != null) {
|
||||||
|
whenNotNull(value, value2)
|
||||||
|
} else {
|
||||||
|
this.showErrorAndExit(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> ifletOrWarn(value: T?, message: String, whenNotNull: (T) -> Unit) {
|
||||||
|
if (value != null) {
|
||||||
|
whenNotNull(value)
|
||||||
|
} else {
|
||||||
|
Log.warn(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T, U> iflet(value: T?, whenNotNull: (T) -> U, whenNull: () -> U): U =
|
||||||
|
if (value != null) {
|
||||||
|
whenNotNull(value)
|
||||||
|
} else {
|
||||||
|
whenNull()
|
||||||
|
}
|
16
src/main/kotlin/link/infra/packwiz/installer/util/Log.kt
Normal file
16
src/main/kotlin/link/infra/packwiz/installer/util/Log.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package link.infra.packwiz.installer.util
|
||||||
|
|
||||||
|
object Log {
|
||||||
|
fun info(message: String) = println(message)
|
||||||
|
|
||||||
|
fun warn(message: String) = println("[Warning] $message")
|
||||||
|
fun warn(message: String, exception: Exception) = println("[Warning] $message: $exception")
|
||||||
|
|
||||||
|
fun fatal(message: String) {
|
||||||
|
println("[FATAL] $message")
|
||||||
|
}
|
||||||
|
fun fatal(message: String, exception: Exception) {
|
||||||
|
println("[FATAL] $message: ")
|
||||||
|
exception.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
266
src/main/resources/META-INF/LICENSES.md
Normal file
266
src/main/resources/META-INF/LICENSES.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# Licenses
|
||||||
|
|
||||||
|
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))
|
||||||
|
- 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))
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Commons CLI
|
||||||
|
Apache Commons CLI
|
||||||
|
Copyright 2001-2017 The Apache Software Foundation
|
||||||
|
|
||||||
|
This product includes software developed at
|
||||||
|
The Apache Software Foundation (http://www.apache.org/).
|
||||||
|
|
||||||
|
## Full license texts
|
||||||
|
|
||||||
|
### MIT
|
||||||
|
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
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
### Apache 2.0
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
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