mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-10-16 16:04:32 +02:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
73d21a475a | ||
|
7568770078 | ||
|
3d1d6db9b4 | ||
|
c6e304bc7f | ||
|
92d6f68f1d | ||
|
07af6046c1 | ||
|
89bdfd9c98 | ||
|
f4dd4fa866 | ||
|
6db8422c87 | ||
|
7d6346c088 | ||
|
aff921f67e | ||
|
afb574d82d | ||
|
8635906b1c | ||
|
bf95f03a18 | ||
|
bca2d758e1 | ||
|
46771ce870 | ||
|
b143f67acd |
102
build.gradle.kts
102
build.gradle.kts
@@ -1,9 +1,9 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.guardsquare:proguard-gradle:7.0.0") {
|
classpath("com.guardsquare:proguard-gradle:7.1.0") {
|
||||||
exclude("com.android.tools.build")
|
exclude("com.android.tools.build")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,30 +12,30 @@ buildscript {
|
|||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
application
|
application
|
||||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||||
id("com.palantir.git-version") version "0.12.3"
|
id("com.palantir.git-version") version "0.13.0"
|
||||||
id("com.github.breadmoirai.github-release") version "2.2.12"
|
id("com.github.breadmoirai.github-release") version "2.2.12"
|
||||||
kotlin("jvm") version "1.4.21"
|
kotlin("jvm") version "1.6.10"
|
||||||
id("com.github.jk1.dependency-license-report") version "1.16"
|
id("com.github.jk1.dependency-license-report") version "2.0"
|
||||||
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
val shrinkClasspath: Configuration by configurations.creating
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
dependencies {
|
|
||||||
implementation("commons-cli:commons-cli:1.4")
|
|
||||||
shrinkClasspath("commons-cli:commons-cli:1.4")
|
|
||||||
implementation("com.moandjiezana.toml:toml4j:0.7.2")
|
|
||||||
implementation("com.google.code.gson:gson:2.8.1")
|
|
||||||
implementation("com.squareup.okio:okio:2.9.0")
|
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
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 {
|
||||||
@@ -59,41 +59,47 @@ licenseReport {
|
|||||||
filters = arrayOf<com.github.jk1.license.filter.DependencyFilter>(com.github.jk1.license.filter.LicenseBundleNormalizer())
|
filters = arrayOf<com.github.jk1.license.filter.DependencyFilter>(com.github.jk1.license.filter.LicenseBundleNormalizer())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: build relocated jar for minecraft launcher lib, non-relocated jar for packwiz-installer
|
|
||||||
//tasks.register<com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation>("relocateShadowJar") {
|
|
||||||
// target = tasks.shadowJar.get()
|
|
||||||
// prefix = "link.infra.packwiz.deps"
|
|
||||||
//}
|
|
||||||
|
|
||||||
// Commons CLI and Minimal JSON are already included in packwiz-installer-bootstrap
|
|
||||||
tasks.shadowJar {
|
tasks.shadowJar {
|
||||||
dependencies {
|
|
||||||
exclude(dependency("commons-cli:commons-cli:1.4"))
|
|
||||||
exclude(dependency("com.eclipsesource.minimal-json:minimal-json:0.9.5"))
|
|
||||||
// TODO: exclude meta inf files
|
|
||||||
}
|
|
||||||
exclude("**/*.kotlin_metadata")
|
exclude("**/*.kotlin_metadata")
|
||||||
exclude("**/*.kotlin_builtins")
|
exclude("**/*.kotlin_builtins")
|
||||||
exclude("META-INF/maven/**/*")
|
exclude("META-INF/maven/**/*")
|
||||||
exclude("META-INF/proguard/**/*")
|
exclude("META-INF/proguard/**/*")
|
||||||
//dependsOn(tasks.named("relocateShadowJar"))
|
|
||||||
|
// Relocate Commons CLI, so that it doesn't clash with old packwiz-installer-bootstrap (that shades it)
|
||||||
|
relocate("org.apache.commons.cli", "link.infra.packwiz.installer.deps.commons-cli")
|
||||||
|
|
||||||
|
// from Commons CLI
|
||||||
|
exclude("META-INF/LICENSE.txt")
|
||||||
|
exclude("META-INF/NOTICE.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<proguard.gradle.ProGuardTask>("shrinkJar") {
|
tasks.register<proguard.gradle.ProGuardTask>("shrinkJar") {
|
||||||
injars(tasks.shadowJar)
|
injars(tasks.shadowJar)
|
||||||
libraryjars(files(shrinkClasspath.files))
|
|
||||||
outjars("build/libs/" + tasks.shadowJar.get().outputs.files.first().name.removeSuffix(".jar") + "-shrink.jar")
|
outjars("build/libs/" + tasks.shadowJar.get().outputs.files.first().name.removeSuffix(".jar") + "-shrink.jar")
|
||||||
if (System.getProperty("java.version").startsWith("1.")) {
|
if (System.getProperty("java.version").startsWith("1.")) {
|
||||||
libraryjars("${System.getProperty("java.home")}/lib/rt.jar")
|
libraryjars("${System.getProperty("java.home")}/lib/rt.jar")
|
||||||
libraryjars("${System.getProperty("java.home")}/lib/jce.jar")
|
libraryjars("${System.getProperty("java.home")}/lib/jce.jar")
|
||||||
} else {
|
} else {
|
||||||
throw RuntimeException("Compiling with Java 9+ not supported!")
|
// Use jmods for Java 9+
|
||||||
|
val mods = listOf("java.base", "java.logging", "java.desktop", "java.sql")
|
||||||
|
for (mod in mods) {
|
||||||
|
libraryjars(mapOf(
|
||||||
|
"jarfilter" to "!**.jar",
|
||||||
|
"filter" to "!module-info.class"
|
||||||
|
), "${System.getProperty("java.home")}/jmods/$mod.jmod")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keep("class link.infra.packwiz.installer.** { *; }")
|
keep("class link.infra.packwiz.installer.** { *; }")
|
||||||
dontoptimize()
|
dontoptimize()
|
||||||
dontobfuscate()
|
dontobfuscate()
|
||||||
|
|
||||||
|
// Used by Okio and OkHttp
|
||||||
dontwarn("org.codehaus.mojo.animal_sniffer.*")
|
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
|
||||||
@@ -126,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-6.7.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,19 +6,20 @@ import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
|||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
import link.infra.packwiz.installer.metadata.hash.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.target.Side
|
||||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||||
import link.infra.packwiz.installer.util.Log
|
import link.infra.packwiz.installer.util.Log
|
||||||
import okio.Buffer
|
import okio.*
|
||||||
import okio.HashingSink
|
import okio.Path.Companion.toOkioPath
|
||||||
import okio.buffer
|
|
||||||
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
|
||||||
@@ -26,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
|
||||||
@@ -123,27 +124,77 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the file in the destination location is already valid
|
||||||
|
* Must be done after metadata retrieval
|
||||||
|
*/
|
||||||
|
fun validateExistingFile(packFolder: String) {
|
||||||
|
if (!alreadyUpToDate) {
|
||||||
|
try {
|
||||||
|
// TODO: only do this for files that didn't exist before or have been modified since last full update?
|
||||||
|
val destPath = Paths.get(packFolder, metadata.destURI.toString())
|
||||||
|
FileSystem.SYSTEM.source(destPath.toOkioPath()).buffer().use { src ->
|
||||||
|
val hash: Hash
|
||||||
|
val fileHashFormat: String
|
||||||
|
val linkedFile = metadata.linkedFile
|
||||||
|
|
||||||
|
if (linkedFile != null) {
|
||||||
|
hash = linkedFile.hash
|
||||||
|
fileHashFormat = linkedFile.download!!.hashFormat!!
|
||||||
|
} else {
|
||||||
|
hash = metadata.getHashObj()
|
||||||
|
fileHashFormat = metadata.hashFormat!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileSource = getHasher(fileHashFormat).getHashingSource(src)
|
||||||
|
fileSource.buffer().readAll(blackholeSink())
|
||||||
|
if (fileSource.hashIsEqual(hash)) {
|
||||||
|
alreadyUpToDate = true
|
||||||
|
|
||||||
|
// Update the manifest file
|
||||||
|
cachedFile = (cachedFile ?: ManifestFile.File()).also {
|
||||||
|
try {
|
||||||
|
it.hash = metadata.getHashObj()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
err = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
it.isOptional = isOptional
|
||||||
|
it.cachedLocation = metadata.destURI.toString()
|
||||||
|
metadata.linkedFile?.let { linked ->
|
||||||
|
try {
|
||||||
|
it.linkedFileHash = linked.hash
|
||||||
|
} catch (e: Exception) {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// Ignore exceptions; if the file doesn't exist we'll be downloading it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun download(packFolder: String, indexUri: SpaceSafeURI) {
|
fun download(packFolder: String, indexUri: SpaceSafeURI) {
|
||||||
if (err != null) return
|
if (err != null) return
|
||||||
|
|
||||||
// TODO: is this necessary if we overwrite?
|
// Ensure wrong-side or optional false files are removed
|
||||||
// Ensure it is 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)
|
||||||
Log.warn("Failed to delete file before downloading", e)
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -163,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)
|
||||||
@@ -183,7 +234,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
|||||||
// isDirectory follows symlinks, but createDirectories doesn't
|
// isDirectory follows symlinks, but createDirectories doesn't
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(destPath.parent)
|
Files.createDirectories(destPath.parent)
|
||||||
} catch (e: FileAlreadyExistsException) {
|
} catch (e: java.nio.file.FileAlreadyExistsException) {
|
||||||
if (!Files.isDirectory(destPath.parent)) {
|
if (!Files.isDirectory(destPath.parent)) {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@@ -196,7 +247,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
|
|||||||
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!")
|
||||||
@@ -241,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,6 +3,7 @@
|
|||||||
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.target.Side
|
||||||
import link.infra.packwiz.installer.ui.cli.CLIHandler
|
import link.infra.packwiz.installer.ui.cli.CLIHandler
|
||||||
import link.infra.packwiz.installer.ui.gui.GUIHandler
|
import link.infra.packwiz.installer.ui.gui.GUIHandler
|
||||||
import link.infra.packwiz.installer.util.Log
|
import link.infra.packwiz.installer.util.Log
|
||||||
@@ -68,7 +69,7 @@ class Main(args: Array<String>) {
|
|||||||
val uOptions = try {
|
val uOptions = try {
|
||||||
UpdateManager.Options.construct(
|
UpdateManager.Options.construct(
|
||||||
downloadURI = SpaceSafeURI(unparsedArgs[0]),
|
downloadURI = SpaceSafeURI(unparsedArgs[0]),
|
||||||
side = cmd.getOptionValue("side")?.let((UpdateManager.Options.Side)::from),
|
side = cmd.getOptionValue("side")?.let((Side)::from),
|
||||||
packFolder = cmd.getOptionValue("pack-folder"),
|
packFolder = cmd.getOptionValue("pack-folder"),
|
||||||
manifestFile = cmd.getOptionValue("meta-file")
|
manifestFile = cmd.getOptionValue("meta-file")
|
||||||
)
|
)
|
||||||
|
@@ -3,33 +3,29 @@ 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.data.InstallProgress
|
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||||
import link.infra.packwiz.installer.util.Log
|
import link.infra.packwiz.installer.util.Log
|
||||||
import link.infra.packwiz.installer.util.ifletOrErr
|
import link.infra.packwiz.installer.util.ifletOrErr
|
||||||
import link.infra.packwiz.installer.util.ifletOrWarn
|
|
||||||
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
|
||||||
@@ -57,56 +53,6 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
Options(downloadURI, manifestFile ?: "packwiz.json", packFolder ?: ".", side ?: Side.CLIENT)
|
Options(downloadURI, manifestFile ?: "packwiz.json", packFolder ?: ".", side ?: Side.CLIENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Side {
|
|
||||||
@SerializedName("client")
|
|
||||||
CLIENT("client"),
|
|
||||||
@SerializedName("server")
|
|
||||||
SERVER("server"),
|
|
||||||
@SerializedName("both")
|
|
||||||
@Suppress("unused")
|
|
||||||
BOTH("both", arrayOf(CLIENT, SERVER));
|
|
||||||
|
|
||||||
private val sideName: String
|
|
||||||
private val depSides: Array<Side>?
|
|
||||||
|
|
||||||
constructor(sideName: String) {
|
|
||||||
this.sideName = sideName.toLowerCase()
|
|
||||||
depSides = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(sideName: String, depSides: Array<Side>) {
|
|
||||||
this.sideName = sideName.toLowerCase()
|
|
||||||
this.depSides = depSides
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = sideName
|
|
||||||
|
|
||||||
fun hasSide(tSide: Side): Boolean {
|
|
||||||
if (this == tSide) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (depSides != null) {
|
|
||||||
for (depSide in depSides) {
|
|
||||||
if (depSide == tSide) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(name: String): Side? {
|
|
||||||
val lowerName = name.toLowerCase()
|
|
||||||
for (side in values()) {
|
|
||||||
if (side.sideName == lowerName) {
|
|
||||||
return side
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun start() {
|
private fun start() {
|
||||||
@@ -140,7 +86,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
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.showErrorAndExit("Failed to parse pack.toml", e)
|
ui.showErrorAndExit("Failed to parse pack.toml", e)
|
||||||
}
|
}
|
||||||
@@ -180,8 +126,11 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
|
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
|
||||||
Log.info("Modpack is already up to date!")
|
|
||||||
// todo: --force?
|
// todo: --force?
|
||||||
|
ui.submitProgress(InstallProgress("Modpack is already up to date!", 1, 1))
|
||||||
|
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
||||||
|
ui.awaitOptionalButton(false)
|
||||||
|
}
|
||||||
if (!ui.optionsButtonPressed) {
|
if (!ui.optionsButtonPressed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -194,7 +143,8 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
handleCancellation()
|
handleCancellation()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ifletOrWarn(pf.index, "No index file found") { index ->
|
// TODO: switch to OkHttp for better redirect handling
|
||||||
|
ui.ifletOrErr(pf.index, "No index file found, or the pack file is empty; note that Java doesn't automatically follow redirects from HTTP to HTTPS (and may cause this error)") { index ->
|
||||||
ui.ifletOrErr(index.hashFormat, index.hash, "Pack has no hash or hashFormat for index") { hashFormat, hash ->
|
ui.ifletOrErr(index.hashFormat, index.hash, "Pack has no hash or hashFormat for index") { hashFormat, hash ->
|
||||||
ui.ifletOrErr(getNewLoc(opts.downloadURI, index.file), "Pack has invalid index file: " + index.file) { newLoc ->
|
ui.ifletOrErr(getNewLoc(opts.downloadURI, index.file), "Pack has invalid index file: " + index.file) { newLoc ->
|
||||||
processIndex(
|
processIndex(
|
||||||
@@ -237,10 +187,17 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
|
|
||||||
private fun processIndex(indexUri: SpaceSafeURI, indexHash: Hash, hashFormat: String, manifest: ManifestFile, invalidatedUris: List<SpaceSafeURI>) {
|
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()) {
|
||||||
Log.info("Modpack files are already up to date!")
|
ui.submitProgress(InstallProgress("Modpack files are already up to date!", 1, 1))
|
||||||
|
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
||||||
|
ui.awaitOptionalButton(false)
|
||||||
|
}
|
||||||
if (!ui.optionsButtonPressed) {
|
if (!ui.optionsButtonPressed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (ui.cancelButtonPressed) {
|
||||||
|
showCancellationDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
manifest.indexFileHash = indexHash
|
manifest.indexFileHash = indexHash
|
||||||
|
|
||||||
@@ -252,7 +209,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.showErrorAndExit("Failed to parse index file", e)
|
ui.showErrorAndExit("Failed to parse index file", e)
|
||||||
}
|
}
|
||||||
@@ -359,8 +316,20 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
// TODO: task failed function?
|
// 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()
|
||||||
|
val optionsChanged = optionTasks.any(DownloadTask::isNewOptional)
|
||||||
|
if (optionTasks.isNotEmpty() && !optionsChanged) {
|
||||||
|
if (!ui.optionsButtonPressed) {
|
||||||
|
// TODO: this is so ugly
|
||||||
|
ui.submitProgress(InstallProgress("Reconfigure optional mods?", 0,1))
|
||||||
|
ui.awaitOptionalButton(true)
|
||||||
|
if (ui.cancelButtonPressed) {
|
||||||
|
showCancellationDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// If options changed, present all options again
|
// If options changed, present all options again
|
||||||
if (ui.optionsButtonPressed || optionTasks.any(DownloadTask::isNewOptional)) {
|
if (ui.optionsButtonPressed || optionsChanged) {
|
||||||
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
|
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
|
||||||
if (ui.showOptions(ArrayList(optionTasks))) {
|
if (ui.showOptions(ArrayList(optionTasks))) {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
@@ -370,6 +339,38 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
|
|||||||
// TODO: keep this enabled? then apply changes after download process?
|
// TODO: keep this enabled? then apply changes after download process?
|
||||||
ui.disableOptionsButton(optionTasks.isNotEmpty())
|
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)
|
||||||
val completionService: CompletionService<DownloadTask> = ExecutorCompletionService(threadPool)
|
val completionService: CompletionService<DownloadTask> = ExecutorCompletionService(threadPool)
|
||||||
@@ -445,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
|
||||||
|
}
|
||||||
|
}
|
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
package link.infra.packwiz.installer.target
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
data class CachedTarget(
|
||||||
|
/**
|
||||||
|
* @see Target.name
|
||||||
|
*/
|
||||||
|
val name: String,
|
||||||
|
/**
|
||||||
|
* The location where the target was last downloaded to.
|
||||||
|
* This is used for removing old files when the destination path changes.
|
||||||
|
* This shouldn't be set to the .disabled path (that is manually appended and checked)
|
||||||
|
*/
|
||||||
|
val cachedLocation: Path,
|
||||||
|
val enabled: Boolean,
|
||||||
|
val hash: Hash,
|
||||||
|
/**
|
||||||
|
* For detecting when a target transitions non-optional -> optional and showing the option selection screen
|
||||||
|
*/
|
||||||
|
val isOptional: Boolean
|
||||||
|
)
|
@@ -0,0 +1,95 @@
|
|||||||
|
package link.infra.packwiz.installer.target
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.file.*
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
|
import kotlin.io.path.relativeTo
|
||||||
|
|
||||||
|
data class CachedTargetStatus(val target: CachedTarget, var isValid: Boolean, var markDisabled: Boolean)
|
||||||
|
|
||||||
|
fun validate(targets: List<CachedTarget>, baseDir: Path) = runCatching {
|
||||||
|
val results = targets.map {
|
||||||
|
CachedTargetStatus(it, isValid = false, markDisabled = false)
|
||||||
|
}
|
||||||
|
val tree = buildTree(results, baseDir)
|
||||||
|
|
||||||
|
// Efficient file exists checking using directory listing, several orders of magnitude faster than Files.exists calls
|
||||||
|
Files.walkFileTree(baseDir, setOf(FileVisitOption.FOLLOW_LINKS), Int.MAX_VALUE, object : FileVisitor<Path> {
|
||||||
|
var currentNode: PathNode<CachedTargetStatus> = tree
|
||||||
|
|
||||||
|
override fun preVisitDirectory(dir: Path?, attrs: BasicFileAttributes?): FileVisitResult {
|
||||||
|
if (dir == null) {
|
||||||
|
return FileVisitResult.SKIP_SUBTREE
|
||||||
|
}
|
||||||
|
val subdirNode = currentNode.subdirs[dir.getName(dir.nameCount - 1)]
|
||||||
|
return if (subdirNode != null) {
|
||||||
|
currentNode = subdirNode
|
||||||
|
FileVisitResult.CONTINUE
|
||||||
|
} else {
|
||||||
|
FileVisitResult.SKIP_SUBTREE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun visitFile(file: Path?, attrs: BasicFileAttributes?): FileVisitResult {
|
||||||
|
if (file == null) {
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
// TODO: these are relative paths to baseDir
|
||||||
|
// TODO: strip the .disabled for lookup
|
||||||
|
val target = currentNode.files[file.getName(file.nameCount - 1)]
|
||||||
|
if (target != null) {
|
||||||
|
val disabledFile = file.endsWith(".disabled")
|
||||||
|
// If a .disabled file and the actual file both exist, mark as invalid if the target is disabled
|
||||||
|
if ((disabledFile )) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun visitFileFailed(file: Path?, exc: IOException?): FileVisitResult {
|
||||||
|
if (exc != null) {
|
||||||
|
throw exc
|
||||||
|
}
|
||||||
|
throw IOException("visitFileFailed called with no exception")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun postVisitDirectory(dir: Path?, exc: IOException?): FileVisitResult {
|
||||||
|
if (exc != null) {
|
||||||
|
throw exc
|
||||||
|
} else {
|
||||||
|
val parent = currentNode.parent
|
||||||
|
if (parent != null) {
|
||||||
|
currentNode = parent
|
||||||
|
} else {
|
||||||
|
throw IOException("Invalid visitor tree structure")
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildTree(targets: List<CachedTargetStatus>, baseDir: Path): PathNode<CachedTargetStatus> {
|
||||||
|
val root = PathNode<CachedTargetStatus>()
|
||||||
|
for (target in targets) {
|
||||||
|
val relPath = target.target.cachedLocation.relativeTo(baseDir)
|
||||||
|
var node = root
|
||||||
|
// Traverse all the directory components, except for the last one
|
||||||
|
for (i in 0 until (relPath.nameCount - 1)) {
|
||||||
|
node = node.createSubdir(relPath.getName(i))
|
||||||
|
}
|
||||||
|
node.files[relPath.getName(relPath.nameCount - 1)] = target
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PathNode<T>(val subdirs: MutableMap<Path, PathNode<T>>, val files: MutableMap<Path, T>, val parent: PathNode<T>?) {
|
||||||
|
constructor() : this(mutableMapOf(), mutableMapOf(), null)
|
||||||
|
|
||||||
|
fun createSubdir(nextComponent: Path) = subdirs.getOrPut(nextComponent, { PathNode(mutableMapOf(), mutableMapOf(), this) })
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
package link.infra.packwiz.installer.target
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okio.FileSystem
|
||||||
|
|
||||||
|
class ClientHolder {
|
||||||
|
// TODO: timeouts?
|
||||||
|
// TODO: a button to increase timeouts temporarily when retrying?
|
||||||
|
val okHttpClient by lazy { OkHttpClient.Builder().build() }
|
||||||
|
|
||||||
|
val fileSystem = FileSystem.SYSTEM
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package link.infra.packwiz.installer.target
|
||||||
|
|
||||||
|
enum class OverwriteMode {
|
||||||
|
/**
|
||||||
|
* Overwrite the destination with the source file, if the source file has changed.
|
||||||
|
*/
|
||||||
|
IF_SRC_CHANGED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Never overwrite the destination; if it exists, it should not be written to.
|
||||||
|
*/
|
||||||
|
NEVER
|
||||||
|
}
|
54
src/main/kotlin/link/infra/packwiz/installer/target/Side.kt
Normal file
54
src/main/kotlin/link/infra/packwiz/installer/target/Side.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package link.infra.packwiz.installer.target
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
enum class Side {
|
||||||
|
@SerializedName("client")
|
||||||
|
CLIENT("client"),
|
||||||
|
@SerializedName("server")
|
||||||
|
SERVER("server"),
|
||||||
|
@SerializedName("both")
|
||||||
|
@Suppress("unused")
|
||||||
|
BOTH("both", arrayOf(CLIENT, SERVER));
|
||||||
|
|
||||||
|
private val sideName: String
|
||||||
|
private val depSides: Array<Side>?
|
||||||
|
|
||||||
|
constructor(sideName: String) {
|
||||||
|
this.sideName = sideName.toLowerCase()
|
||||||
|
depSides = null
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(sideName: String, depSides: Array<Side>) {
|
||||||
|
this.sideName = sideName.toLowerCase()
|
||||||
|
this.depSides = depSides
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString() = sideName
|
||||||
|
|
||||||
|
fun hasSide(tSide: Side): Boolean {
|
||||||
|
if (this == tSide) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (depSides != null) {
|
||||||
|
for (depSide in depSides) {
|
||||||
|
if (depSide == tSide) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(name: String): Side? {
|
||||||
|
val lowerName = name.toLowerCase()
|
||||||
|
for (side in values()) {
|
||||||
|
if (side.sideName == lowerName) {
|
||||||
|
return side
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,45 @@
|
|||||||
|
package link.infra.packwiz.installer.target
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||||
|
|
||||||
|
// TODO: rename to avoid conflicting with @Target
|
||||||
|
interface Target {
|
||||||
|
val src: PackwizPath
|
||||||
|
val dest: PackwizPath
|
||||||
|
val validityToken: ValidityToken
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token interface for types used to compare Target identity. Implementations should use equals to indicate that
|
||||||
|
* these tokens represent the same file; used to preserve optional target choices between file renames.
|
||||||
|
*/
|
||||||
|
interface IdentityToken
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of IdentityToken that assumes files are not renamed; so optional choices do not need to
|
||||||
|
* be preserved across renames.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
value class PathIdentityToken(val path: String): IdentityToken
|
||||||
|
|
||||||
|
val ident: IdentityToken
|
||||||
|
get() = PathIdentityToken(dest.path)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user-friendly name; defaults to the destination path of the file.
|
||||||
|
*/
|
||||||
|
val name: String
|
||||||
|
get() = dest.path
|
||||||
|
val side: Side
|
||||||
|
get() = Side.BOTH
|
||||||
|
val overwriteMode: OverwriteMode
|
||||||
|
get() = OverwriteMode.IF_SRC_CHANGED
|
||||||
|
|
||||||
|
interface Optional: Target {
|
||||||
|
val optional: Boolean
|
||||||
|
val optionalDefaultValue: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionalDescribed: Optional {
|
||||||
|
val optionalDescription: String
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
package link.infra.packwiz.installer.target
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A token used to determine if the source or destination file is changed, and ensure that the destination file is valid.
|
||||||
|
*/
|
||||||
|
interface ValidityToken {
|
||||||
|
// TODO: functions to allow validating this from file metadata, from file, or during the download process
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of ValidityToken based on a single hash.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
value class HashValidityToken(val hash: Hash): ValidityToken
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
package link.infra.packwiz.installer.target.path
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.request.RequestException
|
||||||
|
import link.infra.packwiz.installer.target.ClientHolder
|
||||||
|
import okio.*
|
||||||
|
|
||||||
|
data class FilePathBase(private val path: Path): PackwizPath.Base, PackwizPath.SinkableBase {
|
||||||
|
override fun source(path: String, clientHolder: ClientHolder): BufferedSource {
|
||||||
|
val resolved = this.path.resolve(path, true)
|
||||||
|
try {
|
||||||
|
return clientHolder.fileSystem.source(resolved).buffer()
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
throw RequestException.Response.File.FileNotFound(resolved.toString())
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RequestException.Response.File.Other(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sink(path: String, clientHolder: ClientHolder): BufferedSink {
|
||||||
|
val resolved = this.path.resolve(path, true)
|
||||||
|
try {
|
||||||
|
return clientHolder.fileSystem.sink(resolved).buffer()
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
throw RequestException.Response.File.FileNotFound(resolved.toString())
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RequestException.Response.File.Other(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
package link.infra.packwiz.installer.target.path
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.request.RequestException
|
||||||
|
import link.infra.packwiz.installer.target.ClientHolder
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.IOException
|
||||||
|
|
||||||
|
data class HttpUrlBase(private val url: HttpUrl): PackwizPath.Base {
|
||||||
|
private fun resolve(path: String) = url.newBuilder().addPathSegments(path).build()
|
||||||
|
|
||||||
|
override fun source(path: String, clientHolder: ClientHolder): BufferedSource {
|
||||||
|
val req = Request.Builder()
|
||||||
|
.url(resolve(path))
|
||||||
|
.header("Accept", "application/octet-stream")
|
||||||
|
.header("User-Agent", "packwiz-installer")
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
try {
|
||||||
|
val res = clientHolder.okHttpClient.newCall(req).execute()
|
||||||
|
// Can't use .use since it would close the response body before returning it to the caller
|
||||||
|
try {
|
||||||
|
if (!res.isSuccessful) {
|
||||||
|
throw RequestException.Response.HTTP.ErrorCode(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = res.body ?: throw RequestException.Internal.HTTP.NoResponseBody()
|
||||||
|
return body.source()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If an exception is thrown, close the response and rethrow
|
||||||
|
res.close()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RequestException.Internal.HTTP.RequestFailed(e)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
throw RequestException.Internal.HTTP.IllegalState(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,124 @@
|
|||||||
|
package link.infra.packwiz.installer.target.path
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.request.RequestException
|
||||||
|
import link.infra.packwiz.installer.target.ClientHolder
|
||||||
|
import okio.BufferedSink
|
||||||
|
import okio.BufferedSource
|
||||||
|
|
||||||
|
class PackwizPath(path: String, base: Base) {
|
||||||
|
val path: String
|
||||||
|
val base: Base
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.base = base
|
||||||
|
|
||||||
|
// Check for NUL bytes
|
||||||
|
if (path.contains('\u0000')) { throw RequestException.Validation.PathContainsNUL(path) }
|
||||||
|
// Normalise separator, to prevent differences between Unix/Windows
|
||||||
|
val pathNorm = path.replace('\\', '/')
|
||||||
|
// Split, create new lists for output
|
||||||
|
val split = pathNorm.split('/')
|
||||||
|
val canonicalised = mutableListOf<String>()
|
||||||
|
|
||||||
|
// Backward pass: collapse ".." components, remove "." components and empty components (except an empty component at the end; indicating a folder)
|
||||||
|
var parentComponentCount = 0
|
||||||
|
var first = true
|
||||||
|
for (component in split.asReversed()) {
|
||||||
|
if (first) {
|
||||||
|
first = false
|
||||||
|
if (component == "") {
|
||||||
|
canonicalised += component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// URL-encoded . is normalised
|
||||||
|
val componentNorm = component.replace("%2e", ".")
|
||||||
|
if (componentNorm == "." || componentNorm == "") {
|
||||||
|
// Do nothing
|
||||||
|
} else if (componentNorm == "..") {
|
||||||
|
parentComponentCount++
|
||||||
|
} else if (parentComponentCount > 0) {
|
||||||
|
parentComponentCount--
|
||||||
|
} else {
|
||||||
|
canonicalised += componentNorm
|
||||||
|
// Don't allow volume letters (allows traversal to the root on Windows)
|
||||||
|
if (componentNorm[0] in 'a'..'z' || componentNorm[0] in 'A'..'Z') {
|
||||||
|
if (componentNorm[1] == ':') {
|
||||||
|
throw RequestException.Validation.PathContainsVolumeLetter(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join path
|
||||||
|
this.path = canonicalised.asReversed().joinToString("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
val folder: Boolean get() = path.endsWith("/")
|
||||||
|
|
||||||
|
fun resolve(path: String): PackwizPath {
|
||||||
|
return if (path.startsWith('/') || path.startsWith('\\')) {
|
||||||
|
// Absolute (but still relative to base of pack)
|
||||||
|
PackwizPath(path, base)
|
||||||
|
} else if (folder) {
|
||||||
|
// File in folder; append
|
||||||
|
PackwizPath(this.path + path, base)
|
||||||
|
} else {
|
||||||
|
// File in parent folder; append with parent component
|
||||||
|
PackwizPath(this.path + "/../" + path, base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain a BufferedSource for this path
|
||||||
|
* @throws RequestException When resolving the file failed
|
||||||
|
*/
|
||||||
|
fun source(clientHolder: ClientHolder): BufferedSource = base.source(path, clientHolder)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain a BufferedSink for this path
|
||||||
|
* @throws RequestException.Internal.UnsinkableBase When the base of this path does not have a sink
|
||||||
|
* @throws RequestException When resolving the file failed
|
||||||
|
*/
|
||||||
|
fun sink(clientHolder: ClientHolder): BufferedSink =
|
||||||
|
if (base is SinkableBase) { base.sink(path, clientHolder) } else { throw RequestException.Internal.UnsinkableBase() }
|
||||||
|
|
||||||
|
interface Base {
|
||||||
|
/**
|
||||||
|
* Resolve the given (canonical) path against the base, and get a BufferedSource for this file.
|
||||||
|
* @throws RequestException
|
||||||
|
*/
|
||||||
|
fun source(path: String, clientHolder: ClientHolder): BufferedSource
|
||||||
|
|
||||||
|
operator fun div(path: String) = PackwizPath(path, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SinkableBase: Base {
|
||||||
|
/**
|
||||||
|
* Resolve the given (canonical) path against the base, and get a BufferedSink for this file.
|
||||||
|
* @throws RequestException
|
||||||
|
*/
|
||||||
|
fun sink(path: String, clientHolder: ClientHolder): BufferedSink
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PackwizPath
|
||||||
|
|
||||||
|
if (path != other.path) return false
|
||||||
|
if (base != other.base) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = path.hashCode()
|
||||||
|
result = 31 * result + base.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "base=$base; $path"
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
package link.infra.packwiz.installer.task
|
||||||
|
|
||||||
|
data class CacheKey<T>(val key: String, val version: Int)
|
@@ -0,0 +1,22 @@
|
|||||||
|
package link.infra.packwiz.installer.task
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
class CacheManager {
|
||||||
|
class CacheValue<T> {
|
||||||
|
operator fun getValue(thisVal: Any?, property: KProperty<*>): T {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun setValue(thisVal: Any?, property: KProperty<*>, value: T) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun <T> get(cacheKey: CacheKey<T>): CacheValue<T> {
|
||||||
|
return CacheValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
20
src/main/kotlin/link/infra/packwiz/installer/task/Task.kt
Normal file
20
src/main/kotlin/link/infra/packwiz/installer/task/Task.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package link.infra.packwiz.installer.task
|
||||||
|
|
||||||
|
import kotlin.reflect.KMutableProperty0
|
||||||
|
|
||||||
|
// TODO: task processing on 1 background thread; actual resolving of values calls out to a thread group
|
||||||
|
// TODO: progress bar is updated from each of these tasks
|
||||||
|
// TODO: have everything be lazy so there's no need to determine task ordering upfront? a bit like rust async - task results must be queried to occur
|
||||||
|
|
||||||
|
abstract class Task<T>(protected val ctx: TaskContext): TaskInput<T> {
|
||||||
|
// TODO: lazy wrapper for fallible results
|
||||||
|
// TODO: multithreaded fanout subclass/helper
|
||||||
|
|
||||||
|
protected fun <T> wasUpdated(value: KMutableProperty0<T>, newValue: T): Boolean {
|
||||||
|
if (value.get() == newValue) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
value.set(newValue)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package link.infra.packwiz.installer.task
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object for storing results where result and upToDate are calculated simultaneously
|
||||||
|
*/
|
||||||
|
data class TaskCombinedResult<T>(val result: T, val upToDate: Boolean)
|
@@ -0,0 +1,12 @@
|
|||||||
|
package link.infra.packwiz.installer.task
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.target.ClientHolder
|
||||||
|
|
||||||
|
class TaskContext {
|
||||||
|
// TODO: thread pools, protocol roots
|
||||||
|
// TODO: cache management
|
||||||
|
|
||||||
|
val cache = CacheManager()
|
||||||
|
|
||||||
|
val clients = ClientHolder()
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
package link.infra.packwiz.installer.task
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
interface TaskInput<T> {
|
||||||
|
/**
|
||||||
|
* The value of this task input. May be lazily evaluated; must be threadsafe.
|
||||||
|
*/
|
||||||
|
val value: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the effective value of this input has changed since the task was last run.
|
||||||
|
* Doesn't require evaluation of the input value; should use cached data if possible.
|
||||||
|
* May be lazily evaluated; must be threadsafe.
|
||||||
|
*/
|
||||||
|
val upToDate: Boolean
|
||||||
|
|
||||||
|
operator fun getValue(thisVal: Any?, property: KProperty<*>): T = value
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T> raw(value: T): TaskInput<T> {
|
||||||
|
return object: TaskInput<T> {
|
||||||
|
override val value = value
|
||||||
|
override val upToDate: Boolean
|
||||||
|
get() = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
package link.infra.packwiz.installer.task.formats.packwizv1
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||||
|
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||||
|
|
||||||
|
data class PackwizV1PackFile(val name: String, val indexPath: PackwizPath, val indexHash: Hash)
|
@@ -0,0 +1,48 @@
|
|||||||
|
package link.infra.packwiz.installer.task.formats.packwizv1
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.moandjiezana.toml.Toml
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.HashUtils
|
||||||
|
import link.infra.packwiz.installer.target.path.PackwizPath
|
||||||
|
import link.infra.packwiz.installer.task.CacheKey
|
||||||
|
import link.infra.packwiz.installer.task.Task
|
||||||
|
import link.infra.packwiz.installer.task.TaskCombinedResult
|
||||||
|
import link.infra.packwiz.installer.task.TaskContext
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
class PackwizV1PackTomlTask(ctx: TaskContext, val path: PackwizPath): Task<PackwizV1PackFile>(ctx) {
|
||||||
|
// TODO: make hierarchically defined by caller? - then changing the pack format type doesn't leave junk in the cache
|
||||||
|
private var cache by ctx.cache[CacheKey<Hash>("packwiz.v1.packtoml.hash", 1)]
|
||||||
|
|
||||||
|
private class PackFile {
|
||||||
|
var name: String? = null
|
||||||
|
var index: IndexFileLoc? = null
|
||||||
|
|
||||||
|
class IndexFileLoc {
|
||||||
|
var file: String? = null
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
var hashFormat: String? = null
|
||||||
|
var hash: String? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
var versions: Map<String, String>? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val internalResult by lazy {
|
||||||
|
// TODO: query, parse JSON
|
||||||
|
val packFile = Toml().read(InputStreamReader(path.source(ctx.clients).inputStream(), "UTF-8")).to(PackFile::class.java)
|
||||||
|
|
||||||
|
val resolved = PackwizV1PackFile(packFile.name ?: throw RuntimeException("Name required"), // TODO: better exception handling
|
||||||
|
path.resolve(packFile.index?.file ?: throw RuntimeException("File required")),
|
||||||
|
HashUtils.getHash(packFile.index?.hashFormat ?: throw RuntimeException("Hash format required"),
|
||||||
|
packFile.index?.hash ?: throw RuntimeException("Hash required"))
|
||||||
|
)
|
||||||
|
val hash = HashUtils.getHash("sha256", "whatever was just read")
|
||||||
|
|
||||||
|
TaskCombinedResult(resolved, wasUpdated(::cache, hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
override val value by internalResult::result
|
||||||
|
override val upToDate by internalResult::upToDate
|
||||||
|
}
|
@@ -23,6 +23,8 @@ interface IUserInterface {
|
|||||||
|
|
||||||
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
||||||
|
|
||||||
|
fun awaitOptionalButton(showCancel: Boolean)
|
||||||
|
|
||||||
enum class ExceptionListResult {
|
enum class ExceptionListResult {
|
||||||
CONTINUE, CANCEL, IGNORE
|
CONTINUE, CANCEL, IGNORE
|
||||||
}
|
}
|
||||||
|
@@ -59,4 +59,8 @@ class CLIHandler : IUserInterface {
|
|||||||
}
|
}
|
||||||
return ExceptionListResult.CANCEL
|
return ExceptionListResult.CANCEL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun awaitOptionalButton(showCancel: Boolean) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
}
|
}
|
@@ -125,7 +125,7 @@ class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFutu
|
|||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
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) {}
|
||||||
|
@@ -8,6 +8,7 @@ import link.infra.packwiz.installer.ui.data.InstallProgress
|
|||||||
import link.infra.packwiz.installer.util.Log
|
import link.infra.packwiz.installer.util.Log
|
||||||
import java.awt.EventQueue
|
import java.awt.EventQueue
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
import javax.swing.JDialog
|
import javax.swing.JDialog
|
||||||
import javax.swing.JOptionPane
|
import javax.swing.JOptionPane
|
||||||
import javax.swing.UIManager
|
import javax.swing.UIManager
|
||||||
@@ -18,8 +19,21 @@ class GUIHandler : IUserInterface {
|
|||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
override var optionsButtonPressed = false
|
override var optionsButtonPressed = false
|
||||||
|
set(value) {
|
||||||
|
optionalSelectedLatch.countDown()
|
||||||
|
field = value
|
||||||
|
}
|
||||||
@Volatile
|
@Volatile
|
||||||
override var cancelButtonPressed = false
|
override var cancelButtonPressed = false
|
||||||
|
set(value) {
|
||||||
|
optionalSelectedLatch.countDown()
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
var okButtonPressed = false
|
||||||
|
set(value) {
|
||||||
|
optionalSelectedLatch.countDown()
|
||||||
|
field = value
|
||||||
|
}
|
||||||
@Volatile
|
@Volatile
|
||||||
override var firstInstall = false
|
override var firstInstall = false
|
||||||
|
|
||||||
@@ -42,8 +56,12 @@ class GUIHandler : IUserInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val visibleCountdownLatch = CountDownLatch(1)
|
||||||
|
private val optionalSelectedLatch = CountDownLatch(1)
|
||||||
|
|
||||||
override fun show() = EventQueue.invokeLater {
|
override fun show() = EventQueue.invokeLater {
|
||||||
frmPackwizlauncher.isVisible = true
|
frmPackwizlauncher.isVisible = true
|
||||||
|
visibleCountdownLatch.countDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() = EventQueue.invokeAndWait {
|
override fun dispose() = EventQueue.invokeAndWait {
|
||||||
@@ -147,4 +165,15 @@ class GUIHandler : IUserInterface {
|
|||||||
}
|
}
|
||||||
return future.get()
|
return future.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun awaitOptionalButton(showCancel: Boolean) {
|
||||||
|
EventQueue.invokeAndWait {
|
||||||
|
frmPackwizlauncher.showOk(!showCancel)
|
||||||
|
}
|
||||||
|
visibleCountdownLatch.await()
|
||||||
|
optionalSelectedLatch.await()
|
||||||
|
EventQueue.invokeLater {
|
||||||
|
frmPackwizlauncher.hideOk()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -12,6 +12,9 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
|||||||
private var lblProgresslabel: JLabel
|
private var lblProgresslabel: JLabel
|
||||||
private var progressBar: JProgressBar
|
private var progressBar: JProgressBar
|
||||||
private var btnOptions: JButton
|
private var btnOptions: JButton
|
||||||
|
private val btnCancel: JButton
|
||||||
|
private val btnOk: JButton
|
||||||
|
private val buttonsPanel: JPanel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setBounds(100, 100, 493, 95)
|
setBounds(100, 100, 493, 95)
|
||||||
@@ -35,7 +38,7 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
|||||||
}, BorderLayout.CENTER)
|
}, BorderLayout.CENTER)
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
add(JPanel().apply {
|
buttonsPanel = JPanel().apply {
|
||||||
border = EmptyBorder(0, 5, 0, 5)
|
border = EmptyBorder(0, 5, 0, 5)
|
||||||
layout = GridBagLayout()
|
layout = GridBagLayout()
|
||||||
|
|
||||||
@@ -49,20 +52,28 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
add(btnOptions, GridBagConstraints().apply {
|
add(btnOptions, GridBagConstraints().apply {
|
||||||
gridx = 0
|
gridx = 1
|
||||||
gridy = 0
|
gridy = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
add(JButton("Cancel").apply {
|
btnCancel = JButton("Cancel").apply {
|
||||||
addActionListener {
|
addActionListener {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
handler.cancelButtonPressed = true
|
handler.cancelButtonPressed = true
|
||||||
}
|
}
|
||||||
}, GridBagConstraints().apply {
|
}
|
||||||
gridx = 0
|
add(btnCancel, GridBagConstraints().apply {
|
||||||
|
gridx = 1
|
||||||
gridy = 1
|
gridy = 1
|
||||||
})
|
})
|
||||||
}, BorderLayout.EAST)
|
}
|
||||||
|
|
||||||
|
btnOk = JButton("Continue").apply {
|
||||||
|
addActionListener {
|
||||||
|
handler.okButtonPressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(buttonsPanel, BorderLayout.EAST)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun displayProgress(progress: InstallProgress) {
|
fun displayProgress(progress: InstallProgress) {
|
||||||
@@ -83,4 +94,31 @@ class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
|||||||
isEnabled = false
|
isEnabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showOk(hideCancel: Boolean) {
|
||||||
|
if (hideCancel) {
|
||||||
|
buttonsPanel.add(btnOk, GridBagConstraints().apply {
|
||||||
|
gridx = 1
|
||||||
|
gridy = 1
|
||||||
|
})
|
||||||
|
buttonsPanel.remove(btnCancel)
|
||||||
|
} else {
|
||||||
|
buttonsPanel.add(btnOk, GridBagConstraints().apply {
|
||||||
|
gridx = 0
|
||||||
|
gridy = 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
buttonsPanel.revalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideOk() {
|
||||||
|
buttonsPanel.remove(btnOk)
|
||||||
|
if (!buttonsPanel.components.contains(btnCancel)) {
|
||||||
|
buttonsPanel.add(btnCancel, GridBagConstraints().apply {
|
||||||
|
gridx = 1
|
||||||
|
gridy = 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
buttonsPanel.revalidate()
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,14 +1,22 @@
|
|||||||
# Licenses
|
# Licenses
|
||||||
|
|
||||||
packwiz-installer itself is under the MIT license, except for Murmur2Lib and bundled dependencies as follows:
|
packwiz-installer itself is under the MIT license ([Source](https://github.com/packwiz/packwiz-installer)), except for Murmur2Lib and bundled dependencies as follows:
|
||||||
|
|
||||||
- Murmur2Lib: Apache 2.0 ([Source](https://github.com/prasanthj/hasher/blob/master/src/main/java/hasher/Murmur2.java))
|
- Murmur2Lib: Apache 2.0 ([Source](https://github.com/prasanthj/hasher/blob/master/src/main/java/hasher/Murmur2.java))
|
||||||
- Google Gson 2.8.1: Apache 2.0 ([Source](https://github.com/google/gson))
|
- Copyright 2014 Prasanth Jayachandran
|
||||||
- Okio 2.9.0: Apache 2.0 ([Source](https://github.com/square/okio/))
|
- Google Gson 2.8.9: Apache 2.0 ([Source](https://github.com/google/gson))
|
||||||
- Commons CLI 1.4: Apache 2.0 ([Source](http://commons.apache.org/proper/commons-cli/))
|
- 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))
|
- Jetbrains Annotations 13.0: Apache 2.0 ([Source](https://github.com/JetBrains/java-annotations))
|
||||||
- Kotlin Standard Library 1.4.21: Apache 2.0 ([Source](https://github.com/JetBrains/kotlin))
|
- Copyright 2000-2016 JetBrains s.r.o.
|
||||||
|
- Kotlin Standard Library 1.6.10: Apache 2.0 ([Source](https://github.com/JetBrains/kotlin))
|
||||||
|
- Copyright 2010-2020 JetBrains s.r.o and respective authors and developers
|
||||||
- toml4j 0.7.2: MIT ([Source](https://github.com/mwanji/toml4j))
|
- 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
|
## Associated notices
|
||||||
|
|
||||||
@@ -22,10 +30,6 @@ The Apache Software Foundation (http://www.apache.org/).
|
|||||||
## Full license texts
|
## Full license texts
|
||||||
|
|
||||||
### MIT
|
### MIT
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c)
|
|
||||||
|
|
||||||
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
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
@@ -248,3 +252,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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