diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..efa4625
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 774f975..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,72 +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'
- id 'com.github.breadmoirai.github-release' version '2.2.9'
-}
-
-sourceCompatibility = 1.8
-
-dependencies {
- implementation 'commons-cli:commons-cli:1.4'
- implementation 'com.moandjiezana.toml:toml4j:0.7.2'
- // TODO: Implement tests
- //testImplementation 'junit:junit:4.12'
- implementation 'com.google.code.gson:gson:2.8.1'
- implementation 'com.squareup.okio:okio:2.2.2'
-}
-
-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 and Minimal JSON are already included in packwiz-installer-bootstrap
-shadowJar {
- dependencies {
- exclude(dependency('commons-cli:commons-cli:1.4'))
- exclude(dependency('com.eclipsesource.minimal-json:minimal-json:0.9.5'))
- }
-}
-
-// Used for vscode launch.json
-task copyJar(type: Copy) {
- from shadowJar
- rename "packwiz-installer-(.*)\\.jar", "packwiz-installer.jar"
- into "build/libs/"
-}
-
-build.dependsOn copyJar
-
-if (project.hasProperty("github.token")) {
- githubRelease {
- // IntelliJ u ok?
- //noinspection GroovyAssignabilityCheck
- owner "comp500"
- //noinspection GroovyAssignabilityCheck
- repo "packwiz-installer"
- //noinspection GroovyAssignabilityCheck
- tagName "${project.version}"
- //noinspection GroovyAssignabilityCheck
- releaseName "Release ${project.version}"
- //noinspection GroovyAssignabilityCheck
- draft true
- //noinspection GroovyAssignabilityCheck
- token findProperty("github.token") ?: ""
- releaseAssets = [jar.destinationDirectory.file("packwiz-installer.jar").get()]
- }
-
- tasks.githubRelease.dependsOn(build)
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..40e762a
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,86 @@
+plugins {
+ java
+ application
+ id("com.github.johnrengelman.shadow") version "5.0.0"
+ id("com.palantir.git-version") version "0.11.0"
+ id("com.github.breadmoirai.github-release") version "2.2.9"
+ kotlin("jvm") version "1.3.61"
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+ implementation("commons-cli:commons-cli:1.4")
+ implementation("com.moandjiezana.toml:toml4j:0.7.2")
+ // TODO: Implement tests
+ //testImplementation "junit:junit:4.12"
+ implementation("com.google.code.gson:gson:2.8.1")
+ implementation("com.squareup.okio:okio:2.2.2")
+ implementation(kotlin("stdlib-jdk8"))
+}
+
+repositories {
+ 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
+ }
+}
+
+// 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"))
+ }
+}
+
+// Used for vscode launch.json
+tasks.register("copyJar") {
+ from(tasks.shadowJar)
+ 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"
+ }
+}
+tasks.compileTestKotlin {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/link/infra/packwiz/installer/Main.java b/src/main/java/link/infra/packwiz/installer/Main.java
deleted file mode 100644
index f1fcbdb..0000000
--- a/src/main/java/link/infra/packwiz/installer/Main.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package link.infra.packwiz.installer;
-
-import link.infra.packwiz.installer.metadata.SpaceSafeURI;
-import link.infra.packwiz.installer.ui.CLIHandler;
-import link.infra.packwiz.installer.ui.IUserInterface;
-import link.infra.packwiz.installer.ui.InputStateHandler;
-import link.infra.packwiz.installer.ui.InstallWindow;
-import org.apache.commons.cli.*;
-
-import javax.swing.*;
-import java.awt.*;
-import java.net.URISyntaxException;
-
-@SuppressWarnings("unused")
-public class Main {
-
- // Actual main() is in RequiresBootstrap!
- @SuppressWarnings("unused")
- public Main(String[] args) {
- // Big overarching try/catch just in case everything breaks
- try {
- this.startup(args);
- } catch (Exception e) {
- e.printStackTrace();
- EventQueue.invokeLater(() -> {
- JOptionPane.showMessageDialog(null,
- "A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(),
- "packwiz-installer", JOptionPane.ERROR_MESSAGE);
- System.exit(1);
- });
- // In case the eventqueue is broken, exit after 1 minute
- try {
- Thread.sleep(60 * 1000);
- } catch (InterruptedException e1) {
- // Good, it was already called?
- return;
- }
- System.exit(1);
- }
- }
-
- private void startup(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);
- }
-
- IUserInterface ui;
- // if "headless", GUI creation will fail anyway!
- if (cmd.hasOption("no-gui") || GraphicsEnvironment.isHeadless()) {
- ui = new CLIHandler();
- } else {
- ui = new InstallWindow();
- }
-
- String[] unparsedArgs = cmd.getArgs();
- if (unparsedArgs.length > 1) {
- ui.handleExceptionAndExit(new RuntimeException("Too many arguments specified!"));
- return;
- } else if (unparsedArgs.length < 1) {
- ui.handleExceptionAndExit(new RuntimeException("URI to install from must be specified!"));
- return;
- }
-
- String title = cmd.getOptionValue("title");
- if (title != null) {
- ui.setTitle(title);
- }
-
- InputStateHandler inputStateHandler = new InputStateHandler();
- ui.show(inputStateHandler);
-
- UpdateManager.Options uOptions = new UpdateManager.Options();
-
- String side = cmd.getOptionValue("side");
- if (side != null) {
- uOptions.side = UpdateManager.Options.Side.from(side);
- }
-
- String packFolder = cmd.getOptionValue("pack-folder");
- if (packFolder != null) {
- uOptions.packFolder = packFolder;
- }
-
- String metaFile = cmd.getOptionValue("meta-file");
- if (metaFile != null) {
- uOptions.manifestFile = metaFile;
- }
-
- try {
- uOptions.downloadURI = new SpaceSafeURI(unparsedArgs[0]);
- } catch (URISyntaxException e) {
- // TODO: better error message?
- ui.handleExceptionAndExit(e);
- return;
- }
-
- // Start update process!
- // TODO: start in SwingWorker?
- try {
- ui.executeManager(() -> {
- try {
- new UpdateManager(uOptions, ui, inputStateHandler);
- } catch (Exception e) {
- // TODO: better error message?
- ui.handleExceptionAndExit(e);
- }
- });
- } catch (Exception e) {
- // TODO: better error message?
- ui.handleExceptionAndExit(e);
- }
- }
-
- // Called by packwiz-installer-bootstrap to set up the help command
- @SuppressWarnings("WeakerAccess")
- public static void 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?
- private static void 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!
- }
-
-}
diff --git a/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerGithub.java b/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerGithub.java
deleted file mode 100644
index 0b12613..0000000
--- a/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerGithub.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package link.infra.packwiz.installer.request.handlers;
-
-import link.infra.packwiz.installer.metadata.SpaceSafeURI;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class RequestHandlerGithub extends RequestHandlerZip {
-
- public RequestHandlerGithub() {
- super(true);
- }
-
- @Override
- public SpaceSafeURI getNewLoc(SpaceSafeURI loc) {
- return loc;
- }
-
- // TODO: is caching really needed, if HTTPURLConnection follows redirects correctly?
- private Map zipUriMap = new HashMap<>();
- private final ReentrantReadWriteLock zipUriLock = new ReentrantReadWriteLock();
- private static Pattern repoMatcherPattern = Pattern.compile("/([\\w.-]+/[\\w.-]+).*");
-
- private String getRepoName(SpaceSafeURI loc) {
- Matcher matcher = repoMatcherPattern.matcher(loc.getPath());
- if (matcher.matches()) {
- return matcher.group(1);
- } else {
- return null;
- }
- }
-
- @Override
- protected SpaceSafeURI getZipUri(SpaceSafeURI loc) throws Exception {
- String repoName = getRepoName(loc);
- String branchName = getBranch(loc);
- zipUriLock.readLock().lock();
- SpaceSafeURI zipUri = zipUriMap.get(repoName + "/" + branchName);
- zipUriLock.readLock().unlock();
- if (zipUri != null) {
- return zipUri;
- }
-
- zipUri = new SpaceSafeURI("https://api.github.com/repos/" + repoName + "/zipball/" + branchName);
-
- zipUriLock.writeLock().lock();
- // If another thread sets the value concurrently, use the value of the
- // thread that first acquired the lock.
- SpaceSafeURI zipUriInserted = zipUriMap.putIfAbsent(repoName + "/" + branchName, zipUri);
- if (zipUriInserted != null) {
- zipUri = zipUriInserted;
- }
- zipUriLock.writeLock().unlock();
- return zipUri;
- }
-
- private static Pattern branchMatcherPattern = Pattern.compile("/[\\w.-]+/[\\w.-]+/blob/([\\w.-]+).*");
-
- private String getBranch(SpaceSafeURI loc) {
- Matcher matcher = branchMatcherPattern.matcher(loc.getPath());
- if (matcher.matches()) {
- return matcher.group(1);
- } else {
- return null;
- }
- }
-
- @Override
- protected SpaceSafeURI getLocationInZip(SpaceSafeURI loc) throws Exception {
- String path = "/" + getRepoName(loc) + "/blob/" + getBranch(loc);
- return new SpaceSafeURI(loc.getScheme(), loc.getAuthority(), path, null, null).relativize(loc);
- }
-
- @Override
- public boolean matchesHandler(SpaceSafeURI loc) {
- String scheme = loc.getScheme();
- if (!("http".equals(scheme) || "https".equals(scheme))) {
- return false;
- }
- if (!"github.com".equals(loc.getHost())) {
- return false;
- }
- // TODO: sanity checks, support for more github urls
- return true;
- }
-
-}
diff --git a/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerHTTP.java b/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerHTTP.java
deleted file mode 100644
index 53011ae..0000000
--- a/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerHTTP.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package link.infra.packwiz.installer.request.handlers;
-
-import link.infra.packwiz.installer.metadata.SpaceSafeURI;
-import link.infra.packwiz.installer.request.IRequestHandler;
-import okio.Okio;
-import okio.Source;
-
-import java.net.URLConnection;
-
-public class RequestHandlerHTTP implements IRequestHandler {
-
- @Override
- public boolean matchesHandler(SpaceSafeURI loc) {
- String scheme = loc.getScheme();
- return "http".equals(scheme) || "https".equals(scheme);
- }
-
- @Override
- public Source getFileSource(SpaceSafeURI loc) throws Exception {
- URLConnection conn = loc.toURL().openConnection();
- // TODO: when do we send specific headers??? should there be a way to signal this?
- // github *sometimes* requires it, sometimes not!
- //conn.addRequestProperty("Accept", "application/octet-stream");
- // 30 second read timeout
- conn.setReadTimeout(30 * 1000);
- return Okio.source(conn.getInputStream());
- }
-
-}
diff --git a/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerZip.java b/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerZip.java
deleted file mode 100644
index 90a8dc2..0000000
--- a/src/main/java/link/infra/packwiz/installer/request/handlers/RequestHandlerZip.java
+++ /dev/null
@@ -1,169 +0,0 @@
-package link.infra.packwiz.installer.request.handlers;
-
-import link.infra.packwiz.installer.metadata.SpaceSafeURI;
-import okio.Buffer;
-import okio.BufferedSource;
-import okio.Okio;
-import okio.Source;
-
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.util.HashMap;
-import java.util.Map;
-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;
-
-public abstract class RequestHandlerZip extends RequestHandlerHTTP {
-
- private final boolean modeHasFolder;
-
- public RequestHandlerZip(boolean modeHasFolder) {
- this.modeHasFolder = modeHasFolder;
- }
-
- private String removeFolder(String name) {
- if (modeHasFolder) {
- return name.substring(name.indexOf("/")+1);
- } else {
- return name;
- }
- }
-
- private class ZipReader {
-
- private final ZipInputStream zis;
- private final Map readFiles = new HashMap<>();
- // Write lock implies access to ZipInputStream - only 1 thread must read at a time!
- final ReentrantLock filesLock = new ReentrantLock();
- private ZipEntry entry;
-
- private final BufferedSource zipSource;
-
- ZipReader(Source zip) {
- zis = new ZipInputStream(Okio.buffer(zip).inputStream());
- zipSource = Okio.buffer(Okio.source(zis));
- }
-
- // File lock must be obtained before calling this function
- private Buffer readCurrFile() throws IOException {
- Buffer fileBuffer = new Buffer();
- zipSource.readFully(fileBuffer, entry.getSize());
- return fileBuffer;
- }
-
- // File lock must be obtained before calling this function
- private Buffer findFile(SpaceSafeURI loc) throws IOException, URISyntaxException {
- while (true) {
- entry = zis.getNextEntry();
- if (entry == null) {
- return null;
- }
- Buffer data = readCurrFile();
- SpaceSafeURI fileLoc = new SpaceSafeURI(removeFolder(entry.getName()));
- if (loc.equals(fileLoc)) {
- return data;
- } else {
- readFiles.put(fileLoc, data);
- }
- }
- }
-
- Source getFileSource(SpaceSafeURI loc) throws Exception {
- filesLock.lock();
- // Assume files are only read once, allow GC by removing
- Buffer file = readFiles.remove(loc);
- if (file != null) {
- filesLock.unlock();
- return file;
- }
-
- file = findFile(loc);
- filesLock.unlock();
- return file;
- }
-
- SpaceSafeURI findInZip(Predicate matches) throws Exception {
- filesLock.lock();
- for (SpaceSafeURI file : readFiles.keySet()) {
- if (matches.test(file)) {
- filesLock.unlock();
- return file;
- }
- }
-
- while (true) {
- entry = zis.getNextEntry();
- if (entry == null) {
- filesLock.unlock();
- return null;
- }
- Buffer data = readCurrFile();
- SpaceSafeURI fileLoc = new SpaceSafeURI(removeFolder(entry.getName()));
- readFiles.put(fileLoc, data);
- if (matches.test(fileLoc)) {
- filesLock.unlock();
- return fileLoc;
- }
- }
- }
-
- }
-
- private final Map cache = new HashMap<>();
- private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();
-
- protected abstract SpaceSafeURI getZipUri(SpaceSafeURI loc) throws Exception;
-
- protected abstract SpaceSafeURI getLocationInZip(SpaceSafeURI loc) throws Exception;
-
- @Override
- public abstract boolean matchesHandler(SpaceSafeURI loc);
-
- @Override
- public Source getFileSource(SpaceSafeURI loc) throws Exception {
- SpaceSafeURI zipUri = getZipUri(loc);
- cacheLock.readLock().lock();
- ZipReader zr = cache.get(zipUri);
- cacheLock.readLock().unlock();
- if (zr == null) {
- cacheLock.writeLock().lock();
- // Recheck, because unlocking read lock allows another thread to modify it
- zr = cache.get(zipUri);
- if (zr == null) {
- Source src = super.getFileSource(zipUri);
- if (src == null) {
- cacheLock.writeLock().unlock();
- return null;
- }
- zr = new ZipReader(src);
- cache.put(zipUri, zr);
- }
- cacheLock.writeLock().unlock();
- }
-
- return zr.getFileSource(getLocationInZip(loc));
- }
-
- protected SpaceSafeURI findInZip(SpaceSafeURI loc, Predicate matches) throws Exception {
- SpaceSafeURI zipUri = getZipUri(loc);
- cacheLock.readLock().lock();
- ZipReader zr = cache.get(zipUri);
- cacheLock.readLock().unlock();
- if (zr == null) {
- cacheLock.writeLock().lock();
- // Recheck, because unlocking read lock allows another thread to modify it
- zr = cache.get(zipUri);
- if (zr == null) {
- zr = new ZipReader(super.getFileSource(zipUri));
- cache.put(zipUri, zr);
- }
- cacheLock.writeLock().unlock();
- }
-
- return zr.findInZip(matches);
- }
-
-}
diff --git a/src/main/kotlin/link/infra/packwiz/installer/Main.kt b/src/main/kotlin/link/infra/packwiz/installer/Main.kt
new file mode 100644
index 0000000..9111f34
--- /dev/null
+++ b/src/main/kotlin/link/infra/packwiz/installer/Main.kt
@@ -0,0 +1,123 @@
+package link.infra.packwiz.installer
+
+import link.infra.packwiz.installer.metadata.SpaceSafeURI
+import link.infra.packwiz.installer.ui.CLIHandler
+import link.infra.packwiz.installer.ui.InputStateHandler
+import link.infra.packwiz.installer.ui.InstallWindow
+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) {
+ private fun startup(args: Array) {
+ val options = Options()
+ addNonBootstrapOptions(options)
+ addBootstrapOptions(options)
+
+ val parser = DefaultParser()
+ val cmd = try {
+ parser.parse(options, args)
+ } catch (e: ParseException) {
+ e.printStackTrace()
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
+ } catch (e1: Exception) {
+ // Ignore the exceptions, just continue using the ugly L&F
+ }
+ JOptionPane.showMessageDialog(null, e.message, "packwiz-installer", JOptionPane.ERROR_MESSAGE)
+ exitProcess(1)
+ }
+
+ // if "headless", GUI creation will fail anyway!
+ val ui = if (cmd.hasOption("no-gui") || GraphicsEnvironment.isHeadless()) {
+ CLIHandler()
+ } else InstallWindow()
+
+ val unparsedArgs = cmd.args
+ if (unparsedArgs.size > 1) {
+ ui.handleExceptionAndExit(RuntimeException("Too many arguments specified!"))
+ } else if (unparsedArgs.isEmpty()) {
+ ui.handleExceptionAndExit(RuntimeException("URI to install from must be specified!"))
+ }
+
+ cmd.getOptionValue("title")?.also {
+ ui.setTitle(it)
+ }
+
+ val inputStateHandler = InputStateHandler()
+ ui.show(inputStateHandler)
+
+ val uOptions = UpdateManager.Options().apply {
+ side = cmd.getOptionValue("side")?.let { UpdateManager.Options.Side.from(it) } ?: side
+ packFolder = cmd.getOptionValue("pack-folder") ?: packFolder
+ manifestFile = cmd.getOptionValue("meta-file") ?: manifestFile
+ }
+
+ try {
+ uOptions.downloadURI = SpaceSafeURI(unparsedArgs[0])
+ } catch (e: URISyntaxException) {
+ // TODO: better error message?
+ ui.handleExceptionAndExit(e)
+ }
+
+ // Start update process!
+ // TODO: start in SwingWorker?
+ try {
+ ui.executeManager {
+ try {
+ UpdateManager(uOptions, ui, inputStateHandler)
+ } catch (e: Exception) { // TODO: better error message?
+ ui.handleExceptionAndExit(e)
+ }
+ }
+ } catch (e: Exception) { // TODO: better error message?
+ ui.handleExceptionAndExit(e)
+ }
+ }
+
+ companion object {
+ // Called by packwiz-installer-bootstrap to set up the help command
+ 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?
+ 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) {
+ e.printStackTrace()
+ EventQueue.invokeLater {
+ JOptionPane.showMessageDialog(null,
+ "A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
+ "packwiz-installer", JOptionPane.ERROR_MESSAGE)
+ exitProcess(1)
+ }
+ // In case the EventQueue is broken, exit after 1 minute
+ Thread.sleep(60 * 1000.toLong())
+ exitProcess(1)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/link/infra/packwiz/installer/request/IRequestHandler.java b/src/main/kotlin/link/infra/packwiz/installer/request/IRequestHandler.kt
similarity index 55%
rename from src/main/java/link/infra/packwiz/installer/request/IRequestHandler.java
rename to src/main/kotlin/link/infra/packwiz/installer/request/IRequestHandler.kt
index 35bbca6..d015549 100644
--- a/src/main/java/link/infra/packwiz/installer/request/IRequestHandler.java
+++ b/src/main/kotlin/link/infra/packwiz/installer/request/IRequestHandler.kt
@@ -1,19 +1,18 @@
-package link.infra.packwiz.installer.request;
+package link.infra.packwiz.installer.request
-import link.infra.packwiz.installer.metadata.SpaceSafeURI;
-import okio.Source;
+import link.infra.packwiz.installer.metadata.SpaceSafeURI
+import okio.Source
/**
* IRequestHandler handles requests for locations specified in modpack metadata.
*/
-public interface IRequestHandler {
-
- boolean matchesHandler(SpaceSafeURI loc);
-
- default SpaceSafeURI getNewLoc(SpaceSafeURI loc) {
- return loc;
+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.
@@ -21,6 +20,5 @@ public interface IRequestHandler {
* @return The Source containing the data of the file
* @throws Exception Exception if it failed to download a file!!!
*/
- Source getFileSource(SpaceSafeURI loc) throws Exception;
-
-}
+ fun getFileSource(loc: SpaceSafeURI): Source?
+}
\ No newline at end of file
diff --git a/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerGithub.kt b/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerGithub.kt
new file mode 100644
index 0000000..c27ba8e
--- /dev/null
+++ b/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerGithub.kt
@@ -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 = 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
+ }
+ return "github.com" == loc.host
+ // TODO: sanity checks, support for more github urls
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerHTTP.kt b/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerHTTP.kt
new file mode 100644
index 0000000..3292ec8
--- /dev/null
+++ b/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerHTTP.kt
@@ -0,0 +1,25 @@
+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
+
+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()
+ // TODO: when do we send specific headers??? should there be a way to signal this?
+ // github *sometimes* requires it, sometimes not!
+ //conn.addRequestProperty("Accept", "application/octet-stream");
+ conn.apply {
+ // 30 second read timeout
+ readTimeout = 30 * 1000
+ }
+ return conn.getInputStream().source()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerZip.kt b/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerZip.kt
new file mode 100644
index 0000000..624437e
--- /dev/null
+++ b/src/main/kotlin/link/infra/packwiz/installer/request/handlers/RequestHandlerZip.kt
@@ -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 internal constructor(zip: Source) {
+ private val zis = ZipInputStream(zip.buffer().inputStream())
+ private val readFiles: MutableMap = 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? {
+ 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 = 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? {
+ 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)
+ }
+
+}
\ No newline at end of file