mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-10-16 16:04:32 +02:00
Compare commits
99 Commits
v0.0.1-pre
...
v0.3.0
Author | SHA1 | Date | |
---|---|---|---|
|
03b0f1b09b | ||
|
6c6a0100fd | ||
|
6d47c0d61f | ||
|
226e754547 | ||
|
2c02703101 | ||
|
81a60cc759 | ||
|
92afa93fd7 | ||
|
0858c90079 | ||
|
1d4c94f5b6 | ||
|
74ddca5d54 | ||
|
0df48d19a9 | ||
|
f5b22f37a4 | ||
|
f52cd19ad4 | ||
|
60887a4312 | ||
|
a368268038 | ||
|
8beded7b41 | ||
|
91060dcd54 | ||
|
e06ee21f3b | ||
|
b3370739a5 | ||
|
ecc6f0440a | ||
|
92b44352b3 | ||
|
1d5a787b02 | ||
|
b5983800e8 | ||
|
4b3c279e71 | ||
|
b413371306 | ||
|
1d2ec61232 | ||
|
a0da889a02 | ||
|
432bb4e25f | ||
|
c89d3b1e47 | ||
|
e8538c22bc | ||
|
a15489f5e4 | ||
|
9d3587c72e | ||
|
bead683b7c | ||
|
0770029dc6 | ||
|
ecaab219c2 | ||
|
b45a2983e7 | ||
|
c0c318772b | ||
|
580408b92a | ||
|
dbdd1fb9f3 | ||
|
79a983bc2f | ||
|
0cba5ba17b | ||
|
ce60cdc385 | ||
|
b314fc8e0b | ||
|
ca4a13589d | ||
|
d21668afa6 | ||
|
7946377159 | ||
|
5a54a90f59 | ||
|
465e4973ba | ||
|
ea60175514 | ||
|
78f5d76fe9 | ||
|
452ab15cc7 | ||
|
87b00f316a | ||
|
1623c0f880 | ||
|
46bbc9b82e | ||
|
02b50be782 | ||
|
e6637b9af8 | ||
|
5fc7d6382d | ||
|
afd8e85754 | ||
|
ecbf0b9eba | ||
|
37a1464e11 | ||
|
54fd84a6d8 | ||
|
dcf8d21aad | ||
|
eaed3b2187 | ||
|
b22edf920e | ||
|
4d8e695fc4 | ||
|
a9bd83e96b | ||
|
ae085743be | ||
|
ad79cb3b21 | ||
|
320e56e74e | ||
|
bd95bc15ad | ||
|
794b817eff | ||
|
a5ff63c587 | ||
|
34a86ffb7d | ||
|
780efe2c9f | ||
|
d1647764c4 | ||
|
12bf090895 | ||
|
533c7a3ed5 | ||
|
d18e134140 | ||
|
165c8cc172 | ||
|
d986b39aa5 | ||
|
040bb955ec | ||
|
022af4b5c5 | ||
|
442fb93ca8 | ||
|
81c1ebaa15 | ||
|
917d10c448 | ||
|
26c3261848 | ||
|
fbd54b4604 | ||
|
8be5cb8e60 | ||
|
c181a36edc | ||
|
e32d98fb98 | ||
|
e65d20be79 | ||
|
3d28f0a674 | ||
|
905630cb2a | ||
|
fd87edd6ca | ||
|
2118a8fda1 | ||
|
86c2349fd3 | ||
|
f76a3d2d62 | ||
|
72d27715f8 | ||
|
26b8e1de86 |
127
.gitignore
vendored
127
.gitignore
vendored
@@ -1,74 +1,95 @@
|
||||
# Created by https://www.gitignore.io/api/java,gradle,eclipse
|
||||
# Edit at https://www.gitignore.io/?templates=java,gradle,eclipse
|
||||
|
||||
### Eclipse ###
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij+all
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# PyDev specific (Python IDE for Eclipse)
|
||||
*.pydevproject
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# PDT-specific (PHP Development Tools)
|
||||
.buildpath
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||
.cache-main
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### Eclipse Patch ###
|
||||
# Eclipse Core
|
||||
.project
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
.classpath
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated
|
||||
### Intellij+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.sts4-cache/
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
@@ -114,4 +135,4 @@ gradle-app.setting
|
||||
### Gradle Patch ###
|
||||
**/build/
|
||||
|
||||
# End of https://www.gitignore.io/api/java,gradle,eclipse
|
||||
# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all
|
||||
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019
|
||||
Copyright (c) 2021 comp500
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@@ -1,2 +1,2 @@
|
||||
# packwiz-installer
|
||||
An installer for launching packwiz modpacks with MultiMC.
|
||||
An installer for launching packwiz modpacks with MultiMC. You'll need [the bootstrapper](https://github.com/comp500/packwiz-installer-bootstrap/releases) to actually use this.
|
||||
|
34
build.gradle
34
build.gradle
@@ -1,34 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '5.0.0'
|
||||
id 'com.palantir.git-version' version '0.11.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'commons-cli:commons-cli:1.4'
|
||||
//testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
mainClassName = 'link.infra.packwiz.installer.RequiresBootstrap'
|
||||
version gitVersion()
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Main-Class': 'link.infra.packwiz.installer.RequiresBootstrap',
|
||||
'Implementation-Version': project.version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Commons CLI is already included in packwiz-installer-bootstrap
|
||||
shadowJar {
|
||||
dependencies {
|
||||
exclude(dependency('commons-cli:commons-cli:1.4'))
|
||||
}
|
||||
}
|
137
build.gradle.kts
Normal file
137
build.gradle.kts
Normal file
@@ -0,0 +1,137 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.guardsquare:proguard-gradle:7.0.0") {
|
||||
exclude("com.android.tools.build")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
java
|
||||
application
|
||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||
id("com.palantir.git-version") version "0.12.3"
|
||||
id("com.github.breadmoirai.github-release") version "2.2.12"
|
||||
kotlin("jvm") version "1.4.21"
|
||||
id("com.github.jk1.dependency-license-report") version "1.16"
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
val shrinkClasspath: Configuration by configurations.creating
|
||||
|
||||
dependencies {
|
||||
implementation("commons-cli:commons-cli:1.4")
|
||||
shrinkClasspath("commons-cli:commons-cli:1.4")
|
||||
implementation("com.moandjiezana.toml:toml4j:0.7.2")
|
||||
implementation("com.google.code.gson:gson:2.8.1")
|
||||
implementation("com.squareup.okio:okio:2.9.0")
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
application {
|
||||
mainClassName = "link.infra.packwiz.installer.RequiresBootstrap"
|
||||
}
|
||||
|
||||
val gitVersion: groovy.lang.Closure<*> by extra
|
||||
version = gitVersion()
|
||||
|
||||
tasks.jar {
|
||||
manifest {
|
||||
attributes["Main-Class"] = "link.infra.packwiz.installer.RequiresBootstrap"
|
||||
attributes["Implementation-Version"] = project.version
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
// TODO: build relocated jar for minecraft launcher lib, non-relocated jar for packwiz-installer
|
||||
//tasks.register<com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation>("relocateShadowJar") {
|
||||
// target = tasks.shadowJar.get()
|
||||
// prefix = "link.infra.packwiz.deps"
|
||||
//}
|
||||
|
||||
// Commons CLI and Minimal JSON are already included in packwiz-installer-bootstrap
|
||||
tasks.shadowJar {
|
||||
dependencies {
|
||||
exclude(dependency("commons-cli:commons-cli:1.4"))
|
||||
exclude(dependency("com.eclipsesource.minimal-json:minimal-json:0.9.5"))
|
||||
// TODO: exclude meta inf files
|
||||
}
|
||||
exclude("**/*.kotlin_metadata")
|
||||
exclude("**/*.kotlin_builtins")
|
||||
exclude("META-INF/maven/**/*")
|
||||
exclude("META-INF/proguard/**/*")
|
||||
//dependsOn(tasks.named("relocateShadowJar"))
|
||||
}
|
||||
|
||||
tasks.register<proguard.gradle.ProGuardTask>("shrinkJar") {
|
||||
injars(tasks.shadowJar)
|
||||
libraryjars(files(shrinkClasspath.files))
|
||||
outjars("build/libs/" + tasks.shadowJar.get().outputs.files.first().name.removeSuffix(".jar") + "-shrink.jar")
|
||||
if (System.getProperty("java.version").startsWith("1.")) {
|
||||
libraryjars("${System.getProperty("java.home")}/lib/rt.jar")
|
||||
libraryjars("${System.getProperty("java.home")}/lib/jce.jar")
|
||||
} else {
|
||||
throw RuntimeException("Compiling with Java 9+ not supported!")
|
||||
}
|
||||
|
||||
keep("class link.infra.packwiz.installer.** { *; }")
|
||||
dontoptimize()
|
||||
dontobfuscate()
|
||||
dontwarn("org.codehaus.mojo.animal_sniffer.*")
|
||||
}
|
||||
|
||||
// Used for vscode launch.json
|
||||
tasks.register<Copy>("copyJar") {
|
||||
from(tasks.named("shrinkJar"))
|
||||
rename("packwiz-installer-(.*)\\.jar", "packwiz-installer.jar")
|
||||
into("build/libs/")
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn("copyJar")
|
||||
}
|
||||
|
||||
if (project.hasProperty("github.token")) {
|
||||
githubRelease {
|
||||
owner("comp500")
|
||||
repo("packwiz-installer")
|
||||
tagName("${project.version}")
|
||||
releaseName("Release ${project.version}")
|
||||
draft(true)
|
||||
token(findProperty("github.token") as String? ?: "")
|
||||
releaseAssets(tasks.jar.get().destinationDirectory.file("packwiz-installer.jar").get())
|
||||
}
|
||||
|
||||
tasks.githubRelease {
|
||||
dependsOn(tasks.build)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
||||
}
|
||||
}
|
||||
tasks.compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
||||
}
|
||||
}
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@@ -1,99 +0,0 @@
|
||||
package link.infra.packwiz.installer;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
public class InstallWindow {
|
||||
|
||||
// TODO: move to seperate file, make usable without GUI
|
||||
|
||||
private JFrame frmPackwizlauncher;
|
||||
private UpdateManager updateManager = new UpdateManager();
|
||||
|
||||
/**
|
||||
* Launch the application.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
EventQueue.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
InstallWindow window = new InstallWindow();
|
||||
window.frmPackwizlauncher.setVisible(true);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the application.
|
||||
*/
|
||||
public InstallWindow() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the contents of the frame.
|
||||
*/
|
||||
private void initialize() {
|
||||
frmPackwizlauncher = new JFrame();
|
||||
frmPackwizlauncher.setTitle("Updating modpack...");
|
||||
frmPackwizlauncher.setBounds(100, 100, 493, 95);
|
||||
frmPackwizlauncher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
frmPackwizlauncher.setLocationRelativeTo(null);
|
||||
|
||||
JPanel panel = new JPanel();
|
||||
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
frmPackwizlauncher.getContentPane().add(panel, BorderLayout.CENTER);
|
||||
panel.setLayout(new BorderLayout(0, 0));
|
||||
|
||||
JProgressBar progressBar = new JProgressBar();
|
||||
progressBar.setValue(50);
|
||||
panel.add(progressBar, BorderLayout.CENTER);
|
||||
|
||||
JLabel lblProgresslabel = new JLabel("Loading...");
|
||||
panel.add(lblProgresslabel, BorderLayout.SOUTH);
|
||||
|
||||
JPanel panel_1 = new JPanel();
|
||||
panel_1.setBorder(new EmptyBorder(0, 5, 0, 5));
|
||||
frmPackwizlauncher.getContentPane().add(panel_1, BorderLayout.EAST);
|
||||
GridBagLayout gbl_panel_1 = new GridBagLayout();
|
||||
panel_1.setLayout(gbl_panel_1);
|
||||
|
||||
JButton btnOptions = new JButton("Options...");
|
||||
btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
GridBagConstraints gbc_btnOptions = new GridBagConstraints();
|
||||
gbc_btnOptions.gridx = 0;
|
||||
gbc_btnOptions.gridy = 0;
|
||||
panel_1.add(btnOptions, gbc_btnOptions);
|
||||
|
||||
JButton btnCancel = new JButton("Cancel");
|
||||
btnCancel.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent arg0) {
|
||||
updateManager.cleanup();
|
||||
frmPackwizlauncher.dispose();
|
||||
}
|
||||
});
|
||||
btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
|
||||
gbc_btnCancel.gridx = 0;
|
||||
gbc_btnCancel.gridy = 1;
|
||||
panel_1.add(btnCancel, gbc_btnCancel);
|
||||
}
|
||||
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
package link.infra.packwiz.installer;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.UIManager;
|
||||
|
||||
import org.apache.commons.cli.CommandLine;
|
||||
import org.apache.commons.cli.CommandLineParser;
|
||||
import org.apache.commons.cli.DefaultParser;
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
|
||||
public class Main {
|
||||
|
||||
// Actual main() is in RequiresBootstrap!
|
||||
|
||||
public Main(String[] args) {
|
||||
Options options = new Options();
|
||||
addNonBootstrapOptions(options);
|
||||
addBootstrapOptions(options);
|
||||
|
||||
CommandLineParser parser = new DefaultParser();
|
||||
CommandLine cmd = null;
|
||||
try {
|
||||
cmd = parser.parse(options, args);
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception e1) {
|
||||
// Ignore the exceptions, just continue using the ugly L&F
|
||||
}
|
||||
JOptionPane.showMessageDialog(null, e.getMessage(), "packwiz-installer", JOptionPane.ERROR_MESSAGE);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
System.out.println("Hello World!");
|
||||
}
|
||||
|
||||
// Called by packwiz-installer-bootstrap to set up the help command
|
||||
public static void addNonBootstrapOptions(Options options) {
|
||||
//options.addOption("w", "welp", false, "Testing options");
|
||||
}
|
||||
|
||||
// TODO: link these somehow so they're only defined once?
|
||||
private static void addBootstrapOptions(Options options) {
|
||||
options.addOption(null, "bootstrap-update-url", true, "Github API URL for checking for updates");
|
||||
options.addOption(null, "bootstrap-no-update", false, "Don't update packwiz-installer");
|
||||
options.addOption(null, "bootstrap-main-jar", true, "Location of the packwiz-installer JAR file");
|
||||
options.addOption("g", "no-gui", false, "Don't display a GUI to show update progress");
|
||||
options.addOption("h", "help", false, "Display this message");
|
||||
}
|
||||
|
||||
}
|
@@ -1,10 +1,8 @@
|
||||
package link.infra.packwiz.installer;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.UIManager;
|
||||
|
||||
public class RequiresBootstrap {
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -15,10 +13,10 @@ public class RequiresBootstrap {
|
||||
if (Arrays.stream(args).map(str -> {
|
||||
if (str == null) return "";
|
||||
if (str.startsWith("--")) {
|
||||
return str.substring(2, str.length());
|
||||
return str.substring(2);
|
||||
}
|
||||
if (str.startsWith("-")) {
|
||||
return str.substring(1, str.length());
|
||||
return str.substring(1);
|
||||
}
|
||||
return "";
|
||||
}).anyMatch(str -> str.equals("g") || str.equals("no-gui"))) {
|
||||
|
@@ -1,9 +0,0 @@
|
||||
package link.infra.packwiz.installer;
|
||||
|
||||
public class UpdateManager {
|
||||
Thread updateThread = new Thread(new UpdateThread());
|
||||
|
||||
public void cleanup() {
|
||||
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
package link.infra.packwiz.installer;
|
||||
|
||||
public class UpdateThread implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,166 @@
|
||||
// Obtained from https://github.com/prasanthj/hasher/blob/master/src/main/java/hasher/Murmur2.java
|
||||
/**
|
||||
* Copyright 2014 Prasanth Jayachandran
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package link.infra.packwiz.installer.metadata.hash;
|
||||
|
||||
/**
|
||||
* Murmur2 32 and 64 bit variants.
|
||||
* 32-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash2.cpp#37
|
||||
* 64-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash2.cpp#96
|
||||
*/
|
||||
public class Murmur2Lib {
|
||||
// Constants for 32-bit variant
|
||||
private static final int M_32 = 0x5bd1e995;
|
||||
private static final int R_32 = 24;
|
||||
|
||||
// Constants for 64-bit variant
|
||||
private static final long M_64 = 0xc6a4a7935bd1e995L;
|
||||
private static final int R_64 = 47;
|
||||
private static final int DEFAULT_SEED = 0;
|
||||
|
||||
/**
|
||||
* Murmur2 32-bit variant.
|
||||
*
|
||||
* @param data - input byte array
|
||||
* @return - hashcode
|
||||
*/
|
||||
public static int hash32(byte[] data) {
|
||||
return hash32(data, data.length, DEFAULT_SEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Murmur2 32-bit variant.
|
||||
*
|
||||
* @param data - input byte array
|
||||
* @param length - length of array
|
||||
* @param seed - seed. (default 0)
|
||||
* @return - hashcode
|
||||
*/
|
||||
public static int hash32(byte[] data, int length, int seed) {
|
||||
int h = seed ^ length;
|
||||
int len_4 = length >> 2;
|
||||
|
||||
// body
|
||||
for (int i = 0; i < len_4; i++) {
|
||||
int i_4 = i << 2;
|
||||
int k = (data[i_4] & 0xff)
|
||||
| ((data[i_4 + 1] & 0xff) << 8)
|
||||
| ((data[i_4 + 2] & 0xff) << 16)
|
||||
| ((data[i_4 + 3] & 0xff) << 24);
|
||||
|
||||
// mix functions
|
||||
k *= M_32;
|
||||
k ^= k >>> R_32;
|
||||
k *= M_32;
|
||||
h *= M_32;
|
||||
h ^= k;
|
||||
}
|
||||
|
||||
// tail
|
||||
int len_m = len_4 << 2;
|
||||
int left = length - len_m;
|
||||
if (left != 0) {
|
||||
if (left >= 3) {
|
||||
h ^= (int) data[length - (left - 2)] << 16;
|
||||
}
|
||||
if (left >= 2) {
|
||||
h ^= (int) data[length - (left - 1)] << 8;
|
||||
}
|
||||
if (left >= 1) {
|
||||
h ^= data[length - left];
|
||||
}
|
||||
|
||||
h *= M_32;
|
||||
}
|
||||
|
||||
// finalization
|
||||
h ^= h >>> 13;
|
||||
h *= M_32;
|
||||
h ^= h >>> 15;
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Murmur2 64-bit variant.
|
||||
*
|
||||
* @param data - input byte array
|
||||
* @return - hashcode
|
||||
*/
|
||||
public static long hash64(final byte[] data) {
|
||||
return hash64(data, data.length, DEFAULT_SEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Murmur2 64-bit variant.
|
||||
*
|
||||
* @param data - input byte array
|
||||
* @param length - length of array
|
||||
* @param seed - seed. (default 0)
|
||||
* @return - hashcode
|
||||
*/
|
||||
public static long hash64(final byte[] data, int length, int seed) {
|
||||
long h = (seed & 0xffffffffl) ^ (length * M_64);
|
||||
int length8 = length >> 3;
|
||||
|
||||
// body
|
||||
for (int i = 0; i < length8; i++) {
|
||||
final int i8 = i << 3;
|
||||
long k = ((long) data[i8] & 0xff)
|
||||
| (((long) data[i8 + 1] & 0xff) << 8)
|
||||
| (((long) data[i8 + 2] & 0xff) << 16)
|
||||
| (((long) data[i8 + 3] & 0xff) << 24)
|
||||
| (((long) data[i8 + 4] & 0xff) << 32)
|
||||
| (((long) data[i8 + 5] & 0xff) << 40)
|
||||
| (((long) data[i8 + 6] & 0xff) << 48)
|
||||
| (((long) data[i8 + 7] & 0xff) << 56);
|
||||
|
||||
// mix functions
|
||||
k *= M_64;
|
||||
k ^= k >>> R_64;
|
||||
k *= M_64;
|
||||
h ^= k;
|
||||
h *= M_64;
|
||||
}
|
||||
|
||||
// tail
|
||||
int tailStart = length8 << 3;
|
||||
switch (length - tailStart) {
|
||||
case 7:
|
||||
h ^= (long) (data[tailStart + 6] & 0xff) << 48;
|
||||
case 6:
|
||||
h ^= (long) (data[tailStart + 5] & 0xff) << 40;
|
||||
case 5:
|
||||
h ^= (long) (data[tailStart + 4] & 0xff) << 32;
|
||||
case 4:
|
||||
h ^= (long) (data[tailStart + 3] & 0xff) << 24;
|
||||
case 3:
|
||||
h ^= (long) (data[tailStart + 2] & 0xff) << 16;
|
||||
case 2:
|
||||
h ^= (long) (data[tailStart + 1] & 0xff) << 8;
|
||||
case 1:
|
||||
h ^= data[tailStart] & 0xff;
|
||||
h *= M_64;
|
||||
}
|
||||
|
||||
// finalization
|
||||
h ^= h >>> R_64;
|
||||
h *= M_64;
|
||||
h ^= h >>> R_64;
|
||||
|
||||
return h;
|
||||
}
|
||||
}
|
252
src/main/kotlin/link/infra/packwiz/installer/DownloadTask.kt
Normal file
252
src/main/kotlin/link/infra/packwiz/installer/DownloadTask.kt
Normal file
@@ -0,0 +1,252 @@
|
||||
package link.infra.packwiz.installer
|
||||
|
||||
import link.infra.packwiz.installer.metadata.IndexFile
|
||||
import link.infra.packwiz.installer.metadata.ManifestFile
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||
import link.infra.packwiz.installer.util.Log
|
||||
import okio.Buffer
|
||||
import okio.HashingSink
|
||||
import okio.buffer
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
|
||||
internal class DownloadTask private constructor(val metadata: IndexFile.File, defaultFormat: String, private val downloadSide: UpdateManager.Options.Side) : IOptionDetails {
|
||||
var cachedFile: ManifestFile.File? = null
|
||||
|
||||
private var err: Exception? = null
|
||||
val exceptionDetails get() = err?.let { e -> ExceptionDetails(name, e) }
|
||||
|
||||
fun failed() = err != null
|
||||
|
||||
private var alreadyUpToDate = false
|
||||
private var metadataRequired = true
|
||||
private var invalidated = false
|
||||
// If file is new or isOptional changed to true, the option needs to be presented again
|
||||
private var newOptional = true
|
||||
|
||||
val isOptional get() = metadata.linkedFile?.isOptional ?: false
|
||||
|
||||
fun isNewOptional() = isOptional && newOptional
|
||||
|
||||
fun correctSide() = metadata.linkedFile?.side?.hasSide(downloadSide) ?: true
|
||||
|
||||
override val name get() = metadata.name
|
||||
|
||||
// Ensure that an update is done if it changes from false to true, or from true to false
|
||||
override var optionValue: Boolean
|
||||
get() = cachedFile?.optionValue ?: true
|
||||
set(value) {
|
||||
if (value && !optionValue) { // Ensure that an update is done if it changes from false to true, or from true to false
|
||||
alreadyUpToDate = false
|
||||
}
|
||||
cachedFile?.optionValue = value
|
||||
}
|
||||
|
||||
override val optionDescription get() = metadata.linkedFile?.option?.description ?: ""
|
||||
|
||||
init {
|
||||
if (metadata.hashFormat?.isEmpty() != false) {
|
||||
metadata.hashFormat = defaultFormat
|
||||
}
|
||||
}
|
||||
|
||||
fun invalidate() {
|
||||
invalidated = true
|
||||
alreadyUpToDate = false
|
||||
}
|
||||
|
||||
fun updateFromCache(cachedFile: ManifestFile.File?) {
|
||||
if (err != null) return
|
||||
|
||||
if (cachedFile == null) {
|
||||
this.cachedFile = ManifestFile.File()
|
||||
return
|
||||
}
|
||||
this.cachedFile = cachedFile
|
||||
if (!invalidated) {
|
||||
val currHash = try {
|
||||
getHash(metadata.hashFormat!!, metadata.hash!!)
|
||||
} catch (e: Exception) {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
if (currHash == cachedFile.hash) { // Already up to date
|
||||
alreadyUpToDate = true
|
||||
metadataRequired = false
|
||||
}
|
||||
}
|
||||
if (cachedFile.isOptional) {
|
||||
// Because option selection dialog might set this task to true/false, metadata is always needed to download
|
||||
// the file, and to show the description and name
|
||||
metadataRequired = true
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadMetadata(parentIndexFile: IndexFile, indexUri: SpaceSafeURI) {
|
||||
if (err != null) return
|
||||
|
||||
if (metadataRequired) {
|
||||
try {
|
||||
// Retrieve the linked metadata file
|
||||
metadata.downloadMeta(parentIndexFile, indexUri)
|
||||
} catch (e: Exception) {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
cachedFile?.let { cachedFile ->
|
||||
val linkedFile = metadata.linkedFile
|
||||
if (linkedFile != null) {
|
||||
linkedFile.option?.let { opt ->
|
||||
if (opt.optional) {
|
||||
if (cachedFile.isOptional) {
|
||||
// isOptional didn't change
|
||||
newOptional = false
|
||||
} else {
|
||||
// isOptional false -> true, set option to it's default value
|
||||
// TODO: preserve previous option value, somehow??
|
||||
cachedFile.optionValue = opt.defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedFile.isOptional = isOptional
|
||||
cachedFile.onlyOtherSide = !correctSide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun download(packFolder: String, indexUri: SpaceSafeURI) {
|
||||
if (err != null) return
|
||||
|
||||
// TODO: is this necessary if we overwrite?
|
||||
// Ensure it is removed
|
||||
cachedFile?.let {
|
||||
if (!it.optionValue || !correctSide()) {
|
||||
if (it.cachedLocation == null) return
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(packFolder, it.cachedLocation))
|
||||
} catch (e: IOException) {
|
||||
Log.warn("Failed to delete file before downloading", e)
|
||||
}
|
||||
it.cachedLocation = null
|
||||
}
|
||||
}
|
||||
if (alreadyUpToDate) return
|
||||
|
||||
// TODO: should I be validating JSON properly, or this fine!!!!!!!??
|
||||
assert(metadata.destURI != null)
|
||||
val destPath = Paths.get(packFolder, metadata.destURI.toString())
|
||||
|
||||
// Don't update files marked with preserve if they already exist on disk
|
||||
if (metadata.preserve) {
|
||||
if (destPath.toFile().exists()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: if already exists and has correct hash, ignore?
|
||||
// TODO: add .disabled support?
|
||||
|
||||
try {
|
||||
val hash: Hash
|
||||
val fileHashFormat: String
|
||||
val linkedFile = metadata.linkedFile
|
||||
|
||||
if (linkedFile != null) {
|
||||
hash = linkedFile.hash
|
||||
fileHashFormat = Objects.requireNonNull(Objects.requireNonNull(linkedFile.download)!!.hashFormat)!!
|
||||
} else {
|
||||
hash = metadata.getHashObj()
|
||||
fileHashFormat = Objects.requireNonNull(metadata.hashFormat)!!
|
||||
}
|
||||
|
||||
val src = metadata.getSource(indexUri)
|
||||
val fileSource = getHasher(fileHashFormat).getHashingSource(src)
|
||||
val data = Buffer()
|
||||
|
||||
// Read all the data into a buffer (very inefficient for large files! but allows rollback if hash check fails)
|
||||
// TODO: should we instead rename the existing file, then stream straight to the file and rollback from the renamed file?
|
||||
fileSource.buffer().use {
|
||||
it.readAll(data)
|
||||
}
|
||||
|
||||
if (fileSource.hashIsEqual(hash)) {
|
||||
// isDirectory follows symlinks, but createDirectories doesn't
|
||||
try {
|
||||
Files.createDirectories(destPath.parent)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
if (!Files.isDirectory(destPath.parent)) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING)
|
||||
data.clear()
|
||||
} else {
|
||||
// TODO: move println to something visible in the error window
|
||||
println("Invalid hash for " + metadata.destURI.toString())
|
||||
println("Calculated: " + fileSource.hash)
|
||||
println("Expected: $hash")
|
||||
// Attempt to get the SHA256 hash
|
||||
val sha256 = HashingSink.sha256(okio.blackholeSink())
|
||||
data.readAll(sha256)
|
||||
println("SHA256 hash value: " + sha256.hash)
|
||||
err = Exception("Hash invalid!")
|
||||
data.clear()
|
||||
return
|
||||
}
|
||||
cachedFile?.cachedLocation?.let {
|
||||
if (destPath != Paths.get(packFolder, it)) {
|
||||
// Delete old file if location changes
|
||||
try {
|
||||
Files.delete(Paths.get(packFolder, cachedFile!!.cachedLocation))
|
||||
} catch (e: IOException) {
|
||||
// Continue, as it was probably already deleted?
|
||||
// TODO: log it
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createTasksFromIndex(index: IndexFile, defaultFormat: String, downloadSide: UpdateManager.Options.Side): List<DownloadTask> {
|
||||
val tasks = ArrayList<DownloadTask>()
|
||||
for (file in Objects.requireNonNull(index.files)) {
|
||||
tasks.add(DownloadTask(file, defaultFormat, downloadSide))
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
}
|
||||
}
|
131
src/main/kotlin/link/infra/packwiz/installer/Main.kt
Normal file
131
src/main/kotlin/link/infra/packwiz/installer/Main.kt
Normal file
@@ -0,0 +1,131 @@
|
||||
@file:JvmName("Main")
|
||||
|
||||
package link.infra.packwiz.installer
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.ui.cli.CLIHandler
|
||||
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.Options
|
||||
import org.apache.commons.cli.ParseException
|
||||
import java.awt.EventQueue
|
||||
import java.awt.GraphicsEnvironment
|
||||
import java.net.URISyntaxException
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.UIManager
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@Suppress("unused")
|
||||
class Main(args: Array<String>) {
|
||||
// Don't attempt to start a GUI if we are headless
|
||||
private var guiEnabled = !GraphicsEnvironment.isHeadless()
|
||||
|
||||
private fun startup(args: Array<String>) {
|
||||
val options = Options()
|
||||
addNonBootstrapOptions(options)
|
||||
addBootstrapOptions(options)
|
||||
|
||||
val parser = DefaultParser()
|
||||
val cmd = try {
|
||||
parser.parse(options, args)
|
||||
} catch (e: ParseException) {
|
||||
Log.fatal("Failed to parse command line arguments", e)
|
||||
if (guiEnabled) {
|
||||
EventQueue.invokeAndWait {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
|
||||
} catch (ignored: Exception) {
|
||||
// Ignore the exceptions, just continue using the ugly L&F
|
||||
}
|
||||
JOptionPane.showMessageDialog(null, "Failed to parse command line arguments: $e",
|
||||
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
}
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
if (guiEnabled && cmd.hasOption("no-gui")) {
|
||||
guiEnabled = false
|
||||
}
|
||||
|
||||
val ui = if (guiEnabled) GUIHandler() else CLIHandler()
|
||||
|
||||
val unparsedArgs = cmd.args
|
||||
if (unparsedArgs.size > 1) {
|
||||
ui.showErrorAndExit("Too many arguments specified!")
|
||||
} else if (unparsedArgs.isEmpty()) {
|
||||
ui.showErrorAndExit("pack.toml URI to install from must be specified!")
|
||||
}
|
||||
|
||||
val title = cmd.getOptionValue("title")
|
||||
if (title != null) {
|
||||
ui.title = title
|
||||
}
|
||||
|
||||
ui.show()
|
||||
|
||||
val uOptions = try {
|
||||
UpdateManager.Options.construct(
|
||||
downloadURI = SpaceSafeURI(unparsedArgs[0]),
|
||||
side = cmd.getOptionValue("side")?.let((UpdateManager.Options.Side)::from),
|
||||
packFolder = cmd.getOptionValue("pack-folder"),
|
||||
manifestFile = cmd.getOptionValue("meta-file")
|
||||
)
|
||||
} catch (e: URISyntaxException) {
|
||||
ui.showErrorAndExit("Failed to read pack.toml URI", e)
|
||||
}
|
||||
|
||||
// Start update process!
|
||||
try {
|
||||
UpdateManager(uOptions, ui)
|
||||
} catch (e: Exception) {
|
||||
ui.showErrorAndExit("Update process failed", e)
|
||||
}
|
||||
println("Finished successfully!")
|
||||
ui.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Called by packwiz-installer-bootstrap to set up the help command
|
||||
@JvmStatic
|
||||
fun addNonBootstrapOptions(options: Options) {
|
||||
options.addOption("s", "side", true, "Side to install mods from (client/server, defaults to client)")
|
||||
options.addOption(null, "title", true, "Title of the installer window")
|
||||
options.addOption(null, "pack-folder", true, "Folder to install the pack to (defaults to the JAR directory)")
|
||||
options.addOption(null, "meta-file", true, "JSON file to store pack metadata, relative to the pack folder (defaults to packwiz.json)")
|
||||
}
|
||||
|
||||
// TODO: link these somehow so they're only defined once?
|
||||
@JvmStatic
|
||||
private fun addBootstrapOptions(options: Options) {
|
||||
options.addOption(null, "bootstrap-update-url", true, "Github API URL for checking for updates")
|
||||
options.addOption(null, "bootstrap-update-token", true, "Github API Access Token, for private repositories")
|
||||
options.addOption(null, "bootstrap-no-update", false, "Don't update packwiz-installer")
|
||||
options.addOption(null, "bootstrap-main-jar", true, "Location of the packwiz-installer JAR file")
|
||||
options.addOption("g", "no-gui", false, "Don't display a GUI to show update progress")
|
||||
options.addOption("h", "help", false, "Display this message") // Implemented in packwiz-installer-bootstrap!
|
||||
}
|
||||
}
|
||||
|
||||
// Actual main() is in RequiresBootstrap!
|
||||
init {
|
||||
// Big overarching try/catch just in case everything breaks
|
||||
try {
|
||||
startup(args)
|
||||
} catch (e: Exception) {
|
||||
Log.fatal("Error from main", e)
|
||||
if (guiEnabled) {
|
||||
EventQueue.invokeLater {
|
||||
JOptionPane.showMessageDialog(null,
|
||||
"A fatal error occurred: \n$e",
|
||||
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
|
||||
exitProcess(1)
|
||||
}
|
||||
// In case the EventQueue is broken, exit after 1 minute
|
||||
Thread.sleep(60 * 1000.toLong())
|
||||
}
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
}
|
448
src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt
Normal file
448
src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt
Normal file
@@ -0,0 +1,448 @@
|
||||
package link.infra.packwiz.installer
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonIOException
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.moandjiezana.toml.Toml
|
||||
import link.infra.packwiz.installer.DownloadTask.Companion.createTasksFromIndex
|
||||
import link.infra.packwiz.installer.metadata.IndexFile
|
||||
import link.infra.packwiz.installer.metadata.ManifestFile
|
||||
import link.infra.packwiz.installer.metadata.PackFile
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||
import link.infra.packwiz.installer.ui.IUserInterface
|
||||
import link.infra.packwiz.installer.ui.IUserInterface.CancellationResult
|
||||
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||
import link.infra.packwiz.installer.util.Log
|
||||
import link.infra.packwiz.installer.util.ifletOrErr
|
||||
import link.infra.packwiz.installer.util.ifletOrWarn
|
||||
import okio.buffer
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletionService
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ExecutorCompletionService
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class UpdateManager internal constructor(private val opts: Options, val ui: IUserInterface) {
|
||||
private var cancelled = false
|
||||
private var cancelledStartGame = false
|
||||
private var errorsOccurred = false
|
||||
|
||||
init {
|
||||
start()
|
||||
}
|
||||
|
||||
data class Options(
|
||||
val downloadURI: SpaceSafeURI,
|
||||
val manifestFile: String,
|
||||
val packFolder: String,
|
||||
val side: Side
|
||||
) {
|
||||
// Horrible workaround for default params not working cleanly with nullable values
|
||||
companion object {
|
||||
fun construct(downloadURI: SpaceSafeURI, manifestFile: String?, packFolder: String?, side: Side?) =
|
||||
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() {
|
||||
checkOptions()
|
||||
|
||||
ui.submitProgress(InstallProgress("Loading manifest file..."))
|
||||
val gson = GsonBuilder().registerTypeAdapter(Hash::class.java, Hash.TypeHandler()).create()
|
||||
val manifest = try {
|
||||
gson.fromJson(FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
|
||||
ManifestFile::class.java)
|
||||
} catch (e: FileNotFoundException) {
|
||||
ui.firstInstall = true
|
||||
ManifestFile()
|
||||
} catch (e: JsonSyntaxException) {
|
||||
ui.showErrorAndExit("Invalid local manifest file, try deleting ${opts.manifestFile}", e)
|
||||
} catch (e: JsonIOException) {
|
||||
ui.showErrorAndExit("Failed to read local manifest file, try deleting ${opts.manifestFile}", e)
|
||||
}
|
||||
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
handleCancellation()
|
||||
}
|
||||
|
||||
ui.submitProgress(InstallProgress("Loading pack file..."))
|
||||
val packFileSource = try {
|
||||
val src = getFileSource(opts.downloadURI)
|
||||
getHasher("sha256").getHashingSource(src)
|
||||
} catch (e: Exception) {
|
||||
ui.showErrorAndExit("Failed to download pack.toml", e)
|
||||
}
|
||||
val pf = packFileSource.buffer().use {
|
||||
try {
|
||||
Toml().read(it.inputStream()).to(PackFile::class.java)
|
||||
} catch (e: IllegalStateException) {
|
||||
ui.showErrorAndExit("Failed to parse pack.toml", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
handleCancellation()
|
||||
}
|
||||
|
||||
ui.submitProgress(InstallProgress("Checking local files..."))
|
||||
|
||||
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked
|
||||
val invalidatedUris: MutableList<SpaceSafeURI> = ArrayList()
|
||||
for ((fileUri, file) in manifest.cachedFiles) {
|
||||
// ignore onlyOtherSide files
|
||||
if (file.onlyOtherSide) {
|
||||
continue
|
||||
}
|
||||
|
||||
var invalid = false
|
||||
// if isn't optional, or is optional but optionValue == true
|
||||
if (!file.isOptional || file.optionValue) {
|
||||
if (file.cachedLocation != null) {
|
||||
if (!Paths.get(opts.packFolder, file.cachedLocation).toFile().exists()) {
|
||||
invalid = true
|
||||
}
|
||||
} else {
|
||||
// if cachedLocation == null, should probably be installed!!
|
||||
invalid = true
|
||||
}
|
||||
}
|
||||
if (invalid) {
|
||||
Log.info("File $fileUri invalidated, marked for redownloading")
|
||||
invalidatedUris.add(fileUri)
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
|
||||
Log.info("Modpack is already up to date!")
|
||||
// todo: --force?
|
||||
if (!ui.optionsButtonPressed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Log.info("Modpack name: ${pf.name}")
|
||||
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
handleCancellation()
|
||||
}
|
||||
try {
|
||||
ifletOrWarn(pf.index, "No index file found") { index ->
|
||||
ui.ifletOrErr(index.hashFormat, index.hash, "Pack has no hash or hashFormat for index") { hashFormat, hash ->
|
||||
ui.ifletOrErr(getNewLoc(opts.downloadURI, index.file), "Pack has invalid index file: " + index.file) { newLoc ->
|
||||
processIndex(
|
||||
newLoc,
|
||||
getHash(hashFormat, hash),
|
||||
hashFormat,
|
||||
manifest,
|
||||
invalidatedUris
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e1: Exception) {
|
||||
ui.showErrorAndExit("Failed to process index file", e1)
|
||||
}
|
||||
|
||||
handleCancellation()
|
||||
|
||||
// TODO: update MMC params, java args etc
|
||||
|
||||
// If there were errors, don't write the manifest/index hashes, to ensure they are rechecked later
|
||||
if (errorsOccurred) {
|
||||
manifest.indexFileHash = null
|
||||
manifest.packFileHash = null
|
||||
} else {
|
||||
manifest.packFileHash = packFileSource.hash
|
||||
}
|
||||
|
||||
manifest.cachedSide = opts.side
|
||||
try {
|
||||
FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString()).use { writer -> gson.toJson(manifest, writer) }
|
||||
} catch (e: IOException) {
|
||||
ui.showErrorAndExit("Failed to save local manifest file", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOptions() {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
private fun processIndex(indexUri: SpaceSafeURI, indexHash: Hash, hashFormat: String, manifest: ManifestFile, invalidatedUris: List<SpaceSafeURI>) {
|
||||
if (manifest.indexFileHash == indexHash && invalidatedUris.isEmpty()) {
|
||||
Log.info("Modpack files are already up to date!")
|
||||
if (!ui.optionsButtonPressed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
manifest.indexFileHash = indexHash
|
||||
|
||||
val indexFileSource = try {
|
||||
val src = getFileSource(indexUri)
|
||||
getHasher(hashFormat).getHashingSource(src)
|
||||
} catch (e: Exception) {
|
||||
ui.showErrorAndExit("Failed to download index file", e)
|
||||
}
|
||||
|
||||
val indexFile = try {
|
||||
Toml().read(indexFileSource.buffer().inputStream()).to(IndexFile::class.java)
|
||||
} catch (e: IllegalStateException) {
|
||||
ui.showErrorAndExit("Failed to parse index file", e)
|
||||
}
|
||||
if (!indexFileSource.hashIsEqual(indexHash)) {
|
||||
ui.showErrorAndExit("Your index file hash is invalid! The pack developer should packwiz refresh on the pack again")
|
||||
}
|
||||
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
return
|
||||
}
|
||||
|
||||
ui.submitProgress(InstallProgress("Checking local files..."))
|
||||
// TODO: use kotlin filtering/FP rather than an iterator?
|
||||
val it: MutableIterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> = manifest.cachedFiles.entries.iterator()
|
||||
while (it.hasNext()) {
|
||||
val (uri, file) = it.next()
|
||||
if (file.cachedLocation != null) {
|
||||
var alreadyDeleted = false
|
||||
// Delete if option value has been set to false
|
||||
if (file.isOptional && !file.optionValue) {
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
|
||||
} catch (e: IOException) {
|
||||
Log.warn("Failed to delete optional disabled file", e)
|
||||
}
|
||||
// Set to null, as it doesn't exist anymore
|
||||
file.cachedLocation = null
|
||||
alreadyDeleted = true
|
||||
}
|
||||
if (indexFile.files.none { it.file == uri }) { // File has been removed from the index
|
||||
if (!alreadyDeleted) {
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
|
||||
} catch (e: IOException) {
|
||||
Log.warn("Failed to delete file removed from index", e)
|
||||
}
|
||||
}
|
||||
it.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
return
|
||||
}
|
||||
ui.submitProgress(InstallProgress("Comparing new files..."))
|
||||
|
||||
// TODO: progress bar?
|
||||
if (indexFile.files.isEmpty()) {
|
||||
Log.warn("Index is empty!")
|
||||
}
|
||||
val tasks = createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side)
|
||||
// If the side changes, invalidate EVERYTHING just in case
|
||||
// Might not be needed, but done just to be safe
|
||||
val invalidateAll = opts.side != manifest.cachedSide
|
||||
if (invalidateAll) {
|
||||
Log.info("Side changed, invalidating all mods")
|
||||
}
|
||||
tasks.forEach{ f ->
|
||||
// TODO: should linkedfile be checked as well? should this be done in the download section?
|
||||
if (invalidateAll) {
|
||||
f.invalidate()
|
||||
} else if (invalidatedUris.contains(f.metadata.file)) {
|
||||
f.invalidate()
|
||||
}
|
||||
val file = manifest.cachedFiles[f.metadata.file]
|
||||
// Ensure the file can be reverted later if necessary - the DownloadTask modifies the file so if it fails we need the old version back
|
||||
file?.backup()
|
||||
// If it is null, the DownloadTask will make a new empty cachedFile
|
||||
f.updateFromCache(file)
|
||||
}
|
||||
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
return
|
||||
}
|
||||
|
||||
// Let's hope downloadMetadata is a pure function!!!
|
||||
tasks.parallelStream().forEach { f -> f.downloadMetadata(indexFile, indexUri) }
|
||||
|
||||
val failedTaskDetails = tasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
||||
if (failedTaskDetails.isNotEmpty()) {
|
||||
errorsOccurred = true
|
||||
when (ui.showExceptions(failedTaskDetails, tasks.size, true)) {
|
||||
ExceptionListResult.CONTINUE -> {}
|
||||
ExceptionListResult.CANCEL -> {
|
||||
cancelled = true
|
||||
return
|
||||
}
|
||||
ExceptionListResult.IGNORE -> {
|
||||
cancelledStartGame = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ui.cancelButtonPressed) {
|
||||
showCancellationDialog()
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: task failed function?
|
||||
val nonFailedFirstTasks = tasks.filter { t -> !t.failed() }.toList()
|
||||
val optionTasks = nonFailedFirstTasks.filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).toList()
|
||||
// If options changed, present all options again
|
||||
if (ui.optionsButtonPressed || optionTasks.any(DownloadTask::isNewOptional)) {
|
||||
// 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())
|
||||
|
||||
// TODO: different thread pool type?
|
||||
val threadPool = Executors.newFixedThreadPool(10)
|
||||
val completionService: CompletionService<DownloadTask> = ExecutorCompletionService(threadPool)
|
||||
tasks.forEach { t ->
|
||||
completionService.submit {
|
||||
t.download(opts.packFolder, indexUri)
|
||||
t
|
||||
}
|
||||
}
|
||||
for (i in tasks.indices) {
|
||||
val task: DownloadTask = try {
|
||||
completionService.take().get()
|
||||
} catch (e: InterruptedException) {
|
||||
ui.showErrorAndExit("Interrupted when consuming download tasks", e)
|
||||
} catch (e: ExecutionException) {
|
||||
ui.showErrorAndExit("Failed to execute download task", e)
|
||||
}
|
||||
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
|
||||
task.cachedFile?.let { file ->
|
||||
if (task.failed()) {
|
||||
val oldFile = file.revert
|
||||
if (oldFile != null) {
|
||||
task.metadata.file?.let { uri -> manifest.cachedFiles.putIfAbsent(uri, oldFile) }
|
||||
} else { null }
|
||||
} else {
|
||||
task.metadata.file?.let { uri -> manifest.cachedFiles.putIfAbsent(uri, file) }
|
||||
}
|
||||
}
|
||||
|
||||
val exDetails = task.exceptionDetails
|
||||
val progress = if (exDetails != null) {
|
||||
"Failed to download ${exDetails.name}: ${exDetails.exception.message}"
|
||||
} else {
|
||||
"Downloaded ${task.name}"
|
||||
}
|
||||
ui.submitProgress(InstallProgress(progress, i + 1, tasks.size))
|
||||
|
||||
if (ui.cancelButtonPressed) { // Stop all tasks, don't launch the game (it's in an invalid state!)
|
||||
threadPool.shutdown()
|
||||
cancelled = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Shut down the thread pool when the update is done
|
||||
threadPool.shutdown()
|
||||
|
||||
val failedTasks2ElectricBoogaloo = nonFailedFirstTasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
||||
if (failedTasks2ElectricBoogaloo.isNotEmpty()) {
|
||||
errorsOccurred = true
|
||||
when (ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size, false)) {
|
||||
ExceptionListResult.CONTINUE -> {}
|
||||
ExceptionListResult.CANCEL -> cancelled = true
|
||||
ExceptionListResult.IGNORE -> cancelledStartGame = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCancellationDialog() {
|
||||
when (ui.showCancellationDialog()) {
|
||||
CancellationResult.QUIT -> cancelled = true
|
||||
CancellationResult.CONTINUE -> cancelledStartGame = true
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to UI?
|
||||
private fun handleCancellation() {
|
||||
if (cancelled) {
|
||||
println("Update cancelled by user!")
|
||||
exitProcess(1)
|
||||
} else if (cancelledStartGame) {
|
||||
println("Update cancelled by user! Continuing to start game...")
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import java.io.IOException
|
||||
|
||||
class EfficientBooleanAdapter : TypeAdapter<Boolean?>() {
|
||||
@Throws(IOException::class)
|
||||
override fun write(out: JsonWriter, value: Boolean?) {
|
||||
if (value == null || !value) {
|
||||
out.nullValue()
|
||||
return
|
||||
}
|
||||
out.value(true)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(reader: JsonReader): Boolean? {
|
||||
if (reader.peek() == JsonToken.NULL) {
|
||||
reader.nextNull()
|
||||
return false
|
||||
}
|
||||
return reader.nextBoolean()
|
||||
}
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
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.getHash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import java.nio.file.Paths
|
||||
|
||||
class IndexFile {
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String = "sha-256"
|
||||
var files: MutableList<File> = ArrayList()
|
||||
|
||||
class File {
|
||||
var file: SpaceSafeURI? = null
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
var alias: SpaceSafeURI? = null
|
||||
var metafile = false
|
||||
var preserve = false
|
||||
|
||||
@Transient
|
||||
var linkedFile: ModFile? = null
|
||||
@Transient
|
||||
var linkedFileURI: SpaceSafeURI? = null
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun downloadMeta(parentIndexFile: IndexFile, indexUri: SpaceSafeURI?) {
|
||||
if (!metafile) {
|
||||
return
|
||||
}
|
||||
if (hashFormat?.length ?: 0 == 0) {
|
||||
hashFormat = parentIndexFile.hashFormat
|
||||
}
|
||||
// TODO: throw a proper exception instead of allowing NPE?
|
||||
val fileHash = getHash(hashFormat!!, hash!!)
|
||||
linkedFileURI = getNewLoc(indexUri, file)
|
||||
val src = getFileSource(linkedFileURI!!)
|
||||
val fileStream = getHasher(hashFormat!!).getHashingSource(src)
|
||||
linkedFile = Toml().read(fileStream.buffer().inputStream()).to(ModFile::class.java)
|
||||
if (!fileStream.hashIsEqual(fileHash)) {
|
||||
throw Exception("Invalid mod file hash")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getSource(indexUri: SpaceSafeURI?): Source {
|
||||
return if (metafile) {
|
||||
if (linkedFile == null) {
|
||||
throw Exception("Linked file doesn't exist!")
|
||||
}
|
||||
linkedFile!!.getSource(linkedFileURI)
|
||||
} else {
|
||||
val newLoc = getNewLoc(indexUri, file) ?: throw Exception("Index file URI is invalid")
|
||||
getFileSource(newLoc)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getHashObj(): Hash {
|
||||
if (hash == null) { // TODO: should these be more specific exceptions (e.g. IndexFileException?!)
|
||||
throw Exception("Index file doesn't have a hash")
|
||||
}
|
||||
if (hashFormat == null) {
|
||||
throw Exception("Index file doesn't have a hash format")
|
||||
}
|
||||
return getHash(hashFormat!!, hash!!)
|
||||
}
|
||||
|
||||
// TODO: throw some kind of exception?
|
||||
val name: String
|
||||
get() {
|
||||
if (metafile) {
|
||||
return linkedFile?.name ?: linkedFile?.filename ?:
|
||||
file?.run { Paths.get(path).fileName.toString() } ?: "Invalid file"
|
||||
}
|
||||
return file?.run { Paths.get(path).fileName.toString() } ?: "Invalid file"
|
||||
}
|
||||
|
||||
// TODO: URIs are bad
|
||||
val destURI: SpaceSafeURI?
|
||||
get() {
|
||||
if (alias != null) {
|
||||
return alias
|
||||
}
|
||||
return if (metafile && linkedFile != null) {
|
||||
linkedFile?.filename?.let { file?.resolve(it) }
|
||||
} else {
|
||||
file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import link.infra.packwiz.installer.UpdateManager
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
|
||||
class ManifestFile {
|
||||
var packFileHash: Hash? = null
|
||||
var indexFileHash: Hash? = null
|
||||
var cachedFiles: MutableMap<SpaceSafeURI, File> = HashMap()
|
||||
// If the side changes, EVERYTHING invalidates. FUN!!!
|
||||
var cachedSide = UpdateManager.Options.Side.CLIENT
|
||||
|
||||
// TODO: switch to Kotlin-friendly JSON/TOML libs?
|
||||
class File {
|
||||
@Transient
|
||||
var revert: File? = null
|
||||
private set
|
||||
|
||||
var hash: Hash? = null
|
||||
var linkedFileHash: Hash? = null
|
||||
var cachedLocation: String? = null
|
||||
|
||||
@JsonAdapter(EfficientBooleanAdapter::class)
|
||||
var isOptional = false
|
||||
var optionValue = true
|
||||
|
||||
@JsonAdapter(EfficientBooleanAdapter::class)
|
||||
var onlyOtherSide = false
|
||||
|
||||
// When an error occurs, the state needs to be reverted. To do this, I have a crude revert system.
|
||||
fun backup() {
|
||||
revert = File().also {
|
||||
it.hash = hash
|
||||
it.linkedFileHash = linkedFileHash
|
||||
it.cachedLocation = cachedLocation
|
||||
it.isOptional = isOptional
|
||||
it.optionValue = optionValue
|
||||
it.onlyOtherSide = onlyOtherSide
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import link.infra.packwiz.installer.UpdateManager
|
||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||
import okio.Source
|
||||
|
||||
class ModFile {
|
||||
var name: String? = null
|
||||
var filename: String? = null
|
||||
var side: UpdateManager.Options.Side? = null
|
||||
var download: Download? = null
|
||||
|
||||
class Download {
|
||||
var url: SpaceSafeURI? = null
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
}
|
||||
|
||||
var update: Map<String, Any>? = null
|
||||
var option: Option? = null
|
||||
|
||||
class Option {
|
||||
var optional = false
|
||||
var description: String? = null
|
||||
@SerializedName("default")
|
||||
var defaultValue = false
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getSource(baseLoc: SpaceSafeURI?): Source {
|
||||
download?.let {
|
||||
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)
|
||||
} ?: throw Exception("Metadata file doesn't have download")
|
||||
}
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
val hash: Hash
|
||||
get() {
|
||||
download?.let {
|
||||
return getHash(
|
||||
it.hashFormat ?: throw Exception("Metadata file doesn't have a hash format"),
|
||||
it.hash ?: throw Exception("Metadata file doesn't have a hash")
|
||||
)
|
||||
} ?: throw Exception("Metadata file doesn't have download")
|
||||
}
|
||||
|
||||
val isOptional: Boolean get() = option?.optional ?: false
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class PackFile {
|
||||
var name: String? = null
|
||||
var index: IndexFileLoc? = null
|
||||
|
||||
class IndexFileLoc {
|
||||
var file: SpaceSafeURI? = null
|
||||
@SerializedName("hash-format")
|
||||
var hashFormat: String? = null
|
||||
var hash: String? = null
|
||||
}
|
||||
|
||||
var versions: Map<String, String>? = null
|
||||
var client: Map<String, Any>? = null
|
||||
var server: Map<String, Any>? = null
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import java.io.Serializable
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URL
|
||||
|
||||
// The world's worst URI wrapper
|
||||
@JsonAdapter(SpaceSafeURIParser::class)
|
||||
class SpaceSafeURI : Comparable<SpaceSafeURI>, Serializable {
|
||||
private val u: URI
|
||||
|
||||
@Throws(URISyntaxException::class)
|
||||
constructor(str: String) {
|
||||
u = URI(str.replace(" ", "%20"))
|
||||
}
|
||||
|
||||
constructor(uri: URI) {
|
||||
u = uri
|
||||
}
|
||||
|
||||
@Throws(URISyntaxException::class)
|
||||
constructor(scheme: String?, authority: String?, path: String?, query: String?, fragment: String?) { // TODO: do all components need to be replaced?
|
||||
u = URI(
|
||||
scheme?.replace(" ", "%20"),
|
||||
authority?.replace(" ", "%20"),
|
||||
path?.replace(" ", "%20"),
|
||||
query?.replace(" ", "%20"),
|
||||
fragment?.replace(" ", "%20")
|
||||
)
|
||||
}
|
||||
|
||||
val path: String get() = u.path.replace("%20", " ")
|
||||
|
||||
override fun toString(): String = u.toString().replace("%20", " ")
|
||||
|
||||
fun resolve(path: String): SpaceSafeURI = SpaceSafeURI(u.resolve(path.replace(" ", "%20")))
|
||||
|
||||
fun resolve(loc: SpaceSafeURI): SpaceSafeURI = SpaceSafeURI(u.resolve(loc.u))
|
||||
|
||||
fun relativize(loc: SpaceSafeURI): SpaceSafeURI = SpaceSafeURI(u.relativize(loc.u))
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is SpaceSafeURI) {
|
||||
u == other.u
|
||||
} else false
|
||||
}
|
||||
|
||||
override fun hashCode() = u.hashCode()
|
||||
|
||||
override fun compareTo(other: SpaceSafeURI): Int = u.compareTo(other.u)
|
||||
|
||||
val scheme: String get() = u.scheme
|
||||
val authority: String get() = u.authority
|
||||
val host: String get() = u.host
|
||||
|
||||
@Throws(MalformedURLException::class)
|
||||
fun toURL(): URL = u.toURL()
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package link.infra.packwiz.installer.metadata
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import java.lang.reflect.Type
|
||||
import java.net.URISyntaxException
|
||||
|
||||
/**
|
||||
* This class encodes spaces before parsing the URI, so the URI can actually be
|
||||
* parsed.
|
||||
*/
|
||||
internal class SpaceSafeURIParser : JsonDeserializer<SpaceSafeURI> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): SpaceSafeURI {
|
||||
return try {
|
||||
SpaceSafeURI(json.asString)
|
||||
} catch (e: URISyntaxException) {
|
||||
throw JsonParseException("Failed to parse URI", e)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace this with a better solution?
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.ForwardingSource
|
||||
import okio.Source
|
||||
|
||||
abstract class GeneralHashingSource(delegate: Source) : ForwardingSource(delegate) {
|
||||
abstract val hash: Hash
|
||||
|
||||
fun hashIsEqual(compareTo: Any) = compareTo == hash
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import com.google.gson.*
|
||||
import java.lang.reflect.Type
|
||||
|
||||
abstract class Hash {
|
||||
protected abstract val stringValue: String
|
||||
protected abstract val type: String
|
||||
|
||||
class TypeHandler : JsonDeserializer<Hash>, JsonSerializer<Hash> {
|
||||
override fun serialize(src: Hash, typeOfSrc: Type, context: JsonSerializationContext): JsonElement = JsonObject().apply {
|
||||
add("type", JsonPrimitive(src.type))
|
||||
add("value", JsonPrimitive(src.stringValue))
|
||||
}
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Hash {
|
||||
val obj = json.asJsonObject
|
||||
val type: String
|
||||
val value: String
|
||||
try {
|
||||
type = obj["type"].asString
|
||||
value = obj["value"].asString
|
||||
} catch (e: NullPointerException) {
|
||||
throw JsonParseException("Invalid hash JSON data")
|
||||
}
|
||||
return try {
|
||||
HashUtils.getHash(type, value)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException("Failed to create hash object", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
object HashUtils {
|
||||
private val hashTypeConversion: Map<String, IHasher> = mapOf(
|
||||
"sha256" to HashingSourceHasher("sha256"),
|
||||
"sha512" to HashingSourceHasher("sha512"),
|
||||
"murmur2" to Murmur2Hasher(),
|
||||
"sha1" to HashingSourceHasher("sha1")
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Exception::class)
|
||||
fun getHasher(type: String): IHasher {
|
||||
return hashTypeConversion[type] ?: throw Exception("Hash type not supported: $type")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Exception::class)
|
||||
fun getHash(type: String, value: String): Hash {
|
||||
return hashTypeConversion[type]?.getHash(value) ?: throw Exception("Hash type not supported: $type")
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.HashingSource
|
||||
import okio.Source
|
||||
|
||||
class HashingSourceHasher internal constructor(private val type: String) : IHasher {
|
||||
// i love naming things
|
||||
private inner class HashingSourceGeneralHashingSource(val delegateHashing: HashingSource) : GeneralHashingSource(delegateHashing) {
|
||||
override val hash: Hash by lazy(LazyThreadSafetyMode.NONE) {
|
||||
HashingSourceHash(delegateHashing.hash.hex())
|
||||
}
|
||||
}
|
||||
|
||||
// this some funky inner class stuff
|
||||
// each of these classes is specific to the instance of the HasherHashingSource
|
||||
// therefore HashingSourceHashes from different parent instances will be not instanceof each other
|
||||
private inner class HashingSourceHash(val value: String) : Hash() {
|
||||
override val stringValue get() = value
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is HashingSourceHash) {
|
||||
return false
|
||||
}
|
||||
return stringValue.equals(other.stringValue, ignoreCase = true)
|
||||
}
|
||||
|
||||
override fun toString(): String = "$type: $stringValue"
|
||||
override fun hashCode(): Int = value.hashCode()
|
||||
|
||||
override val type: String get() = this@HashingSourceHasher.type
|
||||
}
|
||||
|
||||
override fun getHashingSource(delegate: Source): GeneralHashingSource {
|
||||
when (type) {
|
||||
"md5" -> return HashingSourceGeneralHashingSource(HashingSource.md5(delegate))
|
||||
"sha256" -> return HashingSourceGeneralHashingSource(HashingSource.sha256(delegate))
|
||||
"sha512" -> return HashingSourceGeneralHashingSource(HashingSource.sha512(delegate))
|
||||
"sha1" -> return HashingSourceGeneralHashingSource(HashingSource.sha1(delegate))
|
||||
}
|
||||
throw RuntimeException("Invalid hash type provided")
|
||||
}
|
||||
|
||||
override fun getHash(value: String): Hash {
|
||||
return HashingSourceHash(value)
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.Source
|
||||
|
||||
interface IHasher {
|
||||
fun getHashingSource(delegate: Source): GeneralHashingSource
|
||||
fun getHash(value: String): Hash
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
package link.infra.packwiz.installer.metadata.hash
|
||||
|
||||
import okio.Buffer
|
||||
import okio.Source
|
||||
import java.io.IOException
|
||||
|
||||
class Murmur2Hasher : IHasher {
|
||||
private inner class Murmur2GeneralHashingSource(delegate: Source) : GeneralHashingSource(delegate) {
|
||||
val internalBuffer = Buffer()
|
||||
val tempBuffer = Buffer()
|
||||
|
||||
override val hash: Hash by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val data = internalBuffer.readByteArray()
|
||||
Murmur2Hash(Murmur2Lib.hash32(data, data.size, 1))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val out = delegate.read(tempBuffer, byteCount)
|
||||
if (out > -1) {
|
||||
sink.write(tempBuffer.clone(), out)
|
||||
computeNormalizedBufferFaster(tempBuffer, internalBuffer)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Credit to https://github.com/modmuss50/CAV2/blob/master/murmur.go
|
||||
// private fun computeNormalizedArray(input: ByteArray): ByteArray {
|
||||
// val output = ByteArray(input.size)
|
||||
// var index = 0
|
||||
// for (b in input) {
|
||||
// when (b) {
|
||||
// 9.toByte(), 10.toByte(), 13.toByte(), 32.toByte() -> {}
|
||||
// else -> {
|
||||
// output[index] = b
|
||||
// index++
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// val outputTrimmed = ByteArray(index)
|
||||
// System.arraycopy(output, 0, outputTrimmed, 0, index)
|
||||
// return outputTrimmed
|
||||
// }
|
||||
|
||||
private fun computeNormalizedBufferFaster(input: Buffer, output: Buffer) {
|
||||
var index = 0
|
||||
val arr = input.readByteArray()
|
||||
for (b in arr) {
|
||||
when (b) {
|
||||
9.toByte(), 10.toByte(), 13.toByte(), 32.toByte() -> {}
|
||||
else -> {
|
||||
arr[index] = b
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
output.write(arr, 0, index)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class Murmur2Hash : Hash {
|
||||
val value: Int
|
||||
|
||||
constructor(value: String) {
|
||||
// Parsing as long then casting to int converts values gt int max value but lt uint max value
|
||||
// into negatives. I presume this is how the murmur2 code handles this.
|
||||
this.value = value.toLong().toInt()
|
||||
}
|
||||
|
||||
constructor(value: Int) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override val stringValue get() = value.toString()
|
||||
override val type get() = "murmur2"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Murmur2Hash) {
|
||||
return false
|
||||
}
|
||||
return value == other.value
|
||||
}
|
||||
|
||||
override fun toString(): String = "murmur2: $value"
|
||||
override fun hashCode(): Int = value
|
||||
}
|
||||
|
||||
override fun getHashingSource(delegate: Source): GeneralHashingSource = Murmur2GeneralHashingSource(delegate)
|
||||
override fun getHash(value: String): Hash = Murmur2Hash(value)
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package link.infra.packwiz.installer.request
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.request.handlers.RequestHandlerFile
|
||||
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub
|
||||
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP
|
||||
import okio.Source
|
||||
|
||||
object HandlerManager {
|
||||
|
||||
private val handlers: List<IRequestHandler> = listOf(
|
||||
RequestHandlerGithub(),
|
||||
RequestHandlerHTTP(),
|
||||
RequestHandlerFile()
|
||||
)
|
||||
|
||||
// TODO: get rid of nullable stuff here
|
||||
@JvmStatic
|
||||
fun getNewLoc(base: SpaceSafeURI?, loc: SpaceSafeURI?): SpaceSafeURI? {
|
||||
if (loc == null) {
|
||||
return null
|
||||
}
|
||||
val dest = base?.run { resolve(loc) } ?: loc
|
||||
for (handler in handlers) with (handler) {
|
||||
if (matchesHandler(dest)) {
|
||||
return getNewLoc(dest)
|
||||
}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// TODO: What if files are read multiple times??
|
||||
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
|
||||
// Caching system? Copy from already downloaded files?
|
||||
|
||||
// TODO: change to use something more idiomatic than exceptions?
|
||||
|
||||
@JvmStatic
|
||||
@Throws(Exception::class)
|
||||
fun getFileSource(loc: SpaceSafeURI): Source {
|
||||
for (handler in handlers) {
|
||||
if (handler.matchesHandler(loc)) {
|
||||
return handler.getFileSource(loc) ?: throw Exception("Couldn't find URI: $loc")
|
||||
}
|
||||
}
|
||||
throw Exception("No handler available for URI: $loc")
|
||||
}
|
||||
|
||||
// TODO: github toml resolution?
|
||||
// e.g. https://github.com/comp500/Demagnetize -> demagnetize.toml
|
||||
// https://github.com/comp500/Demagnetize/blob/master/demagnetize.toml
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package link.infra.packwiz.installer.request
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import okio.Source
|
||||
|
||||
/**
|
||||
* IRequestHandler handles requests for locations specified in modpack metadata.
|
||||
*/
|
||||
interface IRequestHandler {
|
||||
fun matchesHandler(loc: SpaceSafeURI): Boolean
|
||||
|
||||
fun getNewLoc(loc: SpaceSafeURI): SpaceSafeURI {
|
||||
return loc
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Source for a location. Must be threadsafe.
|
||||
* It is assumed that each location is read only once for the duration of an IRequestHandler.
|
||||
* @param loc The location to be read
|
||||
* @return The Source containing the data of the file
|
||||
* @throws Exception Exception if it failed to download a file!!!
|
||||
*/
|
||||
fun getFileSource(loc: SpaceSafeURI): Source?
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package link.infra.packwiz.installer.request.handlers
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.request.IRequestHandler
|
||||
import okio.Source
|
||||
import okio.source
|
||||
import java.nio.file.Paths
|
||||
|
||||
open class RequestHandlerFile : IRequestHandler {
|
||||
override fun matchesHandler(loc: SpaceSafeURI): Boolean {
|
||||
return "file" == loc.scheme
|
||||
}
|
||||
|
||||
override fun getFileSource(loc: SpaceSafeURI): Source? {
|
||||
val path = Paths.get(loc.toURL().toURI())
|
||||
return path.source()
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package link.infra.packwiz.installer.request.handlers
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
|
||||
class RequestHandlerGithub : RequestHandlerZip(true) {
|
||||
override fun getNewLoc(loc: SpaceSafeURI): SpaceSafeURI {
|
||||
return loc
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val repoMatcherPattern = Pattern.compile("/([\\w.-]+/[\\w.-]+).*")
|
||||
private val branchMatcherPattern = Pattern.compile("/[\\w.-]+/[\\w.-]+/blob/([\\w.-]+).*")
|
||||
}
|
||||
|
||||
// TODO: is caching really needed, if HTTPURLConnection follows redirects correctly?
|
||||
private val zipUriMap: MutableMap<String, SpaceSafeURI> = HashMap()
|
||||
private val zipUriLock = ReentrantReadWriteLock()
|
||||
private fun getRepoName(loc: SpaceSafeURI): String? {
|
||||
val matcher = repoMatcherPattern.matcher(loc.path)
|
||||
return if (matcher.matches()) {
|
||||
matcher.group(1)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getZipUri(loc: SpaceSafeURI): SpaceSafeURI {
|
||||
val repoName = getRepoName(loc)
|
||||
val branchName = getBranch(loc)
|
||||
|
||||
zipUriLock.read {
|
||||
zipUriMap["$repoName/$branchName"]
|
||||
}?.let { return it }
|
||||
|
||||
var zipUri = SpaceSafeURI("https://api.github.com/repos/$repoName/zipball/$branchName")
|
||||
zipUriLock.write {
|
||||
// If another thread sets the value concurrently, use the existing value from the
|
||||
// thread that first acquired the lock.
|
||||
zipUri = zipUriMap.putIfAbsent("$repoName/$branchName", zipUri) ?: zipUri
|
||||
}
|
||||
return zipUri
|
||||
}
|
||||
|
||||
private fun getBranch(loc: SpaceSafeURI): String? {
|
||||
val matcher = branchMatcherPattern.matcher(loc.path)
|
||||
return if (matcher.matches()) {
|
||||
matcher.group(1)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLocationInZip(loc: SpaceSafeURI): SpaceSafeURI {
|
||||
val path = "/" + getRepoName(loc) + "/blob/" + getBranch(loc)
|
||||
return SpaceSafeURI(loc.scheme, loc.authority, path, null, null).relativize(loc)
|
||||
}
|
||||
|
||||
override fun matchesHandler(loc: SpaceSafeURI): Boolean {
|
||||
val scheme = loc.scheme
|
||||
if (!("http" == scheme || "https" == scheme)) {
|
||||
return false
|
||||
}
|
||||
// TODO: more match testing?
|
||||
return "github.com" == loc.host && branchMatcherPattern.matcher(loc.path).matches()
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package link.infra.packwiz.installer.request.handlers
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import link.infra.packwiz.installer.request.IRequestHandler
|
||||
import okio.Source
|
||||
import okio.source
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
open class RequestHandlerHTTP : IRequestHandler {
|
||||
override fun matchesHandler(loc: SpaceSafeURI): Boolean {
|
||||
val scheme = loc.scheme
|
||||
return "http" == scheme || "https" == scheme
|
||||
}
|
||||
|
||||
override fun getFileSource(loc: SpaceSafeURI): Source? {
|
||||
val conn = loc.toURL().openConnection() as HttpURLConnection
|
||||
// TODO: when do we send specific headers??? should there be a way to signal this?
|
||||
conn.addRequestProperty("Accept", "application/octet-stream")
|
||||
// TODO: include version?
|
||||
conn.addRequestProperty("User-Agent", "packwiz-installer")
|
||||
|
||||
conn.apply {
|
||||
// 30 second read timeout
|
||||
readTimeout = 30 * 1000
|
||||
requestMethod = "GET"
|
||||
}
|
||||
return conn.inputStream.source()
|
||||
}
|
||||
}
|
@@ -0,0 +1,123 @@
|
||||
package link.infra.packwiz.installer.request.handlers
|
||||
|
||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI
|
||||
import okio.Buffer
|
||||
import okio.Source
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import java.util.function.Predicate
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.concurrent.write
|
||||
|
||||
abstract class RequestHandlerZip(private val modeHasFolder: Boolean) : RequestHandlerHTTP() {
|
||||
private fun removeFolder(name: String): String {
|
||||
return if (modeHasFolder) {
|
||||
// TODO: replace with proper path checks once switched to Path??
|
||||
name.substring(name.indexOf("/") + 1)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ZipReader(zip: Source) {
|
||||
private val zis = ZipInputStream(zip.buffer().inputStream())
|
||||
private val readFiles: MutableMap<SpaceSafeURI, Buffer> = HashMap()
|
||||
// Write lock implies access to ZipInputStream - only 1 thread must read at a time!
|
||||
val filesLock = ReentrantLock()
|
||||
private var entry: ZipEntry? = null
|
||||
|
||||
private val zipSource = zis.source().buffer()
|
||||
|
||||
// File lock must be obtained before calling this function
|
||||
private fun readCurrFile(): Buffer {
|
||||
val fileBuffer = Buffer()
|
||||
zipSource.readFully(fileBuffer, entry!!.size)
|
||||
return fileBuffer
|
||||
}
|
||||
|
||||
// File lock must be obtained before calling this function
|
||||
private fun findFile(loc: SpaceSafeURI): Buffer? {
|
||||
while (true) {
|
||||
entry = zis.nextEntry
|
||||
entry?.also {
|
||||
val data = readCurrFile()
|
||||
val fileLoc = SpaceSafeURI(removeFolder(it.name))
|
||||
if (loc == fileLoc) {
|
||||
return data
|
||||
} else {
|
||||
readFiles[fileLoc] = data
|
||||
}
|
||||
} ?: return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileSource(loc: SpaceSafeURI): Source? {
|
||||
filesLock.withLock {
|
||||
// Assume files are only read once, allow GC by removing
|
||||
readFiles.remove(loc)?.also { return it }
|
||||
return findFile(loc)
|
||||
}
|
||||
}
|
||||
|
||||
fun findInZip(matches: Predicate<SpaceSafeURI>): SpaceSafeURI? {
|
||||
filesLock.withLock {
|
||||
readFiles.keys.find { matches.test(it) }?.let { return it }
|
||||
|
||||
do {
|
||||
val entry = zis.nextEntry?.also {
|
||||
val data = readCurrFile()
|
||||
val fileLoc = SpaceSafeURI(removeFolder(it.name))
|
||||
readFiles[fileLoc] = data
|
||||
if (matches.test(fileLoc)) {
|
||||
return fileLoc
|
||||
}
|
||||
}
|
||||
} while (entry != null)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val cache: MutableMap<SpaceSafeURI, ZipReader> = HashMap()
|
||||
private val cacheLock = ReentrantReadWriteLock()
|
||||
|
||||
protected abstract fun getZipUri(loc: SpaceSafeURI): SpaceSafeURI
|
||||
protected abstract fun getLocationInZip(loc: SpaceSafeURI): SpaceSafeURI
|
||||
abstract override fun matchesHandler(loc: SpaceSafeURI): Boolean
|
||||
|
||||
override fun getFileSource(loc: SpaceSafeURI): Source? {
|
||||
val zipUri = getZipUri(loc)
|
||||
var zr = cacheLock.read { cache[zipUri] }
|
||||
if (zr == null) {
|
||||
cacheLock.write {
|
||||
// Recheck, because unlocking read lock allows another thread to modify it
|
||||
zr = cache[zipUri]
|
||||
|
||||
if (zr == null) {
|
||||
val src = super.getFileSource(zipUri) ?: return null
|
||||
zr = ZipReader(src).also { cache[zipUri] = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
return zr?.getFileSource(getLocationInZip(loc))
|
||||
}
|
||||
|
||||
protected fun findInZip(loc: SpaceSafeURI, matches: Predicate<SpaceSafeURI>): SpaceSafeURI? {
|
||||
val zipUri = getZipUri(loc)
|
||||
return (cacheLock.read { cache[zipUri] } ?: cacheLock.write {
|
||||
// Recheck, because unlocking read lock allows another thread to modify it
|
||||
cache[zipUri] ?: run {
|
||||
// Create the ZipReader if it doesn't exist, return null if getFileSource returns null
|
||||
super.getFileSource(zipUri)?.let { ZipReader(it) }
|
||||
?.also { cache[zipUri] = it }
|
||||
}
|
||||
})?.findInZip(matches)
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package link.infra.packwiz.installer.ui
|
||||
|
||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
||||
|
||||
interface IUserInterface {
|
||||
fun show()
|
||||
fun dispose()
|
||||
|
||||
fun showErrorAndExit(message: String): Nothing {
|
||||
showErrorAndExit(message, null)
|
||||
}
|
||||
fun showErrorAndExit(message: String, e: Exception?): Nothing
|
||||
|
||||
var title: String
|
||||
fun submitProgress(progress: InstallProgress)
|
||||
// Return true if the installation was cancelled!
|
||||
fun showOptions(options: List<IOptionDetails>): Boolean
|
||||
|
||||
fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult
|
||||
fun disableOptionsButton(hasOptions: Boolean) {}
|
||||
|
||||
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
||||
|
||||
enum class ExceptionListResult {
|
||||
CONTINUE, CANCEL, IGNORE
|
||||
}
|
||||
|
||||
enum class CancellationResult {
|
||||
QUIT, CONTINUE
|
||||
}
|
||||
|
||||
var optionsButtonPressed: Boolean
|
||||
var cancelButtonPressed: Boolean
|
||||
|
||||
var firstInstall: Boolean
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
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
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package link.infra.packwiz.installer.ui.data
|
||||
|
||||
data class ExceptionDetails(
|
||||
val name: String,
|
||||
val exception: Exception
|
||||
)
|
@@ -0,0 +1,7 @@
|
||||
package link.infra.packwiz.installer.ui.data
|
||||
|
||||
interface IOptionDetails {
|
||||
val name: String
|
||||
var optionValue: Boolean
|
||||
val optionDescription: String
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package link.infra.packwiz.installer.ui.data
|
||||
|
||||
data class InstallProgress(
|
||||
val message: String,
|
||||
val hasProgress: Boolean = false,
|
||||
val progress: Int = 0,
|
||||
val progressTotal: Int = 0
|
||||
) {
|
||||
constructor(message: String, progress: Int, progressTotal: Int) : this(message, true, progress, progressTotal)
|
||||
|
||||
constructor(message: String) : this(message, false)
|
||||
}
|
@@ -0,0 +1,153 @@
|
||||
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.Desktop
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.swing.*
|
||||
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) {
|
||||
private val lblExceptionStacktrace: JTextArea
|
||||
|
||||
private class ExceptionListModel(private val details: List<ExceptionDetails>) : AbstractListModel<String>() {
|
||||
override fun getSize() = details.size
|
||||
override fun getElementAt(index: Int) = details[index].name
|
||||
fun getExceptionAt(index: Int) = details[index].exception
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the dialog.
|
||||
*/
|
||||
init {
|
||||
setBounds(100, 100, 540, 340)
|
||||
setLocationRelativeTo(parentWindow)
|
||||
|
||||
contentPane.apply {
|
||||
layout = BorderLayout()
|
||||
|
||||
// Error panel
|
||||
add(JPanel().apply {
|
||||
add(JLabel("One or more errors were encountered while installing the modpack!").apply {
|
||||
icon = UIManager.getIcon("OptionPane.warningIcon")
|
||||
})
|
||||
}, BorderLayout.NORTH)
|
||||
|
||||
// Content panel
|
||||
add(JPanel().apply {
|
||||
border = EmptyBorder(5, 5, 5, 5)
|
||||
layout = BorderLayout(0, 0)
|
||||
|
||||
add(JSplitPane().apply {
|
||||
resizeWeight = 0.3
|
||||
|
||||
lblExceptionStacktrace = JTextArea("Select a file")
|
||||
lblExceptionStacktrace.background = UIManager.getColor("List.background")
|
||||
lblExceptionStacktrace.isOpaque = true
|
||||
lblExceptionStacktrace.wrapStyleWord = true
|
||||
lblExceptionStacktrace.lineWrap = true
|
||||
lblExceptionStacktrace.isEditable = false
|
||||
lblExceptionStacktrace.isFocusable = true
|
||||
lblExceptionStacktrace.font = UIManager.getFont("Label.font")
|
||||
lblExceptionStacktrace.border = EmptyBorder(5, 5, 5, 5)
|
||||
|
||||
rightComponent = JScrollPane(lblExceptionStacktrace)
|
||||
|
||||
leftComponent = JScrollPane(JList<String>().apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
border = EmptyBorder(5, 5, 5, 5)
|
||||
val listModel = ExceptionListModel(eList)
|
||||
model = listModel
|
||||
addListSelectionListener {
|
||||
val i = selectedIndex
|
||||
if (i > -1) {
|
||||
val sw = StringWriter()
|
||||
listModel.getExceptionAt(i).printStackTrace(PrintWriter(sw))
|
||||
lblExceptionStacktrace.text = sw.toString()
|
||||
// Scroll to the top
|
||||
lblExceptionStacktrace.caretPosition = 0
|
||||
} else {
|
||||
lblExceptionStacktrace.text = "Select a file"
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}, BorderLayout.CENTER)
|
||||
|
||||
// Button pane
|
||||
add(JPanel().apply {
|
||||
layout = BorderLayout(0, 0)
|
||||
|
||||
// Right buttons
|
||||
add(JPanel().apply {
|
||||
add(JButton("Continue").apply {
|
||||
toolTipText = "Attempt to continue installing, excluding the failed downloads"
|
||||
addActionListener {
|
||||
future.complete(IUserInterface.ExceptionListResult.CONTINUE)
|
||||
this@ExceptionListWindow.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
add(JButton("Cancel launch").apply {
|
||||
toolTipText = "Stop launching the game"
|
||||
addActionListener {
|
||||
future.complete(IUserInterface.ExceptionListResult.CANCEL)
|
||||
this@ExceptionListWindow.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
add(JButton("Ignore update").apply {
|
||||
toolTipText = "Start the game without attempting to update"
|
||||
isEnabled = allowsIgnore
|
||||
addActionListener {
|
||||
future.complete(IUserInterface.ExceptionListResult.IGNORE)
|
||||
this@ExceptionListWindow.dispose()
|
||||
}
|
||||
})
|
||||
}, BorderLayout.EAST)
|
||||
|
||||
// Errored label
|
||||
add(JLabel(eList.size.toString() + "/" + numTotal + " errored").apply {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
}, BorderLayout.CENTER)
|
||||
|
||||
// Left buttons
|
||||
add(JPanel().apply {
|
||||
add(JButton("Report issue").apply {
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
addActionListener {
|
||||
try {
|
||||
Desktop.getDesktop().browse(URI("https://github.com/comp500/packwiz-installer/issues/new"))
|
||||
} catch (e: IOException) {
|
||||
// lol the button just won't work i guess
|
||||
} catch (e: URISyntaxException) {}
|
||||
}
|
||||
} else {
|
||||
isEnabled = false
|
||||
}
|
||||
})
|
||||
}, BorderLayout.WEST)
|
||||
}, BorderLayout.SOUTH)
|
||||
}
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosing(e: WindowEvent) {
|
||||
future.complete(IUserInterface.ExceptionListResult.CANCEL)
|
||||
}
|
||||
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
// Just in case closing didn't get triggered - if something else called dispose() the
|
||||
// future will have already completed
|
||||
future.complete(IUserInterface.ExceptionListResult.CANCEL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,150 @@
|
||||
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 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
|
||||
@Volatile
|
||||
override var cancelButtonPressed = false
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun show() = EventQueue.invokeLater {
|
||||
frmPackwizlauncher.isVisible = true
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
handler.optionsButtonPressed = true
|
||||
}
|
||||
}
|
||||
add(btnOptions, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridy = 0
|
||||
})
|
||||
|
||||
add(JButton("Cancel").apply {
|
||||
addActionListener {
|
||||
isEnabled = false
|
||||
handler.cancelButtonPressed = true
|
||||
}
|
||||
}, GridBagConstraints().apply {
|
||||
gridx = 0
|
||||
gridy = 1
|
||||
})
|
||||
}, 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
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
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
|
||||
internal class OptionTempHandler(private val opt: IOptionDetails) : IOptionDetails {
|
||||
override var optionValue = opt.optionValue
|
||||
|
||||
override val name get() = opt.name
|
||||
override val optionDescription get() = opt.optionDescription
|
||||
|
||||
fun finalise() {
|
||||
opt.optionValue = optionValue
|
||||
}
|
||||
}
|
@@ -0,0 +1,167 @@
|
||||
package link.infra.packwiz.installer.ui.gui
|
||||
|
||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.swing.*
|
||||
import javax.swing.border.EmptyBorder
|
||||
import javax.swing.event.TableModelListener
|
||||
import javax.swing.table.TableModel
|
||||
|
||||
class OptionsSelectWindow internal constructor(optList: List<IOptionDetails>, future: CompletableFuture<Boolean>, parentWindow: JFrame?) : JDialog(parentWindow, "Select optional mods...", true), ActionListener {
|
||||
private val lblOptionDescription: JTextArea
|
||||
private val tableModel: OptionTableModel
|
||||
private val future: CompletableFuture<Boolean>
|
||||
|
||||
private class OptionTableModel(givenOpts: List<IOptionDetails>) : TableModel {
|
||||
private val opts: List<OptionTempHandler>
|
||||
|
||||
init {
|
||||
val mutOpts = ArrayList<OptionTempHandler>()
|
||||
for (opt in givenOpts) {
|
||||
mutOpts.add(OptionTempHandler(opt))
|
||||
}
|
||||
opts = mutOpts
|
||||
}
|
||||
|
||||
override fun getRowCount() = opts.size
|
||||
override fun getColumnCount() = 2
|
||||
|
||||
private val columnNames = arrayOf("Enabled", "Mod name")
|
||||
private val columnTypes = arrayOf(Boolean::class.javaObjectType, String::class.java)
|
||||
private val columnEditables = booleanArrayOf(true, false)
|
||||
|
||||
override fun getColumnName(columnIndex: Int) = columnNames[columnIndex]
|
||||
override fun getColumnClass(columnIndex: Int) = columnTypes[columnIndex]
|
||||
override fun isCellEditable(rowIndex: Int, columnIndex: Int) = columnEditables[columnIndex]
|
||||
|
||||
override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
|
||||
val opt = opts[rowIndex]
|
||||
return if (columnIndex == 0) opt.optionValue else opt.name
|
||||
}
|
||||
|
||||
override fun setValueAt(aValue: Any, rowIndex: Int, columnIndex: Int) {
|
||||
if (columnIndex == 0) {
|
||||
val opt = opts[rowIndex]
|
||||
opt.optionValue = aValue as Boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Noop, the table model doesn't change!
|
||||
override fun addTableModelListener(l: TableModelListener) {}
|
||||
override fun removeTableModelListener(l: TableModelListener) {}
|
||||
|
||||
fun getDescription(rowIndex: Int) = opts[rowIndex].optionDescription
|
||||
|
||||
fun finalise() {
|
||||
for (opt in opts) {
|
||||
opt.finalise()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
if (e.actionCommand == "OK") {
|
||||
tableModel.finalise()
|
||||
future.complete(false)
|
||||
dispose()
|
||||
} else if (e.actionCommand == "Cancel") {
|
||||
future.complete(true)
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the dialog.
|
||||
*/
|
||||
init {
|
||||
tableModel = OptionTableModel(optList)
|
||||
this.future = future
|
||||
|
||||
setBounds(100, 100, 450, 300)
|
||||
setLocationRelativeTo(parentWindow)
|
||||
|
||||
contentPane.apply {
|
||||
layout = BorderLayout()
|
||||
add(JPanel().apply {
|
||||
border = EmptyBorder(5, 5, 5, 5)
|
||||
layout = BorderLayout(0, 0)
|
||||
|
||||
add(JSplitPane().apply {
|
||||
resizeWeight = 0.5
|
||||
|
||||
lblOptionDescription = JTextArea("Select an option...").apply {
|
||||
background = UIManager.getColor("List.background")
|
||||
isOpaque = true
|
||||
wrapStyleWord = true
|
||||
lineWrap = true
|
||||
isEditable = false
|
||||
isFocusable = false
|
||||
font = UIManager.getFont("Label.font")
|
||||
border = EmptyBorder(10, 10, 10, 10)
|
||||
}
|
||||
|
||||
leftComponent = JScrollPane(JTable().apply {
|
||||
showVerticalLines = false
|
||||
showHorizontalLines = false
|
||||
setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
setShowGrid(false)
|
||||
model = tableModel
|
||||
columnModel.getColumn(0).resizable = false
|
||||
columnModel.getColumn(0).preferredWidth = 15
|
||||
columnModel.getColumn(0).maxWidth = 15
|
||||
columnModel.getColumn(1).resizable = false
|
||||
selectionModel.addListSelectionListener {
|
||||
val i = selectedRow
|
||||
if (i > -1) {
|
||||
lblOptionDescription.text = tableModel.getDescription(i)
|
||||
} else {
|
||||
lblOptionDescription.text = "Select an option..."
|
||||
}
|
||||
}
|
||||
tableHeader = null
|
||||
}).apply {
|
||||
viewport.background = UIManager.getColor("List.background")
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
}
|
||||
|
||||
rightComponent = JScrollPane(lblOptionDescription)
|
||||
})
|
||||
|
||||
add(JPanel().apply {
|
||||
layout = FlowLayout(FlowLayout.RIGHT)
|
||||
|
||||
add(JButton("OK").apply {
|
||||
actionCommand = "OK"
|
||||
addActionListener(this@OptionsSelectWindow)
|
||||
|
||||
this@OptionsSelectWindow.rootPane.defaultButton = this
|
||||
})
|
||||
|
||||
add(JButton("Cancel").apply {
|
||||
actionCommand = "Cancel"
|
||||
addActionListener(this@OptionsSelectWindow)
|
||||
})
|
||||
}, BorderLayout.SOUTH)
|
||||
}, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosing(e: WindowEvent) {
|
||||
future.complete(true)
|
||||
}
|
||||
|
||||
override fun windowClosed(e: WindowEvent) {
|
||||
// Just in case closing didn't get triggered - if something else called dispose() the
|
||||
// future will have already completed
|
||||
future.complete(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
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()
|
||||
}
|
||||
}
|
250
src/main/resources/META-INF/LICENSES.md
Normal file
250
src/main/resources/META-INF/LICENSES.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Licenses
|
||||
|
||||
packwiz-installer itself is under the MIT license, except for Murmur2Lib and bundled dependencies as follows:
|
||||
|
||||
- Murmur2Lib: Apache 2.0 ([Source](https://github.com/prasanthj/hasher/blob/master/src/main/java/hasher/Murmur2.java))
|
||||
- Google Gson 2.8.1: Apache 2.0 ([Source](https://github.com/google/gson))
|
||||
- Okio 2.9.0: Apache 2.0 ([Source](https://github.com/square/okio/))
|
||||
- Commons CLI 1.4: Apache 2.0 ([Source](http://commons.apache.org/proper/commons-cli/))
|
||||
- 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))
|
||||
- toml4j 0.7.2: MIT ([Source](https://github.com/mwanji/toml4j))
|
||||
|
||||
## 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
|
||||
MIT License
|
||||
|
||||
Copyright (c)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
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.
|
||||
|
Reference in New Issue
Block a user