Start porting to Kotlin

This commit is contained in:
comp500 2019-12-19 12:25:20 +00:00
parent c0c318772b
commit b45a2983e7
13 changed files with 464 additions and 521 deletions

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View 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
View 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>

View File

@ -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
View 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"
}
}

View File

@ -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!
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View 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)
}
}
}

View File

@ -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?
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}