tempFileItr = tempFiles.listIterator();
+ while (tempFileItr.hasNext()) {
+
+ File fileEntry = tempFileItr.next();
+ fileEntryName = fileEntry.getName();
+ Logger.print(Logger.Level.DEBUG, "Recycling entry: %s", fileEntryName);
+ /*
+ * Make sure the file exists before we attempt to delete it
+ * If it's a directory use AC-IO to delete the directory recursively
+ */
+ if (fileEntry.exists()) {
+ if (fileEntry.isDirectory())
+ FileUtils.deleteDirectory(fileEntry);
+ else
+ // this might not work every time though
+ fileEntry.deleteOnExit();
+ }
+ }
+ } catch (SecurityException | IOException e) {
+ Logger.print(Logger.Level.ERROR, e, "Unable to delete temporary file %s !", fileEntryName);
+ }
+ /*
+ * Time to delete the temporary updater jar file
+ * Do this only if we are in the appropriate run mode
+ */
+ if (Main.isSelfUpdating())
+ updateSelf();
+ }
+
+ /**
+ * Run the final stage of the updater process.
+ * The application will create an un-installer batch script and then run it.
+ * The script will delete the jvm application file as well as itself.
+ * If we ran the {@link #updaterCleanup()} method before, this should leave
+ * our root folder completely clean of all temporary files.
+ */
+ private static void updateSelf() {
+
+ File uninstaller = new File("MTE-Updater-uninstall.bat");
+ try {
+ uninstaller.createNewFile();
+ } catch (IOException e) {
+ Logger.error("Unable to create uninstaller file", e);
+ Execute.exit(1, false);
+ }
+ String[] batchLines =
+ {
+ "@echo off",
+ "set \"process=" + System.getProperty("program.name") + "\"",
+ "set \"logfile=" + Logger.LogFile.NAME + "\"",
+ "echo Running uninstaller script >> %logfile%",
+ ":uninstall",
+ "timeout /t 1 /nobreak > nul",
+ "2>nul ren %process% %process% && goto next || goto uninstall",
+ ":next",
+ "if exist %process% (",
+ " echo Recycling temporary application file >> %logfile%",
+ " del %process%",
+ ") else ( echo [ERROR] Unable to delete application file %process% >> %logfile% )",
+ "if exist %~n0%~x0 (",
+ " echo Recycling uninstaller script >> %logfile%",
+ " del %~n0%~x0",
+ ") else ( echo [ERROR] Unable to delete uninstaller script %~n0%~x0 >> %logfile% )"
+ };
+
+ try (PrintWriter writer = new PrintWriter(uninstaller.getName())) {
+
+ for (int i = 0; i <= batchLines.length - 1; i++) {
+ writer.println(batchLines[i]);
+ }
+ writer.close();
+ }
+ catch (FileNotFoundException e) {
+ Logger.error("ERROR: Unable to locate uninstaller script!", e);
+ return;
+ }
+
+ Logger.debug("Launching uninstaller from JVM");
+ Execute.start(uninstaller.getName(), false, false);
+ }
+
+ /**
+ * Any files added here will be deleted before terminating application
+ * @param tmpFile
+ */
+ void registerTempFile(File tmpFile) {
+
+ if (tmpFile.exists()) {
+ Logger.print(Logger.Level.DEBUG, "Registering temporary file %s", tmpFile.getName());
+ tempFiles.add(tmpFile);
+ }
+ else
+ Logger.print(Logger.Level.WARNING, "Trying to register a non-existing "
+ + "temporary file %s", tmpFile.getName());
+ }
+
+ /**
+ * Here we are using URL openStream method to create the input stream. Then we
+ * are using a file output stream to read data from the input stream and write
+ * to the file.
+ *
+ * @param url
+ * @param file
+ * @throws IOException
+ */
+ boolean downloadUsingStream(URL url, String file) throws IOException {
+
+ Logger.print(Logger.Level.DEBUG, "Downloading file %s from %s", file, url.toString());
+ BufferedInputStream bis = new BufferedInputStream(url.openStream());
+ FileOutputStream fis = new FileOutputStream(file);
+ byte[] buffer = new byte[1024];
+ int count = 0;
+ while ((count = bis.read(buffer, 0, 1024)) != -1) {
+ fis.write(buffer, 0, count);
+ }
+
+ fis.close();
+ bis.close();
+
+ File dlFile = new File(file);
+ if (!dlFile.exists()) {
+ Logger.print(Logger.Level.ERROR, "Unable to find downloaded file %s", dlFile.getName());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Read from a text file and return the compiled string
+ *
+ * @param filename Name of the file to read from the root directory
+ * @return Content of the text file
+ */
+ String readFile(String filename) {
+
+ // Using Apache Commons IO here
+ try (FileInputStream inputStream = new FileInputStream(filename)) {
+ return IOUtils.toString(inputStream, "UTF-8");
+ } catch (IOException e) {
+ Logger.print(Logger.Level.ERROR, e, "Unable to read file %s", filename);
+ return null;
+ }
+ }
+}
diff --git a/MTEUpdater/src/main/java/io/mte/updater/Logger.java b/MTEUpdater/src/main/java/io/mte/updater/Logger.java
new file mode 100644
index 0000000..4abf506
--- /dev/null
+++ b/MTEUpdater/src/main/java/io/mte/updater/Logger.java
@@ -0,0 +1,291 @@
+package io.mte.updater;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.regex.Matcher;
+
+public class Logger {
+ /**
+ *
+ * The following logger levels regulate what kind of
+ * log output is permitted by the application.
+ * Define logger level by running the application with the appropriate JVM argument.
+ *
+ * LOG
+ *
+ * - This is the default behavior, prints only normal logs.
+ * - Run with JVM argument: none
+ *
+ * VERBOSE
+ *
+ * - When you want more information, prints extended logs and warnings.
+ * - Run with JVM argument: -v, -verbose
+ *
+ * DEBUG
+ *
+ * - When you are debugging the application, print everything including debug logs.
+ * - Run with JVM argument: -d, -debug
+ *
+ */
+ // TODO Add new level type that prints stackTrace directly in console
+ public enum Level {
+
+ LOG(Short.parseShort("0"), "", "[LOG]", ""),
+ ERROR(Short.parseShort("0"), "ERROR", "[ERROR]", ""),
+ WARNING(Short.parseShort("1"), "Warning", "[WARNING]", ""),
+ VERBOSE(Short.parseShort("1"), "", "[LOG]", "-v", "-verbose"),
+ DEBUG(Short.parseShort("2"), "DEBUG", "[DEBUG]", "-d", "-debug");
+
+ private final short level;
+ private final String[] arguments;
+ private final String[] tags;
+
+ Level(short lvl, String consoleTag, String logTag, String...args) {
+
+ tags = new String[] { consoleTag, logTag };
+ level = lvl;
+ arguments = args;
+ }
+
+ public static Level getLoggerLevel(String[] args) {
+ /*
+ * Iterate through every enum entry
+ */
+ for (Level entry : Level.values()) {
+ /*
+ * Iterate through the list of arguments supplied
+ */
+ for (int i = args.length - 1; i >= 0; i--) {
+ /*
+ * Compare current argument supplied with each argument in the
+ * current level entry to find a match
+ */
+ for (int i2 = entry.arguments.length - 1; i2 >= 0; i2--) {
+ if (args[i].equals(entry.arguments[i2]))
+ return entry;
+ }
+ }
+ }/*
+ * If no logging argument has been passed
+ * just set the regular logging level
+ */
+ return LOG;
+ }
+ public String[] getArguments() {
+ return arguments;
+ }
+ private String getConsoleTag() {
+ return tags[0];
+ }
+ private String getLogTag() {
+ return tags[1];
+ }
+ }
+
+ public static class LogFile {
+
+ public static final String NAME = "MTE-Updater.log";
+
+ private static LogFile instance;
+ private static PrintWriter writer;
+ private static File file;
+
+ LogFile() {
+ try {
+ file = new File(NAME);
+ file.createNewFile();
+ writer = new PrintWriter(NAME);
+ }
+ catch (java.io.IOException e) {
+ Logger.print(Level.ERROR, e, "Unable to create log or access log file %s", NAME);
+ }
+ }
+ /**
+ * Purge the log file and create a new instance
+ */
+ private static void init() {
+ if (instance == null)
+ instance = new LogFile();
+ else
+ warning("Trying to initialize LogFile more then once");
+ }
+ public static void close() {
+ writer.close();
+ }
+ /*private static void clear() {
+ try {
+ writer = new PrintWriter(NAME);
+ writer.close();
+ }
+ catch (java.io.FileNotFoundException e) {
+ error("Unable to close log file, missing in action", e);
+ }
+ }*/
+ public static void print(String log, Level lvl) {
+ if (instance != null) {
+ writer.println(lvl.getLogTag() + " " + log.replace("\n", ""));
+ writer.flush();
+ }
+ }
+ public static void print(String log) {
+ print(log, Logger.Level.LOG);
+ }
+ public static void print(Exception e) {
+ if (e != null && instance != null)
+ e.printStackTrace(writer);
+ }
+ }
+
+ private static Level LOGGER_LEVEL;
+ private static Logger logger;
+
+ private Logger(String[] args) {
+ LOGGER_LEVEL = Level.getLoggerLevel(args);
+ LogFile.init();
+ }
+ /**
+ * Call only once from the main method to create a new logger instance
+ * @param args jvm arguments to search for a logger level argument
+ * @param test perform a series of logging tests
+ */
+ public static void init(String[] args, boolean test) {
+
+ if (logger != null)
+ warning("Trying to initialize logger more then once");
+
+ Logger.logger = new Logger(args);
+ verbose("Logger initialized with level: " + Logger.getLevel());
+ if (test == true) test();
+ }
+
+ /* Public getter function to retrieve logger level */
+ public static Level getLevel() {
+ return LOGGER_LEVEL;
+ }
+
+ /** Did the application start in debug mode */
+ public static boolean isDebug() {
+ return LOGGER_LEVEL == Level.DEBUG;
+ }
+
+ /**
+ * See if we are allowed to print the log with argument level
+ * @param lvl Level of the log we want to print
+ * */
+ private static boolean canPrintLog(Level lvl) {
+
+ if (logger == null) {
+ print("[Warning] " + "Trying to print a log before the logger has initialized");
+ return false;
+ }
+ else
+ return lvl.level <= Logger.getLevel().level;
+ }
+
+ /**
+ * Employ {@code printf} method to output log when you have string items you want
+ * wrapped in single quotation marks. Also accepts a single item as an argument.
+ *
+ * @param lvl Logging level of this log
+ * @param format A format string to process and print
+ * @param items Array of string items to wrap
+ */
+ public static void print(Level lvl, String format, String...items) {
+
+ if (items == null || items.length == 1 && items[0].isEmpty()) {
+ warning("Attempting to print log with incorrect number of arguments (0)");
+ }
+ else if (canPrintLog(lvl)) {
+ if (items.length > 1) {
+ /*
+ * Wrap each string item with single quotation marks
+ */
+ items = String.join("' '", items).split("\\s+");
+ items[0] = "'" + items[0];
+ items[items.length - 1] += "'";
+ /*
+ * Format the log string like printf method does
+ */
+ for (int i = 0; i <= items.length - 1; i++) {
+ format = format.replaceFirst("%s", Matcher.quoteReplacement(items[i]));
+ }
+ print(format, lvl);
+ }
+ else {
+ String format2 = (String)("'" + Matcher.quoteReplacement(items[0]) + "'");
+ print(format.replaceFirst("%s", format2), lvl);
+ }
+ }
+ }
+ /**
+ *
+ * Employ {@code printf} method to output log when you have string
+ * items you want wrapped in single
quotation marks
+ * Use this overload method when you want to print stack trace to logfile.
+ *
+ * See the {@link Logger#print(Level, String, String...) overloaded method} for additional information
+ *
+ */
+ public static void print(Level lvl, Exception e, String format, String...items) {
+ print(lvl, format, items);
+ LogFile.print(e);
+ }
+
+ public static void print(String log) {
+ System.out.println(log);
+ LogFile.print(log, Level.LOG);
+ }
+ private static boolean print(String log, Level lvl) {
+ if (canPrintLog(lvl)) {
+ String tag = lvl.getConsoleTag();
+ System.out.println(tag + (tag.isEmpty() ? "" : ": ") + log);
+ LogFile.print(log, lvl);
+ return true;
+ }
+ else return false;
+ }
+
+ public static void error(String log) {
+ /*
+ * Don't ask for permission here because there are situations where
+ * a method might request error logging before the logger has initialized
+ *
+ * if (canPrintLog(Level.ERROR))
+ */
+ print(log, Level.ERROR);
+ LogFile.print(new Exception());
+ }
+ public static void error(String log, Exception e) {
+ //if (canPrintLog(Level.ERROR))
+ print(log, Level.ERROR);
+ LogFile.print(e);
+ }
+ public static void verbose(String log) {
+ print(log, Level.VERBOSE);
+ }
+ public static boolean warning(String log) {
+ return print(log, Level.WARNING);
+ }
+ public static void warning(String msg, Exception e) {
+ if (warning(msg) == true)
+ LogFile.print(e);
+ }
+ public static void debug(String log) {
+ print(log, Level.DEBUG);
+ }
+
+ public static void test() {
+
+ print("Performing a series of logging tests:\n");
+ print("This is a regular log");
+ print(Logger.Level.LOG, "This %s a %s log", "is", "constructed");
+ print(Logger.Level.LOG, "This is also a %s log", "constructed");
+ verbose("This is a verbose log");
+ warning("This is a warning log");
+ warning("This is a warning log with a stack trace", new Exception());
+ error("This is an error log");
+ error("This is an error log with a stack trace", new Exception());
+ debug("This is a debug log");
+ print("Finished testing the logging system!");
+ }
+}
diff --git a/MTEUpdater/src/main/java/io/mte/updater/Main.java b/MTEUpdater/src/main/java/io/mte/updater/Main.java
new file mode 100644
index 0000000..5a73867
--- /dev/null
+++ b/MTEUpdater/src/main/java/io/mte/updater/Main.java
@@ -0,0 +1,162 @@
+package io.mte.updater;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import io.mte.updater.UserInput.Key;
+
+public class Main {
+
+ public static final Path root = Paths.get(System.getProperty("user.dir"));
+ public static final Path appPath = Paths.get(root + File.separator + System.getProperty("program.name"));
+ public static final short processId = Execute.getProcessId();
+
+ // TODO: Document this variable
+ public static String runMode;
+
+ // Use this class instance to handle all file related stuff
+ public static final FileHandler fileHandler = new FileHandler();
+
+ public static void main(String[] args)
+ {
+ try {
+ // Initialize logger first so we can output logs
+ Logger.init(args, false);
+
+ if (args != null && args.length > 0)
+ processJVMArguments(args);
+
+ updateMWSE();
+ runMTEUpdater();
+ Execute.exit(0, true);
+ }
+ catch(Exception e) {
+ Logger.error("Unhandled exception occured in main method", e);
+ Execute.exit(1, false);
+ }
+ }
+
+ private static void processJVMArguments(String[] args) {
+
+ Logger.print(Logger.Level.DEBUG, "Started application with %s arguments", String.join(" ", args));
+ //for (int i = args.length - 1; i >= 0; i--) {}
+
+ // The first argument should always be a run mode definition
+ runMode = args[0];
+
+ if (runMode == null || runMode.isEmpty()) {
+ Exception e = new IllegalArgumentException();
+ Logger.error("Application run mode has not been defined", e);
+ Execute.exit(1, false);
+ }
+ else if (isLauncher()) {
+ FileHandler.launchApplication();
+ }
+ else if (isSelfUpdating()) {
+ /*
+ * We expect to find the process id of the main Java
+ * application in the following argument
+ */
+ if (args.length > 1 && !args[1].isEmpty()) {
+ /*
+ * The launcher process should exit on its own
+ * but if it hang for some reason we terminate it here
+ */
+ if (Execute.isProcessRunning(args[1])) {
+
+ Logger.debug("Launcher application is still running, terminating now...");
+ if (!Execute.kill(Integer.parseInt(args[1]), 5)) {
+ Logger.error("Unable to terminate java application");
+ Execute.exit(1, false);
+ }
+ }
+ }
+ else {
+ Logger.error("Expected launcher PID passed as JVM argument...");
+ Execute.exit(1, false);
+ }
+ }
+ else {
+ Exception e = new IllegalArgumentException();
+ Logger.error("Unknown application run mode definition", e);
+ Execute.exit(1, false);
+ }
+ }
+
+ private static void runMTEUpdater() {
+
+ Logger.verbose("Start updating mte...");
+
+ Logger.print("\nDownloading mte version file...");
+ if (!RemoteHandler.downloadRemoteVersionFile(fileHandler))
+ return;
+
+ fileHandler.registerRemoteVersionFile();
+
+ Logger.print("Comparing version numbers...");
+
+ String remoteSHA = fileHandler.remote.getCommitSHA();
+ String localSHA = fileHandler.local.getCommitSHA();
+
+ // Compare version numbers to see if we need to update
+ if (!remoteSHA.equals(localSHA)) {
+ Logger.print("\nYour version of the guide is out of date");
+
+ if (localSHA.isEmpty()) {
+ Logger.verbose("Local version file not found, skip showing updates");
+ fileHandler.doUpdate(localSHA, remoteSHA);
+ return;
+ }
+ // Continue asking for input until the user says yes or no
+ Logger.print("Would you like to see a list of recent updates?");
+ Key input = UserInput.waitFor(Key.YES, Key.NO);
+
+ if (input == Key.YES) {
+ fileHandler.doUpdate(localSHA, remoteSHA);
+ }
+ else if (input == Key.NO) {
+
+ Logger.print("\nIt is strongly recommended that you update");
+ Logger.print("You can always check the release section of our repository on Github:");
+ Logger.print(RemoteHandler.Link.releasesPage.toString() + "\n");
+ }
+ } else
+ Logger.print("\nYour version of the guide is up-to-date!");
+ }
+
+ /**
+ *
Run the MWSE auto-updater program
+ * This is intended to make the users life easier so they only
+ * have to run one updater that does it all for them
+ */
+ private static void updateMWSE() {
+ /*
+ * Don't update mwse if we are running in debug mode
+ */
+ if (!Logger.isDebug()) {
+ Logger.print("Attempting to update MWSE build...");
+ File mwse = new File("MWSE-Update.exe");
+
+ if (mwse != null && mwse.exists()) {
+ Process proc = Execute.start(mwse.getName(), true, true);
+ if (proc == null || proc.exitValue() != 0)
+ Logger.warning("Unable to update, check logfile for more details");
+ }
+ else
+ Logger.verbose("Unable to find mwse updater, skipping...");
+ }
+ }
+
+ // TODO: Move these values into an enum
+
+ /**
+ * Did the JVM run as a launcher?
+ */
+ public static boolean isLauncher() {
+ return runMode.equals("--launcher");
+ }
+ public static boolean isSelfUpdating() {
+ return runMode.equals("--update-self");
+ }
+}
diff --git a/MTEUpdater/src/main/java/io/mte/updater/RemoteHandler.java b/MTEUpdater/src/main/java/io/mte/updater/RemoteHandler.java
new file mode 100644
index 0000000..095b46c
--- /dev/null
+++ b/MTEUpdater/src/main/java/io/mte/updater/RemoteHandler.java
@@ -0,0 +1,127 @@
+package io.mte.updater;
+
+import java.net.URI;
+import java.net.URL;
+
+public class RemoteHandler {
+
+ public final static String RELEASE_FILENAME = "MTE-Release.7z";
+ public final static String VERSION_FILENAME = "mte-version.txt";
+
+ public static class Link {
+
+ /* These URL definitions should probably never change */
+ private static final URL github = constructURL("https://github.com");
+ private static final URL ghusercontent = constructURL("https://raw.githubusercontent.com");
+
+ /* These relative paths are used to construct URL's */
+ private static final String repoPath = "Tyler799/Morrowind-2019";
+ private static final String updaterBranch = "updater";
+ private static final String masterBranch = "master";
+ private static final String comparePath = "compare";
+ private static final String releasePath = "releases/download";
+
+ /* Append this to compare links to activate rich diff display */
+ private static final String richDiff = "?short_path=4a4f391#diff-4a4f391a7396ba51c9ba42372b55d34e";
+
+ public static final URL repository = constructURL(github, repoPath);
+ public static final URL commitCompare = constructURL(github, repoPath, comparePath);
+ public static final URL versionFile = constructURL(ghusercontent, repoPath, masterBranch, VERSION_FILENAME);
+ public static final URL releasesPage = constructURL(github, repoPath, "download");
+
+ private static URL constructURL(URL url, String...paths) {
+ return constructURL(url.toString() + "/" + String.join("/", paths));
+ }
+ private static URL constructURL(URL url, String path) {
+ return constructURL(url.toString() + "/" + path);
+ }
+
+ private static URL constructURL(String url) {
+
+ try {
+ return new URL(url);
+ } catch (java.net.MalformedURLException e) {
+ Logger.print(Logger.Level.ERROR, e, "%s is not a valid URL format", url.toString());
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Create a hyperlink to a direct comparison between two commits made in the
+ * guide repository. It's recommended to use richDiff and displayCommits.
+ *
+ * @param commit1 SHA of base commit to compare against
+ * @param commit2 SHA of comparing commit
+ * @param richDiff Make the comparison display more user friendly
+ * @param range View default comparison for the given commit range
+ * @return URI wrapped url or {@code null} if an exception was thrown
+ */
+ static URI getGithubCompareLink(String commit1, String commit2, boolean richDiff, boolean range) {
+
+ URL compareUrl = Link.constructURL(Link.commitCompare, commit1 +
+ (range ? "..." : "..") + commit2 + (richDiff ? Link.richDiff : ""));
+
+ // Apply URI wrapper to string
+ try {
+ return new java.net.URI(compareUrl.toString());
+ } catch (java.net.URISyntaxException e) {
+ Logger.error("URL string violates RFC 2396!", e);
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Open a webpage with the provided hyperlink using the default user browser
+ *
+ * @param url web address we want to browse
+ * @return {@code true} if the operation was successful, {@code false} otherwise
+ */
+ static boolean browseWebpage(URI url) {
+
+ try {
+ java.awt.Desktop.getDesktop();
+ if (java.awt.Desktop.isDesktopSupported()) {
+ java.awt.Desktop.getDesktop().browse(url);
+ return true;
+ } else {
+ Logger.error("Desktop class is not suppored on this platform");
+ return false;
+ }
+ } catch (Exception e) {
+ if (e instanceof java.io.IOException)
+ Logger.error("Unable to open web browser, default browser is not found or it failed to launch", e);
+ else if (e instanceof SecurityException)
+ Logger.error("Unable to open web browser, security manager denied permission", e);
+ return false;
+ }
+ }
+
+ static boolean downloadRemoteVersionFile(FileHandler handler) {
+
+ try {
+ return handler.downloadUsingStream(Link.versionFile, VERSION_FILENAME + ".remote");
+ }
+ catch (java.io.IOException e) {
+ Logger.error("Unable to download project version file!", e);
+ return false;
+ }
+ }
+
+ static boolean downloadLatestRelease(FileHandler handler) {
+
+ try {
+ URL releaseLink = Link.constructURL(Link.repository, Link.releasePath, "v" + handler.remote.getReleaseVersion(), RELEASE_FILENAME);
+ if (handler.downloadUsingStream(releaseLink, RELEASE_FILENAME)) {
+ handler.registerTempFile(new java.io.File(RELEASE_FILENAME));
+ return true;
+ }
+ return false;
+ }
+ catch (java.io.IOException e) {
+ Logger.error("Unable to download repository files!", e);
+ return false;
+ }
+ }
+}
diff --git a/MTEUpdater/src/main/java/io/mte/updater/UnzipUtility.java b/MTEUpdater/src/main/java/io/mte/updater/UnzipUtility.java
new file mode 100644
index 0000000..b43dc5b
--- /dev/null
+++ b/MTEUpdater/src/main/java/io/mte/updater/UnzipUtility.java
@@ -0,0 +1,244 @@
+package io.mte.updater;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
+import org.apache.commons.compress.archivers.sevenz.SevenZFile;
+import org.apache.commons.io.FilenameUtils;
+
+public class UnzipUtility {
+ /**
+ * Size of the buffer to read/write data
+ */
+ private static final int BUFFER_SIZE = 4096;
+
+ /**
+ * Extracts a zip file specified by the zipFilePath to a directory specified by
+ * destDirectory (will be created if does not exists)
+ *
+ * @param zipFilePath
+ * @param destDirectory
+ */
+ public boolean unzip(String zipFilePath, String destDirectory) throws IOException {
+
+ // Make sure the zip file under the given path exists
+ File zipFile = new File(zipFilePath);
+ if (!zipFile.exists()) {
+ Logger.print(Logger.Level.ERROR, "Unable to find zip file with path %s", zipFilePath);
+ return false;
+ }
+ File destDir = new File(destDirectory);
+ if (!destDir.exists()) {
+ destDir.mkdir();
+ }
+
+ Logger.print(Logger.Level.DEBUG, "Unziping from %s to %s \n", zipFilePath, destDirectory);
+ /*
+ * Handle 7z files using Apache Commons Compress library
+ */
+ if (FilenameUtils.getExtension(zipFilePath).equals("7z")) {
+ Logger.debug("Detected 7Zip archive, using Apache Commons Compress library");
+ try (SevenZFile sevenZFile = new SevenZFile(new File(zipFilePath))) {
+ SevenZArchiveEntry entry = sevenZFile.getNextEntry();
+
+ // Abort if zip file is empty
+ if (entry == null) {
+ Logger.error("Zip contains no valid entries!");
+ Logger.print("Aborting unzipping operation...");
+ return false;
+ }
+ while (entry != null) {
+
+ Logger.print(Logger.Level.DEBUG, "Iterating over zip entry %s", entry.getName());
+ String filePath = destDirectory + File.separator + entry.getName();
+
+ if (!entry.isDirectory()) {
+ // if the entry is a file, extracts it
+ File extrFile = extractFile(sevenZFile, entry, filePath);
+ if (extrFile == null || !extrFile.exists()) {
+ Logger.print(Logger.Level.ERROR, "Unable to find extracted file %s!", entry.getName());
+ }
+ } else {
+ // if the entry is a directory, make the directory
+ File dir = new File(filePath);
+ dir.mkdir();
+ }
+ // iterate over next zip entry
+ entry = sevenZFile.getNextEntry();
+ }
+ /*
+ * Don't forget to close the archive here, as no warning messages
+ * are displayed before compiling the application
+ */
+ sevenZFile.close();
+ }
+ catch (IOException e) {
+ Logger.print(Logger.Level.ERROR, e, "Unable to read archive %s!", zipFile.getName());
+ return false;
+ }
+ return true;
+ }
+ /*
+ * Use the standard method for regular zip files
+ */
+ ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
+ ZipEntry entry = null;
+
+ try {
+ entry = zipIn.getNextEntry();
+ // Abort if zip file is empty
+ if (entry == null) {
+ Logger.error("Zip contains no valid entries!");
+ Logger.print("Aborting unzipping operation...");
+ closeZipInputStream(zipIn);
+ return false;
+ }
+ }
+ catch (IOException e) {
+ Logger.error("ZIP file error has occurred, unable to get new zip entry!", e);
+ closeZipInputStream(zipIn);
+ return false;
+ }
+ // iterates over entries in the zip file
+ while (entry != null) {
+
+ Logger.print(Logger.Level.DEBUG, "Iterating over zip entry %s", entry.getName());
+ String filePath = destDirectory + File.separator + entry.getName();
+
+ if (!entry.isDirectory()) {
+ // if the entry is a file, extracts it
+ File extrFile = extractFile(zipIn, entry.getName(), filePath);
+ if (extrFile == null || !extrFile.exists()) {
+ Logger.print(Logger.Level.ERROR, "Unable to find extracted file %s!", entry.getName());
+ }
+ } else {
+ // if the entry is a directory, make the directory
+ File dir = new File(filePath);
+ dir.mkdir();
+ }
+ try {
+ zipIn.closeEntry();
+ // iterate over next zip entry
+ entry = zipIn.getNextEntry();
+ }
+ catch (IOException e) {
+ Logger.error("Unable to close or get next zip entry!", e);
+ Logger.print("Aborting unzipping operation...");
+ return false;
+ }
+ }
+ if (!closeZipInputStream(zipIn))
+ return false;
+
+ return true;
+ }
+
+ /**
+ * Extracts a regular zip file entry using standard methods
+ *
+ * @param zipIn Zip stream we are extracting from
+ * @param filename Used for debug purposes
+ * @param filePath Where we want to extract
+ * @return New instance of the extracted file or {@code null} if an error occurred
+ */
+ @SuppressWarnings("resource")
+ private File extractFile(ZipInputStream zipIn, String filename, String filePath) {
+
+ Logger.print(Logger.Level.DEBUG, "Extracting zip file %s to %s", filename, filePath);
+ BufferedOutputStream bos = null;
+ try {
+ bos = new BufferedOutputStream(new FileOutputStream(filePath));
+ }
+ catch (java.io.FileNotFoundException e) {
+ Logger.print(Logger.Level.ERROR, e, "Unable to create new output stream for path %s!", filePath);
+ return null;
+ }
+ byte[] bytesIn = new byte[BUFFER_SIZE];
+ int read = 0;
+ try {
+ while ((read = zipIn.read(bytesIn)) != -1) {
+ bos.write(bytesIn, 0, read);
+ }
+ }
+ catch (IOException e) {
+ Logger.error("Unable to read or write from zip input stream!", e);
+ closeZipOutputStream(bos);
+ return null;
+ }
+ if (!closeZipOutputStream(bos))
+ return null;
+ /*
+ * We are getting a warning here that the output stream might not be closed
+ * but that is not true, we are trying to close it in 'closeZipOutputStream()'
+ */
+ return new File(filePath);
+ }
+
+ /**
+ * Extract a 7z archive file using Apache Commons Compress library
+ *
+ * @param sevenZFile Zip file we are extracting from
+ * @param entry Zip entry that we are extracting
+ * @param filePath Extraction destination
+ * @return New instance of the extracted file or {@code null} if an error occurred
+ */
+ @SuppressWarnings("resource")
+ private File extractFile(SevenZFile sevenZFile, SevenZArchiveEntry entry, String filePath) {
+
+ Logger.print(Logger.Level.DEBUG, "Extracting zip file %s to %s", entry.getName(), filePath);
+ BufferedOutputStream out = null;
+ try {
+ out = new BufferedOutputStream(new FileOutputStream(filePath));
+ }
+ catch (java.io.FileNotFoundException e) {
+ Logger.print(Logger.Level.ERROR, e, "Unable to create new output stream for path %s!", filePath);
+ return null;
+ }
+ try {
+ byte[] content = new byte[(int) entry.getSize()];
+ sevenZFile.read(content, 0, content.length);
+ out.write(content);
+ }
+ catch (IOException e) {
+ Logger.error("Unable to read or write from zip input stream!", e);
+ closeZipOutputStream(out);
+ return null;
+ }
+ if (!closeZipOutputStream(out))
+ return null;
+ /*
+ * We are getting a warning here that the output stream might not be closed
+ * but that is not true, we are trying to close it in 'closeZipOutputStream()'
+ */
+ return new File(filePath);
+ }
+
+ private boolean closeZipOutputStream(BufferedOutputStream stream)
+ {
+ try {
+ stream.close();
+ return true;
+ }
+ catch (IOException e) {
+ Logger.error("Unable to close zip output stream!", e);
+ return false;
+ }
+ }
+ private boolean closeZipInputStream(ZipInputStream stream)
+ {
+ try {
+ stream.close();
+ return true;
+ }
+ catch (IOException e) {
+ Logger.error("Unable to close zip input stream!", e);
+ return false;
+ }
+ }
+}
diff --git a/MTEUpdater/src/main/java/io/mte/updater/UserInput.java b/MTEUpdater/src/main/java/io/mte/updater/UserInput.java
new file mode 100644
index 0000000..c25ff62
--- /dev/null
+++ b/MTEUpdater/src/main/java/io/mte/updater/UserInput.java
@@ -0,0 +1,99 @@
+package io.mte.updater;
+
+import java.io.IOException;
+import java.util.Scanner;
+
+/**
+ * Always use this instead of System InputStream to read user input.
+ */
+public class UserInput {
+
+ private final Scanner reader = new Scanner(System.in);
+ private final java.io.InputStream is = System.in;
+ private static final UserInput ui = new UserInput();
+
+ public enum Key {
+
+ YES("y", "yes"),
+ NO("n", "no");
+
+ private final String[] keys;
+
+ private Key(String...s) {
+ keys = s;
+ }
+ public static boolean isValid(String input, Key key) {
+
+ for (int i = key.keys.length - 1; i >= 0; i--)
+ {
+ if (input.equalsIgnoreCase(key.keys[i]))
+ return true;
+ }
+ return false;
+ }
+ }
+ /**
+ * Close the system input stream. Note that you will not be able to read user input
+ * after doing this so this should only be done just before exiting the application
+ */
+ public static void close() {
+ ui.reader.close();
+ }
+ /**
+ * Read last user keyboard input (safe to call each operation cycle).
+ * @return last user keyboard input
+ */
+ public static String read() {
+ return ui.reader.next();
+ }
+ /**
+ * Block thread execution until correct input data is available,
+ * the end of the stream is detected, or an exception is thrown
+ * @param keys list of user input keys to wait for
+ */
+ public static Key waitFor(Key...keys) {
+
+ if (keys == null || keys.length == 0) {
+ Logger.error("Invalid key argument passed");
+ Execute.exit(1, false, false);
+ return null;
+ }
+ else {
+ if (Logger.isDebug()) {
+ String[] keyNames = java.util.Arrays.stream(keys).map(Enum::name).toArray(String[]::new);
+ Logger.print(Logger.Level.DEBUG, "Wating for user input: %s", String.join(", ", keyNames));
+ }
+ while (ui.reader.hasNext())
+ {
+ String input = ui.reader.next();
+ for (int i = keys.length - 1; i >= 0; i--)
+ if (Key.isValid(input, keys[i]))
+ {
+ Logger.print(Logger.Level.DEBUG, "Found valid user key input: %s", keys[i].toString());
+ return keys[i];
+ }
+ }
+ Logger.error("No more tokes in user input stream");
+ Execute.exit(1, false, false);
+ return null;
+ }
+ }
+ /** Block thread execution until the user presses {@code enter} key */
+ public static void waitForEnter() {
+ /*
+ * Do not use scanner to scan for user input, I've been getting unknown exceptions being
+ * thrown with no message or stack trace. It just doesn't seem to work for some reason
+ *
+ * Using direct System InputStream seems like the best idea, and although it only works
+ * for ENTER at least it works and won't crash
+ */
+ try {
+ Logger.print("Press Enter to continue...");
+ ui.is.read();
+ }
+ catch(IOException e) {
+ Logger.error("Something went wrong while reading user input", e);
+ Execute.exit(1, false, false);
+ }
+ }
+}
diff --git a/mte-version.txt b/mte-version.txt
new file mode 100644
index 0000000..6f56aed
--- /dev/null
+++ b/mte-version.txt
@@ -0,0 +1 @@
+1.4 4f5c3bd786cacb05f3f29b44b52d968ff672e3a1
\ No newline at end of file