diff --git a/src/main/java/net/knarcraft/minecraftserverlauncher/profile/Profile.java b/src/main/java/net/knarcraft/minecraftserverlauncher/profile/Profile.java index 3884c48..57b442c 100644 --- a/src/main/java/net/knarcraft/minecraftserverlauncher/profile/Profile.java +++ b/src/main/java/net/knarcraft/minecraftserverlauncher/profile/Profile.java @@ -11,6 +11,7 @@ import javax.naming.ConfigurationException; import javax.swing.*; import java.io.IOException; import java.util.ArrayList; +import java.util.List; /** * Keeps track of a set of servers and some user settings @@ -22,7 +23,7 @@ import java.util.ArrayList; public class Profile { private static final ServerLauncherGUI serverLauncherGui = Main.getController().getGUI(); - private final ArrayList collections; + private final List collections; private final String name; private boolean runInBackground; private int delayStartup; @@ -106,7 +107,7 @@ public class Profile { * * @return

All collections stored by this profile

*/ - public ArrayList getCollections() { + public List getCollections() { return this.collections; } diff --git a/src/main/java/net/knarcraft/minecraftserverlauncher/userinterface/BackupGUI.java b/src/main/java/net/knarcraft/minecraftserverlauncher/userinterface/BackupGUI.java new file mode 100644 index 0000000..4357647 --- /dev/null +++ b/src/main/java/net/knarcraft/minecraftserverlauncher/userinterface/BackupGUI.java @@ -0,0 +1,94 @@ +package net.knarcraft.minecraftserverlauncher.userinterface; + +import net.knarcraft.minecraftserverlauncher.utility.BackupUtil; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * The Backup GUI is used to display backup progress + */ +public class BackupGUI implements ActionListener { + private static JFrame frame; + private static JTextArea progressTextArea; + private static JProgressBar progressBar; + private static JButton cancelButton; + + /** + * Instantiates a new GUI + */ + public BackupGUI() { + instantiate(); + } + + /** + * Initializes the server consoles frame + */ + public void instantiate() { + if (frame != null) { + return; + } + frame = new JFrame("Running backup..."); + frame.setBounds(100, 100, 500, 140); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + progressTextArea = new JTextArea(); + progressTextArea.setEditable(false); + progressBar = new JProgressBar(); + cancelButton = new JButton("Cancel"); + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); + panel.add(progressTextArea); + panel.add(Box.createRigidArea(new Dimension(0,5))); + panel.add(progressBar); + + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); + buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); + buttonPane.add(Box.createHorizontalGlue()); + buttonPane.add(cancelButton); + cancelButton.addActionListener(BackupGUI.this); + + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + Container contentPane = frame.getContentPane(); + contentPane.add(panel, BorderLayout.CENTER); + contentPane.add(buttonPane, BorderLayout.PAGE_END); + frame.setVisible(true); + } + + /** + * Updates information about the backup progress + * + * @param infoText

The text to display

+ * @param progressPercent

The new percent of the progress bar

+ */ + public static void updateProgress(String infoText, int progressPercent) { + if (progressTextArea == null || progressBar == null) { + return; + } + progressTextArea.setText(infoText); + progressBar.setValue(progressPercent); + } + + /** + * Destroys the backup GUI + */ + public static void destroy() { + BackupUtil.abortBackup(); + progressTextArea = null; + progressBar = null; + cancelButton = null; + if (frame != null) { + frame.dispose(); + } + frame = null; + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + if (actionEvent.getSource() == cancelButton) { + destroy(); + } + } +} diff --git a/src/main/java/net/knarcraft/minecraftserverlauncher/userinterface/ServerLauncherGUI.java b/src/main/java/net/knarcraft/minecraftserverlauncher/userinterface/ServerLauncherGUI.java index a47bbb0..b7f4f00 100644 --- a/src/main/java/net/knarcraft/minecraftserverlauncher/userinterface/ServerLauncherGUI.java +++ b/src/main/java/net/knarcraft/minecraftserverlauncher/userinterface/ServerLauncherGUI.java @@ -4,7 +4,7 @@ import net.knarcraft.minecraftserverlauncher.Main; import net.knarcraft.minecraftserverlauncher.profile.Collection; import net.knarcraft.minecraftserverlauncher.profile.ServerLauncherController; import net.knarcraft.minecraftserverlauncher.server.ServerHandler; -import net.knarcraft.minecraftserverlauncher.utility.CommonFunctions; +import net.knarcraft.minecraftserverlauncher.utility.BackupUtil; import javax.imageio.ImageIO; import javax.naming.ConfigurationException; @@ -334,7 +334,7 @@ public class ServerLauncherGUI extends MessageHandler implements ActionListener, addServer(); } else if (actionSource == backupButton) { //Run backup in its own thread to prevent locking up - Executors.newSingleThreadExecutor().execute(() -> CommonFunctions.backup(this)); + Executors.newSingleThreadExecutor().execute(() -> BackupUtil.backup(this)); } else if (actionSource == addProfileButton) { controller.addProfile(JOptionPane.showInputDialog("Profile name: ")); updateProfiles(); diff --git a/src/main/java/net/knarcraft/minecraftserverlauncher/utility/BackupUtil.java b/src/main/java/net/knarcraft/minecraftserverlauncher/utility/BackupUtil.java new file mode 100644 index 0000000..ed6df5d --- /dev/null +++ b/src/main/java/net/knarcraft/minecraftserverlauncher/utility/BackupUtil.java @@ -0,0 +1,223 @@ +package net.knarcraft.minecraftserverlauncher.utility; + +import net.knarcraft.minecraftserverlauncher.Main; +import net.knarcraft.minecraftserverlauncher.profile.Collection; +import net.knarcraft.minecraftserverlauncher.server.Server; +import net.knarcraft.minecraftserverlauncher.userinterface.BackupGUI; +import net.knarcraft.minecraftserverlauncher.userinterface.GUI; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * A helper class for performing server backup + */ +public class BackupUtil { + + private static boolean backupAborted; + private static boolean backupRunning = false; + + /** + * Aborts the currently running backup + */ + public static void abortBackup() { + backupAborted = true; + } + + /** + * Recursively copies a folder to another location + * + * @param source

The folder to copy

+ * @param destination

Target destination

+ * @throws IOException

If we can't start a file stream

+ */ + private static synchronized long backupFolder(File source, File destination, long backupFileSize, long alreadyCopied) throws IOException { + if (backupAborted) { + return 0L; + } + if (!source.isDirectory()) { + long copiedFileSize = copyFile(source, destination); + BackupGUI.updateProgress("Copying " + source + "\n to " + destination, + (int)((alreadyCopied + copiedFileSize) * 100 / backupFileSize)); + return copiedFileSize; + } else { + if (!destination.exists() && !destination.mkdir()) { + return 0L; + } + String[] files = source.list(); + long copiedFilesSize = 0; + if (files != null) { + for (String file : files) { + File srcFile = new File(source, file); + File destinationFile = new File(destination, file); + copiedFilesSize += backupFolder(srcFile, destinationFile, backupFileSize, + alreadyCopied + copiedFilesSize); + BackupGUI.updateProgress("Copying " + source + "\n to " + destination, + (int)((alreadyCopied + copiedFilesSize) * 100 / backupFileSize)); + } + } + return copiedFilesSize; + } + } + + /** + * Copies a file from one location to another + * + * @param source

The file to copy

+ * @param destination

The location of the copied file

+ * @throws IOException

If reading or writing fails

+ */ + private static long copyFile(File source, File destination) throws IOException { + InputStream in = new FileInputStream(source); + OutputStream out = new FileOutputStream(destination); + byte[] buffer = new byte[1024]; + int length; + while ((length = in.read(buffer)) > 0) { + out.write(buffer, 0, length); + } + in.close(); + out.close(); + return Files.size(source.toPath()); + } + + /** + * Copies all server directories to a folder specified by the user + */ + public synchronized static void backup(GUI gui) { + backupAborted = false; + if (backupRunning) { + gui.setStatus("A backup is already running"); + return; + } else { + backupRunning = true; + } + + //Get the folder to save the backed up files in + File path = gui.askForDirectory("Backup folder"); + if (path == null || !path.isDirectory()) { + backupRunning = false; + return; + } + + gui.setStatus("Backup running..."); + + List> serverFolders = getFoldersOfEnabledServers(path); + + long backupFileSize = getFolderSize(gui, serverFolders); + + gui.setStatus("Backing up " + (backupFileSize / 1000000) + "MB"); + + new BackupGUI(); + BackupGUI.updateProgress("Backup starting...", 0); + long alreadyCopied = 0; + for (List serverFolder : serverFolders) { + if (backupAborted) { + gui.setStatus("Backup aborted"); + backupRunning = false; + return; + } + File srcFolder = serverFolder.get(0); + File destinationFolder = serverFolder.get(1); + + //Create child folders + if (!destinationFolder.exists() && !destinationFolder.mkdirs()) { + backupRunning = false; + gui.logError("Unable to create necessary sub-folder in the backup folder"); + throw new IllegalArgumentException("Unable to create necessary sub-folder in the backup folder"); + } + //Backup + try { + alreadyCopied += backupFolder(srcFolder, destinationFolder, backupFileSize, alreadyCopied); + } catch (IOException e) { + e.printStackTrace(); + gui.setStatus("Backup caused an error"); + backupRunning = false; + } + } + backupRunning = false; + BackupGUI.destroy(); + if (backupAborted) { + gui.setStatus("Backup aborted"); + } else { + gui.setStatus("Backup finished"); + } + } + + /** + * Gets the size of a list of folders + * + * @param gui

The GUI to write any errors to

+ * @param serverFolders

The folder to find the size of

+ * @return

The size of the given folders

+ */ + private static long getFolderSize(GUI gui, List> serverFolders) { + long folderSize = 0; + for (List serverFolder : serverFolders) { + File srcFolder = serverFolder.get(0); + try (Stream walk = Files.walk(srcFolder.toPath())) { + folderSize += walk.filter(Files::isRegularFile).mapToLong(BackupUtil::getFileSize).sum(); + } catch (IOException e) { + gui.setStatus(String.format("IO errors %s", e)); + } + } + return folderSize; + } + + /** + * Gets the input and output folders for enabled servers + * + *

The input folders are the folders to copy, while the output folders are the folders to write the backup to. + * Each list element contains a list of exactly two File items. The first one is the input folder, and the second + * one is the output folder

+ * + * @param path

The path of the backup folder, as given by the user

+ * @return

The folders to copy from/to

+ */ + private static List> getFoldersOfEnabledServers(File path) { + List> serverFolders = new ArrayList<>(); + + //Get folders of servers to back up + List collections = Main.getController().getCurrentProfile().getCollections(); + for (Collection collection : collections) { + //Ignore disabled and invalid servers + if (collection.getServer().getPath().equals("") || !collection.getServer().isEnabled()) { + continue; + } + //Decide sub-folders + Server targetServer = collection.getServer(); + String name = targetServer.getName(); + File srcFolder = new File(targetServer.getPath()); + File destinationFolder = new File(path, name); + + List serverFolder = new ArrayList<>(); + serverFolder.add(srcFolder); + serverFolder.add(destinationFolder); + serverFolders.add(serverFolder); + } + return serverFolders; + } + + /** + * Gets the size of a file given its path + * @param path

The path to a file

+ * @return

The size of the file in bytes, or 0 if an exception is thrown

+ */ + private static long getFileSize(Path path) { + try { + return Files.size(path); + } catch (IOException exception) { + System.out.printf("Failed to get size of %s%n%s", path, exception); + return 0L; + } + } + +} diff --git a/src/main/java/net/knarcraft/minecraftserverlauncher/utility/CommonFunctions.java b/src/main/java/net/knarcraft/minecraftserverlauncher/utility/CommonFunctions.java index 08efa37..252a417 100644 --- a/src/main/java/net/knarcraft/minecraftserverlauncher/utility/CommonFunctions.java +++ b/src/main/java/net/knarcraft/minecraftserverlauncher/utility/CommonFunctions.java @@ -1,9 +1,6 @@ package net.knarcraft.minecraftserverlauncher.utility; import net.knarcraft.minecraftserverlauncher.Main; -import net.knarcraft.minecraftserverlauncher.profile.Collection; -import net.knarcraft.minecraftserverlauncher.server.Server; -import net.knarcraft.minecraftserverlauncher.userinterface.GUI; import net.knarcraft.minecraftserverlauncher.userinterface.WebBrowser; import java.io.BufferedReader; @@ -16,7 +13,6 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.URI; import java.net.URISyntaxException; @@ -36,7 +32,6 @@ import java.util.Scanner; public final class CommonFunctions { private static final String filesDirectory = Main.getApplicationWorkDirectory() + File.separator + "files"; - private static boolean backupRunning = false; /** * Creates all folders necessary for tests and normal operation @@ -211,55 +206,6 @@ public final class CommonFunctions { } } - /** - * Recursively copies a folder to another location - * - * @param source

The folder to copy

- * @param destination

Target destination

- * @throws IOException

If we can't start a file stream

- */ - private static void copyFolder(GUI serverLauncherGui, File source, File destination) throws IOException { - if (!source.isDirectory()) { - copyFile(serverLauncherGui, source, destination); - } else { - serverLauncherGui.setStatus("Copying directory " + source); - if (!destination.exists() && !destination.mkdir()) { - return; - } - String[] files = source.list(); - if (files != null) { - for (String file : files) { - File srcFile = new File(source, file); - File destinationFile = new File(destination, file); - copyFolder(serverLauncherGui, srcFile, destinationFile); - } - } - serverLauncherGui.setStatus("Copied directory " + source); - } - } - - /** - * Copies a file from one location to another - * - * @param serverLauncherGui

The serverLauncherGui to use for alerting the user

- * @param source

The file to copy

- * @param destination

The location of the copied file

- * @throws IOException

If reading or writing fails

- */ - private static void copyFile(GUI serverLauncherGui, File source, File destination) throws IOException { - serverLauncherGui.setStatus("Copying file " + source + "..."); - InputStream in = new FileInputStream(source); - OutputStream out = new FileOutputStream(destination); - byte[] buffer = new byte[1024]; - int length; - while ((length = in.read(buffer)) > 0) { - out.write(buffer, 0, length); - } - in.close(); - out.close(); - serverLauncherGui.setStatus("Copied file " + source); - } - /** * Opens an url in the user's default application. * @@ -296,50 +242,7 @@ public final class CommonFunctions { return text.toString().trim(); } - /** - * Copies all server directories to a folder specified by the user. - */ - public synchronized static void backup(GUI gui) { - if (backupRunning) { - return; - } else { - backupRunning = true; - } - //Get the folder to save the backed up files in - File path = gui.askForDirectory("Backup folder"); - if (path == null || !path.isDirectory()) { - backupRunning = false; - return; - } - - gui.setStatus("Backup running..."); - for (Collection collection : Main.getController().getCurrentProfile().getCollections()) { - //Ignore disabled and invalid servers - if (collection.getServer().getPath().equals("") || !collection.getServer().isEnabled()) { - continue; - } - //Decide sub-folders - Server targetServer = collection.getServer(); - String name = targetServer.getName(); - File srcFolder = new File(targetServer.getPath()); - File destinationFolder = new File(path, name); - //Create child folders - if (!destinationFolder.exists() && !destinationFolder.mkdirs()) { - backupRunning = false; - throw new IllegalArgumentException("Unable to create necessary sub-folder in the backup folder"); - } - //Backup - try { - CommonFunctions.copyFolder(gui, srcFolder, destinationFolder); - } catch (IOException e) { - e.printStackTrace(); - backupRunning = false; - } - } - gui.setStatus("Backup finished"); - backupRunning = false; - } /** * Validates that a name is not empty and does not contain invalid characters