mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-04-19 21:16:30 +02:00
Start porting to Kotlin
This commit is contained in:
parent
c0c318772b
commit
b45a2983e7
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
20
.idea/jarRepositories.xml
generated
Normal file
20
.idea/jarRepositories.xml
generated
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
72
build.gradle
72
build.gradle
@ -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)
|
||||
}
|
86
build.gradle.kts
Normal file
86
build.gradle.kts
Normal file
@ -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<Copy>("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"
|
||||
}
|
||||
}
|
@ -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!
|
||||
}
|
||||
|
||||
}
|
@ -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<String, SpaceSafeURI> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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<SpaceSafeURI, Buffer> 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<SpaceSafeURI> 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<SpaceSafeURI, ZipReader> 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<SpaceSafeURI> 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);
|
||||
}
|
||||
|
||||
}
|
123
src/main/kotlin/link/infra/packwiz/installer/Main.kt
Normal file
123
src/main/kotlin/link/infra/packwiz/installer/Main.kt
Normal file
@ -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<String>) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,16 @@
|
||||
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 {
|
||||
interface IRequestHandler {
|
||||
fun matchesHandler(loc: SpaceSafeURI): Boolean
|
||||
|
||||
boolean matchesHandler(SpaceSafeURI loc);
|
||||
|
||||
default SpaceSafeURI getNewLoc(SpaceSafeURI loc) {
|
||||
return loc;
|
||||
fun getNewLoc(loc: SpaceSafeURI): SpaceSafeURI {
|
||||
return loc
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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?
|
||||
}
|
@ -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
|
||||
}
|
||||
return "github.com" == loc.host
|
||||
// TODO: sanity checks, support for more github urls
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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<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)
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user