diff --git a/build.gradle b/build.gradle index 769986b..744232e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group 'github.kasuminova' -version '1.2.0-STABLE-PREVIEW_2' +version '1.2.0-STABLE-PREVIEW_4' repositories { diff --git a/exe4j_configuration.exe4j b/exe4j_configuration.exe4j index 30151f3..2cbd786 100644 --- a/exe4j_configuration.exe4j +++ b/exe4j_configuration.exe4j @@ -9,7 +9,7 @@ - + diff --git a/src/main/java/github/kasuminova/balloonserver/BalloonServer.java b/src/main/java/github/kasuminova/balloonserver/BalloonServer.java index 8ca68f8..5042809 100644 --- a/src/main/java/github/kasuminova/balloonserver/BalloonServer.java +++ b/src/main/java/github/kasuminova/balloonserver/BalloonServer.java @@ -6,15 +6,17 @@ import github.kasuminova.balloonserver.GUI.LayoutManager.VFlowLayout; import github.kasuminova.balloonserver.GUI.Panels.AboutPanel; import github.kasuminova.balloonserver.GUI.Panels.SettingsPanel; +import github.kasuminova.balloonserver.Servers.AbstractLittleServer; +import github.kasuminova.balloonserver.Servers.LegacyLittleServer; import github.kasuminova.balloonserver.Servers.LittleServer; import github.kasuminova.balloonserver.Servers.LittleServerInterface; import github.kasuminova.balloonserver.UpdateChecker.ApplicationVersion; +import github.kasuminova.balloonserver.UpdateChecker.Checker; import github.kasuminova.balloonserver.Utils.FileUtil; import github.kasuminova.balloonserver.Utils.GUILogger; import github.kasuminova.balloonserver.Utils.Security; import javax.swing.*; -import javax.swing.Timer; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; @@ -27,8 +29,10 @@ import java.lang.management.MemoryMXBean; import java.util.*; import java.util.List; +import java.util.Timer; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.logging.Logger; @@ -42,7 +46,13 @@ public class BalloonServer { //设置全局主题,字体等 SetupSwing.init(); } - public static final ApplicationVersion VERSION = new ApplicationVersion("1.2.0-STABLE-PREVIEW_2"); + public static final ApplicationVersion VERSION = new ApplicationVersion("1.2.0-STABLE-PREVIEW_4"); + /* + 可执行文件名称。 + 如 BalloonServer-GUI-1.2.0-STABLE.jar, + 如果为 exe 格式则为 C:/Users/username/AppData/Local/Temp/e4j+随机缓存名/BalloonServer-GUI-1.2.0-STABLE.jar + */ + public static final String ARCHIVE_NAME = BalloonServer.class.getProtectionDomain().getCodeSource().getLocation().getFile(); private static final long START = System.currentTimeMillis(); private static final JFrame PREMAIN_FRAME = new JFrame("加载中"); private static final JPanel STATUS_PANEL = new JPanel(new BorderLayout()); @@ -63,14 +73,17 @@ public class BalloonServer { public static final BalloonServerConfig CONFIG = new BalloonServerConfig(); //可用服务端接口列表,与 Tab 同步 public static final List availableCustomServerInterfaces = Collections.synchronizedList(new ArrayList<>()); + //支持放入多个任务的 Timer + public static final Timer GLOBAL_QUERY_TIMER = new Timer(false); private static void init() { //大小设置 - MAIN_FRAME.setSize(1350,780); + MAIN_FRAME.setSize(1375,780); MAIN_FRAME.setMinimumSize(new Dimension((int) (MAIN_FRAME.getWidth() * 0.8), MAIN_FRAME.getHeight())); //标签页配置 PRE_LOAD_PROGRESS_BAR.setString("载入主面板..."); TABBED_PANE.putClientProperty("JTabbedPane.tabAreaAlignment", "fill"); + mainPanel.add(TABBED_PANE, BorderLayout.CENTER); //载入主配置文件 loadConfig(); @@ -96,7 +109,7 @@ private static void init() { //定义变量 LittleServerInterface serverInterface = availableCustomServerInterfaces.get(tabIndex); String serverName = serverInterface.getServerName(); - if (!stopLittleServer(serverInterface, serverName, tabIndex)) return; + if (!stopLittleServer(serverInterface, serverName, tabIndex, true)) return; SERVER_TABBED_PANE.removeTabAt(tabIndex); availableCustomServerInterfaces.remove((int) tabIndex); @@ -106,13 +119,13 @@ private static void init() { SERVER_TABBED_PANE.putClientProperty("JTabbedPane.scrollButtonsPlacement", "both"); Thread serverThread = new Thread(() -> { - LittleServer littleServer; + AbstractLittleServer abstractLittleServer; //自动启动服务器检测 if (CONFIG.isAutoStartServer()) { - littleServer = new LittleServer("littleserver", true); + abstractLittleServer = new LittleServer("littleserver", true); //自动启动服务器(仅本次) } else if (CONFIG.isAutoStartServerOnce()) { - littleServer = new LittleServer("littleserver", true); + abstractLittleServer = new LittleServer("littleserver", true); CONFIG.setAutoStartServerOnce(false); @@ -123,11 +136,14 @@ private static void init() { GLOBAL_LOGGER.warning("保存主程序配置文件失败!"); } } else { - littleServer = new LittleServer("littleserver", false); + abstractLittleServer = new LittleServer("littleserver", false); } - availableCustomServerInterfaces.add(littleServer.getServerInterface()); - SERVER_TABBED_PANE.addTab("主服务端", DEFAULT_SERVER_ICON, littleServer.getPanel()); - mainPanel.add(TABBED_PANE, BorderLayout.CENTER); + SERVER_TABBED_PANE.addTab("主服务端 (4.1.15+)", DEFAULT_SERVER_ICON, abstractLittleServer.getPanel()); + availableCustomServerInterfaces.add(abstractLittleServer.getServerInterface()); + + abstractLittleServer = new LegacyLittleServer("littleserver_legacy", false); + availableCustomServerInterfaces.add(abstractLittleServer.getServerInterface()); + SERVER_TABBED_PANE.addTab("旧版服务端 (4.x.x - 4.1.14)", DEFAULT_SERVER_ICON, abstractLittleServer.getPanel()); }); serverThread.start(); @@ -168,7 +184,52 @@ public void windowClosing(WindowEvent e) { MAIN_FRAME.setVisible(true); PREMAIN_FRAME.dispose(); + + //当前是否在检查更新,防止长时间静置未操作弹出多个对话框 + AtomicBoolean isCheckingUpdate = new AtomicBoolean(false); + //更新检查线程,每一小时检查一次最新版本 + GLOBAL_QUERY_TIMER.schedule(new TimerTask() { + @Override + public void run() { + if (CONFIG.isAutoCheckUpdates() && !isCheckingUpdate.get()) { + GLOBAL_LOGGER.info("开始检查更新..."); + isCheckingUpdate.set(true); + GLOBAL_THREAD_POOL.execute(() -> { + Checker.checkUpdates(); + isCheckingUpdate.set(false); + }); + } + } + },0,3600000); + } + + /** + * 关闭所有服务端实例 + * @param inquireUser 是否向用户确认关闭正在运行的服务端 + */ + public static void stopAllServers(boolean inquireUser) { + //停止所有运行的实例 + for (int i = 0; i < availableCustomServerInterfaces.size(); i++) { + LittleServerInterface serverInterface = availableCustomServerInterfaces.get(i); + stopLittleServer(serverInterface, serverInterface.getServerName(), i, inquireUser); + if (i != 0) { + SERVER_TABBED_PANE.removeTabAt(i); + availableCustomServerInterfaces.remove(i); + i--; + } + } + + //删除除主服务端除外的服务端实例标签页 + for (int i = 1; i < SERVER_TABBED_PANE.getTabCount(); i++) { + SERVER_TABBED_PANE.remove(i); + } + //删除除主服务端除外的服务端实例接口 + for (int i = 1; i < availableCustomServerInterfaces.size(); i++) { + availableCustomServerInterfaces.remove(i); + i--; + } } + /** * 载入配置文件 */ @@ -188,9 +249,9 @@ private static void loadConfig() { } /** - * 保存配置文件 + * 保存主程序配置文件 */ - private static void saveConfig() { + public static void saveConfig() { try { ConfigurationManager.saveConfigurationToFile(CONFIG, "./", "balloonserver"); GLOBAL_LOGGER.info("成功保存主程序配置文件."); @@ -208,7 +269,7 @@ private static void loadMenuBar() { newMenu.setIcon(PLUS_ICON); menuBar.add(newMenu); - JMenuItem createNewLittleServer = new JMenuItem("创建一个新的更新服务器实例", PLUS_ICON); + JMenuItem createNewLittleServer = new JMenuItem("创建更新服务端实例 (兼容 4.1.15+ 版本客户端)", PLUS_ICON); newMenu.add(createNewLittleServer); createNewLittleServer.addActionListener(e -> { String serverName = JOptionPane.showInputDialog(MAIN_FRAME,"请输入服务器实例名称","创建",JOptionPane.INFORMATION_MESSAGE); @@ -236,6 +297,35 @@ private static void loadMenuBar() { }); }); + JMenuItem createNewLegacyLittleServer = new JMenuItem("创建旧版更新服务端实例 (兼容 4.x.x - 4.1.14 版本客户端)", PLUS_ICON); + newMenu.add(createNewLegacyLittleServer); + createNewLegacyLittleServer.addActionListener(e -> { + String serverName = JOptionPane.showInputDialog(MAIN_FRAME,"请输入服务器实例名称","创建",JOptionPane.INFORMATION_MESSAGE); + if (Security.stringIsUnsafe(MAIN_FRAME, serverName, new String[]{"balloonserver","littleserver","res-cache",".lscfg",".json"})) return; + + if (new File(String.format("./%s.legacy.lscfg.json", serverName)).exists() || new File(String.format("./%s.lscfg.json", serverName)).exists()) { + JOptionPane.showMessageDialog(MAIN_FRAME, + "已存在此服务器名,请使用载入服务器.","已存在服务器", + JOptionPane.WARNING_MESSAGE); + return; + } + if (checkSameServer(serverName + ".legacy")) return; + if (checkSameServer(serverName)) return; + + GLOBAL_THREAD_POOL.execute(() -> { + long start = System.currentTimeMillis(); + GLOBAL_LOGGER.info(String.format("正在创建新的服务器实例:%s", serverName)); + + LegacyLittleServer customServer = new LegacyLittleServer(serverName, false); + + availableCustomServerInterfaces.add(customServer.getServerInterface()); + SERVER_TABBED_PANE.addTab(serverName, CUSTOM_SERVER_ICON, customServer.getPanel()); + SERVER_TABBED_PANE.setSelectedIndex(SERVER_TABBED_PANE.getTabCount() - 1); + + GLOBAL_LOGGER.info(String.format("实例创建耗时 %sms.", System.currentTimeMillis() - start)); + }); + }); + JMenu loadMenu = new JMenu("实例管理"); loadMenu.setIcon(RESOURCE_ICON); menuBar.add(loadMenu); @@ -245,19 +335,20 @@ private static void loadMenuBar() { loadLittleServer.addActionListener(e -> { JFileChooser fileChooser = new JFileChooser("."); fileChooser.setMultiSelectionEnabled(true); - fileChooser.setFileFilter(new FileUtil.SimpleFileFilter(new String[]{"lscfg.json"}, new String[]{"res-cache", "littleserver", "balloonserver"} ,"LittleServer 配置文件 (*.lscfg.json)")); + fileChooser.setFileFilter(new FileUtil.SimpleFileFilter(new String[]{"lscfg.json", "legacy.lscfg.json"}, new String[]{"res-cache", "littleserver", "balloonserver"} ,"LittleServer 配置文件 (*.lscfg.json, *.legacy.lscfg.json)")); if (!(fileChooser.showDialog(MAIN_FRAME, "载入选中实例") == JFileChooser.APPROVE_OPTION)) { return; } File[] selectedFiles = fileChooser.getSelectedFiles(); + String[] serverNames = new String[selectedFiles.length]; for (int i = 0; i < selectedFiles.length; i++) { serverNames[i] = selectedFiles[i].getName().replace(".lscfg.json", ""); } - LittleServer[] customServers = new LittleServer[serverNames.length]; + AbstractLittleServer[] customServers = new AbstractLittleServer[serverNames.length]; ArrayList threadList = new ArrayList<>(); //检查是否存在非法名称或已存在的名称 for (String serverName : serverNames) { @@ -276,7 +367,12 @@ private static void loadMenuBar() { Thread thread = new Thread(() -> { long start = System.currentTimeMillis(); GLOBAL_LOGGER.info(String.format("正在载入服务器实例:%s", serverName)); - LittleServer customServer = new LittleServer(serverName, false); + AbstractLittleServer customServer; + if (serverName.endsWith(".legacy")) { + customServer = new LegacyLittleServer(serverName.replace(".legacy", ""), false); + } else { + customServer = new LittleServer(serverName, false); + } customServers[panelArrayIndex] = customServer; GLOBAL_LOGGER.info(String.format("实例载入耗时 %sms.", System.currentTimeMillis() - start)); }); @@ -311,7 +407,7 @@ private static void loadMenuBar() { LittleServerInterface serverInterface = availableCustomServerInterfaces.get(selected); String serverName = serverInterface.getServerName(); - if (stopLittleServer(serverInterface, serverName, selected)) { + if (stopLittleServer(serverInterface, serverName, selected, true)) { GLOBAL_THREAD_POOL.execute(() -> { LittleServer littleServer; if (serverName.equals("littleserver")) { @@ -335,24 +431,31 @@ private static void loadMenuBar() { * @param serverInterface 服务器接口 * @param serverName 服务器名 * @param index 服务端列表位置 + * @param inquireUser 是否向用户确认关闭服务端 * @return 用户是否确认关闭了服务器 */ - private static boolean stopLittleServer(LittleServerInterface serverInterface, String serverName, int index) { + private static boolean stopLittleServer(LittleServerInterface serverInterface, String serverName, int index, boolean inquireUser) { boolean isStarted = serverInterface.isStarted().get(); //如果服务器已启动,则提示是否关闭服务器 if (isStarted) { - int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, String.format("%s 实例正在运行,你希望关闭服务器后再关闭此实例吗?", serverName), "警告", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - if (!(selection == JOptionPane.YES_OPTION)) return false; + if (inquireUser) { + int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, String.format("%s 实例正在运行,你希望关闭服务器后再关闭此实例吗?", serverName), "警告", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (!(selection == JOptionPane.YES_OPTION)) return false; + } + + //停止服务器 if (!serverInterface.stopServer()) { JOptionPane.showMessageDialog(MAIN_FRAME, "无法正常关闭服务器,请检查窗口.", "错误", JOptionPane.ERROR_MESSAGE); SERVER_TABBED_PANE.setSelectedIndex(index); return false; } } else { - int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, String.format("你要关闭 %s 这个实例吗?", serverName) , "提示", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE); - if (!(selection == JOptionPane.YES_OPTION)) return false; + if (inquireUser) { + int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, String.format("你要关闭 %s 这个实例吗?", serverName) , "提示", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE); + if (!(selection == JOptionPane.YES_OPTION)) return false; + } } - //保存配置并移除 + //保存配置 serverInterface.saveConfig(); return true; } @@ -364,7 +467,12 @@ private static boolean stopLittleServer(LittleServerInterface serverInterface, S */ private static boolean checkSameServer(String serverName) { for (int i = 0; i < SERVER_TABBED_PANE.getTabCount(); i++) { - if (SERVER_TABBED_PANE.getTitleAt(i).equals(serverName)) { + if (serverName.endsWith(".legacy")) { + if (SERVER_TABBED_PANE.getTitleAt(i).equals(serverName.replace(".legacy", ""))) { + JOptionPane.showMessageDialog(MAIN_FRAME, String.format("名为 %s 的配置文件已经载入到服务器中了!", serverName), "错误", JOptionPane.ERROR_MESSAGE); + return true; + } + } else if (SERVER_TABBED_PANE.getTitleAt(i).equals(serverName)) { JOptionPane.showMessageDialog(MAIN_FRAME, String.format("名为 %s 的配置文件已经载入到服务器中了!", serverName), "错误", JOptionPane.ERROR_MESSAGE); return true; } @@ -400,22 +508,24 @@ private static void loadStatusBar() { GLOBAL_STATUS_PROGRESSBAR.setVisible(false); STATUS_PANEL.add(GLOBAL_STATUS_PROGRESSBAR); mainPanel.add(STATUS_PANEL, BorderLayout.SOUTH); - //定时器, 更新内存和线程信息 - Timer statusTimer = new Timer(500, e -> { - MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - long memoryUsed = memoryMXBean.getHeapMemoryUsage().getUsed(); - long memoryTotal = memoryMXBean.getHeapMemoryUsage().getCommitted(); - - threadCount.setText(String.format("当前运行的线程数量:%s", Thread.activeCount())); - - memBar.setValue((int) ((double) memoryUsed * memBar.getMaximum() / memoryTotal)); - memBar.setString(String.format("%s M / %s M - Max: %s M", - memoryUsed / (1024 * 1024), - memoryTotal / (1024 * 1024), - memoryMXBean.getHeapMemoryUsage().getMax() / (1024 * 1024) - )); - }); - statusTimer.start(); + //新建任务,每 0.5 秒更新内存和线程信息 + GLOBAL_QUERY_TIMER.schedule(new TimerTask() { + @Override + public void run() { + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long memoryUsed = memoryMXBean.getHeapMemoryUsage().getUsed(); + long memoryTotal = memoryMXBean.getHeapMemoryUsage().getCommitted(); + + threadCount.setText(String.format("当前运行的线程数量:%s", Thread.activeCount())); + + memBar.setValue((int) ((double) memoryUsed * memBar.getMaximum() / memoryTotal)); + memBar.setString(String.format("%s M / %s M - Max: %s M", + memoryUsed / (1024 * 1024), + memoryTotal / (1024 * 1024), + memoryMXBean.getHeapMemoryUsage().getMax() / (1024 * 1024) + )); + } + }, 0, 500); } /** diff --git a/src/main/java/github/kasuminova/balloonserver/Configurations/ConfigurationManager.java b/src/main/java/github/kasuminova/balloonserver/Configurations/ConfigurationManager.java index f2c6f91..2dafebc 100644 --- a/src/main/java/github/kasuminova/balloonserver/Configurations/ConfigurationManager.java +++ b/src/main/java/github/kasuminova/balloonserver/Configurations/ConfigurationManager.java @@ -13,29 +13,17 @@ */ public class ConfigurationManager { public static void loadLittleServerConfigFromFile(String path, LittleServerConfig oldConfig) throws IOException { - LittleServerConfig config = JSON.parseObject(Files.newInputStream(Paths.get(path)), LittleServerConfig.class); + LittleServerConfig newConfig = JSON.parseObject(Files.newInputStream(Paths.get(path)), LittleServerConfig.class); - //配置文件版本验证 - if (config.getConfigVersion() == 0) { - LittleServerConfig newConfig = new LittleServerConfig(); - newConfig.setIp(config.getIp()); - newConfig.setFileChangeListener(config.isFileChangeListener()); - newConfig.setPort(config.getPort()); - newConfig.setMainDirPath(config.getMainDirPath()); - newConfig.setJksFilePath(config.getJksFilePath()); - newConfig.setJksSslPassword(config.getJksSslPassword()); - return; - } - - oldConfig.setConfigVersion(config.getConfigVersion()); - oldConfig.setIp(config.getIp()); - oldConfig.setPort(config.getPort()); - oldConfig.setMainDirPath(config.getMainDirPath()); - oldConfig.setFileChangeListener(config.isFileChangeListener()); - oldConfig.setJksFilePath(config.getJksFilePath()); - oldConfig.setJksSslPassword(config.getJksSslPassword()); - oldConfig.setCommonMode(config.getCommonMode()); - oldConfig.setOnceMode(config.getOnceMode()); + oldConfig.setConfigVersion(newConfig.getConfigVersion()); + oldConfig.setIp(newConfig.getIp()); + oldConfig.setPort(newConfig.getPort()); + oldConfig.setMainDirPath(newConfig.getMainDirPath()); + oldConfig.setFileChangeListener(newConfig.isFileChangeListener()); + oldConfig.setJksFilePath(newConfig.getJksFilePath()); + oldConfig.setJksSslPassword(newConfig.getJksSslPassword()); + oldConfig.setCommonMode(newConfig.getCommonMode()); + oldConfig.setOnceMode(newConfig.getOnceMode()); } public static void loadBalloonServerConfigFromFile(String path, BalloonServerConfig oldConfig) throws IOException { BalloonServerConfig config = JSON.parseObject(Files.newInputStream(Paths.get(path)), BalloonServerConfig.class); diff --git a/src/main/java/github/kasuminova/balloonserver/Configurations/LittleServerConfig.java b/src/main/java/github/kasuminova/balloonserver/Configurations/LittleServerConfig.java index 9634f27..6214258 100644 --- a/src/main/java/github/kasuminova/balloonserver/Configurations/LittleServerConfig.java +++ b/src/main/java/github/kasuminova/balloonserver/Configurations/LittleServerConfig.java @@ -18,7 +18,7 @@ public LittleServerConfig() { */ public void reset() { configVersion = 1; - ip = "0.0.0.0"; + ip = "127.0.0.1"; port = 8080; mainDirPath = "/res"; fileChangeListener = true; diff --git a/src/main/java/github/kasuminova/balloonserver/GUI/ConfirmExitDialog.java b/src/main/java/github/kasuminova/balloonserver/GUI/ConfirmExitDialog.java index 91f2f8f..b0238fb 100644 --- a/src/main/java/github/kasuminova/balloonserver/GUI/ConfirmExitDialog.java +++ b/src/main/java/github/kasuminova/balloonserver/GUI/ConfirmExitDialog.java @@ -11,6 +11,7 @@ import java.awt.*; import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_LOGGER; +import static github.kasuminova.balloonserver.BalloonServer.stopAllServers; public class ConfirmExitDialog extends JDialog { public ConfirmExitDialog(JFrame frame, BalloonServerConfig config) { @@ -57,7 +58,8 @@ public ConfirmExitDialog(JFrame frame, BalloonServerConfig config) { GLOBAL_LOGGER.warning("主程序配置文件保存失败!\n" + GUILogger.stackTraceToString(ex)); } } - + //停止所有正在运行的服务器并保存配置 + stopAllServers(true); System.exit(0); } //保存配置并最小化窗口 diff --git a/src/main/java/github/kasuminova/balloonserver/GUI/Panels/SettingsPanel.java b/src/main/java/github/kasuminova/balloonserver/GUI/Panels/SettingsPanel.java index 26f90bc..b39f798 100644 --- a/src/main/java/github/kasuminova/balloonserver/GUI/Panels/SettingsPanel.java +++ b/src/main/java/github/kasuminova/balloonserver/GUI/Panels/SettingsPanel.java @@ -51,14 +51,14 @@ public static JPanel getPanel() { JLabel autoStartDefaultServerOnceDesc = new JLabel("此项选中后,BalloonServer 在启动时会自动启动主服务端的服务器,仅生效一次,生效后自动关闭."); //自动检查更新 - JLabel autoCheckUpdatesDesc = new JLabel("此项选中后,BalloonServer 在会定时检查最新更新."); + JLabel autoCheckUpdatesDesc = new JLabel("此项选中后,BalloonServer 在会在启动时检查最新更新."); //自动更新 - JLabel autoUpdateDesc = new JLabel("此项选中后,BalloonServer 在检查到更新后,会自动下载并自动重启应用更新(仅支持 exe 格式服务端)."); + JLabel autoUpdateDesc = new JLabel("此项及“自动检查更新”项选中后,BalloonServer 在检查到更新后,会自动下载并自动重启应用更新,如果主服务端正在运行,则下次启动会自动启动服务器."); //如果程序非 exe 格式则设置为禁用 - if (!SettingsPanel.class.getProtectionDomain().getCodeSource().getLocation().getFile().endsWith(".exe")) { + if (!ARCHIVE_NAME.contains("e4j")) { autoUpdate.setEnabled(false); - autoUpdate.setText("自动更新(不支持)"); + autoUpdate.setText("自动更新(不支持,目前仅支持 exe 格式服务端)"); } //关闭选项 diff --git a/src/main/java/github/kasuminova/balloonserver/GUI/SwingSystemTray.java b/src/main/java/github/kasuminova/balloonserver/GUI/SwingSystemTray.java index 27439d5..718ac3e 100644 --- a/src/main/java/github/kasuminova/balloonserver/GUI/SwingSystemTray.java +++ b/src/main/java/github/kasuminova/balloonserver/GUI/SwingSystemTray.java @@ -14,6 +14,7 @@ import java.net.URL; import static github.kasuminova.balloonserver.BalloonServer.CONFIG; +import static github.kasuminova.balloonserver.BalloonServer.stopAllServers; /** * 中文系统托盘弹出菜单不乱码。 @@ -59,6 +60,8 @@ public void firePopupMenuWillBecomeInvisible() { } catch (IOException ex) { BalloonServer.GLOBAL_LOGGER.warning("保存主程序配置文件失败!"); } + //停止所有正在运行的服务器并保存配置 + stopAllServers(true); System.exit(0); }); JMenuItem showMainFrame = new JMenuItem("显示窗口"); diff --git a/src/main/java/github/kasuminova/balloonserver/HTTPServer/HttpRequestHandler.java b/src/main/java/github/kasuminova/balloonserver/HTTPServer/HttpRequestHandler.java index 989bc93..bd27faf 100644 --- a/src/main/java/github/kasuminova/balloonserver/HTTPServer/HttpRequestHandler.java +++ b/src/main/java/github/kasuminova/balloonserver/HTTPServer/HttpRequestHandler.java @@ -6,6 +6,7 @@ import github.kasuminova.balloonserver.Servers.LittleServerInterface; import github.kasuminova.balloonserver.Utils.FileUtil; import github.kasuminova.balloonserver.Utils.GUILogger; +import github.kasuminova.balloonserver.Utils.HashCalculator; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.handler.codec.http.*; @@ -31,11 +32,13 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler commonModeList = new ArrayList<>(); + protected final List onceModeList = new ArrayList<>(); + //服务器启动状态 + protected final AtomicBoolean isStarted = new AtomicBoolean(false); + //服务端是否在生成缓存,防止同一时间多个线程生成缓存导致程序混乱 + protected final AtomicBoolean isGenerating = new AtomicBoolean(false); + protected final JPanel requestListPanel = new JPanel(new VFlowLayout()); + protected final JPanel littleServerPanel = new JPanel(new BorderLayout()); + protected final JButton startOrStop = new JButton("保存配置并启动服务器"); + //IP 输入框 + protected final JTextField IPTextField = new JTextField("0.0.0.0"); + //端口输入框 + protected final JSpinner portSpinner = new JSpinner(); + //Jks 证书密码 + protected final JPasswordField JksSslPassField = new JPasswordField(); + //资源文件夹输入框 + protected final JTextField mainDirTextField = new JTextField("/res"); + //实时文件监听 + protected final JCheckBox fileChangeListener = new JCheckBox("启用实时文件监听"); + //证书文件名(不可编辑) + protected final JTextField JksSslTextField = new JTextField("请选择证书文件"); + //普通模式 + protected final JList commonMode = new JList<>(); + //补全模式 + protected final JList onceMode = new JList<>(); + protected LittleServerInterface serverInterface; + protected HttpServerInterface httpServerInterface; + + /** + * 返回当前实例的面板 + * @return 服务器面板 + */ + public JPanel getPanel() { + return littleServerPanel; + } + + /** + * 返回当前服务器实例的接口 + * @return LittleServerInterface + */ + public LittleServerInterface getServerInterface() { + return serverInterface; + } + + /** + * 返回当前服务器实例的 HTTP 服务器接口 + * @return HttpServerInterface + */ + public HttpServerInterface getHttpServerInterface() { + return httpServerInterface; + } + + /** + * 创建一个服务器面板,并绑定一个新的服务器实例 + * @param serverName LittleServerConfig 配置文件路径 + * @param autoStart 是否在创建对象后自动启动服务器 + */ + public AbstractLittleServer(String serverName, boolean autoStart) { + this.serverName = serverName; + long start = System.currentTimeMillis(); + //设置 Logger,主体为 logPanel + JPanel logPanel = new JPanel(new BorderLayout()); + logPanel.setMinimumSize(new Dimension((int) (MAIN_FRAME.getWidth() * 0.525), 0)); + + logPanel.setBorder(new TitledBorder("服务器实例日志")); + JTextPane logPane = new JTextPane(); + logPane.setEditable(false); + JScrollPane logScrollPane = new JScrollPane(logPane); + logPanel.add(logScrollPane, BorderLayout.CENTER); + + //初始化 Logger + logger = new GUILogger(serverName, logPane); + + //日志窗口菜单 + JPopupMenu logPaneMenu = new JPopupMenu(); + JMenuItem cleanLogPane = new JMenuItem("清空日志", DELETE_ICON); + cleanLogPane.addActionListener(e -> { + try { + logPane.getDocument().remove(0,logPane.getDocument().getLength()); + logger.info("已清空当前服务器实例日志."); + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + }); + logPaneMenu.add(cleanLogPane); + logPane.addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + logPaneMenu.show(logPane, e.getX(), e.getY()); + } + } + }); + + //控制面板 + JPanel controlPanel = new JPanel(new BorderLayout()); + controlPanel.setBorder(new TitledBorder("控制面板")); + controlPanel.setMinimumSize(new Dimension((int) (MAIN_FRAME.getWidth() * 0.25), 0)); + + //配置窗口 + JPanel configPanel = new JPanel(new VFlowLayout()); + configPanel.setBorder(new TitledBorder("程序配置")); + + //IP 配置 + Box IPPortBox = Box.createHorizontalBox(); + IPPortBox.add(new JLabel("监听 IP:")); + IPTextField.putClientProperty("JTextField.showClearButton", true); + IPPortBox.add(IPTextField); + //端口配置 + SpinnerNumberModel portSpinnerModel = new SpinnerNumberModel(8080,1,65535,1); + portSpinner.setModel(portSpinnerModel); + JSpinner.NumberEditor portSpinnerEditor = new JSpinner.NumberEditor(portSpinner, "#"); + portSpinner.setEditor(portSpinnerEditor); + IPPortBox.add(new JLabel(" 端口:")); + IPPortBox.add(portSpinner); + + configPanel.add(IPPortBox); + + //资源文件夹 + Box mainDirBox = Box.createHorizontalBox(); + JLabel mainDirLabel = new JLabel("资源文件夹:"); + mainDirTextField.putClientProperty("JTextField.showClearButton", true); + mainDirTextField.setToolTipText(""" + 仅支持程序当前目录下的文件夹或子文件夹,请勿输入其他文件夹。 + 默认为 /res , 也可输入其他文件夹, 如 /resources、/content、/.minecraft 等."""); + mainDirBox.add(mainDirLabel); + mainDirBox.add(mainDirTextField); + configPanel.add(mainDirBox); + + /* + SSL 证书框,仅用于显示。实际操作为右方按钮。 + */ + Box JksSslBox = Box.createHorizontalBox(); + JksSslBox.add(new JLabel("JKS 证书文件:")); + JksSslTextField.setEditable(false); + JButton selectJksSslFile = new JButton("..."); + + selectJksSslFile.addActionListener(e -> { + JFileChooser fileChooser = new JFileChooser("."); + fileChooser.setFileFilter(new FileUtil.SimpleFileFilter(new String[]{"jks"}, null,"JKS 证书 (*.jks)")); + + if (fileChooser.showDialog(littleServerPanel, "载入证书") == JFileChooser.APPROVE_OPTION) { + File JKS = fileChooser.getSelectedFile(); + if (JKS != null && JKS.exists()) { + config.setJksFilePath(JKS.getPath()); + JksSslTextField.setText(JKS.getName()); + logger.info("已载入证书 " + JKS.getName()); + } + } + }); + + JksSslBox.add(JksSslTextField); + JksSslBox.add(selectJksSslFile); + + Box JksSslPassBox = Box.createHorizontalBox(); + JksSslPassBox.add(new JLabel("JKS 证书密码:")); + JksSslPassBox.add(JksSslPassField); + configPanel.add(JksSslBox); + configPanel.add(JksSslPassBox); + + //额外功能 + JPanel extraFeaturesPanel = new JPanel(new BorderLayout()); + //实时文件监听 + fileChangeListener.setToolTipText(""" + 开启后,启动服务器的同时会启动文件监听服务. + 文件监听服务会每隔 5 - 7 秒会监听资源文件夹的变化,如果资源一有变化会立即重新生成资源缓存. + 注意:不推荐在超大文件夹(10000 文件/文件夹 以上)上使用此功能,可能会造成 I/O 卡顿."""); + fileChangeListener.setSelected(true); + extraFeaturesPanel.add(fileChangeListener,BorderLayout.WEST); + configPanel.add(extraFeaturesPanel); + + /* + 普通更新模式和补全更新模式的 List 变动是实时监听的,无需重载配置文件。 + */ + //普通更新模式 + JPanel commonModePanel = new JPanel(new VFlowLayout()); + commonModePanel.setBorder(new TitledBorder("普通更新模式")); + commonMode.setVisibleRowCount(6); + JScrollPane common_ModeScrollPane = new JScrollPane( + commonMode, + JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + + commonModePanel.add(common_ModeScrollPane); + configPanel.add(commonModePanel); + + //菜单 + JPopupMenu commonModeMenu = new JPopupMenu(); + JMenuItem openCommonModeRuleEditor = new JMenuItem("打开更新规则编辑器"); + openCommonModeRuleEditor.setIcon(EDIT_ICON); + //普通更新规则编辑器 + openCommonModeRuleEditor.addActionListener(new RuleEditorActionListener(commonMode, commonModeList)); + commonModeMenu.add(openCommonModeRuleEditor); + commonModeMenu.addSeparator(); + //添加更新规则 + JMenuItem addNewCommonRule = new JMenuItem("添加更新规则"); + addNewCommonRule.setIcon(PLUS_ICON); + addNewCommonRule.addActionListener(new AddUpdateRule(commonMode,commonModeList, MAIN_FRAME)); + commonModeMenu.add(addNewCommonRule); + //删除指定规则 + JMenuItem deleteCommonRule = new JMenuItem("删除选定的规则"); + deleteCommonRule.setIcon(REMOVE_ICON); + deleteCommonRule.addActionListener(new DeleteUpdateRule(commonMode,commonModeList, MAIN_FRAME)); + commonModeMenu.add(deleteCommonRule); + //鼠标监听 + commonMode.addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()){ + commonModeMenu.show(commonMode,e.getX(),e.getY()); + } + } + }); + + //补全更新模式 + JPanel onceModePanel = new JPanel(new VFlowLayout()); + onceModePanel.setBorder(new TitledBorder("补全更新模式")); + onceMode.setVisibleRowCount(6); + JScrollPane once_ModeScrollPane = new JScrollPane( + onceMode, + JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + + onceModePanel.add(once_ModeScrollPane); + + //菜单 + JPopupMenu onceModeMenu = new JPopupMenu(); + JMenuItem openOnceModeRuleEditor = new JMenuItem("打开更新规则编辑器"); + openOnceModeRuleEditor.setIcon(EDIT_ICON); + //补全更新规则编辑器 + openOnceModeRuleEditor.addActionListener(new RuleEditorActionListener(onceMode, onceModeList)); + onceModeMenu.add(openOnceModeRuleEditor); + onceModeMenu.addSeparator(); + //添加更新规则 + JMenuItem addNewOnceRule = new JMenuItem("添加更新规则"); + addNewOnceRule.setIcon(PLUS_ICON); + addNewOnceRule.addActionListener(new AddUpdateRule(onceMode,onceModeList, MAIN_FRAME)); + onceModeMenu.add(addNewOnceRule); + //删除指定规则 + JMenuItem deleteOnceRule = new JMenuItem("删除选定的规则"); + deleteOnceRule.setIcon(REMOVE_ICON); + deleteOnceRule.addActionListener(new DeleteUpdateRule(onceMode,onceModeList, MAIN_FRAME)); + //鼠标监听 + onceMode.addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()){ + onceModeMenu.show(onceMode,e.getX(),e.getY()); + } + } + }); + onceModeMenu.add(deleteOnceRule); + configPanel.add(onceModePanel); + + //载入配置文件并初始化 HTTP 服务端 + loadConfigurationFromFile(); + loadHttpServer(); + + JPanel southControlPanel = new JPanel(new VFlowLayout()); + JLabel tipLabel = new JLabel("上方配置修改后,请点击保存配置按钮来载入配置."); + tipLabel.setForeground(new Color(255,75,75)); + southControlPanel.add(tipLabel); + + //存储当前配置 + JButton saveConfigButton = new JButton("保存配置"); + saveConfigButton.setToolTipText("以控制面板当前的配置应用到服务器,并保存配置到磁盘."); + southControlPanel.add(saveConfigButton); + saveConfigButton.addActionListener(e -> saveConfigurationToFile()); + + //重新生成资源文件夹缓存 + JButton regenDirectoryStructureCache = new JButton("重新生成资源文件夹缓存"); + southControlPanel.add(regenDirectoryStructureCache); + regenDirectoryStructureCache.addActionListener(e -> { + if (isGenerating.get()) { + JOptionPane.showMessageDialog(MAIN_FRAME, + "当前正在生成资源缓存,请稍后再试。","注意", + JOptionPane.WARNING_MESSAGE); + return; + } + regenResCache(); + }); + + //启动/关闭 服务器 + startOrStop.addActionListener(e -> { + if (isGenerating.get()) { + JOptionPane.showMessageDialog(MAIN_FRAME, + "当前正在生成资源缓存,请稍后再试。","注意", + JOptionPane.WARNING_MESSAGE); + return; + } + //如果启动则关闭服务器 + if (!isStarted.get()) { + saveConfigurationToFile(); + + startServer(); + } else { + stopServer(); + } + }); + southControlPanel.add(startOrStop); + + //上传列表 + JScrollPane requestListScrollPane = new JScrollPane( + requestListPanel, + JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + requestListScrollPane.getVerticalScrollBar().setUnitIncrement(50); + requestListScrollPane.setBorder(new TitledBorder("上传列表")); + requestListScrollPane.setPreferredSize(new Dimension((int) (MAIN_FRAME.getWidth() * 0.19), littleServerPanel.getHeight())); + //组装控制面板 + controlPanel.add(configPanel); + controlPanel.add(southControlPanel, BorderLayout.SOUTH); + //服务器窗口组装 + //日志面板和控制面板的组件 + JSplitPane logAndControlSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + logAndControlSplitPane.setOneTouchExpandable(true); + logAndControlSplitPane.setLeftComponent(logPanel); + //控制面板和上传列表的组件 + JSplitPane controlSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + controlSplitPane.setOneTouchExpandable(true); + controlSplitPane.setLeftComponent(controlPanel); + controlSplitPane.setRightComponent(requestListScrollPane); + //组装控制面板和上传列表 + logAndControlSplitPane.setRightComponent(controlSplitPane); + littleServerPanel.add(logAndControlSplitPane, BorderLayout.CENTER); + + logger.debug(String.format("载入服务器耗时 %sms.", System.currentTimeMillis() - start)); + + if (autoStart) { + logger.info("检测到自动启动服务器选项已开启,正在启动服务器..."); + startServer(); + } + } + + //更新规则编辑器类 + protected class RuleEditorActionListener implements ActionListener { + protected final JList ruleList; + protected final List rules; + public RuleEditorActionListener(JList ruleList, List rules) { + this.ruleList = ruleList; + this.rules = rules; + } + protected void showRuleEditorDialog(JSONArray jsonArray) { + //锁定窗口,防止用户误操作 + MAIN_FRAME.setEnabled(false); + RuleEditor editorDialog = new RuleEditor(jsonArray, config.getMainDirPath().replace("/","")); + editorDialog.setModal(true); + + MAIN_FRAME.setEnabled(true); + editorDialog.setVisible(true); + + if (!editorDialog.getResult().isEmpty()) { + rules.addAll(editorDialog.getResult()); + ruleList.setListData(rules.toArray(new String[0])); + } + } + + @Override + public void actionPerformed(ActionEvent e) { + if (isGenerating.get()) { + JOptionPane.showMessageDialog(MAIN_FRAME, + "当前正在生成资源缓存,请稍后再试。","注意", + JOptionPane.WARNING_MESSAGE); + return; + } + + if (new File(String.format("./%s.%s.json", serverName, resJsonFileExtensionName)).exists()) { + int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, + "检测到本地 JSON 缓存,是否以 JSON 缓存启动规则编辑器?", + "已检测到本地 JSON 缓存", JOptionPane.YES_NO_OPTION); + if (!(selection == JOptionPane.YES_OPTION)) return; + + try { + String json = FileUtil.readStringFromFile(new File(String.format("./%s.%s.json", serverName, resJsonFileExtensionName))); + showRuleEditorDialog(JSONArray.parseArray(json)); + } catch (IOException ex) { + JOptionPane.showMessageDialog(MAIN_FRAME, + "无法读取本地 JSON 缓存" + ex,"错误", + JOptionPane.ERROR_MESSAGE); + } + return; + } + + int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, + "未检测到 JSON 缓存,是否立即生成 JSON 缓存并启动规则编辑器?", + "未检测到 JSON 缓存", JOptionPane.YES_NO_OPTION); + if (!(selection == JOptionPane.YES_OPTION)) return; + + GLOBAL_THREAD_POOL.execute(() -> { + new CacheUtils(serverInterface,httpServerInterface,startOrStop).updateDirCache(null); + if (new File(String.format("./%s.%s.json", serverName, resJsonFileExtensionName)).exists()) { + try { + String json = FileUtil.readStringFromFile(new File(String.format("./%s.%s.json", serverName, resJsonFileExtensionName))); + showRuleEditorDialog(JSONArray.parseArray(json)); + + } catch (IOException ex) { + JOptionPane.showMessageDialog(MAIN_FRAME, + "无法读取本地 JSON 缓存" + ex, "错误", + JOptionPane.ERROR_MESSAGE); + } + } + }); + } + } + + /** + * 启动服务器 + */ + protected void startServer() { + GLOBAL_THREAD_POOL.execute(() -> { + File jsonCache = new File(String.format("./%s.%s.json", serverName, resJsonFileExtensionName)); + + CacheUtils cacheUtil = new CacheUtils(serverInterface, httpServerInterface, startOrStop); + if (jsonCache.exists()) { + try { + String jsonString = FileUtil.readStringFromFile(jsonCache); + JSONArray jsonArray = JSONArray.parseArray(jsonString); + GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCacheAndStartServer(jsonArray)); + } catch (Exception ex) { + logger.error("读取缓存文件的时候出现了一些问题...", ex); + logger.warn("缓存文件读取失败, 重新生成缓存..."); + GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCacheAndStartServer(null)); + } + } else { + GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCacheAndStartServer(null)); + } + }); + } + + /** + * 初始化 HTTP 服务器 + */ + protected void loadHttpServer() { + serverInterface = new LittleServerInterface() { + @Override + public GUILogger getLogger() { + return logger; + } + + @Override + public LittleServerConfig getConfig() { + return config; + } + + @Override + public JPanel getRequestListPanel() { + return requestListPanel; + } + + @Override + public AtomicBoolean isGenerating() { + return isGenerating; + } + + @Override + public AtomicBoolean isStarted() { + return isStarted; + } + + @Override + public String getResJson() { + return resJson; + } + + @Override + public String getServerName() { + return serverName; + } + + @Override + public String getResJsonFileExtensionName() { + return resJsonFileExtensionName; + } + + @Override + public void setResJson(String newResJson) { + resJson = newResJson; + } + + @Override + public void regenCache() { + regenResCache(); + } + + @Override + public boolean stopServer() { + return AbstractLittleServer.this.stopServer(); + } + + @Override + public void saveConfig() { + saveConfigurationToFile(); + } + + @Override + public String getHashAlgorithm() { + return hashAlgorithm; + } + }; + server = new HttpServer(serverInterface); + httpServerInterface = () -> server.start(); + } + + /** + * 重新生成缓存 + */ + protected void regenResCache() { + GLOBAL_THREAD_POOL.execute(() -> { + File jsonCache = new File(String.format("./%s.%s.json", serverName, resJsonFileExtensionName)); + CacheUtils cacheUtil = new CacheUtils(serverInterface, httpServerInterface, startOrStop); + if (jsonCache.exists()) { + try { + String jsonString = FileUtil.readStringFromFile(jsonCache); + JSONArray jsonArray = JSONArray.parseArray(jsonString); + cacheUtil.updateDirCache(jsonArray); + } catch (Exception ex) { + logger.error("读取缓存文件的时候出现了一些问题...", ex); + logger.warn("缓存文件读取失败, 重新生成缓存..."); + cacheUtil.updateDirCache(null); + } + } else { + cacheUtil.updateDirCache(null); + } + }); + } + + /** + * 关闭服务器 + * @return 是否关闭成功 + */ + protected boolean stopServer() { + try { + server.stop(); + isStarted.set(false); + startOrStop.setText("重载配置并启动服务器"); + return true; + } catch (Exception ex) { + logger.error("无法正常关闭服务器", ex); + return false; + } + } + + /** + * 保存配置文件至磁盘 + */ + protected void saveConfigurationToFile() { + reloadConfigurationFromGUI(); + try { + ConfigurationManager.saveConfigurationToFile(config,"./",serverName + ".lscfg"); + logger.info("已保存配置至磁盘."); + } catch (IOException ex) { + logger.error("保存配置文件的时候出现了问题...", ex); + } + } + + /** + * 从文件加载配置文件 + */ + protected void loadConfigurationFromFile() { + if (!new File("./" + serverName + ".lscfg.json").exists()) { + try { + logger.warn("未找到配置文件,正在尝试在程序当前目录生成配置文件..."); + ConfigurationManager.saveConfigurationToFile(new LittleServerConfig(), "./", String.format("%s.lscfg", serverName)); + logger.info("配置生成成功."); + logger.info("目前正在使用程序默认配置."); + } catch (Exception e) { + logger.error("生成配置文件的时候出现了问题...", e); + logger.info("目前正在使用程序默认配置."); + } + return; + } + try { + ConfigurationManager.loadLittleServerConfigFromFile("./" + serverName + ".lscfg.json", config); + } catch (IOException e) { + logger.error("加载配置文件的时候出现了问题...", e); + logger.info("目前正在使用程序默认配置."); + return; + } + //IP + IPTextField.setText(config.getIp()); + //端口 + portSpinner.setValue(config.getPort()); + //资源文件夹 + mainDirTextField.setText(config.getMainDirPath()); + //实时文件监听器 + fileChangeListener.setSelected(config.isFileChangeListener()); + //Jks 证书 + if (!config.getJksFilePath().isEmpty()) { + File JksSsl = new File(config.getJksFilePath()); + if (JksSsl.exists()) { + JksSslTextField.setText(JksSsl.getName()); + } else { + logger.warn("JKS 证书文件不存在."); + } + } + //Jks 证书密码 + JksSslPassField.setText(config.getJksSslPassword()); + //普通模式 + commonModeList.clear(); + commonModeList.addAll(Arrays.asList(config.getCommonMode())); + commonMode.setListData(config.getCommonMode()); + //补全模式 + onceModeList.clear(); + onceModeList.addAll(Arrays.asList(config.getOnceMode())); + onceMode.setListData(config.getOnceMode()); + + logger.info("已载入配置文件."); + } + + /** + * 以面板当前配置重载配置 + */ + protected void reloadConfigurationFromGUI() { + //设置端口 + config.setPort((Integer) portSpinner.getValue()); + //设置 IP + String IP = IPTextField.getText(); + String IPType = IPAddressUtil.checkAddress(IP); + if (IPType != null) { + if (IP.contains("0.0.0.0")) { + config.setIp("127.0.0.1"); + } else if ("v4".equals(IPType) || "v6".equals(IPType)) { + config.setIp(IP); + } + } else { + config.setIp("127.0.0.1"); + logger.warn("配置中的 IP 格式错误,使用默认 IP 地址 127.0.0.1"); + } + //设置资源目录 + config.setMainDirPath(mainDirTextField.getText()); + //设置实时文件监听器 + config.setFileChangeListener(fileChangeListener.isSelected()); + //设置 Jks 证书密码 + config.setJksSslPassword(String.valueOf(JksSslPassField.getPassword())); + //设置普通模式 + config.setCommonMode(commonModeList.toArray(new String[0])); + //设置补全模式 + config.setOnceMode(onceModeList.toArray(new String[0])); + logger.info("已加载配置."); + } +} diff --git a/src/main/java/github/kasuminova/balloonserver/Servers/LegacyLittleServer.java b/src/main/java/github/kasuminova/balloonserver/Servers/LegacyLittleServer.java new file mode 100644 index 0000000..72164d9 --- /dev/null +++ b/src/main/java/github/kasuminova/balloonserver/Servers/LegacyLittleServer.java @@ -0,0 +1,86 @@ +package github.kasuminova.balloonserver.Servers; + +import github.kasuminova.balloonserver.Configurations.ConfigurationManager; +import github.kasuminova.balloonserver.Configurations.LittleServerConfig; +import github.kasuminova.balloonserver.Utils.HashCalculator; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +/** + * 旧版 LittleServer 面板实例,支持多实例化,兼容 4.x.x - 4.1.14 版本的客户端 + * @author Kasumi_Nova + */ +public class LegacyLittleServer extends AbstractLittleServer { + public LegacyLittleServer(String serverName, boolean autoStart) { + super(serverName, autoStart); + this.resJsonFileExtensionName = "legacy_res-cache"; + this.hashAlgorithm = HashCalculator.SHA1; + } + + @Override + protected void saveConfigurationToFile() { + reloadConfigurationFromGUI(); + try { + ConfigurationManager.saveConfigurationToFile(config,"./",serverName + ".legacy.lscfg"); + logger.info("已保存配置至磁盘."); + } catch (IOException ex) { + logger.error("保存配置文件的时候出现了问题...", ex); + } + } + + /** + * 从文件加载配置文件 + */ + protected void loadConfigurationFromFile() { + if (!new File("./" + serverName + ".legacy.lscfg.json").exists()) { + try { + logger.warn("未找到配置文件,正在尝试在程序当前目录生成配置文件..."); + ConfigurationManager.saveConfigurationToFile(new LittleServerConfig(), "./", String.format("%s.legacy.lscfg", serverName)); + logger.info("配置生成成功."); + logger.info("目前正在使用程序默认配置."); + } catch (Exception e) { + logger.error("生成配置文件的时候出现了问题...", e); + logger.info("目前正在使用程序默认配置."); + } + return; + } + try { + ConfigurationManager.loadLittleServerConfigFromFile("./" + serverName + ".legacy.lscfg.json", config); + } catch (IOException e) { + logger.error("加载配置文件的时候出现了问题...", e); + logger.info("目前正在使用程序默认配置."); + return; + } + //IP + IPTextField.setText(config.getIp()); + //端口 + portSpinner.setValue(config.getPort()); + //资源文件夹 + mainDirTextField.setText(config.getMainDirPath()); + //实时文件监听器 + fileChangeListener.setSelected(config.isFileChangeListener()); + //Jks 证书 + if (!config.getJksFilePath().isEmpty()) { + File JksSsl = new File(config.getJksFilePath()); + if (JksSsl.exists()) { + JksSslTextField.setText(JksSsl.getName()); + } else { + logger.warn("JKS 证书文件不存在."); + } + } + //Jks 证书密码 + JksSslPassField.setText(config.getJksSslPassword()); + //普通模式 + commonModeList.clear(); + commonModeList.addAll(Arrays.asList(config.getCommonMode())); + commonMode.setListData(config.getCommonMode()); + //补全模式 + onceModeList.clear(); + onceModeList.addAll(Arrays.asList(config.getOnceMode())); + onceMode.setListData(config.getOnceMode()); + + logger.info("已载入配置文件."); + } +} diff --git a/src/main/java/github/kasuminova/balloonserver/Servers/LittleServer.java b/src/main/java/github/kasuminova/balloonserver/Servers/LittleServer.java index 6ebd622..18a55d3 100644 --- a/src/main/java/github/kasuminova/balloonserver/Servers/LittleServer.java +++ b/src/main/java/github/kasuminova/balloonserver/Servers/LittleServer.java @@ -1,690 +1,14 @@ package github.kasuminova.balloonserver.Servers; -import com.alibaba.fastjson2.JSONArray; -import github.kasuminova.balloonserver.Configurations.ConfigurationManager; -import github.kasuminova.balloonserver.Configurations.LittleServerConfig; -import github.kasuminova.balloonserver.GUI.RuleEditor; -import github.kasuminova.balloonserver.GUI.LayoutManager.VFlowLayout; -import github.kasuminova.balloonserver.HTTPServer.HttpServer; -import github.kasuminova.balloonserver.HTTPServer.HttpServerInterface; -import github.kasuminova.balloonserver.Utils.*; - -import javax.swing.*; -import javax.swing.border.TitledBorder; -import javax.swing.text.BadLocationException; - -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import static github.kasuminova.balloonserver.BalloonServer.*; -import static github.kasuminova.balloonserver.Utils.SvgIcons.*; +import github.kasuminova.balloonserver.Utils.HashCalculator; /** - * LittleServer 面板实例,支持多实例化 + * LittleServer 面板实例,支持多实例化,兼容 4.1.14 以上的客户端 * @author Kasumi_Nova */ -public class LittleServer { - private HttpServer server; - private String resJson; - private final String serverName; - private final GUILogger logger; - private final LittleServerConfig config = new LittleServerConfig(); - private final List commonModeList = new ArrayList<>(); - private final List onceModeList = new ArrayList<>(); - //服务器启动状态 - private final AtomicBoolean isStarted = new AtomicBoolean(false); - //服务端是否在生成缓存,防止同一时间多个线程生成缓存导致程序混乱 - private final AtomicBoolean isGenerating = new AtomicBoolean(false); - private final JPanel requestListPanel = new JPanel(new VFlowLayout()); - private final JPanel littleServerPanel = new JPanel(new BorderLayout()); - private final JButton startOrStop = new JButton("保存配置并启动服务器"); - //IP 输入框 - private final JTextField IPTextField = new JTextField("0.0.0.0"); - //端口输入框 - private final JSpinner portSpinner = new JSpinner(); - //Jks 证书密码 - private final JPasswordField JksSslPassField = new JPasswordField(); - //资源文件夹输入框 - private final JTextField mainDirTextField = new JTextField("/res"); - //实时文件监听 - private final JCheckBox fileChangeListener = new JCheckBox("启用实时文件监听"); - //证书文件名(不可编辑) - private final JTextField JksSslTextField = new JTextField("请选择证书文件"); - //普通模式 - private final JList commonMode = new JList<>(); - //补全模式 - private final JList onceMode = new JList<>(); - private LittleServerInterface serverInterface; - private HttpServerInterface httpServerInterface; - - /** - * 返回当前实例的面板 - * @return 服务器面板 - */ - public JPanel getPanel() { - return littleServerPanel; - } - - /** - * 返回当前服务器实例的接口 - * @return LittleServerInterface - */ - public LittleServerInterface getServerInterface() { - return serverInterface; - } - - /** - * 返回当前服务器实例的 HTTP 服务器接口 - * @return HttpServerInterface - */ - public HttpServerInterface getHttpServerInterface() { - return httpServerInterface; - } - /** - * 创建一个服务器面板,并绑定一个新的服务器实例 - * @param serverName LittleServerConfig 配置文件路径 - * @param autoStart 是否在创建对象后自动启动服务器 - */ +public class LittleServer extends AbstractLittleServer { public LittleServer(String serverName, boolean autoStart) { - this.serverName = serverName; - long start = System.currentTimeMillis(); - //设置 Logger,主体为 logPanel - JPanel logPanel = new JPanel(new BorderLayout()); - logPanel.setMinimumSize(new Dimension((int) (MAIN_FRAME.getWidth() * 0.525), 0)); - - logPanel.setBorder(new TitledBorder("服务器实例日志")); - JTextPane logPane = new JTextPane(); - logPane.setEditable(false); - JScrollPane logScrollPane = new JScrollPane(logPane); - logPanel.add(logScrollPane, BorderLayout.CENTER); - - //初始化 Logger - logger = new GUILogger(serverName, logPane); - - //日志窗口菜单 - JPopupMenu logPaneMenu = new JPopupMenu(); - JMenuItem cleanLogPane = new JMenuItem("清空日志", DELETE_ICON); - cleanLogPane.addActionListener(e -> { - try { - logPane.getDocument().remove(0,logPane.getDocument().getLength()); - logger.info("已清空当前服务器实例日志."); - } catch (BadLocationException ex) { - ex.printStackTrace(); - } - }); - logPaneMenu.add(cleanLogPane); - logPane.addMouseListener(new MouseAdapter() { - @Override - public void mouseReleased(MouseEvent e) { - if (e.isPopupTrigger()) { - logPaneMenu.show(logPane, e.getX(), e.getY()); - } - } - }); - - //控制面板 - JPanel controlPanel = new JPanel(new BorderLayout()); - controlPanel.setBorder(new TitledBorder("控制面板")); - controlPanel.setMinimumSize(new Dimension((int) (MAIN_FRAME.getWidth() * 0.25), 0)); - - //配置窗口 - JPanel configPanel = new JPanel(new VFlowLayout()); - configPanel.setBorder(new TitledBorder("程序配置")); - - //IP 配置 - Box IPPortBox = Box.createHorizontalBox(); - IPPortBox.add(new JLabel("监听 IP:")); - IPTextField.putClientProperty("JTextField.showClearButton", true); - IPPortBox.add(IPTextField); - //端口配置 - SpinnerNumberModel portSpinnerModel = new SpinnerNumberModel(8080,1,65535,1); - portSpinner.setModel(portSpinnerModel); - JSpinner.NumberEditor portSpinnerEditor = new JSpinner.NumberEditor(portSpinner, "#"); - portSpinner.setEditor(portSpinnerEditor); - IPPortBox.add(new JLabel(" 端口:")); - IPPortBox.add(portSpinner); - - configPanel.add(IPPortBox); - - //资源文件夹 - Box mainDirBox = Box.createHorizontalBox(); - JLabel mainDirLabel = new JLabel("资源文件夹:"); - mainDirTextField.putClientProperty("JTextField.showClearButton", true); - mainDirTextField.setToolTipText(""" - 仅支持程序当前目录下的文件夹或子文件夹,请勿输入其他文件夹。 - 默认为 /res , 也可输入其他文件夹, 如 /resources、/content、/.minecraft 等."""); - mainDirBox.add(mainDirLabel); - mainDirBox.add(mainDirTextField); - configPanel.add(mainDirBox); - - /* - SSL 证书框,仅用于显示。实际操作为右方按钮。 - */ - Box JksSslBox = Box.createHorizontalBox(); - JksSslBox.add(new JLabel("JKS 证书文件:")); - JksSslTextField.setEditable(false); - JButton selectJksSslFile = new JButton("..."); - - selectJksSslFile.addActionListener(e -> { - JFileChooser fileChooser = new JFileChooser("."); - fileChooser.setFileFilter(new FileUtil.SimpleFileFilter(new String[]{"jks"}, null,"JKS 证书 (*.jks)")); - - if (fileChooser.showDialog(littleServerPanel, "载入证书") == JFileChooser.APPROVE_OPTION) { - File JKS = fileChooser.getSelectedFile(); - if (JKS != null && JKS.exists()) { - config.setJksFilePath(JKS.getPath()); - JksSslTextField.setText(JKS.getName()); - logger.info("已载入证书 " + JKS.getName()); - } - } - }); - - JksSslBox.add(JksSslTextField); - JksSslBox.add(selectJksSslFile); - - Box JksSslPassBox = Box.createHorizontalBox(); - JksSslPassBox.add(new JLabel("JKS 证书密码:")); - JksSslPassBox.add(JksSslPassField); - configPanel.add(JksSslBox); - configPanel.add(JksSslPassBox); - - //额外功能 - JPanel extraFeaturesPanel = new JPanel(new BorderLayout()); - //实时文件监听 - fileChangeListener.setToolTipText(""" - 开启后,启动服务器的同时会启动文件监听服务. - 文件监听服务会每隔 5 - 7 秒会监听资源文件夹的变化,如果资源一有变化会立即重新生成资源缓存. - 注意:不推荐在超大文件夹(10000 文件/文件夹 以上)上使用此功能,可能会造成 I/O 卡顿."""); - fileChangeListener.setSelected(true); - extraFeaturesPanel.add(fileChangeListener,BorderLayout.WEST); - configPanel.add(extraFeaturesPanel); - - /* - 普通更新模式和补全更新模式的 List 变动是实时监听的,无需重载配置文件。 - */ - //普通更新模式 - JPanel commonModePanel = new JPanel(new VFlowLayout()); - commonModePanel.setBorder(new TitledBorder("普通更新模式")); - commonMode.setVisibleRowCount(6); - JScrollPane common_ModeScrollPane = new JScrollPane( - commonMode, - JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, - JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - - commonModePanel.add(common_ModeScrollPane); - configPanel.add(commonModePanel); - - //菜单 - JPopupMenu commonModeMenu = new JPopupMenu(); - JMenuItem openCommonModeRuleEditor = new JMenuItem("打开更新规则编辑器"); - openCommonModeRuleEditor.setIcon(EDIT_ICON); - //普通更新规则编辑器 - openCommonModeRuleEditor.addActionListener(new RuleEditorActionListener(commonMode, commonModeList)); - commonModeMenu.add(openCommonModeRuleEditor); - commonModeMenu.addSeparator(); - //添加更新规则 - JMenuItem addNewCommonRule = new JMenuItem("添加更新规则"); - addNewCommonRule.setIcon(PLUS_ICON); - addNewCommonRule.addActionListener(new AddUpdateRule(commonMode,commonModeList, MAIN_FRAME)); - commonModeMenu.add(addNewCommonRule); - //删除指定规则 - JMenuItem deleteCommonRule = new JMenuItem("删除选定的规则"); - deleteCommonRule.setIcon(REMOVE_ICON); - deleteCommonRule.addActionListener(new DeleteUpdateRule(commonMode,commonModeList, MAIN_FRAME)); - commonModeMenu.add(deleteCommonRule); - //鼠标监听 - commonMode.addMouseListener(new MouseAdapter() { - @Override - public void mouseReleased(MouseEvent e) { - if (e.isPopupTrigger()){ - commonModeMenu.show(commonMode,e.getX(),e.getY()); - } - } - }); - - //补全更新模式 - JPanel onceModePanel = new JPanel(new VFlowLayout()); - onceModePanel.setBorder(new TitledBorder("补全更新模式")); - onceMode.setVisibleRowCount(6); - JScrollPane once_ModeScrollPane = new JScrollPane( - onceMode, - JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, - JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - - onceModePanel.add(once_ModeScrollPane); - - //菜单 - JPopupMenu onceModeMenu = new JPopupMenu(); - JMenuItem openOnceModeRuleEditor = new JMenuItem("打开更新规则编辑器"); - openOnceModeRuleEditor.setIcon(EDIT_ICON); - //补全更新规则编辑器 - openOnceModeRuleEditor.addActionListener(new RuleEditorActionListener(onceMode, onceModeList)); - onceModeMenu.add(openOnceModeRuleEditor); - onceModeMenu.addSeparator(); - //添加更新规则 - JMenuItem addNewOnceRule = new JMenuItem("添加更新规则"); - addNewOnceRule.setIcon(PLUS_ICON); - addNewOnceRule.addActionListener(new AddUpdateRule(onceMode,onceModeList, MAIN_FRAME)); - onceModeMenu.add(addNewOnceRule); - //删除指定规则 - JMenuItem deleteOnceRule = new JMenuItem("删除选定的规则"); - deleteOnceRule.setIcon(REMOVE_ICON); - deleteOnceRule.addActionListener(new DeleteUpdateRule(onceMode,onceModeList, MAIN_FRAME)); - //鼠标监听 - onceMode.addMouseListener(new MouseAdapter() { - @Override - public void mouseReleased(MouseEvent e) { - if (e.isPopupTrigger()){ - onceModeMenu.show(onceMode,e.getX(),e.getY()); - } - } - }); - onceModeMenu.add(deleteOnceRule); - configPanel.add(onceModePanel); - - //载入配置文件并初始化 HTTP 服务端 - loadConfigurationFromFile(String.format("./%s.lscfg.json", serverName)); - loadHttpServer(); - - JPanel southControlPanel = new JPanel(new VFlowLayout()); - JLabel tipLabel = new JLabel("上方配置修改后,请点击保存配置按钮来载入配置."); - tipLabel.setForeground(new Color(255,75,75)); - southControlPanel.add(tipLabel); - - //存储当前配置 - JButton saveConfigButton = new JButton("保存配置"); - saveConfigButton.setToolTipText("以控制面板当前的配置应用到服务器,并保存配置到磁盘."); - southControlPanel.add(saveConfigButton); - saveConfigButton.addActionListener(e -> { - reloadConfigurationFromGUI(); - try { - ConfigurationManager.saveConfigurationToFile(config,"./",serverName); - logger.info("已保存配置至磁盘."); - } catch (IOException ex) { - logger.error("保存配置文件的时候出现了问题...", ex); - } - }); - - //重新生成资源文件夹缓存 - JButton regenDirectoryStructureCache = new JButton("重新生成资源文件夹缓存"); - southControlPanel.add(regenDirectoryStructureCache); - regenDirectoryStructureCache.addActionListener(e -> { - if (isGenerating.get()) { - JOptionPane.showMessageDialog(MAIN_FRAME, - "当前正在生成资源缓存,请稍后再试。","注意", - JOptionPane.WARNING_MESSAGE); - return; - } - File jsonCache = new File(String.format("./%s.res-cache.json", serverName)); - CacheUtils cacheUtil = new CacheUtils(serverInterface, httpServerInterface, startOrStop); - if (jsonCache.exists()) { - try { - String jsonString = FileUtil.readStringFromFile(jsonCache); - JSONArray jsonArray = JSONArray.parseArray(jsonString); - GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCache(jsonArray)); - } catch (Exception ex) { - logger.error("读取缓存文件的时候出现了一些问题...", ex); - logger.warn("缓存文件读取失败, 重新生成缓存..."); - GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCache(null)); - } - } else { - GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCache(null)); - } - }); - - //启动/关闭 服务器 - startOrStop.addActionListener(e -> { - if (isGenerating.get()) { - JOptionPane.showMessageDialog(MAIN_FRAME, - "当前正在生成资源缓存,请稍后再试。","注意", - JOptionPane.WARNING_MESSAGE); - return; - } - //如果启动则关闭服务器 - if (!isStarted.get()) { - reloadConfigurationFromGUI(); - //保存配置文件 - try { - ConfigurationManager.saveConfigurationToFile(config,"./",serverName); - logger.info("已保存配置至磁盘."); - } catch (IOException ex) { - logger.error("保存配置文件的时候出现了问题...", ex); - } - - startServer(); - } else { - stopServer(); - } - }); - southControlPanel.add(startOrStop); - - //上传列表 - JScrollPane requestListScrollPane = new JScrollPane( - requestListPanel, - JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, - JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - requestListScrollPane.getVerticalScrollBar().setUnitIncrement(50); - requestListScrollPane.setBorder(new TitledBorder("上传列表")); - requestListScrollPane.setPreferredSize(new Dimension((int) (MAIN_FRAME.getWidth() * 0.19), littleServerPanel.getHeight())); - //组装控制面板 - controlPanel.add(configPanel); - controlPanel.add(southControlPanel, BorderLayout.SOUTH); - //服务器窗口组装 - //日志面板和控制面板的组件 - JSplitPane logAndControlSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); - logAndControlSplitPane.setOneTouchExpandable(true); - logAndControlSplitPane.setLeftComponent(logPanel); - //控制面板和上传列表的组件 - JSplitPane controlSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); - controlSplitPane.setOneTouchExpandable(true); - controlSplitPane.setLeftComponent(controlPanel); - controlSplitPane.setRightComponent(requestListScrollPane); - //组装控制面板和上传列表 - logAndControlSplitPane.setRightComponent(controlSplitPane); - littleServerPanel.add(logAndControlSplitPane, BorderLayout.CENTER); - - logger.debug(String.format("载入服务器耗时 %sms.", System.currentTimeMillis() - start)); - - if (autoStart) { - logger.info("检测到自动启动服务器选项已开启,正在启动服务器..."); - startServer(); - } - } - - //更新规则编辑器类 - private class RuleEditorActionListener implements ActionListener { - private final JList ruleList; - private final List rules; - public RuleEditorActionListener(JList ruleList, List rules) { - this.ruleList = ruleList; - this.rules = rules; - } - private void showRuleEditorDialog(JSONArray jsonArray) { - //锁定窗口,防止用户误操作 - MAIN_FRAME.setEnabled(false); - RuleEditor editorDialog = new RuleEditor(jsonArray, config.getMainDirPath().replace("/","")); - editorDialog.setModal(true); - - MAIN_FRAME.setEnabled(true); - editorDialog.setVisible(true); - - if (!editorDialog.getResult().isEmpty()) { - rules.addAll(editorDialog.getResult()); - ruleList.setListData(rules.toArray(new String[0])); - } - } - - @Override - public void actionPerformed(ActionEvent e) { - if (isGenerating.get()) { - JOptionPane.showMessageDialog(MAIN_FRAME, - "当前正在生成资源缓存,请稍后再试。","注意", - JOptionPane.WARNING_MESSAGE); - return; - } - - if (new File(String.format("./%s.res-cache.json", serverName)).exists()) { - int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, - "检测到本地 JSON 缓存,是否以 JSON 缓存启动规则编辑器?", - "已检测到本地 JSON 缓存", JOptionPane.YES_NO_OPTION); - if (!(selection == JOptionPane.YES_OPTION)) return; - - try { - String json = FileUtil.readStringFromFile(new File(String.format("./%s.res-cache.json", serverName))); - showRuleEditorDialog(JSONArray.parseArray(json)); - } catch (IOException ex) { - JOptionPane.showMessageDialog(MAIN_FRAME, - "无法读取本地 JSON 缓存" + ex,"错误", - JOptionPane.ERROR_MESSAGE); - } - return; - } - - int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, - "未检测到 JSON 缓存,是否立即生成 JSON 缓存并启动规则编辑器?", - "未检测到 JSON 缓存", JOptionPane.YES_NO_OPTION); - if (!(selection == JOptionPane.YES_OPTION)) return; - - GLOBAL_THREAD_POOL.execute(() -> { - new CacheUtils(serverInterface,httpServerInterface,startOrStop).updateDirCache(null); - if (new File(String.format("./%s.res-cache.json", serverName)).exists()) { - try { - String json = FileUtil.readStringFromFile(new File(String.format("./%s.res-cache.json", serverName))); - showRuleEditorDialog(JSONArray.parseArray(json)); - - } catch (IOException ex) { - JOptionPane.showMessageDialog(MAIN_FRAME, - "无法读取本地 JSON 缓存" + ex, "错误", - JOptionPane.ERROR_MESSAGE); - } - } - }); - } - } - - /** - * 启动服务器 - */ - private void startServer() { - GLOBAL_THREAD_POOL.execute(() -> { - File jsonCache = new File(String.format("./%s.res-cache.json", serverName)); - - CacheUtils cacheUtil = new CacheUtils(serverInterface, httpServerInterface, startOrStop); - if (jsonCache.exists()) { - try { - String jsonString = FileUtil.readStringFromFile(jsonCache); - JSONArray jsonArray = JSONArray.parseArray(jsonString); - GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCacheAndStartServer(jsonArray)); - } catch (Exception ex) { - logger.error("读取缓存文件的时候出现了一些问题...", ex); - logger.warn("缓存文件读取失败, 重新生成缓存..."); - GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCacheAndStartServer(null)); - } - } else { - GLOBAL_THREAD_POOL.execute(() -> cacheUtil.updateDirCacheAndStartServer(null)); - } - }); - } - /** - * 初始化 HTTP 服务器 - */ - private void loadHttpServer() { - serverInterface = new LittleServerInterface() { - @Override - public GUILogger getLogger() { - return logger; - } - - @Override - public LittleServerConfig getConfig() { - return config; - } - - @Override - public JPanel getRequestListPanel() { - return requestListPanel; - } - - @Override - public AtomicBoolean isGenerating() { - return isGenerating; - } - - @Override - public AtomicBoolean isStarted() { - return isStarted; - } - - @Override - public String getResJson() { - return resJson; - } - - @Override - public String getServerName() { - return serverName; - } - - @Override - public void setResJson(String newResJson) { - resJson = newResJson; - } - - @Override - public void regenCache() { - regenResCache(); - } - - @Override - public boolean stopServer() { - return LittleServer.this.stopServer(); - } - - @Override - public void saveConfig() { - reloadConfigurationFromGUI(); - //保存配置文件 - try { - ConfigurationManager.saveConfigurationToFile(config,"./",serverName); - logger.info("已保存配置至磁盘."); - } catch (IOException ex) { - logger.error("保存配置文件的时候出现了问题...", ex); - } - } - }; - server = new HttpServer(serverInterface); - httpServerInterface = () -> server.start(); - } - /** - * 重新生成缓存 - */ - private void regenResCache() { - File jsonCache = new File(String.format("./%s.res-cache.json", serverName)); - CacheUtils cacheUtil = new CacheUtils(serverInterface,httpServerInterface,startOrStop); - if (jsonCache.exists()) { - try { - String jsonString = FileUtil.readStringFromFile(jsonCache); - JSONArray jsonArray = JSONArray.parseArray(jsonString); - cacheUtil.updateDirCache(jsonArray); - } catch (Exception ex) { - logger.error("读取缓存文件的时候出现了一些问题...", ex); - logger.warn("缓存文件读取失败, 重新生成缓存..."); - cacheUtil.updateDirCache(null); - } - } else { - cacheUtil.updateDirCache(null); - } - } - - /** - * 关闭服务器 - * @return 是否关闭成功 - */ - private boolean stopServer() { - try { - server.stop(); - isStarted.set(false); - startOrStop.setText("重载配置并启动服务器"); - return true; - } catch (Exception ex) { - logger.error("无法正常关闭服务器", ex); - return false; - } - } - - /** - * 从文件加载配置文件 - * @param configFilePath 配置文件路径 - */ - private void loadConfigurationFromFile(String configFilePath) { - if (!new File(configFilePath).exists()) { - try { - logger.warn("未找到配置文件,正在尝试在程序当前目录生成配置文件..."); - ConfigurationManager.saveConfigurationToFile(new LittleServerConfig(), "./", String.format("%s.lscfg", serverName)); - logger.info("配置生成成功."); - logger.info("目前正在使用程序默认配置."); - } catch (Exception e) { - logger.error("生成配置文件的时候出现了问题...", e); - logger.info("目前正在使用程序默认配置."); - } - return; - } - try { - ConfigurationManager.loadLittleServerConfigFromFile(configFilePath, config); - } catch (IOException e) { - logger.error("加载配置文件的时候出现了问题...", e); - logger.info("目前正在使用程序默认配置."); - return; - } - //IP - IPTextField.setText(config.getIp()); - //端口 - portSpinner.setValue(config.getPort()); - //资源文件夹 - mainDirTextField.setText(config.getMainDirPath()); - //实时文件监听器 - fileChangeListener.setSelected(config.isFileChangeListener()); - //Jks 证书 - if (!config.getJksFilePath().isEmpty()) { - File JksSsl = new File(config.getJksFilePath()); - if (JksSsl.exists()) { - JksSslTextField.setText(JksSsl.getName()); - } else { - logger.warn("JKS 证书文件不存在."); - } - } - //Jks 证书密码 - JksSslPassField.setText(config.getJksSslPassword()); - //普通模式 - commonModeList.clear(); - commonModeList.addAll(Arrays.asList(config.getCommonMode())); - commonMode.setListData(config.getCommonMode()); - //补全模式 - onceModeList.clear(); - onceModeList.addAll(Arrays.asList(config.getOnceMode())); - onceMode.setListData(config.getOnceMode()); - - logger.info("已载入配置文件."); - } - - /** - * 以面板当前配置重载配置 - */ - private void reloadConfigurationFromGUI() { - //设置端口 - config.setPort((Integer) portSpinner.getValue()); - //设置 IP - String IP = IPTextField.getText(); - String IPType = IPAddressUtil.checkAddress(IP); - if (IPType != null) { - if (IP.contains("0.0.0.0")) { - config.setIp("0.0.0.0"); - } else if ("v4".equals(IPType) || "v6".equals(IPType)) { - config.setIp(IP); - } - } else { - config.setIp("0.0.0.0"); - logger.warn("配置中的 IP 格式错误,使用默认 IP 地址 0.0.0.0"); - } - //设置资源目录 - config.setMainDirPath(mainDirTextField.getText()); - //设置实时文件监听器 - config.setFileChangeListener(fileChangeListener.isSelected()); - //设置 Jks 证书密码 - config.setJksSslPassword(String.valueOf(JksSslPassField.getPassword())); - //设置普通模式 - config.setCommonMode(commonModeList.toArray(new String[0])); - //设置补全模式 - config.setOnceMode(onceModeList.toArray(new String[0])); - logger.info("已加载配置."); + super(serverName, autoStart); + this.hashAlgorithm = HashCalculator.CRC32; } } diff --git a/src/main/java/github/kasuminova/balloonserver/Servers/LittleServerInterface.java b/src/main/java/github/kasuminova/balloonserver/Servers/LittleServerInterface.java index 42872e6..22fbe16 100644 --- a/src/main/java/github/kasuminova/balloonserver/Servers/LittleServerInterface.java +++ b/src/main/java/github/kasuminova/balloonserver/Servers/LittleServerInterface.java @@ -19,8 +19,10 @@ public interface LittleServerInterface { //获取文件结构 JSON String getResJson(); String getServerName(); + String getResJsonFileExtensionName(); //设置新的文件结构 JSON void setResJson(String newResJson); + /** * 重新生成缓存 */ @@ -31,8 +33,14 @@ public interface LittleServerInterface { * @return 是否关闭成功 */ boolean stopServer(); + /** * 保存配置 */ void saveConfig(); + + /** + * 获取 Hash 算法 + */ + String getHashAlgorithm(); } diff --git a/src/main/java/github/kasuminova/balloonserver/UpdateChecker/Checker.java b/src/main/java/github/kasuminova/balloonserver/UpdateChecker/Checker.java index 7fb1324..2807a5b 100644 --- a/src/main/java/github/kasuminova/balloonserver/UpdateChecker/Checker.java +++ b/src/main/java/github/kasuminova/balloonserver/UpdateChecker/Checker.java @@ -3,38 +3,145 @@ import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import github.kasuminova.balloonserver.BalloonServer; +import github.kasuminova.balloonserver.Utils.GUILogger; + +import javax.swing.*; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import static github.kasuminova.balloonserver.BalloonServer.*; +import static github.kasuminova.balloonserver.UpdateChecker.HttpClient.downloadFileWithURL; public class Checker { - public static void main(String[] args) { + /** + * 从 Gitee 仓库获取最新 Release 信息 + */ + public static void checkUpdates() { String apiURL = "https://gitee.com/api/v5/repos/hikari_nova/BalloonServer/releases"; String giteeAPIJson = HttpClient.getStringWithURL(apiURL); if (giteeAPIJson.startsWith("ERROR:")) { return; } JSONArray jsonArray = JSONArray.parseArray(giteeAPIJson); - JSONObject newRelease = jsonArray.getJSONObject(0); - ApplicationVersion newVersion = new ApplicationVersion(newRelease.getString("tag_name")); + JSONObject latestRelease = jsonArray.getJSONObject(0); + ApplicationVersion newVersion = new ApplicationVersion(latestRelease.getString("tag_name")); ApplicationVersion applicationVersion = BalloonServer.VERSION; - //大版本更新检查 - if (applicationVersion.getBigVersion() < newVersion.getBigVersion()) { - //业务代码... - System.out.println("Has a BigVersion Update"); - - //子版本更新检查 - } else if (applicationVersion.getSubVersion() < newVersion.getSubVersion()) { - System.out.println("Has a SubVersion Update"); - //业务代码... - - //小版本更新检查 - } else if (applicationVersion.getMinorVersion() < newVersion.getMinorVersion()) { - System.out.println("Has a MinorVersion Update"); - //业务代码... + + //如果当前版本高于仓库最新版本则忽略更新 + if (applicationVersion.getBigVersion() > newVersion.getBigVersion() || + applicationVersion.getSubVersion() > newVersion.getSubVersion() || + applicationVersion.getMinorVersion() > newVersion.getMinorVersion()) + { + GLOBAL_LOGGER.info("当前版本为最新版本."); + return; + } + + //版本更新检查 + if (applicationVersion.getBigVersion() < newVersion.getBigVersion() || + applicationVersion.getSubVersion() < newVersion.getSubVersion() || + applicationVersion.getMinorVersion() < newVersion.getMinorVersion()) + { + if (CONFIG.isAutoUpdate() && ARCHIVE_NAME.contains("e4j") && ARCHIVE_NAME.contains("Temp")) { + String fileName = downloadUpdate(latestRelease, false); + if (fileName != null) startProgram(fileName, true); + return; + } + int operation = JOptionPane.showConfirmDialog(MAIN_FRAME, + String.format("检测到有版本更新 (%s),您要下载更新吗?", newVersion), + "更新提示", + JOptionPane.YES_NO_OPTION); + + if (operation != JOptionPane.YES_NO_OPTION) return; + + downloadUpdate(latestRelease, true); } else { - System.out.println("No Update"); + GLOBAL_LOGGER.info("当前版本为最新版本."); } + } + + /** + * 启动指定名称的程序 + * @param fileName 程序名 + */ + public static void startProgram(String fileName, boolean exitThisProgram) { + try { + Runtime.getRuntime().exec(String.format(".\\%s", fileName)); + if (exitThisProgram) { + //如果主服务端正在运行,则打开自动启动服务器(仅一次)选项并保存,下次启动服务端时自动启动服务器 + if (availableCustomServerInterfaces.get(0).isStarted().get()) { + CONFIG.setAutoStartServerOnce(true); + BalloonServer.saveConfig(); + } + //停止所有正在运行的服务器并保存配置 + BalloonServer.stopAllServers(false); + System.exit(0); + } + } catch (Exception e) { + JOptionPane.showMessageDialog(MAIN_FRAME, + String.format("无法启动服务端。\n%s", GUILogger.stackTraceToString(e)),"错误", + JOptionPane.ERROR_MESSAGE); + } + } - System.out.println(BalloonServer.VERSION); - System.out.println(newVersion); + /** + * 下载最新的程序版本,返回下载完成后的文件名 + * @param latestRelease Release API 的 JSON 信息 + * @param showCompleteDialog 完成下载后是否弹出完成对话框 + * @return 文件名, 如果无匹配服务端或下载失败返回 null + */ + private static String downloadUpdate(JSONObject latestRelease, boolean showCompleteDialog) { + JSONArray assets = latestRelease.getJSONArray("assets"); + assets.remove(assets.size() - 1); + + if (ARCHIVE_NAME.contains("e4j")) { + for (int i = 0; i < assets.size(); i++) { + JSONObject asset = assets.getJSONObject(i); + String fileName = asset.getString("name"); + if (fileName.endsWith(".exe")) { + try { + //下载文件 + downloadFileWithURL( + new URL(asset.getString("browser_download_url")), + new File(String.format("./%s", fileName))); + if (showCompleteDialog) { + JOptionPane.showMessageDialog(MAIN_FRAME, + String.format("程序下载完成!已保存至当前程序路径(%s)。", fileName), + "完成", + JOptionPane.INFORMATION_MESSAGE); + } + return fileName; + } catch (IOException e) { + GLOBAL_LOGGER.warning(String.format("下载更新失败\n%s", GUILogger.stackTraceToString(e))); + } + } + } + return null; + } + + for (int i = 0; i < assets.size(); i++) { + JSONObject asset = assets.getJSONObject(i); + String fileName = asset.getString("name"); + if (fileName.endsWith(".jar")) { + try { + //下载文件 + downloadFileWithURL( + new URL(asset.getString("browser_download_url")), + new File(String.format("./%s", fileName))); + if (showCompleteDialog) { + JOptionPane.showMessageDialog(MAIN_FRAME, + String.format("程序下载完成!已保存至当前程序路径(%s)。", fileName), + "完成", + JOptionPane.INFORMATION_MESSAGE); + } + return fileName; + } catch (IOException e) { + GLOBAL_LOGGER.warning(String.format("下载更新失败,可能是因为用户取消了操作或无法连接至服务器.\n%s", GUILogger.stackTraceToString(e))); + } + } + } + return null; } } diff --git a/src/main/java/github/kasuminova/balloonserver/UpdateChecker/HttpClient.java b/src/main/java/github/kasuminova/balloonserver/UpdateChecker/HttpClient.java index b02773a..48bab03 100644 --- a/src/main/java/github/kasuminova/balloonserver/UpdateChecker/HttpClient.java +++ b/src/main/java/github/kasuminova/balloonserver/UpdateChecker/HttpClient.java @@ -1,5 +1,10 @@ package github.kasuminova.balloonserver.UpdateChecker; +import github.kasuminova.balloonserver.GUI.LayoutManager.VFlowLayout; +import github.kasuminova.balloonserver.Utils.FileUtil; + +import javax.swing.*; +import java.awt.*; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; @@ -9,16 +14,6 @@ import java.nio.file.StandardOpenOption; public class HttpClient { - public static void main(String[] args) { - String url = "https://gitee.com/hikari_nova/BalloonServer/releases/download/1.1.3-BETA/BalloonServer-GUI-1.1.3-BETA.jar"; - - try { - downloadFileWithURL(new URL(url), new File("./BalloonServer-GUI-1.1.3-BETA.jar")); - } catch (IOException e) { - e.printStackTrace(); - } - } - /** * 从指定 URL 中获取 String 字符串 * @param urlStr 链接 @@ -27,7 +22,7 @@ public static void main(String[] args) { public static String getStringWithURL(String urlStr) { try { URL url = new URL(urlStr); - String SUBMIT_METHOD_GET = "GET"; // 一定要是大写,否则请求无效 + String SUBMIT_METHOD_GET = "GET"; HttpURLConnection connection; InputStream is = null; @@ -79,25 +74,80 @@ public static String getStringWithURL(String urlStr) { } } - private static void downloadFileWithURL(URL url, File file) throws IOException{ - if (!file.exists() && !file.createNewFile()) { + public static void downloadFileWithURL(URL url, File file) throws IOException { + if (file.exists()) { + if (!file.delete()) { + throw new IOException("Could Not Delete File " + file.getPath()); + } + } + if (!file.createNewFile()){ throw new IOException("Could Not Create File " + file.getPath()); } + + JFrame downloadingFrame = new JFrame("下载更新中"); + downloadingFrame.setSize(new Dimension(350,145)); + downloadingFrame.setResizable(false); + + JPanel mainPanel = new JPanel(new VFlowLayout(0,10,5,true,false)); + mainPanel.add(new JLabel("进度:")); + + JProgressBar progressBar = new JProgressBar(); + progressBar.setValue(progressBar.getMaximum()); + progressBar.setIndeterminate(true); + progressBar.setStringPainted(true); + progressBar.setString("连接中..."); + + mainPanel.add(progressBar); + mainPanel.add(new JLabel("在此期间你可以继续使用程序。")); + JButton cancel = new JButton("取消下载"); + mainPanel.add(cancel); + + downloadingFrame.add(mainPanel); + downloadingFrame.setLocationRelativeTo(null); + downloadingFrame.setVisible(true); + BufferedInputStream bis = new BufferedInputStream(url.openStream()); FileChannel foc = FileChannel.open(file.toPath(), StandardOpenOption.WRITE); ByteBuffer byteBuffer = ByteBuffer.allocate(4096); - long total = 0; + //[0] 为已完成进度,[1] 为速度缓存进度 + final long[] total = {0,1}; + //定时更新查询器 + Timer timer = new Timer(250, e -> { + progressBar.setString(String.format("已完成: %s - 速度: %s/s", + FileUtil.formatFileSizeToStr(total[0]), + FileUtil.formatFileSizeToStr((total[0] - total[1]) * 4) + )); + total[1] = total[0]; + }); + timer.start(); + Thread currentThread = Thread.currentThread(); + cancel.addActionListener(e -> { + //中断线程后再完成其他停止操作 + currentThread.interrupt(); + + timer.stop(); + progressBar.setString("停止中..."); + try { + bis.close(); + foc.close(); + } catch (IOException ignored) {} + downloadingFrame.dispose(); + }); + int count; while((count = bis.read(byteBuffer.array(),0,4096)) != -1) { - foc.write(byteBuffer, total); - total += count; + foc.write(byteBuffer, total[0]); + total[0] += count; byteBuffer.flip(); byteBuffer.clear(); } - bis.close(); foc.close(); + + //关闭定时器和窗口 + timer.stop(); + downloadingFrame.dispose(); } } diff --git a/src/main/java/github/kasuminova/balloonserver/Utils/CacheUtils.java b/src/main/java/github/kasuminova/balloonserver/Utils/CacheUtils.java index 316caba..f859920 100644 --- a/src/main/java/github/kasuminova/balloonserver/Utils/CacheUtils.java +++ b/src/main/java/github/kasuminova/balloonserver/Utils/CacheUtils.java @@ -48,6 +48,8 @@ public CacheUtils(LittleServerInterface serverInterface, HttpServerInterface htt * 传入的参数如果为 null,则完整生成一次缓存 */ public void updateDirCache(JSONArray jsonCache) { + long start = System.currentTimeMillis(); + if (jsonCache != null) { if (!genDirCache(jsonCache)) { return; @@ -55,15 +57,16 @@ public void updateDirCache(JSONArray jsonCache) { try { //等待线程结束 counterThread.join(); - System.gc(); - logger.info("内存已完成回收."); timer.stop(); - logger.info("资源变化计算完毕, 正在向磁盘生成 JSON 缓存."); + logger.info(String.format("资源变化计算完毕, 正在向磁盘生成 JSON 缓存. (%sms)", System.currentTimeMillis() - start)); //输出并向服务器设置 JSON generateJsonToDiskAndSetServerJson(jsonArray); + System.gc(); + logger.info("内存已完成回收."); + //隐藏状态栏进度条 GLOBAL_STATUS_PROGRESSBAR.setVisible(false); //重置变量 @@ -75,17 +78,18 @@ public void updateDirCache(JSONArray jsonCache) { try { //等待线程结束 counterThread.join(); - System.gc(); - logger.info("内存已完成回收."); timer.stop(); - logger.info("资源目录缓存生成完毕, 正在向磁盘生成 JSON 缓存."); + logger.info(String.format("资源变化计算完毕, 正在向磁盘生成 JSON 缓存. (%sms)", System.currentTimeMillis() - start)); //输出并向服务器设置 JSON jsonArray.clear(); jsonArray.addAll(fileObjList); generateJsonToDiskAndSetServerJson(jsonArray); + System.gc(); + logger.info("内存已完成回收."); + //隐藏状态栏进度条 GLOBAL_STATUS_PROGRESSBAR.setVisible(false); //重置变量 @@ -182,7 +186,7 @@ private void generateJsonToDiskAndSetServerJson(JSONArray jsonArray) { String resJSONStr = jsonArray.toJSONString(); serverInterface.setResJson(resJSONStr); try { - FileUtil.createJsonFile(resJSONStr, "./", String.format("%s.res-cache", serverInterface.getServerName())); + FileUtil.createJsonFile(resJSONStr, "./", String.format("%s.%s", serverInterface.getServerName(), serverInterface.getResJsonFileExtensionName())); logger.info("JSON 缓存生成完毕."); } catch (IOException ex) { logger.error("生成 JSON 缓存的时候出现了问题...", ex); @@ -227,7 +231,7 @@ private boolean genDirCache(JSONArray jsonCache) { String totalSize = FileUtil.formatFileSizeToStr(dirSize[0]); - FileCacheCalculator fileCacheCalculator = new FileCacheCalculator(logger); + FileCacheCalculator fileCacheCalculator = new FileCacheCalculator(logger, serverInterface.getHashAlgorithm()); logger.info(String.format("文件夹大小:%s, 文件数量:%s", totalSize,dirSize[1])); if (jsonCache != null) { logger.info("检测到已缓存的 JSON, 正在检查变化..."); @@ -251,7 +255,7 @@ private boolean genDirCache(JSONArray jsonCache) { logger.info("正在生成资源目录缓存..."); File finalDir = dir; //新建资源计算器实例 - fileListUtils = new NextFileListUtils(); + fileListUtils = new NextFileListUtils(serverInterface.getHashAlgorithm()); //创建新线程实例并执行 counterThread = new Thread(() -> { fileObjList.clear(); diff --git a/src/main/java/github/kasuminova/balloonserver/Utils/FileCacheCalculator.java b/src/main/java/github/kasuminova/balloonserver/Utils/FileCacheCalculator.java index 0edfdff..91c13aa 100644 --- a/src/main/java/github/kasuminova/balloonserver/Utils/FileCacheCalculator.java +++ b/src/main/java/github/kasuminova/balloonserver/Utils/FileCacheCalculator.java @@ -7,29 +7,28 @@ import github.kasuminova.balloonserver.Utils.FileObject.SimpleFileObject; import java.io.File; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.security.MessageDigest; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.zip.CRC32; import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_THREAD_POOL; +import static github.kasuminova.balloonserver.Utils.HashCalculator.getCRC32; +import static github.kasuminova.balloonserver.Utils.HashCalculator.getSHA1; /** * 一个多线程计算文件缓存差异的工具类 * @author Kasumi_Nova */ public class FileCacheCalculator { - public FileCacheCalculator(GUILogger logger) { + public FileCacheCalculator(GUILogger logger, String hashAlgorithm) { this.logger = logger; + this.hashAlgorithm = hashAlgorithm; } private final ExecutorService FILE_THREAD_POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); private final GUILogger logger; + private final String hashAlgorithm; public final AtomicInteger completedFiles = new AtomicInteger(0); /** @@ -240,82 +239,20 @@ public SimpleDirectoryObject call() { * 创建一个线程, 计算文件信息 * 计算完毕后, 返回 SimpleFileObject */ - public static class FileCounterThread implements Callable { + public class FileCounterThread implements Callable { private final File file; public FileCounterThread(File file) { this.file = file; } @Override - public SimpleFileObject call() { - String hash = getSHA1(file); - return new SimpleFileObject(file.getName(),file.length(),hash,file.lastModified() / 1000); - } - } - - public static String getSHA1(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - int len; - MessageDigest md = MessageDigest.getInstance("SHA1"); - while ((len = fc.read(byteBuffer)) > 0) { - md.update(byteBuffer.array(), 0, len); - byteBuffer.flip(); - byteBuffer.clear(); - } - fc.close(); - //转换并返回包含 16 个元素字节数组,返回数值范围为 -128 到 127 - byte[] md5Bytes = md.digest(); - //1 代表绝对值 - BigInteger bigInt = new BigInteger(1, md5Bytes); - //转换为 16 进制 - return bigInt.toString(16); - } catch (Exception e) { - e.printStackTrace(); - } - return "ERROR"; - } - - public static String getMD5(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - int len; - MessageDigest md = MessageDigest.getInstance("MD5"); - while ((len = fc.read(byteBuffer)) > 0) { - md.update(byteBuffer.array(), 0, len); - byteBuffer.flip(); - byteBuffer.clear(); - } - fc.close(); - //转换并返回包含 16 个元素字节数组,返回数值范围为 -128 到 127 - byte[] md5Bytes = md.digest(); - //1 代表绝对值 - BigInteger bigInt = new BigInteger(1, md5Bytes); - //转换为 16 进制 - return bigInt.toString(16); - } catch (Exception e) { - e.printStackTrace(); - } - return "ERROR"; - } - - public static String getCRC32(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - int len; - CRC32 crc32 = new CRC32(); - while ((len = fc.read(byteBuffer)) > 0) { - crc32.update(byteBuffer.array(), 0, len); - byteBuffer.flip(); - byteBuffer.clear(); + public SimpleFileObject call() throws IOException, NoSuchAlgorithmException { + String hash; + if (hashAlgorithm.equals(HashCalculator.SHA1)) { + hash = getSHA1(file); + } else { + hash = getCRC32(file); } - fc.close(); - return String.valueOf(crc32.getValue()); - } catch (Exception e) { - e.printStackTrace(); + return new SimpleFileObject(file.getName(),file.length(),hash,file.lastModified() / 1000); } - return "ERROR"; } } \ No newline at end of file diff --git a/src/main/java/github/kasuminova/balloonserver/Utils/HashCalculator.java b/src/main/java/github/kasuminova/balloonserver/Utils/HashCalculator.java index cdad752..5b11100 100644 --- a/src/main/java/github/kasuminova/balloonserver/Utils/HashCalculator.java +++ b/src/main/java/github/kasuminova/balloonserver/Utils/HashCalculator.java @@ -1,88 +1,107 @@ package github.kasuminova.balloonserver.Utils; import java.io.File; +import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.atomic.AtomicLong; import java.util.zip.CRC32; /** * 公用 Hash 计算类 */ public class HashCalculator { + public static final String SHA1 = "sha1"; + public static final String CRC32 = "crc32"; + /** * 获取文件 SHA1 * BalloonUpdate 的默认方法 * @param file 目标文件 - * @return String + * @return SHA1 值 **/ - public static String getSHA1(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - int len; - MessageDigest md = MessageDigest.getInstance("SHA1"); - while ((len = fc.read(byteBuffer)) > 0) { - md.update(byteBuffer.array(), 0, len); - byteBuffer.flip(); - byteBuffer.clear(); - } - fc.close(); - //转换并返回包含 16 个元素字节数组,返回数值范围为 -128 到 127 - byte[] sha1Bytes = md.digest(); - //1 代表绝对值 - BigInteger bigInt = new BigInteger(1, sha1Bytes); - //转换为 16 进制 - return bigInt.toString(16); - } catch (Exception e) { - e.printStackTrace(); + public static String getSHA1(File file) throws IOException, NoSuchAlgorithmException { + FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); + ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); + int len; + MessageDigest md = MessageDigest.getInstance("SHA1"); + while ((len = fc.read(byteBuffer)) > 0) { + md.update(byteBuffer.array(), 0, len); + byteBuffer.clear(); } - return "ERROR"; + fc.close(); + + //转换为 16 进制 + return new BigInteger(1, md.digest()).toString(16); } - public static String getMD5(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - int len; - MessageDigest md = MessageDigest.getInstance("MD5"); - while ((len = fc.read(byteBuffer)) > 0) { - md.update(byteBuffer.array(), 0, len); - byteBuffer.flip(); - byteBuffer.clear(); - } - fc.close(); - //转换并返回包含 16 个元素字节数组,返回数值范围为 -128 到 127 - byte[] md5Bytes = md.digest(); - //1 代表绝对值 - BigInteger bigInt = new BigInteger(1, md5Bytes); - //转换为 16 进制 - return bigInt.toString(16); - } catch (Exception e) { - e.printStackTrace(); + /** + * 获取文件 CRC32 + * @param file 目标文件 + * @return CRC32 值 + */ + public static String getCRC32(File file) throws IOException { + FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); + ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); + int len; + CRC32 crc32 = new CRC32(); + while ((len = fc.read(byteBuffer)) > 0) { + crc32.update(byteBuffer.array(), 0, len); + byteBuffer.clear(); } - return "ERROR"; + fc.close(); + + //转换为 16 进制 + return Integer.toHexString((int) crc32.getValue()); } - public static String getCRC32(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - int len; - CRC32 crc32 = new CRC32(); - while ((len = fc.read(byteBuffer)) > 0) { - crc32.update(byteBuffer.array(), 0, len); - byteBuffer.flip(); - byteBuffer.clear(); - } - fc.close(); - return String.valueOf(crc32.getValue()); - } catch (Exception e) { - e.printStackTrace(); + /** + * 获取文件 SHA1 + * BalloonUpdate 的默认方法 + * @param file 目标文件 + * @param progress 进度变量 + * @return SHA1 值 + **/ + public static String getSHA1(File file, AtomicLong progress) throws IOException, NoSuchAlgorithmException { + FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); + ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); + int len; + MessageDigest md = MessageDigest.getInstance("SHA1"); + while ((len = fc.read(byteBuffer)) > 0) { + md.update(byteBuffer.array(), 0, len); + byteBuffer.clear(); + progress.getAndAdd(len); + } + fc.close(); + + //转换为 16 进制 + return new BigInteger(1, md.digest()).toString(16); + } + + /** + * 获取文件 CRC32 + * @param file 目标文件 + * @param progress 进度变量 + * @return CRC32 值 + */ + public static String getCRC32(File file, AtomicLong progress) throws IOException { + FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); + ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); + int len; + CRC32 crc32 = new CRC32(); + while ((len = fc.read(byteBuffer)) > 0) { + crc32.update(byteBuffer.array(), 0, len); + byteBuffer.clear(); + progress.getAndAdd(len); } - return "ERROR"; + fc.close(); + + //转换为 16 进制 + return Integer.toHexString((int) crc32.getValue()); } } \ No newline at end of file diff --git a/src/main/java/github/kasuminova/balloonserver/Utils/NextFileListUtils.java b/src/main/java/github/kasuminova/balloonserver/Utils/NextFileListUtils.java index 3550a30..5015d3d 100644 --- a/src/main/java/github/kasuminova/balloonserver/Utils/NextFileListUtils.java +++ b/src/main/java/github/kasuminova/balloonserver/Utils/NextFileListUtils.java @@ -1,17 +1,12 @@ package github.kasuminova.balloonserver.Utils; import java.io.File; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.security.MessageDigest; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import java.util.zip.CRC32; import github.kasuminova.balloonserver.Utils.FileObject.AbstractSimpleFileObject; import github.kasuminova.balloonserver.Utils.FileObject.SimpleDirectoryObject; @@ -20,13 +15,19 @@ import javax.swing.*; import static github.kasuminova.balloonserver.BalloonServer.*; +import static github.kasuminova.balloonserver.Utils.HashCalculator.getCRC32; +import static github.kasuminova.balloonserver.Utils.HashCalculator.getSHA1; /** * 计算资源缓存的公用类 */ public class NextFileListUtils { + public NextFileListUtils(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } private final AtomicLong completedBytes = new AtomicLong(0); private final AtomicInteger completedFiles = new AtomicInteger(0); + private final String hashAlgorithm; private final ExecutorService FILE_THREAD_POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); public long getCompletedBytes() { @@ -144,90 +145,22 @@ public FileCounterTask(File file) { this.file = file; } @Override - public SimpleFileObject call() { - String sha1 = getSHA1(file); + public SimpleFileObject call() throws IOException, NoSuchAlgorithmException { + String hash; + if (hashAlgorithm.equals(HashCalculator.SHA1)) { + hash = getSHA1(file, completedBytes); + } else { + hash = getCRC32(file, completedBytes); + } completedFiles.getAndIncrement(); return new SimpleFileObject( file.getName(), file.length(), - sha1, + hash, file.lastModified() / 1000); } } - private String getMD5(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - MessageDigest md = MessageDigest.getInstance("MD5"); - - int len; - while ((len = fc.read(byteBuffer)) > 0) { - md.update(byteBuffer.array(), 0, len); - completedBytes.getAndAdd(len); - byteBuffer.flip(); - byteBuffer.clear(); - } - fc.close(); - //转换并返回包含 16 个元素字节数组,返回数值范围为 -128 到 127 - byte[] md5Bytes = md.digest(); - //1 代表绝对值 - BigInteger bigInt = new BigInteger(1, md5Bytes); - //转换为 16 进制 - return bigInt.toString(16); - } catch (Exception e) { - e.printStackTrace(); - } - return "ERROR"; - } - - private String getSHA1(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - MessageDigest md = MessageDigest.getInstance("SHA1"); - - int len; - while ((len = fc.read(byteBuffer)) > 0) { - md.update(byteBuffer.array(), 0, len); - completedBytes.getAndAdd(len); - byteBuffer.flip(); - byteBuffer.clear(); - } - fc.close(); - //转换并返回包含 16 个元素字节数组,返回数值范围为 -128 到 127 - byte[] md5Bytes = md.digest(); - //1 代表绝对值 - BigInteger bigInt = new BigInteger(1, md5Bytes); - //转换为 16 进制 - return bigInt.toString(16); - } catch (Exception e) { - e.printStackTrace(); - } - return "ERROR"; - } - - private String getCRC32(File file) { - try { - FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); - ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); - CRC32 crc32 = new CRC32(); - - int len; - while ((len = fc.read(byteBuffer)) > 0) { - crc32.update(byteBuffer.array(), 0, len); - completedBytes.getAndAdd(len); - byteBuffer.flip(); - byteBuffer.clear(); - } - fc.close(); - return String.valueOf(crc32.getValue()); - } catch (Exception e) { - e.printStackTrace(); - } - return "ERROR"; - } - /** * 计算文件夹内容大小 */