mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-10-17 00:14:33 +02:00
Compare commits
29 Commits
v0.0.1-pre
...
v0.0.9-pre
Author | SHA1 | Date | |
---|---|---|---|
|
794b817eff | ||
|
a5ff63c587 | ||
|
34a86ffb7d | ||
|
780efe2c9f | ||
|
d1647764c4 | ||
|
12bf090895 | ||
|
533c7a3ed5 | ||
|
d18e134140 | ||
|
165c8cc172 | ||
|
d986b39aa5 | ||
|
040bb955ec | ||
|
022af4b5c5 | ||
|
442fb93ca8 | ||
|
81c1ebaa15 | ||
|
917d10c448 | ||
|
26c3261848 | ||
|
fbd54b4604 | ||
|
8be5cb8e60 | ||
|
c181a36edc | ||
|
e32d98fb98 | ||
|
e65d20be79 | ||
|
3d28f0a674 | ||
|
905630cb2a | ||
|
fd87edd6ca | ||
|
2118a8fda1 | ||
|
86c2349fd3 | ||
|
f76a3d2d62 | ||
|
72d27715f8 | ||
|
26b8e1de86 |
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.classpath": true,
|
||||||
|
"**/.project": true,
|
||||||
|
"**/.settings": true,
|
||||||
|
"**/.factorypath": true
|
||||||
|
},
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive"
|
||||||
|
}
|
16
build.gradle
16
build.gradle
@@ -7,7 +7,11 @@ plugins {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'commons-cli:commons-cli:1.4'
|
implementation 'commons-cli:commons-cli:1.4'
|
||||||
|
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||||
|
// TODO: Implement tests
|
||||||
//testImplementation 'junit:junit:4.12'
|
//testImplementation 'junit:junit:4.12'
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.1'
|
||||||
|
implementation 'com.squareup.okio:okio:2.2.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -26,9 +30,19 @@ jar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commons CLI is already included in packwiz-installer-bootstrap
|
// Commons CLI and Minimal JSON are already included in packwiz-installer-bootstrap
|
||||||
shadowJar {
|
shadowJar {
|
||||||
dependencies {
|
dependencies {
|
||||||
exclude(dependency('commons-cli:commons-cli:1.4'))
|
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
|
@@ -1,99 +0,0 @@
|
|||||||
package link.infra.packwiz.installer;
|
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
|
||||||
import java.awt.Component;
|
|
||||||
import java.awt.EventQueue;
|
|
||||||
import java.awt.GridBagConstraints;
|
|
||||||
import java.awt.GridBagLayout;
|
|
||||||
|
|
||||||
import javax.swing.JButton;
|
|
||||||
import javax.swing.JFrame;
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JPanel;
|
|
||||||
import javax.swing.JProgressBar;
|
|
||||||
import javax.swing.UIManager;
|
|
||||||
import javax.swing.border.EmptyBorder;
|
|
||||||
import java.awt.event.ActionListener;
|
|
||||||
import java.awt.event.ActionEvent;
|
|
||||||
|
|
||||||
public class InstallWindow {
|
|
||||||
|
|
||||||
// TODO: move to seperate file, make usable without GUI
|
|
||||||
|
|
||||||
private JFrame frmPackwizlauncher;
|
|
||||||
private UpdateManager updateManager = new UpdateManager();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch the application.
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
EventQueue.invokeLater(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
|
||||||
InstallWindow window = new InstallWindow();
|
|
||||||
window.frmPackwizlauncher.setVisible(true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the application.
|
|
||||||
*/
|
|
||||||
public InstallWindow() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the contents of the frame.
|
|
||||||
*/
|
|
||||||
private void initialize() {
|
|
||||||
frmPackwizlauncher = new JFrame();
|
|
||||||
frmPackwizlauncher.setTitle("Updating modpack...");
|
|
||||||
frmPackwizlauncher.setBounds(100, 100, 493, 95);
|
|
||||||
frmPackwizlauncher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
|
||||||
frmPackwizlauncher.setLocationRelativeTo(null);
|
|
||||||
|
|
||||||
JPanel panel = new JPanel();
|
|
||||||
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
|
||||||
frmPackwizlauncher.getContentPane().add(panel, BorderLayout.CENTER);
|
|
||||||
panel.setLayout(new BorderLayout(0, 0));
|
|
||||||
|
|
||||||
JProgressBar progressBar = new JProgressBar();
|
|
||||||
progressBar.setValue(50);
|
|
||||||
panel.add(progressBar, BorderLayout.CENTER);
|
|
||||||
|
|
||||||
JLabel lblProgresslabel = new JLabel("Loading...");
|
|
||||||
panel.add(lblProgresslabel, BorderLayout.SOUTH);
|
|
||||||
|
|
||||||
JPanel panel_1 = new JPanel();
|
|
||||||
panel_1.setBorder(new EmptyBorder(0, 5, 0, 5));
|
|
||||||
frmPackwizlauncher.getContentPane().add(panel_1, BorderLayout.EAST);
|
|
||||||
GridBagLayout gbl_panel_1 = new GridBagLayout();
|
|
||||||
panel_1.setLayout(gbl_panel_1);
|
|
||||||
|
|
||||||
JButton btnOptions = new JButton("Options...");
|
|
||||||
btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT);
|
|
||||||
GridBagConstraints gbc_btnOptions = new GridBagConstraints();
|
|
||||||
gbc_btnOptions.gridx = 0;
|
|
||||||
gbc_btnOptions.gridy = 0;
|
|
||||||
panel_1.add(btnOptions, gbc_btnOptions);
|
|
||||||
|
|
||||||
JButton btnCancel = new JButton("Cancel");
|
|
||||||
btnCancel.addActionListener(new ActionListener() {
|
|
||||||
public void actionPerformed(ActionEvent arg0) {
|
|
||||||
updateManager.cleanup();
|
|
||||||
frmPackwizlauncher.dispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
|
||||||
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
|
|
||||||
gbc_btnCancel.gridx = 0;
|
|
||||||
gbc_btnCancel.gridy = 1;
|
|
||||||
panel_1.add(btnCancel, gbc_btnCancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,5 +1,10 @@
|
|||||||
package link.infra.packwiz.installer;
|
package link.infra.packwiz.installer;
|
||||||
|
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.UIManager;
|
import javax.swing.UIManager;
|
||||||
|
|
||||||
@@ -9,11 +14,40 @@ import org.apache.commons.cli.DefaultParser;
|
|||||||
import org.apache.commons.cli.Options;
|
import org.apache.commons.cli.Options;
|
||||||
import org.apache.commons.cli.ParseException;
|
import org.apache.commons.cli.ParseException;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.CLIHandler;
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface;
|
||||||
|
import link.infra.packwiz.installer.ui.InstallWindow;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
|
|
||||||
// Actual main() is in RequiresBootstrap!
|
// Actual main() is in RequiresBootstrap!
|
||||||
|
|
||||||
public Main(String[] args) {
|
public Main(String[] args) {
|
||||||
|
// Big overarching try/catch just in case everything breaks
|
||||||
|
try {
|
||||||
|
this.startup(args);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
EventQueue.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void startup(String[] args) {
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
addNonBootstrapOptions(options);
|
addNonBootstrapOptions(options);
|
||||||
addBootstrapOptions(options);
|
addBootstrapOptions(options);
|
||||||
@@ -33,21 +67,93 @@ public class Main {
|
|||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("Hello World!");
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.show();
|
||||||
|
|
||||||
|
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 URI(unparsedArgs[0]);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
// TODO: better error message?
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start update process!
|
||||||
|
// TODO: start in SwingWorker?
|
||||||
|
try {
|
||||||
|
ui.executeManager(new Runnable(){
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
new UpdateManager(uOptions, ui);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: better error message?
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: better error message?
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by packwiz-installer-bootstrap to set up the help command
|
// Called by packwiz-installer-bootstrap to set up the help command
|
||||||
public static void addNonBootstrapOptions(Options options) {
|
public static void addNonBootstrapOptions(Options options) {
|
||||||
//options.addOption("w", "welp", false, "Testing 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?
|
// TODO: link these somehow so they're only defined once?
|
||||||
private static void addBootstrapOptions(Options options) {
|
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-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-no-update", false, "Don't update packwiz-installer");
|
||||||
options.addOption(null, "bootstrap-main-jar", true, "Location of the packwiz-installer JAR file");
|
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("g", "no-gui", false, "Don't display a GUI to show update progress");
|
||||||
options.addOption("h", "help", false, "Display this message");
|
options.addOption("h", "help", false, "Display this message"); // Implemented in packwiz-installer-bootstrap!
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,427 @@
|
|||||||
package link.infra.packwiz.installer;
|
package link.infra.packwiz.installer;
|
||||||
|
|
||||||
public class UpdateManager {
|
import java.io.FileNotFoundException;
|
||||||
Thread updateThread = new Thread(new UpdateThread());
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.CompletionService;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorCompletionService;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public void cleanup() {
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonIOException;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.moandjiezana.toml.Toml;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.IndexFile;
|
||||||
|
import link.infra.packwiz.installer.metadata.ManifestFile;
|
||||||
|
import link.infra.packwiz.installer.metadata.PackFile;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
||||||
|
import link.infra.packwiz.installer.request.HandlerManager;
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface;
|
||||||
|
import link.infra.packwiz.installer.ui.InstallProgress;
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public class UpdateManager {
|
||||||
|
|
||||||
|
public final Options opts;
|
||||||
|
public final IUserInterface ui;
|
||||||
|
|
||||||
|
public static class Options {
|
||||||
|
public URI downloadURI = null;
|
||||||
|
public String manifestFile = "packwiz.json"; // TODO: make configurable
|
||||||
|
public String packFolder = ".";
|
||||||
|
public Side side = Side.CLIENT;
|
||||||
|
|
||||||
|
public static enum Side {
|
||||||
|
@SerializedName("client")
|
||||||
|
CLIENT("client"), @SerializedName("server")
|
||||||
|
SERVER("server"), @SerializedName("both")
|
||||||
|
BOTH("both", new Side[] { CLIENT, SERVER });
|
||||||
|
|
||||||
|
private final String sideName;
|
||||||
|
private final Side[] depSides;
|
||||||
|
|
||||||
|
Side(String sideName) {
|
||||||
|
this.sideName = sideName.toLowerCase();
|
||||||
|
this.depSides = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Side(String sideName, Side[] depSides) {
|
||||||
|
this.sideName = sideName.toLowerCase();
|
||||||
|
this.depSides = depSides;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.sideName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSide(Side tSide) {
|
||||||
|
if (this.equals(tSide)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.depSides != null) {
|
||||||
|
for (int i = 0; i < this.depSides.length; i++) {
|
||||||
|
if (this.depSides[i].equals(tSide)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Side from(String name) {
|
||||||
|
String lowerName = name.toLowerCase();
|
||||||
|
for (Side side : Side.values()) {
|
||||||
|
if (side.sideName == lowerName) {
|
||||||
|
return side;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpdateManager(Options opts, IUserInterface ui) {
|
||||||
|
this.opts = opts;
|
||||||
|
this.ui = ui;
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void start() {
|
||||||
|
this.checkOptions();
|
||||||
|
|
||||||
|
ui.submitProgress(new InstallProgress("Loading manifest file..."));
|
||||||
|
Gson gson = new GsonBuilder().registerTypeAdapter(Hash.class, new Hash.TypeHandler()).create();
|
||||||
|
ManifestFile manifest;
|
||||||
|
try {
|
||||||
|
manifest = gson.fromJson(new FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
|
||||||
|
ManifestFile.class);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
manifest = new ManifestFile();
|
||||||
|
} catch (JsonSyntaxException | JsonIOException e) {
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.submitProgress(new InstallProgress("Loading pack file..."));
|
||||||
|
GeneralHashingSource packFileSource;
|
||||||
|
try {
|
||||||
|
Source src = HandlerManager.getFileSource(opts.downloadURI);
|
||||||
|
packFileSource = HashUtils.getHasher("sha256").getHashingSource(src);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: still launch the game if updating doesn't work?
|
||||||
|
// TODO: ask user if they want to launch the game, exit(1) if they don't
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PackFile pf;
|
||||||
|
try {
|
||||||
|
pf = new Toml().read(Okio.buffer(packFileSource).inputStream()).to(PackFile.class);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.submitProgress(new InstallProgress("Checking local files..."));
|
||||||
|
|
||||||
|
List<URI> invalidatedUris = new ArrayList<>();
|
||||||
|
if (manifest.cachedFiles != null) {
|
||||||
|
Iterator<Map.Entry<URI, ManifestFile.File>> it = manifest.cachedFiles.entrySet().iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Map.Entry<URI, ManifestFile.File> entry = it.next();
|
||||||
|
if (entry.getValue().cachedLocation != null) {
|
||||||
|
if (!Files.exists(Paths.get(opts.packFolder, entry.getValue().cachedLocation))) {
|
||||||
|
URI fileUri = entry.getKey();
|
||||||
|
System.out.println("File " + fileUri.toString() + " invalidated, marked for redownloading");
|
||||||
|
invalidatedUris.add(fileUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash) && invalidatedUris.size() == 0) {
|
||||||
|
System.out.println("Modpack is already up to date!");
|
||||||
|
// todo: --force?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Modpack name: " + pf.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file),
|
||||||
|
HashUtils.getHash(pf.index.hashFormat, pf.index.hash), pf.index.hashFormat, manifest, invalidatedUris);
|
||||||
|
} catch (Exception e1) {
|
||||||
|
ui.handleExceptionAndExit(e1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: update MMC params, java args etc
|
||||||
|
|
||||||
|
manifest.packFileHash = packFileSource.getHash();
|
||||||
|
try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
|
||||||
|
gson.toJson(manifest, writer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO: add message?
|
||||||
|
ui.handleException(e);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void checkOptions() {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void processIndex(URI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<URI> invalidatedUris) {
|
||||||
|
if (manifest.indexFileHash != null && manifest.indexFileHash.equals(indexHash) && invalidatedUris.size() == 0) {
|
||||||
|
System.out.println("Modpack files are already up to date!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manifest.indexFileHash = indexHash;
|
||||||
|
|
||||||
|
GeneralHashingSource indexFileSource;
|
||||||
|
try {
|
||||||
|
Source src = HandlerManager.getFileSource(indexUri);
|
||||||
|
indexFileSource = HashUtils.getHasher(hashFormat).getHashingSource(src);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: still launch the game if updating doesn't work?
|
||||||
|
// TODO: ask user if they want to launch the game, exit(1) if they don't
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IndexFile indexFile;
|
||||||
|
try {
|
||||||
|
indexFile = new Toml().read(Okio.buffer(indexFileSource).inputStream()).to(IndexFile.class);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!indexFileSource.hashIsEqual(indexHash)) {
|
||||||
|
// TODO: throw exception
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.cachedFiles == null) {
|
||||||
|
manifest.cachedFiles = new HashMap<URI, ManifestFile.File>();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.submitProgress(new InstallProgress("Checking local files..."));
|
||||||
|
Iterator<Map.Entry<URI, ManifestFile.File>> it = manifest.cachedFiles.entrySet().iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Map.Entry<URI, ManifestFile.File> entry = it.next();
|
||||||
|
if (entry.getValue().cachedLocation != null) {
|
||||||
|
if (!indexFile.files.stream().anyMatch(f -> f.file.equals(entry.getKey()))) {
|
||||||
|
// File has been removed from the index
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation));
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO: should this be shown to the user in some way?
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.submitProgress(new InstallProgress("Comparing new files..."));
|
||||||
|
|
||||||
|
// TODO: progress bar
|
||||||
|
ConcurrentLinkedQueue<Exception> exceptionQueue = new ConcurrentLinkedQueue<Exception>();
|
||||||
|
List<IndexFile.File> newFiles = indexFile.files.stream().map(f -> {
|
||||||
|
if (f.hashFormat == null || f.hashFormat.length() == 0) {
|
||||||
|
f.hashFormat = indexFile.hashFormat;
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}).filter(f -> {
|
||||||
|
if (invalidatedUris.contains(f.file)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ManifestFile.File cachedFile = manifest.cachedFiles.get(f.file);
|
||||||
|
Hash newHash;
|
||||||
|
try {
|
||||||
|
newHash = HashUtils.getHash(f.hashFormat, f.hash);
|
||||||
|
} catch (Exception e) {
|
||||||
|
exceptionQueue.add(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cachedFile == null || !newHash.equals(cachedFile.hash);
|
||||||
|
}).parallel().map(f -> {
|
||||||
|
try {
|
||||||
|
f.downloadMeta(indexFile, indexUri);
|
||||||
|
} catch (Exception e) {
|
||||||
|
exceptionQueue.add(e);
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (Exception e : exceptionQueue) {
|
||||||
|
// TODO: collect all exceptions, present in one dialog
|
||||||
|
ui.handleException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: present options
|
||||||
|
// TODO: all options should be presented, not just new files!!!!!!!
|
||||||
|
// and options should be readded to newFiles after option -> true
|
||||||
|
newFiles.stream().filter(f -> f.linkedFile != null).filter(f -> f.linkedFile.option != null).map(f -> {
|
||||||
|
return "option: " + (f.linkedFile.option.description == null ? "null" : f.linkedFile.option.description);
|
||||||
|
}).forEachOrdered(desc -> {
|
||||||
|
System.out.println(desc);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: different thread pool type?
|
||||||
|
ExecutorService threadPool = Executors.newFixedThreadPool(10);
|
||||||
|
CompletionService<DownloadCompletion> completionService = new ExecutorCompletionService<DownloadCompletion>(
|
||||||
|
threadPool);
|
||||||
|
|
||||||
|
for (IndexFile.File f : newFiles) {
|
||||||
|
ManifestFile.File cachedFile = manifest.cachedFiles.get(f.file);
|
||||||
|
completionService.submit(new Callable<DownloadCompletion>() {
|
||||||
|
public DownloadCompletion call() {
|
||||||
|
DownloadCompletion dc = new DownloadCompletion();
|
||||||
|
dc.file = f;
|
||||||
|
|
||||||
|
if (cachedFile != null && cachedFile.linkedFileHash != null && f.linkedFile != null) {
|
||||||
|
try {
|
||||||
|
if (cachedFile.linkedFileHash.equals(f.linkedFile.getHash())) {
|
||||||
|
// Do nothing, the file didn't change
|
||||||
|
// TODO: but if the hash of the metafile changed, what did change?????
|
||||||
|
// should this be checked somehow??
|
||||||
|
return dc;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Path destPath = Paths.get(opts.packFolder, f.getDestURI().toString());
|
||||||
|
|
||||||
|
// Don't update files marked with preserve if they already exist on disk
|
||||||
|
if (f.preserve) {
|
||||||
|
if (Files.exists(destPath)) {
|
||||||
|
return dc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Hash hash;
|
||||||
|
String fileHashFormat;
|
||||||
|
if (f.linkedFile != null) {
|
||||||
|
hash = f.linkedFile.getHash();
|
||||||
|
fileHashFormat = f.linkedFile.download.hashFormat;
|
||||||
|
} else {
|
||||||
|
hash = f.getHash();
|
||||||
|
fileHashFormat = f.hashFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
Source src = f.getSource(indexUri);
|
||||||
|
GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src);
|
||||||
|
Buffer data = new Buffer();
|
||||||
|
Okio.buffer(fileSource).readAll(data);
|
||||||
|
|
||||||
|
if (fileSource.hashIsEqual(hash)) {
|
||||||
|
Files.createDirectories(destPath.getParent());
|
||||||
|
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} else {
|
||||||
|
System.out.println("Invalid hash for " + f.getDestURI().toString());
|
||||||
|
System.out.println("Calculated: " + fileSource.getHash());
|
||||||
|
System.out.println("Expected: " + hash);
|
||||||
|
dc.err = new Exception("Hash invalid!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedFile != null && !destPath.equals(Paths.get(opts.packFolder, cachedFile.cachedLocation))) {
|
||||||
|
// Delete old file if location changes
|
||||||
|
Files.delete(Paths.get(opts.packFolder, cachedFile.cachedLocation));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dc;
|
||||||
|
} catch (Exception e) {
|
||||||
|
dc.err = e;
|
||||||
|
return dc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < newFiles.size(); i++) {
|
||||||
|
DownloadCompletion ret;
|
||||||
|
try {
|
||||||
|
ret = completionService.take().get();
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
// TODO: collect all exceptions, present in one dialog
|
||||||
|
ui.handleException(e);
|
||||||
|
ret = null;
|
||||||
|
}
|
||||||
|
// Update manifest
|
||||||
|
if (ret != null && ret.err == null && ret.file != null) {
|
||||||
|
ManifestFile.File newCachedFile = new ManifestFile.File();
|
||||||
|
try {
|
||||||
|
newCachedFile.hash = ret.file.getHash();
|
||||||
|
if (newCachedFile.hash == null) {
|
||||||
|
throw new Exception("Invalid hash!");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
ret.err = e;
|
||||||
|
}
|
||||||
|
if (ret.file.metafile && ret.file.linkedFile != null) {
|
||||||
|
newCachedFile.isOptional = ret.file.linkedFile.isOptional();
|
||||||
|
if (newCachedFile.isOptional) {
|
||||||
|
newCachedFile.optionValue = ret.file.optionValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
newCachedFile.linkedFileHash = ret.file.linkedFile.getHash();
|
||||||
|
} catch (Exception e) {
|
||||||
|
ret.err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newCachedFile.cachedLocation = ret.file.getDestURI().toString();
|
||||||
|
manifest.cachedFiles.put(ret.file.file, newCachedFile);
|
||||||
|
}
|
||||||
|
// TODO: show errors properly?
|
||||||
|
String progress;
|
||||||
|
if (ret != null) {
|
||||||
|
if (ret.err != null) {
|
||||||
|
if (ret.file != null) {
|
||||||
|
progress = "Failed to download " + ret.file.getName() + ": " + ret.err.getMessage();
|
||||||
|
} else {
|
||||||
|
progress = "Failed to download: " + ret.err.getMessage();
|
||||||
|
}
|
||||||
|
ret.err.printStackTrace();
|
||||||
|
} else if (ret.file != null) {
|
||||||
|
progress = "Downloaded " + ret.file.getName();
|
||||||
|
} else {
|
||||||
|
progress = "Failed to download, unknown reason";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
progress = "Failed to download, unknown reason";
|
||||||
|
}
|
||||||
|
ui.submitProgress(new InstallProgress(progress, i + 1, newFiles.size()));
|
||||||
|
}
|
||||||
|
// option = false file hashes should be stored to disk, but not downloaded
|
||||||
|
// TODO: don't include optional files in progress????
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DownloadCompletion {
|
||||||
|
Exception err;
|
||||||
|
IndexFile.File file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
package link.infra.packwiz.installer;
|
|
||||||
|
|
||||||
public class UpdateThread implements Runnable {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -0,0 +1,108 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.moandjiezana.toml.Toml;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
||||||
|
import link.infra.packwiz.installer.request.HandlerManager;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public class IndexFile {
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
public String hashFormat;
|
||||||
|
public List<File> files;
|
||||||
|
|
||||||
|
public static class File {
|
||||||
|
@JsonAdapter(SpaceSafeURIParser.class)
|
||||||
|
public URI file;
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
public String hashFormat;
|
||||||
|
public String hash;
|
||||||
|
public URI alias;
|
||||||
|
public boolean metafile;
|
||||||
|
public boolean preserve;
|
||||||
|
|
||||||
|
public transient ModFile linkedFile;
|
||||||
|
public transient URI linkedFileURI;
|
||||||
|
public transient boolean optionValue = true;
|
||||||
|
|
||||||
|
public void downloadMeta(IndexFile parentIndexFile, URI indexUri) throws Exception {
|
||||||
|
if (!metafile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hashFormat == null || hashFormat.length() == 0) {
|
||||||
|
hashFormat = parentIndexFile.hashFormat;
|
||||||
|
}
|
||||||
|
Hash fileHash = HashUtils.getHash(hashFormat, hash);
|
||||||
|
linkedFileURI = HandlerManager.getNewLoc(indexUri, file);
|
||||||
|
Source src = HandlerManager.getFileSource(linkedFileURI);
|
||||||
|
GeneralHashingSource fileStream = HashUtils.getHasher(hashFormat).getHashingSource(src);
|
||||||
|
|
||||||
|
linkedFile = new Toml().read(Okio.buffer(fileStream).inputStream()).to(ModFile.class);
|
||||||
|
if (!fileStream.hashIsEqual(fileHash)) {
|
||||||
|
throw new Exception("Invalid mod file hash");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Source getSource(URI indexUri) throws Exception {
|
||||||
|
if (metafile) {
|
||||||
|
if (linkedFile == null) {
|
||||||
|
throw new Exception("Linked file doesn't exist!");
|
||||||
|
}
|
||||||
|
return linkedFile.getSource(linkedFileURI);
|
||||||
|
} else {
|
||||||
|
URI newLoc = HandlerManager.getNewLoc(indexUri, file);
|
||||||
|
if (newLoc == null) {
|
||||||
|
throw new Exception("Index file URI is invalid");
|
||||||
|
}
|
||||||
|
return HandlerManager.getFileSource(newLoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hash getHash() throws Exception {
|
||||||
|
if (hash == null) {
|
||||||
|
throw new Exception("Index file doesn't have a hash");
|
||||||
|
}
|
||||||
|
if (hashFormat == null) {
|
||||||
|
throw new Exception("Index file doesn't have a hash format");
|
||||||
|
}
|
||||||
|
return HashUtils.getHash(hashFormat, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
if (metafile) {
|
||||||
|
if (linkedFile != null) {
|
||||||
|
if (linkedFile.name != null) {
|
||||||
|
return linkedFile.name;
|
||||||
|
} else if (linkedFile.filename != null) {
|
||||||
|
return linkedFile.filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file != null) {
|
||||||
|
return Paths.get(file.getPath()).getFileName().toString();
|
||||||
|
}
|
||||||
|
return file.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI getDestURI() {
|
||||||
|
if (alias != null) {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
if (metafile && linkedFile != null) {
|
||||||
|
// TODO: URIs are bad
|
||||||
|
return file.resolve(linkedFile.filename.replace(" ", "%20"));
|
||||||
|
} else {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
|
||||||
|
public class ManifestFile {
|
||||||
|
|
||||||
|
public Hash packFileHash = null;
|
||||||
|
public Hash indexFileHash = null;
|
||||||
|
public Map<URI, File> cachedFiles;
|
||||||
|
|
||||||
|
public static class File {
|
||||||
|
public Hash hash = null;
|
||||||
|
public boolean isOptional = false;
|
||||||
|
public boolean optionValue = true;
|
||||||
|
public Hash linkedFileHash = null;
|
||||||
|
public String cachedLocation = null;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,74 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.UpdateManager.Options.Side;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
||||||
|
import link.infra.packwiz.installer.request.HandlerManager;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public class ModFile {
|
||||||
|
public String name;
|
||||||
|
public String filename;
|
||||||
|
public Side side;
|
||||||
|
|
||||||
|
public Download download;
|
||||||
|
public static class Download {
|
||||||
|
@JsonAdapter(SpaceSafeURIParser.class)
|
||||||
|
public URI url;
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
public String hashFormat;
|
||||||
|
public String hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> update;
|
||||||
|
|
||||||
|
public Option option;
|
||||||
|
public static class Option {
|
||||||
|
public boolean optional;
|
||||||
|
public String description;
|
||||||
|
@SerializedName("default")
|
||||||
|
public boolean defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Source getSource(URI baseLoc) throws Exception {
|
||||||
|
if (download == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have download");
|
||||||
|
}
|
||||||
|
if (download.url == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have a download URI");
|
||||||
|
}
|
||||||
|
URI newLoc = HandlerManager.getNewLoc(baseLoc, download.url);
|
||||||
|
if (newLoc == null) {
|
||||||
|
throw new Exception("Metadata file URI is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
return HandlerManager.getFileSource(newLoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hash getHash() throws Exception {
|
||||||
|
if (download == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have download");
|
||||||
|
}
|
||||||
|
if (download.hash == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have a hash");
|
||||||
|
}
|
||||||
|
if (download.hashFormat == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have a hash format");
|
||||||
|
}
|
||||||
|
return HashUtils.getHash(download.hashFormat, download.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOptional() {
|
||||||
|
if (option != null) {
|
||||||
|
return option.optional;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
public class PackFile {
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
public IndexFileLoc index;
|
||||||
|
public static class IndexFileLoc {
|
||||||
|
@JsonAdapter(SpaceSafeURIParser.class)
|
||||||
|
public URI file;
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
public String hashFormat;
|
||||||
|
public String hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> versions;
|
||||||
|
public Map<String, Object> client;
|
||||||
|
public Map<String, Object> server;
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import com.google.gson.JsonDeserializationContext;
|
||||||
|
import com.google.gson.JsonDeserializer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class encodes spaces before parsing the URI, so the URI can actually be
|
||||||
|
* parsed.
|
||||||
|
*/
|
||||||
|
class SpaceSafeURIParser implements JsonDeserializer<URI> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||||
|
throws JsonParseException {
|
||||||
|
String uriString = json.getAsString().replace(" ", "%20");
|
||||||
|
try {
|
||||||
|
return new URI(uriString);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new JsonParseException("Failed to parse URI", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace this with a better solution?
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import okio.ForwardingSource;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public abstract class GeneralHashingSource extends ForwardingSource {
|
||||||
|
|
||||||
|
public GeneralHashingSource(Source delegate) {
|
||||||
|
super(delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Hash getHash();
|
||||||
|
|
||||||
|
public boolean hashIsEqual(Object compareTo) {
|
||||||
|
return compareTo.equals(getHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,48 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
|
||||||
|
import com.google.gson.JsonDeserializationContext;
|
||||||
|
import com.google.gson.JsonDeserializer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.google.gson.JsonSerializationContext;
|
||||||
|
import com.google.gson.JsonSerializer;
|
||||||
|
|
||||||
|
public abstract class Hash {
|
||||||
|
protected abstract String getStringValue();
|
||||||
|
|
||||||
|
protected abstract String getType();
|
||||||
|
|
||||||
|
public static class TypeHandler implements JsonDeserializer<Hash>, JsonSerializer<Hash> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Hash src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
JsonObject out = new JsonObject();
|
||||||
|
out.add("type", new JsonPrimitive(src.getType()));
|
||||||
|
out.add("value", new JsonPrimitive(src.getStringValue()));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||||
|
throws JsonParseException {
|
||||||
|
JsonObject obj = json.getAsJsonObject();
|
||||||
|
String type, value;
|
||||||
|
try {
|
||||||
|
type = obj.get("type").getAsString();
|
||||||
|
value = obj.get("value").getAsString();
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
throw new JsonParseException("Invalid hash JSON data");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return HashUtils.getHash(type, value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new JsonParseException("Failed to create hash object", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class HashUtils {
|
||||||
|
private static final Map<String, IHasher> hashTypeConversion = new HashMap<String, IHasher>();
|
||||||
|
static {
|
||||||
|
hashTypeConversion.put("sha256", new HashingSourceHasher("sha256"));
|
||||||
|
hashTypeConversion.put("murmur2", new Murmur2Hasher());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IHasher getHasher(String type) throws Exception {
|
||||||
|
IHasher hasher = hashTypeConversion.get(type);
|
||||||
|
if (hasher == null) {
|
||||||
|
throw new Exception("Hash type not supported: " + type);
|
||||||
|
}
|
||||||
|
return hasher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Hash getHash(String type, String value) throws Exception {
|
||||||
|
if (hashTypeConversion.containsKey(type)) {
|
||||||
|
return hashTypeConversion.get(type).getHash(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Hash type not supported: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,86 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import okio.HashingSource;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public class HashingSourceHasher implements IHasher {
|
||||||
|
String type;
|
||||||
|
|
||||||
|
public HashingSourceHasher(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// i love naming things
|
||||||
|
private class HashingSourceGeneralHashingSource extends GeneralHashingSource {
|
||||||
|
HashingSource delegateHashing;
|
||||||
|
HashingSourceHash value;
|
||||||
|
|
||||||
|
public HashingSourceGeneralHashingSource(HashingSource delegate) {
|
||||||
|
super(delegate);
|
||||||
|
delegateHashing = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash getHash() {
|
||||||
|
if (value == null) {
|
||||||
|
value = new HashingSourceHash(delegateHashing.hash().hex());
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// this some funky inner class stuff
|
||||||
|
// each of these classes is specific to the instance of the HasherHashingSource
|
||||||
|
// therefore HashingSourceHashes from different parent instances will be not instanceof each other
|
||||||
|
private class HashingSourceHash extends Hash {
|
||||||
|
String value;
|
||||||
|
private HashingSourceHash(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof HashingSourceHash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HashingSourceHash objHash = (HashingSourceHash) obj;
|
||||||
|
if (value != null) {
|
||||||
|
return value.equals(objHash.value);
|
||||||
|
} else {
|
||||||
|
return objHash.value == null ? true : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return type + ": " + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getStringValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GeneralHashingSource getHashingSource(Source delegate) {
|
||||||
|
switch (type) {
|
||||||
|
case "sha256":
|
||||||
|
return new HashingSourceGeneralHashingSource(HashingSource.sha256(delegate));
|
||||||
|
// TODO: support other hash types
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Invalid hash type provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash getHash(String value) {
|
||||||
|
return new HashingSourceHash(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public interface IHasher {
|
||||||
|
public GeneralHashingSource getHashingSource(Source delegate);
|
||||||
|
public Hash getHash(String value);
|
||||||
|
}
|
@@ -0,0 +1,104 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public class Murmur2Hasher implements IHasher {
|
||||||
|
private class Murmur2GeneralHashingSource extends GeneralHashingSource {
|
||||||
|
Murmur2Hash value;
|
||||||
|
Buffer internalBuffer = new Buffer();
|
||||||
|
Buffer tempBuffer = new Buffer();
|
||||||
|
Source delegate;
|
||||||
|
|
||||||
|
public Murmur2GeneralHashingSource(Source delegate) {
|
||||||
|
super(delegate);
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long read(Buffer sink, long byteCount) throws IOException {
|
||||||
|
long out = delegate.read(tempBuffer, byteCount);
|
||||||
|
if (out > -1) {
|
||||||
|
sink.write(tempBuffer.clone(), out);
|
||||||
|
internalBuffer.write(tempBuffer, out);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash getHash() {
|
||||||
|
if (value == null) {
|
||||||
|
byte[] data = computeNormalizedArray(internalBuffer.readByteArray());
|
||||||
|
value = new Murmur2Hash(Murmur2Lib.hash32(data, data.length, 1));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credit to https://github.com/modmuss50/CAV2/blob/master/murmur.go
|
||||||
|
private byte[] computeNormalizedArray(byte[] input) {
|
||||||
|
byte[] output = new byte[input.length];
|
||||||
|
int num = 0;
|
||||||
|
for (int i = 0; i < input.length; i++) {
|
||||||
|
byte b = input[i];
|
||||||
|
if (!(b == 9 || b == 10 || b == 13 || b == 32)) {
|
||||||
|
output[num] = b;
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] outputTrimmed = new byte[num];
|
||||||
|
System.arraycopy(output, 0, outputTrimmed, 0, num);
|
||||||
|
return outputTrimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Murmur2Hash extends Hash {
|
||||||
|
int value;
|
||||||
|
private Murmur2Hash(String value) {
|
||||||
|
// Parsing as long then casting to int converts values gt int max value but lt uint max value
|
||||||
|
// into negatives. I presume this is how the murmur2 code handles this.
|
||||||
|
this.value = (int)Long.parseLong(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Murmur2Hash(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof Murmur2Hash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Murmur2Hash objHash = (Murmur2Hash) obj;
|
||||||
|
return value == objHash.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "murmur2: " + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getStringValue() {
|
||||||
|
return Integer.toString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getType() {
|
||||||
|
return "murmur2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GeneralHashingSource getHashingSource(Source delegate) {
|
||||||
|
return new Murmur2GeneralHashingSource(delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash getHash(String value) {
|
||||||
|
return new Murmur2Hash(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,166 @@
|
|||||||
|
// Obtained from https://github.com/prasanthj/hasher/blob/master/src/main/java/hasher/Murmur2.java
|
||||||
|
/**
|
||||||
|
* Copyright 2014 Prasanth Jayachandran
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Murmur2 32 and 64 bit variants.
|
||||||
|
* 32-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash2.cpp#37
|
||||||
|
* 64-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash2.cpp#96
|
||||||
|
*/
|
||||||
|
public class Murmur2Lib {
|
||||||
|
// Constants for 32-bit variant
|
||||||
|
private static final int M_32 = 0x5bd1e995;
|
||||||
|
private static final int R_32 = 24;
|
||||||
|
|
||||||
|
// Constants for 64-bit variant
|
||||||
|
private static final long M_64 = 0xc6a4a7935bd1e995L;
|
||||||
|
private static final int R_64 = 47;
|
||||||
|
private static final int DEFAULT_SEED = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Murmur2 32-bit variant.
|
||||||
|
*
|
||||||
|
* @param data - input byte array
|
||||||
|
* @return - hashcode
|
||||||
|
*/
|
||||||
|
public static int hash32(byte[] data) {
|
||||||
|
return hash32(data, data.length, DEFAULT_SEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Murmur2 32-bit variant.
|
||||||
|
*
|
||||||
|
* @param data - input byte array
|
||||||
|
* @param length - length of array
|
||||||
|
* @param seed - seed. (default 0)
|
||||||
|
* @return - hashcode
|
||||||
|
*/
|
||||||
|
public static int hash32(byte[] data, int length, int seed) {
|
||||||
|
int h = seed ^ length;
|
||||||
|
int len_4 = length >> 2;
|
||||||
|
|
||||||
|
// body
|
||||||
|
for (int i = 0; i < len_4; i++) {
|
||||||
|
int i_4 = i << 2;
|
||||||
|
int k = (data[i_4] & 0xff)
|
||||||
|
| ((data[i_4 + 1] & 0xff) << 8)
|
||||||
|
| ((data[i_4 + 2] & 0xff) << 16)
|
||||||
|
| ((data[i_4 + 3] & 0xff) << 24);
|
||||||
|
|
||||||
|
// mix functions
|
||||||
|
k *= M_32;
|
||||||
|
k ^= k >>> R_32;
|
||||||
|
k *= M_32;
|
||||||
|
h *= M_32;
|
||||||
|
h ^= k;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tail
|
||||||
|
int len_m = len_4 << 2;
|
||||||
|
int left = length - len_m;
|
||||||
|
if (left != 0) {
|
||||||
|
if (left >= 3) {
|
||||||
|
h ^= (int) data[length - 3] << 16;
|
||||||
|
}
|
||||||
|
if (left >= 2) {
|
||||||
|
h ^= (int) data[length - 2] << 8;
|
||||||
|
}
|
||||||
|
if (left >= 1) {
|
||||||
|
h ^= (int) data[length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
h *= M_32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalization
|
||||||
|
h ^= h >>> 13;
|
||||||
|
h *= M_32;
|
||||||
|
h ^= h >>> 15;
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Murmur2 64-bit variant.
|
||||||
|
*
|
||||||
|
* @param data - input byte array
|
||||||
|
* @return - hashcode
|
||||||
|
*/
|
||||||
|
public static long hash64(final byte[] data) {
|
||||||
|
return hash64(data, data.length, DEFAULT_SEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Murmur2 64-bit variant.
|
||||||
|
*
|
||||||
|
* @param data - input byte array
|
||||||
|
* @param length - length of array
|
||||||
|
* @param seed - seed. (default 0)
|
||||||
|
* @return - hashcode
|
||||||
|
*/
|
||||||
|
public static long hash64(final byte[] data, int length, int seed) {
|
||||||
|
long h = (seed & 0xffffffffl) ^ (length * M_64);
|
||||||
|
int length8 = length >> 3;
|
||||||
|
|
||||||
|
// body
|
||||||
|
for (int i = 0; i < length8; i++) {
|
||||||
|
final int i8 = i << 3;
|
||||||
|
long k = ((long) data[i8] & 0xff)
|
||||||
|
| (((long) data[i8 + 1] & 0xff) << 8)
|
||||||
|
| (((long) data[i8 + 2] & 0xff) << 16)
|
||||||
|
| (((long) data[i8 + 3] & 0xff) << 24)
|
||||||
|
| (((long) data[i8 + 4] & 0xff) << 32)
|
||||||
|
| (((long) data[i8 + 5] & 0xff) << 40)
|
||||||
|
| (((long) data[i8 + 6] & 0xff) << 48)
|
||||||
|
| (((long) data[i8 + 7] & 0xff) << 56);
|
||||||
|
|
||||||
|
// mix functions
|
||||||
|
k *= M_64;
|
||||||
|
k ^= k >>> R_64;
|
||||||
|
k *= M_64;
|
||||||
|
h ^= k;
|
||||||
|
h *= M_64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tail
|
||||||
|
int tailStart = length8 << 3;
|
||||||
|
switch (length - tailStart) {
|
||||||
|
case 7:
|
||||||
|
h ^= (long) (data[tailStart + 6] & 0xff) << 48;
|
||||||
|
case 6:
|
||||||
|
h ^= (long) (data[tailStart + 5] & 0xff) << 40;
|
||||||
|
case 5:
|
||||||
|
h ^= (long) (data[tailStart + 4] & 0xff) << 32;
|
||||||
|
case 4:
|
||||||
|
h ^= (long) (data[tailStart + 3] & 0xff) << 24;
|
||||||
|
case 3:
|
||||||
|
h ^= (long) (data[tailStart + 2] & 0xff) << 16;
|
||||||
|
case 2:
|
||||||
|
h ^= (long) (data[tailStart + 1] & 0xff) << 8;
|
||||||
|
case 1:
|
||||||
|
h ^= (long) (data[tailStart] & 0xff);
|
||||||
|
h *= M_64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalization
|
||||||
|
h ^= h >>> R_64;
|
||||||
|
h *= M_64;
|
||||||
|
h ^= h >>> R_64;
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,59 @@
|
|||||||
|
package link.infra.packwiz.installer.request;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub;
|
||||||
|
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public abstract class HandlerManager {
|
||||||
|
|
||||||
|
public static List<IRequestHandler> handlers = new ArrayList<IRequestHandler>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
handlers.add(new RequestHandlerGithub());
|
||||||
|
handlers.add(new RequestHandlerHTTP());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static URI getNewLoc(URI base, URI loc) {
|
||||||
|
if (loc == null) return null;
|
||||||
|
if (base != null) {
|
||||||
|
loc = base.resolve(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (IRequestHandler handler : handlers) {
|
||||||
|
if (handler.matchesHandler(loc)) {
|
||||||
|
return handler.getNewLoc(loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: What if files are read multiple times??
|
||||||
|
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
|
||||||
|
// Caching system? Copy from already downloaded files?
|
||||||
|
|
||||||
|
public static Source getFileSource(URI loc) throws Exception {
|
||||||
|
for (IRequestHandler handler : handlers) {
|
||||||
|
if (handler.matchesHandler(loc)) {
|
||||||
|
Source src = handler.getFileSource(loc);
|
||||||
|
if (src == null) {
|
||||||
|
throw new Exception("Couldn't find URI: " + loc.toString());
|
||||||
|
} else {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: specialised exception classes??
|
||||||
|
throw new Exception("No handler available for URI: " + loc.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// github toml resolution
|
||||||
|
// e.g. https://github.com/comp500/Demagnetize -> demagnetize.toml
|
||||||
|
// https://github.com/comp500/Demagnetize/blob/master/demagnetize.toml
|
||||||
|
|
||||||
|
// To handle "progress", just count tasks, rather than individual progress
|
||||||
|
// It'll look bad, especially for zip-based things, but it should work fine
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
package link.infra.packwiz.installer.request;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IRequestHandler handles requests for locations specified in modpack metadata.
|
||||||
|
*/
|
||||||
|
public interface IRequestHandler {
|
||||||
|
|
||||||
|
public boolean matchesHandler(URI loc);
|
||||||
|
|
||||||
|
public default URI getNewLoc(URI loc) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Source for a location. Must be threadsafe.
|
||||||
|
* It is assumed that each location is read only once for the duration of an IRequestHandler.
|
||||||
|
* @param loc The location to be read
|
||||||
|
* @return The Source containing the data of the file
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public Source getFileSource(URI loc) throws Exception;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
package link.infra.packwiz.installer.request.handlers;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
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 URI getNewLoc(URI loc) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: is caching really needed, if HTTPURLConnection follows redirects correctly?
|
||||||
|
private Map<String, URI> zipUriMap = new HashMap<String, URI>();
|
||||||
|
final ReentrantReadWriteLock zipUriLock = new ReentrantReadWriteLock();
|
||||||
|
private static Pattern repoMatcherPattern = Pattern.compile("/([\\w.-]+/[\\w.-]+).*");
|
||||||
|
|
||||||
|
private String getRepoName(URI loc) {
|
||||||
|
Matcher matcher = repoMatcherPattern.matcher(loc.getPath());
|
||||||
|
matcher.matches();
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected URI getZipUri(URI loc) throws Exception {
|
||||||
|
String repoName = getRepoName(loc);
|
||||||
|
String branchName = getBranch(loc);
|
||||||
|
zipUriLock.readLock().lock();
|
||||||
|
URI zipUri = zipUriMap.get(repoName + "/" + branchName);
|
||||||
|
zipUriLock.readLock().unlock();
|
||||||
|
if (zipUri != null) {
|
||||||
|
return zipUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
zipUri = new URI("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.
|
||||||
|
URI 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(URI loc) {
|
||||||
|
Matcher matcher = branchMatcherPattern.matcher(loc.getPath());
|
||||||
|
matcher.matches();
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected URI getLocationInZip(URI loc) throws Exception {
|
||||||
|
String path = "/" + getRepoName(loc) + "/blob/" + getBranch(loc);
|
||||||
|
return new URI(loc.getScheme(), loc.getAuthority(), path, null, null).relativize(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matchesHandler(URI 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
package link.infra.packwiz.installer.request.handlers;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.request.IRequestHandler;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public class RequestHandlerHTTP implements IRequestHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matchesHandler(URI loc) {
|
||||||
|
String scheme = loc.getScheme();
|
||||||
|
return "http".equals(scheme) || "https".equals(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Source getFileSource(URI 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,172 @@
|
|||||||
|
package link.infra.packwiz.installer.request.handlers;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.BufferedSource;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
||||||
|
|
||||||
|
protected 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<URI, Buffer> readFiles = new HashMap<URI, Buffer>();
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
public 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(URI loc) throws IOException, URISyntaxException {
|
||||||
|
while (true) {
|
||||||
|
entry = zis.getNextEntry();
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Buffer data = readCurrFile();
|
||||||
|
URI fileLoc = new URI(removeFolder(entry.getName()));
|
||||||
|
if (loc.equals(fileLoc)) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
readFiles.put(fileLoc, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Source getFileSource(URI 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();
|
||||||
|
if (file != null) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI findInZip(Predicate<URI> matches) throws Exception {
|
||||||
|
filesLock.lock();
|
||||||
|
for (URI 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();
|
||||||
|
URI fileLoc = new URI(removeFolder(entry.getName()));
|
||||||
|
readFiles.put(fileLoc, data);
|
||||||
|
if (matches.test(fileLoc)) {
|
||||||
|
filesLock.unlock();
|
||||||
|
return fileLoc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<URI, ZipReader> cache = new HashMap<URI, ZipReader>();
|
||||||
|
final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
|
protected abstract URI getZipUri(URI loc) throws Exception;
|
||||||
|
|
||||||
|
protected abstract URI getLocationInZip(URI loc) throws Exception;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract boolean matchesHandler(URI loc);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Source getFileSource(URI loc) throws Exception {
|
||||||
|
URI 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 URI findInZip(URI loc, Predicate<URI> matches) throws Exception {
|
||||||
|
URI 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public class CLIHandler implements IUserInterface {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleException(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void show() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void submitProgress(InstallProgress progress) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (progress.hasProgress) {
|
||||||
|
sb.append('(');
|
||||||
|
sb.append(progress.progress);
|
||||||
|
sb.append('/');
|
||||||
|
sb.append(progress.progressTotal);
|
||||||
|
sb.append(") ");
|
||||||
|
}
|
||||||
|
sb.append(progress.message);
|
||||||
|
System.out.println(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executeManager(Runnable task) {
|
||||||
|
task.run();
|
||||||
|
System.out.println("Finished successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public interface IUserInterface {
|
||||||
|
|
||||||
|
public void show();
|
||||||
|
|
||||||
|
public void handleException(Exception e);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This might not exit straight away, return after calling this!
|
||||||
|
*/
|
||||||
|
public default void handleExceptionAndExit(Exception e) {
|
||||||
|
handleException(e);
|
||||||
|
System.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
public default void setTitle(String title) {};
|
||||||
|
|
||||||
|
public void submitProgress(InstallProgress progress);
|
||||||
|
|
||||||
|
public void executeManager(Runnable task);
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public class InstallProgress {
|
||||||
|
public final String message;
|
||||||
|
public final boolean hasProgress;
|
||||||
|
public final int progress;
|
||||||
|
public final int progressTotal;
|
||||||
|
|
||||||
|
public InstallProgress(String message) {
|
||||||
|
this.message = message;
|
||||||
|
hasProgress = false;
|
||||||
|
progress = 0;
|
||||||
|
progressTotal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstallProgress(String message, int progress, int progressTotal) {
|
||||||
|
this.message = message;
|
||||||
|
hasProgress = true;
|
||||||
|
this.progress = progress;
|
||||||
|
this.progressTotal = progressTotal;
|
||||||
|
}
|
||||||
|
}
|
189
src/main/java/link/infra/packwiz/installer/ui/InstallWindow.java
Normal file
189
src/main/java/link/infra/packwiz/installer/ui/InstallWindow.java
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.awt.GridBagConstraints;
|
||||||
|
import java.awt.GridBagLayout;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JProgressBar;
|
||||||
|
import javax.swing.UIManager;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
|
||||||
|
public class InstallWindow implements IUserInterface {
|
||||||
|
|
||||||
|
private JFrame frmPackwizlauncher;
|
||||||
|
private JLabel lblProgresslabel;
|
||||||
|
private JProgressBar progressBar;
|
||||||
|
|
||||||
|
private String title = "Updating modpack...";
|
||||||
|
private SwingWorkerButWithPublicPublish<Void, InstallProgress> worker;
|
||||||
|
private AtomicBoolean aboutToCrash = new AtomicBoolean();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void show() {
|
||||||
|
EventQueue.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
InstallWindow.this.initialize();
|
||||||
|
InstallWindow.this.frmPackwizlauncher.setVisible(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the contents of the frame.
|
||||||
|
*/
|
||||||
|
private void initialize() {
|
||||||
|
frmPackwizlauncher = new JFrame();
|
||||||
|
frmPackwizlauncher.setTitle(title);
|
||||||
|
frmPackwizlauncher.setBounds(100, 100, 493, 95);
|
||||||
|
frmPackwizlauncher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
|
frmPackwizlauncher.setLocationRelativeTo(null);
|
||||||
|
|
||||||
|
JPanel panel = new JPanel();
|
||||||
|
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||||
|
frmPackwizlauncher.getContentPane().add(panel, BorderLayout.CENTER);
|
||||||
|
panel.setLayout(new BorderLayout(0, 0));
|
||||||
|
|
||||||
|
progressBar = new JProgressBar();
|
||||||
|
progressBar.setIndeterminate(true);
|
||||||
|
panel.add(progressBar, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
lblProgresslabel = new JLabel("Loading...");
|
||||||
|
panel.add(lblProgresslabel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
JPanel panel_1 = new JPanel();
|
||||||
|
panel_1.setBorder(new EmptyBorder(0, 5, 0, 5));
|
||||||
|
frmPackwizlauncher.getContentPane().add(panel_1, BorderLayout.EAST);
|
||||||
|
GridBagLayout gbl_panel_1 = new GridBagLayout();
|
||||||
|
panel_1.setLayout(gbl_panel_1);
|
||||||
|
|
||||||
|
JButton btnOptions = new JButton("Options...");
|
||||||
|
btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||||
|
GridBagConstraints gbc_btnOptions = new GridBagConstraints();
|
||||||
|
gbc_btnOptions.gridx = 0;
|
||||||
|
gbc_btnOptions.gridy = 0;
|
||||||
|
panel_1.add(btnOptions, gbc_btnOptions);
|
||||||
|
|
||||||
|
JButton btnCancel = new JButton("Cancel");
|
||||||
|
btnCancel.addActionListener(new ActionListener() {
|
||||||
|
public void actionPerformed(ActionEvent arg0) {
|
||||||
|
if (worker != null) {
|
||||||
|
worker.cancel(true);
|
||||||
|
}
|
||||||
|
frmPackwizlauncher.dispose();
|
||||||
|
// TODO: show window to ask user what to do
|
||||||
|
System.out.println("Update process cancelled by user!");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||||
|
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
|
||||||
|
gbc_btnCancel.gridx = 0;
|
||||||
|
gbc_btnCancel.gridy = 1;
|
||||||
|
panel_1.add(btnCancel, gbc_btnCancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleException(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
EventQueue.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
JOptionPane.showMessageDialog(null, "An error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleExceptionAndExit(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
// Used to prevent the done() handler of SwingWorker executing if the invokeLater hasn't happened yet
|
||||||
|
aboutToCrash.set(true);
|
||||||
|
EventQueue.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
JOptionPane.showMessageDialog(null, "A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
if (frmPackwizlauncher != null) {
|
||||||
|
EventQueue.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
InstallWindow.this.frmPackwizlauncher.setTitle(title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void submitProgress(InstallProgress progress) {
|
||||||
|
if (worker != null) {
|
||||||
|
worker.publishPublic(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executeManager(Runnable task) {
|
||||||
|
EventQueue.invokeLater(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
worker = new SwingWorkerButWithPublicPublish<Void, InstallProgress>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground() throws Exception {
|
||||||
|
task.run();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void process(List<InstallProgress> chunks) {
|
||||||
|
// Only process last chunk
|
||||||
|
if (chunks.size() > 0) {
|
||||||
|
InstallProgress prog = chunks.get(chunks.size() - 1);
|
||||||
|
if (prog.hasProgress) {
|
||||||
|
progressBar.setIndeterminate(false);
|
||||||
|
progressBar.setValue(prog.progress);
|
||||||
|
progressBar.setMaximum(prog.progressTotal);
|
||||||
|
} else {
|
||||||
|
progressBar.setIndeterminate(true);
|
||||||
|
progressBar.setValue(0);
|
||||||
|
}
|
||||||
|
lblProgresslabel.setText(prog.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void done() {
|
||||||
|
if (aboutToCrash.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: a better way to do this?
|
||||||
|
frmPackwizlauncher.dispose();
|
||||||
|
System.out.println("Finished successfully!");
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
worker.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import javax.swing.SwingWorker;
|
||||||
|
|
||||||
|
// Q: AAA WHAT HAVE YOU DONE THIS IS DISGUSTING
|
||||||
|
// A: it just makes things easier, so i can easily have one interface for CLI/GUI
|
||||||
|
// if someone has a better way to do this please PR it
|
||||||
|
public abstract class SwingWorkerButWithPublicPublish<T,V> extends SwingWorker<T,V> {
|
||||||
|
@SafeVarargs
|
||||||
|
public final void publishPublic(V... chunks) {
|
||||||
|
publish(chunks);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user