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