├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── encodings.xml ├── gradle.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── jarRepositories.xml ├── kotlinc.xml ├── misc.xml ├── modules.xml ├── modules │ └── FileSyncUtils.test.iml ├── uiDesigner.xml └── vcs.xml ├── Docs.md ├── Docs └── 1.png ├── LICENSE ├── README.MD ├── build.gradle ├── exe4j_configuration.exe4j ├── filechangelistener.gif ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── main.gif ├── multiserver.gif ├── ruleeditor.gif ├── settings.gradle └── src └── main ├── java └── github │ └── kasuminova │ ├── balloonserver │ ├── BalloonServer.java │ ├── configurations │ │ ├── BalloonServerConfig.java │ │ ├── CloseOperation.java │ │ ├── Configuration.java │ │ ├── ConfigurationManager.java │ │ ├── IntegratedServerConfig.java │ │ └── RemoteClientConfig.java │ ├── gui │ │ ├── ConfirmExitDialog.java │ │ ├── SetupSwing.java │ │ ├── SmoothProgressBar.java │ │ ├── SwingSystemTray.java │ │ ├── checkboxtree │ │ │ ├── CheckBoxTreeCellRenderer.java │ │ │ ├── CheckBoxTreeLabel.java │ │ │ ├── CheckBoxTreeNode.java │ │ │ └── CheckBoxTreeNodeSelectionListener.java │ │ ├── fileobjectbrowser │ │ │ ├── FileObjectBrowser.java │ │ │ └── ImageListCellRenderer.java │ │ ├── layoutmanager │ │ │ └── VFlowLayout.java │ │ ├── panels │ │ │ ├── AboutPanel.java │ │ │ └── SettingsPanel.java │ │ └── ruleeditor │ │ │ ├── RuleEditor.java │ │ │ └── RuleEditorActionListener.java │ ├── httpserver │ │ ├── ContentRanges.java │ │ ├── DecodeProxy.java │ │ ├── HttpRequestHandler.java │ │ ├── HttpServer.java │ │ ├── HttpServerInitializer.java │ │ ├── HttpServerInterface.java │ │ └── SslContextFactoryOne.java │ ├── remoteclient │ │ ├── AbstractRemoteClientChannel.java │ │ ├── LastChannel.java │ │ ├── RemoteClient.java │ │ ├── RemoteClientChannel.java │ │ ├── RemoteClientFileChannel.java │ │ └── RemoteClientInitializer.java │ ├── servers │ │ ├── AbstractGUIServer.java │ │ ├── AbstractServer.java │ │ ├── GUIServerInterface.java │ │ ├── LogPaneMouseAdapter.java │ │ ├── ServerInterface.java │ │ ├── localserver │ │ │ ├── AddUpdateRule.java │ │ │ ├── DeleteUpdateRule.java │ │ │ ├── IntegratedServer.java │ │ │ ├── IntegratedServerInterface.java │ │ │ └── ShowOrHideComponentActionListener.java │ │ └── remoteserver │ │ │ ├── RemoteClientInterface.java │ │ │ └── RemoteIntegratedServerClient.java │ ├── updatechecker │ │ ├── ApplicationVersion.java │ │ ├── Checker.java │ │ └── HttpClient.java │ └── utils │ │ ├── BatchUtils.java │ │ ├── CustomThreadFactory.java │ │ ├── FileUtil.java │ │ ├── GUILogger.java │ │ ├── HashCalculator.java │ │ ├── HashStrings.java │ │ ├── IPAddressUtil.java │ │ ├── MiscUtils.java │ │ ├── ModernColors.java │ │ ├── NextFileListener.java │ │ ├── NextHashCalculator.java │ │ ├── Security.java │ │ ├── SvgIcons.java │ │ ├── filecacheutils │ │ ├── DirSizeCalculatorThread.java │ │ ├── FileCacheCalculator.java │ │ ├── JsonCacheCheckerTask.java │ │ └── JsonCacheUtils.java │ │ └── fileobject │ │ ├── AbstractSimpleFileObject.java │ │ ├── DirInfoTask.java │ │ ├── FileInfoTask.java │ │ ├── SimpleDirectoryObject.java │ │ └── SimpleFileObject.java │ └── messages │ ├── AbstractMessage.java │ ├── AuthSuccessMessage.java │ ├── FileListMessage.java │ ├── LogMessage.java │ ├── MessageProcessor.java │ ├── MethodMessage.java │ ├── RequestMessage.java │ ├── StatusMessage.java │ ├── TokenMessage.java │ ├── filemessages │ ├── FileInfoMsg.java │ ├── FileMessage.java │ ├── FileObjMessage.java │ └── FileRequestMsg.java │ └── processor │ └── MessageProcessor.java └── resources ├── font ├── HarmonyOS_Sans_SC+JetBrains_Mono.ttf ├── HarmonyOS_Sans_SC+Saira.ttf ├── HarmonyOS_Sans_SC_LICENSE.txt ├── JetBrains_Mono_OFL.txt └── Saira_OFL.txt ├── icons ├── custom_server.svg ├── default_server.svg ├── delete.svg ├── edit.svg ├── file_types │ ├── class.svg │ ├── dir.svg │ ├── doc_docx.svg │ ├── exe.svg │ ├── file_default.svg │ ├── jar.svg │ ├── java.svg │ ├── jpg.svg │ ├── json.svg │ ├── md.svg │ ├── ppt_pptx.svg │ ├── txt.svg │ ├── xls_xlsx.svg │ ├── xml.svg │ ├── yml.svg │ └── zip.svg ├── info.svg ├── play.svg ├── plus.svg ├── reload.svg ├── remove.svg ├── resource.svg ├── serverList.svg ├── settings.svg ├── stop.svg └── terminal.svg └── image ├── icon_16x16.ico ├── icon_16x16.png └── splash.png /.gitignore: -------------------------------------------------------------------------------- 1 | /src/test/ 2 | /res/ 3 | *.json 4 | /build 5 | /Test 6 | /java17-jre 7 | /out 8 | /.gradle 9 | /logs -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules/FileSyncUtils.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Docs.md: -------------------------------------------------------------------------------- 1 | # BalloonServer 服务端 Manual 2 | BalloonServer 是 LittleServer 的衍生图形化服务端,并且底层基于高性能的 Netty-IO,性能更佳。 3 | ## 优点 4 | - 完全可视化操作,上手简单便捷 5 | - 多实例化,可以同时载入多个服务器 6 | - 开箱即用,支持双击启动和 Shell 启动 7 | - 支持配置热重载 8 | - 支持 SSL 证书 9 | - 支持实时文件监听 10 | - 支持全自动更新服务端(仅 EXE 版本),实现全自动服务端维护 11 | - 支持最小化到任务栏托盘(需要系统支持) 12 | - 跨平台(Linux, Windows, MacOS) 13 | - 高性能多线程处理,最大化利用服务器资源,减少卡顿 14 | ## 下载 15 | 你可以在 [GitHub Release](https://github.com/BalloonUpdate/BalloonServer/releases) 或在我们的 [官方群聊](https://jq.qq.com/?_wv=1027&k=bhNBCnUQ) 内找到本软件的发行版。 16 | 17 | **注意:从 1.0.6-BETA 版本起,程序的最低 JAVA 版本要求提高到了 17。** 18 | 19 | 下载程序后,双击 JAR 或执行命令 `java -jar BalloonServer-1.x.x-BETA.jar` 即可启动程序。 20 | 21 | ## 窗口介绍 22 | ![](./Docs/1.png) 23 | 24 | - 最大的窗口为`服务器实例日志`窗口,这里将会输出服务器的相关日志。 25 | - `控制面板`是每个服务端实例的配置界面,这里将会是你后期最经常接触的面板。 26 | - `集成服务端`标签页是本程序的主服务端,而`旧版集成服务端`是为了兼容旧版客户端而生的服务端。 27 | - `上传列表`会展示出当前正常向客户端发送的文件列表和进度。 28 | - 最上方菜单栏为实例管理菜单,用于 创建/管理 自定义服务器实例,通常情况下,大部分用户只需要使用主服务端即可。 29 | ***提示:如果操作系统支持系统托盘,则关闭窗口的时候不会关闭程序,而是会最小化到任务栏。*** 30 | ***左击托盘图标即可打开程序,右击托盘图标可打开菜单以退出程序。*** 31 | 32 | ### 控制面板 33 | 控制面板将会是你后期最经常接触的面板,如果你不知道这些配置的含义,**请仔细阅读下方内容。** 34 | #### 监听 IP, 端口 35 | 点击 `重载配置并启动服务器` 按钮时,程序将会监听此 `IP` 指定的`端口`的的传入请求。**如果你不知道这些内容的含义,请不要动它。** 36 | #### 资源文件夹 37 | 程序将会 扫描/监听 的文件夹,默认为 `/res`,**如果无特殊需求,请不要动它。** 38 | - 资源文件夹是所有客户端的入口,以如果玩家访问除 `res.json` `index.json` `/res` 之外的路径,将会返回 403 错误。 39 | - 例如如果你需要更新客户端模组,请复制 Minecraft 客户端中所有的模组文件到 `res/.minecraft/mods/` 里(内部目录请自行创建),注意是所有文件。如果你要更新其它文件,同样按上面的方法,复制到 `/res` 目录里对应的路径的目录上(比如 `vexview` 的贴图复制到 `/res/.minecraft/vexview/textures/` 下,其它文件同理) 40 | #### JKS 证书文件 41 | 用于 `HTTPS` 验证所需要的文件,如果没有 `JKS 证书`,则服务器默认使用 `HTTP` 协议与客户端传输。 42 | - `JKS 证书`即为后缀名为 `.jks` 的文件,点击输入框右方的 `选择` 按钮即可选择证书,证书可以在任何路径 43 | #### JKS 证书密码 44 | 用于 `HTTPS` 验证所需要的文件,如果没有 `JKS 证书`,则服务器默认使用 `HTTP` 协议与客户端传输。 45 | 46 | - `JKS 证书密码`是用于验证完整性的密钥,如果没有它,即使拥有 `JKS 证书文件` 也无法正常使用 `HTTPS` 协议。 47 | 48 | #### 实时文件监听 49 | 此选项开启后,启动服务器的同时会启动文件监听服务。 50 | 51 | 文件监听服务会每隔 5 - 7 秒会统计一次资源文件夹的变化,如果资源一有变化就会**立即**重新生成资源缓存。 52 | 53 | 此功能使用最小化更新模式的方法生成缓存,并且**不需要**重启服务端。 54 | 55 | 适合在频繁变动文件的情况下使用此功能。 56 | 57 | #### 普通更新模式 补全更新模式 58 | `普通更新模式` :客户端从服务器获取信息时,在此列表内的匹配的 `文件 / 文件夹` **都将会被更新**,规则可以是`正则表达式`、`Glob 表达式` 59 | 60 | `补全更新模式` : 客户端从服务器获取信息时,只会在首次 `文件 / 文件夹` **不存在** 时会进行一次下载,如果后续文件存在,就会跳过更新**不会覆盖**已有内容。一般用来补全一些配置文件,规则可以是`正则表达式`、`Glob 表达式`。 61 | 62 | - 要新建一个更新规则,请在对应的列表内右击,然后在弹出的菜单内点击 `添加更新规则`,然后在弹出的对话框内输入更新规则。 63 | - 要删除一个更新规则,请先选中一个要删除的更新规则,然后右击,接着在弹出的菜单内点击 `删除更新规则`。 64 | - [一些更新规则示例](https://github.com/BalloonUpdate/Docs/blob/old-servers/server/reference.md) 65 | 66 | #### 重载配置 67 | 点击后,服务端会将当前的程序配置应用到程序内,但是不会应用到服务器内。 68 | 69 | #### 保存配置并重载 70 | 点击后,服务端会将当前的程序配置应用到程序内,并保存当前的配置文件至磁盘,但是不会应用到服务器内。 71 | 72 | #### 重新生成资源文件夹缓存 73 | 点击后,服务端会主动生成资源文件夹的缓存,并保存至磁盘。并且会重载服务器的资源文件夹缓存。 74 | ***即使服务器正在运行,程序也可以重载服务器的缓存*** 75 | 76 | #### 重载配置并启动服务器 77 | 点击后,服务端会将当前的程序配置应用到程序内,并应用到服务器内,然后生成资源文件夹缓存,最后启动服务器。如果启用了 `实时文件监听` 功能,程序还会启动实时文件监听器。 78 | 79 | #### 关闭服务器 80 | 点击后,服务端将会在完成最后任务后停止监听端口,如果启用了 `实时文件监听` 功能,程序也会关闭实时文件监听器。 81 | 82 | *Enjoy it~* 83 | -------------------------------------------------------------------------------- /Docs/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/Docs/1.png -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## BalloonServer 2 | BalloonServer 是 LittleServer 的完全图形化版本,基于 Netty-IO 的增强实现。 3 | 4 | [程序使用文档](./Docs.md) 5 | 6 | ### 程序运行实例: 7 | 8 | #### 主功能 9 | ![image](main.gif) 10 | 11 | #### 可视化更新规则编辑器 12 | ![image](ruleeditor.gif) 13 | 14 | #### 实时文件监听器 15 | ![image](filechangelistener.gif) 16 | 17 | #### 多服务端实例 18 | ![image](multiserver.gif) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'github.kasuminova' 6 | version '1.4.0-BETA' 7 | 8 | repositories { 9 | maven { 10 | url 'https://maven.aliyun.com/nexus/content/groups/public/' 11 | } 12 | maven { 13 | url 'https://maven.aliyun.com/repository/central' 14 | } 15 | mavenCentral() 16 | } 17 | 18 | jar { 19 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 20 | archivesBaseName = 'BalloonServer-GUI'//基本的文件名 21 | archiveVersion = version //版本 22 | manifest { //配置 Jar 文件的 Manifest 23 | attributes ( 24 | "Manifest-Version": 1.0, 25 | 'Main-Class': 'github.kasuminova.balloonserver.BalloonServer', //指定 Main 方法所在的文件 26 | 'SplashScreen-Image': 'image/splash.png' 27 | ) 28 | } 29 | 30 | //打包依赖包 31 | from { 32 | (configurations.runtimeClasspath).collect { 33 | it.isDirectory() ? it : zipTree(it) 34 | } 35 | } 36 | } 37 | 38 | dependencies { 39 | testImplementation 'org.projectlombok:lombok:1.18.24' 40 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' 41 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' 42 | 43 | implementation 'com.alibaba.fastjson2:fastjson2:2.0.19' 44 | 45 | implementation 'io.netty:netty-buffer:4.1.85.Final' 46 | implementation 'io.netty:netty-codec:4.1.85.Final' 47 | implementation 'io.netty:netty-codec-http:4.1.85.Final' 48 | implementation 'io.netty:netty-handler:4.1.85.Final' 49 | implementation 'io.netty:netty-transport:4.1.85.Final' 50 | 51 | implementation 'com.formdev:flatlaf:2.6' 52 | implementation 'com.formdev:flatlaf-extras:2.6' 53 | implementation 'com.formdev:flatlaf-intellij-themes:2.6' 54 | 55 | implementation 'cn.hutool:hutool-core:5.8.9' 56 | implementation 'cn.hutool:hutool-system:5.8.9' 57 | implementation 'cn.hutool:hutool-log:5.8.9' 58 | implementation 'cn.hutool:hutool-http:5.8.9' 59 | } 60 | 61 | tasks.withType(JavaCompile) { 62 | options.encoding = "UTF-8" 63 | // options.compilerArgs += "--enable-preview" 64 | } 65 | 66 | test { 67 | useJUnitPlatform() 68 | } -------------------------------------------------------------------------------- /exe4j_configuration.exe4j: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /filechangelistener.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/filechangelistener.gif -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-milestone-1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /main.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/main.gif -------------------------------------------------------------------------------- /multiserver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/multiserver.gif -------------------------------------------------------------------------------- /ruleeditor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/ruleeditor.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'BalloonServer' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/configurations/BalloonServerConfig.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.configurations; 2 | 3 | import com.alibaba.fastjson2.annotation.JSONField; 4 | 5 | public class BalloonServerConfig extends Configuration { 6 | public static final CloseOperation QUERY = new CloseOperation(0, "每次询问"); 7 | public static final CloseOperation HIDE_ON_CLOSE = new CloseOperation(1, "最小化至托盘"); 8 | public static final CloseOperation EXIT_ON_CLOSE = new CloseOperation(2, "退出程序"); 9 | /** 10 | * 自动启动服务器 11 | */ 12 | @JSONField(ordinal = 1) 13 | private boolean autoStartServer = false; 14 | /** 15 | * 自动启动服务器(仅一次) 16 | */ 17 | @JSONField(ordinal = 2) 18 | private boolean autoStartServerOnce = false; 19 | /** 20 | * 自动检查更新 21 | */ 22 | @JSONField(ordinal = 3) 23 | private boolean autoCheckUpdates = false; 24 | /** 25 | * 自动更新 26 | */ 27 | @JSONField(ordinal = 4) 28 | private boolean autoUpdate = false; 29 | /** 30 | * 关闭窗口的操作 31 | */ 32 | @JSONField(ordinal = 5) 33 | private int closeOperation = 0; 34 | /** 35 | * 单线程模式 36 | */ 37 | @JSONField(ordinal = 6) 38 | private boolean singleThreadMode = false; 39 | /** 40 | * 文件线程池大小 41 | */ 42 | @JSONField(ordinal = 7) 43 | private int fileThreadPoolSize = 0; 44 | /** 45 | * DEBUG 模式 46 | */ 47 | @JSONField(ordinal = 8) 48 | private boolean debugMode = false; 49 | 50 | public BalloonServerConfig() { 51 | configVersion = 0; 52 | } 53 | 54 | @Override 55 | public BalloonServerConfig setConfigVersion(int configVersion) { 56 | this.configVersion = configVersion; 57 | return this; 58 | } 59 | 60 | public boolean isDebugMode() { 61 | return debugMode; 62 | } 63 | 64 | public BalloonServerConfig setDebugMode(boolean debugMode) { 65 | this.debugMode = debugMode; 66 | return this; 67 | } 68 | 69 | public boolean isAutoStartServer() { 70 | return autoStartServer; 71 | } 72 | 73 | public BalloonServerConfig setAutoStartServer(boolean autoStartServer) { 74 | this.autoStartServer = autoStartServer; 75 | return this; 76 | } 77 | 78 | public boolean isAutoStartServerOnce() { 79 | return autoStartServerOnce; 80 | } 81 | 82 | public BalloonServerConfig setAutoStartServerOnce(boolean autoStartServerOnce) { 83 | this.autoStartServerOnce = autoStartServerOnce; 84 | return this; 85 | } 86 | 87 | public int getCloseOperation() { 88 | return closeOperation; 89 | } 90 | 91 | public BalloonServerConfig setCloseOperation(int closeOperation) { 92 | this.closeOperation = closeOperation; 93 | return this; 94 | } 95 | 96 | public boolean isAutoUpdate() { 97 | return autoUpdate; 98 | } 99 | 100 | public BalloonServerConfig setAutoUpdate(boolean autoUpdate) { 101 | this.autoUpdate = autoUpdate; 102 | return this; 103 | } 104 | 105 | public boolean isAutoCheckUpdates() { 106 | return autoCheckUpdates; 107 | } 108 | 109 | public BalloonServerConfig setAutoCheckUpdates(boolean autoCheckUpdates) { 110 | this.autoCheckUpdates = autoCheckUpdates; 111 | return this; 112 | } 113 | 114 | public boolean isSingleThreadMode() { 115 | return singleThreadMode; 116 | } 117 | 118 | public BalloonServerConfig setSingleThreadMode(boolean singleThreadMode) { 119 | this.singleThreadMode = singleThreadMode; 120 | return this; 121 | } 122 | 123 | public int getFileThreadPoolSize() { 124 | return fileThreadPoolSize; 125 | } 126 | 127 | public BalloonServerConfig setFileThreadPoolSize(int fileThreadPoolSize) { 128 | this.fileThreadPoolSize = fileThreadPoolSize; 129 | return this; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/configurations/CloseOperation.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.configurations; 2 | 3 | public class CloseOperation { 4 | private final int operation; 5 | private String desc; 6 | 7 | public CloseOperation(int operation, String desc) { 8 | this.operation = operation; 9 | this.desc = desc; 10 | } 11 | 12 | public int getOperation() { 13 | return operation; 14 | } 15 | 16 | public String getDesc() { 17 | return desc; 18 | } 19 | 20 | public void setDesc(String desc) { 21 | this.desc = desc; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return desc; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/configurations/Configuration.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.configurations; 2 | 3 | import com.alibaba.fastjson2.annotation.JSONField; 4 | 5 | /** 6 | * 配置文件抽象类 7 | */ 8 | abstract class Configuration { 9 | public static final int DEFAULT_PORT = 8080; 10 | 11 | @JSONField(ordinal = 100) 12 | public int configVersion; 13 | 14 | public int getConfigVersion() { 15 | return configVersion; 16 | } 17 | 18 | /** 19 | * 设置配置文件版本 20 | * @param configVersion 版本 21 | * @return Configuration 22 | */ 23 | public abstract Configuration setConfigVersion(int configVersion); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/configurations/ConfigurationManager.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.configurations; 2 | 3 | import cn.hutool.core.io.IORuntimeException; 4 | import com.alibaba.fastjson2.JSON; 5 | import com.alibaba.fastjson2.JSONObject; 6 | import com.alibaba.fastjson2.JSONWriter; 7 | import github.kasuminova.balloonserver.utils.FileUtil; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | 13 | /** 14 | * @author Kasumi_Nova 15 | */ 16 | public class ConfigurationManager { 17 | public static void loadLittleServerConfigFromFile(String path, IntegratedServerConfig oldConfig) throws IOException { 18 | IntegratedServerConfig newConfig = JSON.parseObject(Files.newInputStream(Paths.get(path)), IntegratedServerConfig.class); 19 | 20 | oldConfig.setConfigVersion(newConfig.getConfigVersion()) 21 | .setIp(newConfig.getIp()) 22 | .setPort(newConfig.getPort()) 23 | .setMainDirPath(newConfig.getMainDirPath()) 24 | .setFileChangeListener(newConfig.isFileChangeListener()) 25 | .setCompatibleMode(newConfig.isCompatibleMode()) 26 | .setJksFilePath(newConfig.getJksFilePath()) 27 | .setJksSslPassword(newConfig.getJksSslPassword()) 28 | .setCommonMode(newConfig.getCommonMode()) 29 | .setOnceMode(newConfig.getOnceMode()); 30 | } 31 | 32 | public static void loadBalloonServerConfigFromFile(String path, BalloonServerConfig oldConfig) throws IOException { 33 | BalloonServerConfig config = JSON.parseObject(Files.newInputStream(Paths.get(path)), BalloonServerConfig.class); 34 | oldConfig.setAutoStartServer(config.isAutoStartServer()) 35 | .setAutoStartServerOnce(config.isAutoStartServerOnce()) 36 | .setDebugMode(config.isDebugMode()) 37 | .setCloseOperation(config.getCloseOperation()) 38 | .setAutoCheckUpdates(config.isAutoCheckUpdates()) 39 | .setAutoUpdate(config.isAutoUpdate()) 40 | .setSingleThreadMode(config.isSingleThreadMode()) 41 | .setFileThreadPoolSize(config.getFileThreadPoolSize()); 42 | } 43 | 44 | public static void loadRemoteClientConfigFromFile(String path, RemoteClientConfig oldConfig) throws IOException { 45 | RemoteClientConfig config = JSON.parseObject(Files.newInputStream(Paths.get(path)), RemoteClientConfig.class); 46 | oldConfig.setToken(config.getToken()) 47 | .setIp(config.getIp()) 48 | .setPort(config.getPort()); 49 | } 50 | 51 | public static void saveConfigurationToFile(Configuration configuration, String path, String name) throws IORuntimeException { 52 | FileUtil.createJsonFile(JSONObject.toJSONString(configuration, JSONWriter.Feature.PrettyFormat), path, name); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/configurations/IntegratedServerConfig.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.configurations; 2 | 3 | import com.alibaba.fastjson2.annotation.JSONField; 4 | 5 | import java.io.Serial; 6 | import java.io.Serializable; 7 | 8 | /** 9 | * @author Kasumi_Nova 10 | */ 11 | public class IntegratedServerConfig extends Configuration implements Serializable { 12 | @Serial 13 | private static final long serialVersionUID = 1L; 14 | 15 | public static final String DEFAULT_IP = "127.0.0.1"; 16 | public static final String DEFAULT_MAIN_DIR_PATH = "/res"; 17 | public static final boolean DEFAULT_FILE_CHANGE_LISTENER = true; 18 | public static final boolean DEFAULT_COMPATIBLE_MODE = false; 19 | 20 | @JSONField(ordinal = 1) 21 | private String ip = DEFAULT_IP; 22 | @JSONField(ordinal = 2) 23 | private int port = DEFAULT_PORT; 24 | @JSONField(ordinal = 3) 25 | private String mainDirPath = DEFAULT_MAIN_DIR_PATH; 26 | @JSONField(ordinal = 4) 27 | private boolean fileChangeListener = DEFAULT_FILE_CHANGE_LISTENER; 28 | @JSONField(ordinal = 5) 29 | private boolean compatibleMode = DEFAULT_COMPATIBLE_MODE; 30 | @JSONField(ordinal = 6) 31 | private String jksFilePath = ""; 32 | @JSONField(ordinal = 7) 33 | private String jksSslPassword = ""; 34 | @JSONField(ordinal = 8) 35 | private String[] commonMode = new String[0]; 36 | @JSONField(ordinal = 9) 37 | private String[] onceMode = new String[0]; 38 | 39 | public IntegratedServerConfig() { 40 | configVersion = 1; 41 | } 42 | 43 | /** 44 | * 重置配置文件 45 | */ 46 | public IntegratedServerConfig reset() { 47 | configVersion = 1; 48 | ip = "127.0.0.1"; 49 | port = DEFAULT_PORT; 50 | mainDirPath = "/res"; 51 | fileChangeListener = true; 52 | jksFilePath = ""; 53 | jksSslPassword = ""; 54 | commonMode = new String[0]; 55 | onceMode = new String[0]; 56 | return this; 57 | } 58 | 59 | @Override 60 | public IntegratedServerConfig setConfigVersion(int configVersion) { 61 | this.configVersion = configVersion; 62 | return this; 63 | } 64 | 65 | public String getIp() { 66 | return ip; 67 | } 68 | 69 | public IntegratedServerConfig setIp(String ip) { 70 | this.ip = ip; 71 | return this; 72 | } 73 | 74 | public int getPort() { 75 | return port; 76 | } 77 | 78 | public IntegratedServerConfig setPort(int port) { 79 | this.port = port; 80 | return this; 81 | } 82 | 83 | public String getJksFilePath() { 84 | return jksFilePath; 85 | } 86 | 87 | public IntegratedServerConfig setJksFilePath(String jksFilePath) { 88 | this.jksFilePath = jksFilePath; 89 | return this; 90 | } 91 | 92 | public String getJksSslPassword() { 93 | return jksSslPassword; 94 | } 95 | 96 | public IntegratedServerConfig setJksSslPassword(String jksSslPassword) { 97 | this.jksSslPassword = jksSslPassword; 98 | return this; 99 | } 100 | 101 | public String[] getCommonMode() { 102 | return commonMode; 103 | } 104 | 105 | public IntegratedServerConfig setCommonMode(String[] commonMode) { 106 | this.commonMode = commonMode; 107 | return this; 108 | } 109 | 110 | public String[] getOnceMode() { 111 | return onceMode; 112 | } 113 | 114 | public IntegratedServerConfig setOnceMode(String[] onceMode) { 115 | this.onceMode = onceMode; 116 | return this; 117 | } 118 | 119 | public String getMainDirPath() { 120 | return mainDirPath; 121 | } 122 | 123 | public IntegratedServerConfig setMainDirPath(String mainDirPath) { 124 | this.mainDirPath = mainDirPath; 125 | return this; 126 | } 127 | 128 | public boolean isFileChangeListener() { 129 | return fileChangeListener; 130 | } 131 | 132 | public IntegratedServerConfig setFileChangeListener(boolean fileChangeListener) { 133 | this.fileChangeListener = fileChangeListener; 134 | return this; 135 | } 136 | 137 | public boolean isCompatibleMode() { 138 | return compatibleMode; 139 | } 140 | 141 | public IntegratedServerConfig setCompatibleMode(boolean compatibleMode) { 142 | this.compatibleMode = compatibleMode; 143 | return this; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/configurations/RemoteClientConfig.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.configurations; 2 | 3 | import com.alibaba.fastjson2.annotation.JSONField; 4 | 5 | /** 6 | * @author Kasumi_Nova 7 | */ 8 | public class RemoteClientConfig extends Configuration { 9 | @JSONField(ordinal = 1) 10 | private String ip = "127.0.0.1"; 11 | @JSONField(ordinal = 2) 12 | private int port = 8080; 13 | @JSONField(ordinal = 3) 14 | private String token = ""; 15 | 16 | public RemoteClientConfig() { 17 | this.configVersion = 0; 18 | } 19 | 20 | @Override 21 | public RemoteClientConfig setConfigVersion(int configVersion) { 22 | this.configVersion = configVersion; 23 | return this; 24 | } 25 | 26 | public String getIp() { 27 | return ip; 28 | } 29 | 30 | public RemoteClientConfig setIp(String ip) { 31 | this.ip = ip; 32 | return this; 33 | } 34 | 35 | public int getPort() { 36 | return port; 37 | } 38 | 39 | public RemoteClientConfig setPort(int port) { 40 | this.port = port; 41 | return this; 42 | } 43 | 44 | public String getToken() { 45 | return token; 46 | } 47 | 48 | public RemoteClientConfig setToken(String token) { 49 | this.token = token; 50 | return this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/ConfirmExitDialog.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui; 2 | 3 | import github.kasuminova.balloonserver.BalloonServer; 4 | import github.kasuminova.balloonserver.configurations.BalloonServerConfig; 5 | import github.kasuminova.balloonserver.configurations.ConfigurationManager; 6 | import github.kasuminova.balloonserver.gui.layoutmanager.VFlowLayout; 7 | 8 | import javax.swing.*; 9 | import javax.swing.border.EmptyBorder; 10 | import java.awt.*; 11 | import java.awt.event.WindowAdapter; 12 | import java.awt.event.WindowEvent; 13 | 14 | import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_LOGGER; 15 | import static github.kasuminova.balloonserver.BalloonServer.stopAllServers; 16 | 17 | /** 18 | * @author Kasumi_Nova 19 | */ 20 | public class ConfirmExitDialog extends JDialog { 21 | 22 | public static final int DIALOG_WIDTH = 360; 23 | public static final int DIALOG_HEIGHT = 165; 24 | 25 | public ConfirmExitDialog(JFrame frame, BalloonServerConfig config) { 26 | setTitle(BalloonServer.TITLE); 27 | setIconImage(BalloonServer.ICON.getImage()); 28 | setSize(DIALOG_WIDTH, DIALOG_HEIGHT); 29 | setResizable(false); 30 | setLocationRelativeTo(null); 31 | 32 | JPanel contentPane = (JPanel) getContentPane(); 33 | contentPane.setLayout(new VFlowLayout()); 34 | contentPane.setBorder(new EmptyBorder(0, 10, 0, 10)); 35 | contentPane.add(new JLabel("请选择点击关闭按钮时程序的操作:")); 36 | 37 | //选择 退出程序 或 最小化任务栏 38 | ButtonGroup selections = new ButtonGroup(); 39 | JRadioButton miniSizeToTray = new JRadioButton("最小化到任务栏", true); 40 | JRadioButton exit = new JRadioButton("退出程序"); 41 | selections.add(miniSizeToTray); 42 | selections.add(exit); 43 | JPanel radioButtonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 5)); 44 | radioButtonsPanel.add(miniSizeToTray); 45 | radioButtonsPanel.add(exit); 46 | contentPane.add(radioButtonsPanel); 47 | 48 | //始终保存选项 49 | JCheckBox saveSelection = new JCheckBox("保存选项, 下次不再提醒"); 50 | saveSelection.setBorder(new EmptyBorder(0, 5, 0, 0)); 51 | contentPane.add(saveSelection); 52 | 53 | Box buttonBox = new Box(BoxLayout.LINE_AXIS); 54 | buttonBox.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); 55 | 56 | //确定取消按钮 57 | JButton yes = new JButton("确定"); 58 | yes.addActionListener(e -> { 59 | //保存配置并退出程序 60 | if (exit.isSelected()) { 61 | //如果始终保存选项选中,则写入配置 62 | if (saveSelection.isSelected()) { 63 | config.setCloseOperation(BalloonServerConfig.EXIT_ON_CLOSE.getOperation()); 64 | try { 65 | ConfigurationManager.saveConfigurationToFile(config, "./", "balloonserver"); 66 | } catch (Exception ex) { 67 | GLOBAL_LOGGER.error("主程序配置文件保存失败!", ex); 68 | } 69 | } 70 | //停止所有正在运行的服务器并保存配置 71 | stopAllServers(true); 72 | System.exit(0); 73 | } 74 | //保存配置并最小化窗口 75 | if (miniSizeToTray.isSelected()) { 76 | frame.setVisible(false); 77 | 78 | //如果始终保存选项选中,则写入配置 79 | if (saveSelection.isSelected()) { 80 | frame.setDefaultCloseOperation(HIDE_ON_CLOSE); 81 | config.setCloseOperation(BalloonServerConfig.HIDE_ON_CLOSE.getOperation()); 82 | try { 83 | ConfigurationManager.saveConfigurationToFile(config, "./", "balloonserver"); 84 | } catch (Exception ex) { 85 | GLOBAL_LOGGER.error("主程序配置文件保存失败!", ex); 86 | } 87 | } 88 | } 89 | 90 | frame.setEnabled(true); 91 | dispose(); 92 | }); 93 | JButton cancel = new JButton("取消"); 94 | cancel.addActionListener(e -> { 95 | frame.setEnabled(true); 96 | dispose(); 97 | }); 98 | addWindowListener(new WindowClosingAdapter(frame)); 99 | 100 | buttonBox.add(cancel); 101 | buttonBox.add(new JLabel(" ")); 102 | buttonBox.add(yes); 103 | contentPane.add(buttonBox); 104 | } 105 | 106 | private static class WindowClosingAdapter extends WindowAdapter { 107 | private final JFrame frame; 108 | 109 | private WindowClosingAdapter(JFrame frame) { 110 | this.frame = frame; 111 | } 112 | 113 | @Override 114 | public void windowClosing(WindowEvent e) { 115 | frame.setEnabled(true); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/SmoothProgressBar.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui; 2 | 3 | import cn.hutool.core.thread.ExecutorBuilder; 4 | import cn.hutool.core.thread.ThreadUtil; 5 | import github.kasuminova.balloonserver.utils.CustomThreadFactory; 6 | 7 | import javax.swing.*; 8 | import java.util.concurrent.LinkedBlockingQueue; 9 | import java.util.concurrent.ThreadPoolExecutor; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * 平滑进度条 14 | */ 15 | public class SmoothProgressBar extends JProgressBar { 16 | public static final long TIME_MULTIPLIER = 3L; 17 | //单线程线程池,用于保证进度条的操作顺序 18 | private final ThreadPoolExecutor singleThreadExecutor = ExecutorBuilder.create() 19 | .setCorePoolSize(1) 20 | .setMaxPoolSize(1) 21 | .setKeepAliveTime(10, TimeUnit.SECONDS) 22 | .setWorkQueue(new LinkedBlockingQueue<>()) 23 | .setThreadFactory(CustomThreadFactory.create("SmoothProgressBarQueueThread-{}")) 24 | .build(); 25 | 26 | private final int flowTime; 27 | //每秒刷新频率 28 | private final int frequency; 29 | 30 | /** 31 | * 创建一个平滑进度条,基于 JProgressBar 32 | * 33 | * @param max 进度条最大值 34 | * @param flowTime 每次变动进度时消耗的时间,时间越长进度条越平滑,并除以 10 作为 frequency 35 | */ 36 | public SmoothProgressBar(int max, int flowTime) { 37 | super(0, max); 38 | this.flowTime = flowTime; 39 | frequency = flowTime / 10; 40 | } 41 | 42 | /** 43 | *

44 | * 以平滑方式设置进度 45 | *

46 | * 47 | * @param value 新进度 48 | */ 49 | @Override 50 | public void setValue(int value) { 51 | singleThreadExecutor.execute(() -> { 52 | int currentValue = getValue(); 53 | if (value < currentValue) { 54 | decrement(currentValue - value); 55 | } else { 56 | increment(value - currentValue); 57 | } 58 | }); 59 | } 60 | 61 | @Override 62 | public void setVisible(boolean aFlag) { 63 | //保证在进度条在完成先前所有的 加/减 操作后,再进行 setVisible 操作 64 | singleThreadExecutor.execute(() -> super.setVisible(aFlag)); 65 | } 66 | 67 | /** 68 | * 重置进度条进度至 0, 不使用平滑进度 69 | */ 70 | public void reset() { 71 | super.setValue(0); 72 | } 73 | 74 | /** 75 | * 将进度条进度增长指定数值, 使用平滑方式 76 | * 77 | * @param value 增长的数值 78 | */ 79 | public void increment(int value) { 80 | int currentValue = getValue(); 81 | //如果变动的数值小于刷新速度,则使用变动数值作为刷新速度,否则使用默认刷新速度 82 | int finalFrequency = Math.min(frequency, value); 83 | for (int i = 1; i <= finalFrequency; i++) { 84 | int queueSize = singleThreadExecutor.getQueue().size(); 85 | 86 | super.setValue(currentValue + ((value / finalFrequency) * i)); 87 | 88 | //如果线程池中的任务过多则加快进度条速度(即降低 sleep 时间) 89 | if (queueSize >= 1) { 90 | ThreadUtil.safeSleep((flowTime / (finalFrequency + (i * TIME_MULTIPLIER))) * (1 / queueSize)); 91 | } else { 92 | ThreadUtil.safeSleep(flowTime / (finalFrequency + (i * TIME_MULTIPLIER))); 93 | } 94 | } 95 | 96 | //如果最后进度条的值差异过大,则重新进行一次 increment 97 | int lastValue = (currentValue + value) - getValue(); 98 | if (lastValue >= 3) { 99 | increment(lastValue); 100 | } else { 101 | //防止差异,设置为最终结果值 102 | super.setValue(currentValue + value); 103 | } 104 | } 105 | 106 | /** 107 | * 将进度条进度减少指定数值, 使用平滑方式 108 | * 109 | * @param value 减少的数值 110 | */ 111 | public void decrement(int value) { 112 | int currentValue = getValue(); 113 | //如果变动的数值小于刷新速度,则使用变动数值作为刷新速度,否则使用默认刷新速度 114 | int finalFrequency = Math.min(frequency, value); 115 | for (int i = 0; i < finalFrequency; i++) { 116 | int queueSize = singleThreadExecutor.getQueue().size(); 117 | 118 | super.setValue(currentValue - ((value / finalFrequency) * i)); 119 | 120 | //如果线程池中的任务过多则加快进度条速度(即降低 sleep 时间) 121 | if (queueSize >= 1) { 122 | ThreadUtil.safeSleep((flowTime / (finalFrequency + (i * TIME_MULTIPLIER))) * (1 / queueSize)); 123 | } else { 124 | ThreadUtil.safeSleep(flowTime / (finalFrequency + (i * TIME_MULTIPLIER))); 125 | } 126 | } 127 | 128 | //如果最后进度条的值差异过大,则重新进行一次 increment 129 | int lastValue = (currentValue - value) - getValue(); 130 | if (lastValue >= 3) { 131 | decrement(lastValue); 132 | } else { 133 | //防止差异,设置为最终结果值 134 | super.setValue(currentValue - value); 135 | } 136 | } 137 | 138 | @Override 139 | public void removeNotify() { 140 | super.removeNotify(); 141 | singleThreadExecutor.shutdownNow(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/SwingSystemTray.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui; 2 | 3 | import cn.hutool.core.io.IORuntimeException; 4 | import github.kasuminova.balloonserver.BalloonServer; 5 | import github.kasuminova.balloonserver.configurations.BalloonServerConfig; 6 | import github.kasuminova.balloonserver.configurations.ConfigurationManager; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.event.MouseAdapter; 11 | import java.awt.event.MouseEvent; 12 | import java.awt.event.WindowAdapter; 13 | import java.awt.event.WindowEvent; 14 | import java.net.URL; 15 | 16 | import static github.kasuminova.balloonserver.BalloonServer.*; 17 | import static github.kasuminova.balloonserver.utils.SvgIcons.STOP_ICON; 18 | import static github.kasuminova.balloonserver.utils.SvgIcons.TERMINAL_ICON; 19 | 20 | /** 21 | * 中文系统托盘弹出菜单不乱码。 22 | * 网上抄的() 23 | * 24 | * @author Kasumi_Nova 25 | */ 26 | public class SwingSystemTray { 27 | /** 28 | * 载入托盘 29 | * 30 | * @param frame 主窗口 31 | */ 32 | public static void initSystemTrayAndFrame(JFrame frame) { 33 | //如果系统不支持任务栏,则设置为关闭窗口时退出程序 34 | if (!SystemTray.isSupported()) { 35 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 36 | return; 37 | } 38 | if (CONFIG.getCloseOperation() == BalloonServerConfig.EXIT_ON_CLOSE.getOperation()) { 39 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 40 | } else { 41 | frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 42 | } 43 | //使用JDialog 作为JPopupMenu载体 44 | JDialog dialog = new JDialog(); 45 | //关闭JDialog的装饰器 46 | dialog.setUndecorated(true); 47 | //jDialog作为JPopupMenu载体不需要多大的size 48 | dialog.setSize(1, 1); 49 | 50 | //创建JPopupMenu 51 | //重写firePopupMenuWillBecomeInvisible 52 | //消失后将绑定的组件一起消失 53 | JPopupMenu trayMenu = new JPopupMenu() { 54 | @Override 55 | public void firePopupMenuWillBecomeInvisible() { 56 | dialog.setVisible(false); 57 | } 58 | }; 59 | trayMenu.setSize(100, 30); 60 | 61 | //添加菜单选项 62 | JMenuItem exit = new JMenuItem("退出程序", STOP_ICON); 63 | exit.addActionListener(e -> { 64 | try { 65 | ConfigurationManager.saveConfigurationToFile(CONFIG, "./", "balloonserver"); 66 | BalloonServer.GLOBAL_LOGGER.info("已保存主程序配置文件."); 67 | } catch (IORuntimeException ex) { 68 | BalloonServer.GLOBAL_LOGGER.error("保存主程序配置文件失败!"); 69 | } 70 | //停止所有正在运行的服务器并保存配置 71 | stopAllServers(true); 72 | System.exit(0); 73 | }); 74 | JMenuItem showMainFrame = new JMenuItem("显示窗口", TERMINAL_ICON); 75 | showMainFrame.addActionListener(e -> { 76 | //显示窗口 77 | frame.setVisible(true); 78 | frame.toFront(); 79 | }); 80 | 81 | trayMenu.add(showMainFrame); 82 | trayMenu.add(exit); 83 | 84 | URL resource = SwingSystemTray.class.getResource("/image/icon_16x16.png"); 85 | // 创建托盘图标 86 | Image image = Toolkit.getDefaultToolkit().createImage(resource); 87 | // 创建系统托盘图标 88 | TrayIcon trayIcon = new TrayIcon(image); 89 | trayIcon.setToolTip("BalloonServer " + BalloonServer.VERSION); 90 | // 自动调整系统托盘图标大小 91 | trayIcon.setImageAutoSize(true); 92 | 93 | // 给托盘图标添加鼠标监听 94 | trayIcon.addMouseListener(new MouseAdapter() { 95 | @Override 96 | public void mouseReleased(MouseEvent e) { 97 | //左键点击 98 | if (e.getButton() == 1) { 99 | //显示窗口 100 | frame.setVisible(true); 101 | frame.toFront(); 102 | } else if (e.getButton() == MouseEvent.BUTTON3 && e.isPopupTrigger()) { 103 | // 右键点击弹出JPopupMenu绑定的载体以及JPopupMenu 104 | dialog.setLocation( 105 | (int) (e.getXOnScreen() / SetupSwing.SCREEN_SCALE) + 5, 106 | (int) (e.getYOnScreen() / SetupSwing.SCREEN_SCALE) - trayMenu.getHeight() - 5); 107 | // 显示载体 108 | dialog.setVisible(true); 109 | dialog.toFront(); 110 | // 在载体的 0,0 处显示对话框 111 | trayMenu.show(dialog, 0, 0); 112 | } 113 | } 114 | }); 115 | // 添加托盘图标到系统托盘 116 | systemTrayAdd(trayIcon); 117 | // 关闭窗口时显示信息 118 | frame.addWindowListener(new WindowAdapter() { 119 | @Override 120 | public void windowClosed(WindowEvent e) { 121 | trayIcon.displayMessage("提示", "程序已最小化至后台运行。", TrayIcon.MessageType.INFO); 122 | } 123 | }); 124 | } 125 | 126 | /** 127 | * 添加托盘图标到系统托盘中。 128 | * 129 | * @param trayIcon 系统托盘图标。 130 | */ 131 | private static void systemTrayAdd(TrayIcon trayIcon) { 132 | // 将托盘图标添加到系统的托盘实例中 133 | SystemTray tray = SystemTray.getSystemTray(); 134 | try { 135 | tray.add(trayIcon); 136 | } catch (AWTException ex) { 137 | GLOBAL_LOGGER.error(ex); 138 | } 139 | } 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/checkboxtree/CheckBoxTreeCellRenderer.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui.checkboxtree; 2 | 3 | import github.kasuminova.balloonserver.utils.SvgIcons; 4 | 5 | import javax.swing.*; 6 | import javax.swing.plaf.ColorUIResource; 7 | import javax.swing.tree.TreeCellRenderer; 8 | import javax.swing.tree.TreeNode; 9 | import java.awt.*; 10 | 11 | 12 | public class CheckBoxTreeCellRenderer extends JPanel implements TreeCellRenderer { 13 | protected final JCheckBox check; 14 | protected final CheckBoxTreeLabel label; 15 | 16 | public CheckBoxTreeCellRenderer() { 17 | setLayout(null); 18 | add(check = new JCheckBox()); 19 | add(label = new CheckBoxTreeLabel()); 20 | check.setBackground(UIManager.getColor("Tree.textBackground")); 21 | label.setForeground(UIManager.getColor("Tree.textForeground")); 22 | } 23 | 24 | /** 25 | * 返回的是一个{@code JPanel}对象,该对象中包含一个{@code JCheckBox}对象 26 | * 和一个{@code JLabel}对象。并且根据每个结点是否被选中来决定{@code JCheckBox} 27 | * 是否被选中。 28 | */ 29 | @Override 30 | public Component getTreeCellRendererComponent(JTree tree, Object value, 31 | boolean selected, boolean expanded, boolean leaf, int row, 32 | boolean hasFocus) { 33 | String stringValue = tree.convertValueToText(value, selected, expanded, leaf, row, hasFocus); 34 | setEnabled(tree.isEnabled()); 35 | check.setSelected(((CheckBoxTreeNode) value).isSelected()); 36 | label.setFont(tree.getFont()); 37 | label.setText(stringValue); 38 | label.setSelected(selected); 39 | label.setFocus(hasFocus); 40 | if (((TreeNode) value).getAllowsChildren()) { 41 | label.setIcon(SvgIcons.DIR_ICON); 42 | // label.setIcon(UIManager.getIcon("Tree.closedIcon")); 43 | // } else if (expanded) { 44 | // label.setIcon(UIManager.getIcon("Tree.openIcon")); 45 | } else { 46 | setFileIcon(); 47 | } 48 | 49 | return this; 50 | } 51 | 52 | @Override 53 | public Dimension getPreferredSize() { 54 | Dimension dCheck = check.getPreferredSize(); 55 | Dimension dLabel = label.getPreferredSize(); 56 | return new Dimension(dCheck.width + dLabel.width, Math.max(dCheck.height, dLabel.height)); 57 | } 58 | 59 | @Override 60 | public void doLayout() { 61 | Dimension dCheck = check.getPreferredSize(); 62 | Dimension dLabel = label.getPreferredSize(); 63 | int yCheck = 0; 64 | int yLabel = 0; 65 | if (dCheck.height < dLabel.height) 66 | yCheck = (dLabel.height - dCheck.height) / 2; 67 | else 68 | yLabel = (dCheck.height - dLabel.height) / 2; 69 | check.setLocation(0, yCheck); 70 | check.setBounds(0, yCheck, dCheck.width, dCheck.height); 71 | label.setLocation(dCheck.width, yLabel); 72 | label.setBounds(dCheck.width, yLabel, dLabel.width, dLabel.height); 73 | } 74 | 75 | @Override 76 | public void setBackground(Color color) { 77 | if (color instanceof ColorUIResource) 78 | color = null; 79 | super.setBackground(color); 80 | } 81 | 82 | public void setFileIcon() { 83 | String fileName = label.getText(); 84 | 85 | if (fileName.endsWith(".class")) { 86 | label.setIcon(SvgIcons.CLASS_FILE_ICON); 87 | } else if (fileName.endsWith("doc") || fileName.endsWith("docx")) { 88 | label.setIcon(SvgIcons.DOC_FILE_ICON); 89 | } else if (fileName.endsWith("ppt") || fileName.endsWith("pptx")) { 90 | label.setIcon(SvgIcons.PPT_FILE_ICON); 91 | } else if (fileName.endsWith("xls") || fileName.endsWith("xlsx")) { 92 | label.setIcon(SvgIcons.XLS_FILE_ICON); 93 | } else if (fileName.endsWith("exe")) { 94 | label.setIcon(SvgIcons.EXE_FILE_ICON); 95 | } else if (fileName.endsWith("jar")) { 96 | label.setIcon(SvgIcons.JAR_FILE_ICON); 97 | } else if (fileName.endsWith("java")) { 98 | label.setIcon(SvgIcons.JAVA_FILE_ICON); 99 | } else if (fileName.endsWith("jpg")) { 100 | label.setIcon(SvgIcons.JPG_FILE_ICON); 101 | } else if (fileName.endsWith("json")) { 102 | label.setIcon(SvgIcons.JSON_FILE_ICON); 103 | } else if (fileName.endsWith("md")) { 104 | label.setIcon(SvgIcons.MD_FILE_ICON); 105 | } else if (fileName.endsWith("txt")) { 106 | label.setIcon(SvgIcons.TXT_FILE_ICON); 107 | } else if (fileName.endsWith("xml")) { 108 | label.setIcon(SvgIcons.XML_FILE_ICON); 109 | } else if (fileName.endsWith("yml")) { 110 | label.setIcon(SvgIcons.YML_FILE_ICON); 111 | } else if (fileName.endsWith("zip")) { 112 | label.setIcon(SvgIcons.ZIP_FILE_ICON); 113 | } else { 114 | label.setIcon(SvgIcons.FILE_ICON); 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/checkboxtree/CheckBoxTreeLabel.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui.checkboxtree; 2 | 3 | import javax.swing.*; 4 | import javax.swing.plaf.ColorUIResource; 5 | import java.awt.*; 6 | 7 | public class CheckBoxTreeLabel extends JLabel { 8 | private boolean isSelected; 9 | private boolean hasFocus; 10 | 11 | @Override 12 | public void setBackground(Color color) { 13 | if (color instanceof ColorUIResource) 14 | color = null; 15 | super.setBackground(color); 16 | } 17 | 18 | @Override 19 | public void paint(Graphics g) { 20 | String str; 21 | if ((str = getText()) != null) { 22 | if (!str.isEmpty()) { 23 | if (isSelected) 24 | g.setColor(UIManager.getColor("Tree.selectionBackground")); 25 | else 26 | g.setColor(UIManager.getColor("Tree.textBackground")); 27 | Dimension dim = getPreferredSize(); 28 | int imageOffset = 0; 29 | Icon currentIcon = getIcon(); 30 | if (currentIcon != null) 31 | imageOffset = currentIcon.getIconWidth() + Math.max(0, getIconTextGap() - 1); 32 | g.fillRect(imageOffset, 0, dim.width - 1 - imageOffset, dim.height); 33 | if (hasFocus) { 34 | g.setColor(UIManager.getColor("Tree.selectionBorderColor")); 35 | g.drawRect(imageOffset, 0, dim.width - 1 - imageOffset, dim.height - 1); 36 | } 37 | } 38 | } 39 | super.paint(g); 40 | } 41 | 42 | @Override 43 | public Dimension getPreferredSize() { 44 | Dimension retDimension = super.getPreferredSize(); 45 | if (retDimension != null) 46 | retDimension = new Dimension(retDimension.width + 3, retDimension.height); 47 | return retDimension; 48 | } 49 | 50 | public void setSelected(boolean isSelected) { 51 | this.isSelected = isSelected; 52 | } 53 | 54 | public void setFocus(boolean hasFocus) { 55 | this.hasFocus = hasFocus; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/checkboxtree/CheckBoxTreeNode.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui.checkboxtree; 2 | 3 | import javax.swing.tree.DefaultMutableTreeNode; 4 | 5 | public class CheckBoxTreeNode extends DefaultMutableTreeNode { 6 | protected boolean isSelected; 7 | 8 | public CheckBoxTreeNode() { 9 | this(null); 10 | } 11 | 12 | public CheckBoxTreeNode(Object userObject) { 13 | this(userObject, true, false); 14 | } 15 | 16 | public CheckBoxTreeNode(Object userObject, boolean allowsChildren, boolean isSelected) { 17 | super(userObject, allowsChildren); 18 | this.isSelected = isSelected; 19 | } 20 | 21 | public CheckBoxTreeNode getChildAt(int index) { 22 | if (children == null) { 23 | throw new ArrayIndexOutOfBoundsException("node has no children"); 24 | } 25 | return (CheckBoxTreeNode) children.elementAt(index); 26 | } 27 | 28 | public boolean isSelected() { 29 | return isSelected; 30 | } 31 | 32 | public void setSelected(boolean _isSelected) { 33 | this.isSelected = _isSelected; 34 | 35 | if (_isSelected) { 36 | // 如果选中,则将其所有的子结点都选中 37 | if (children != null) { 38 | for (Object obj : children) { 39 | CheckBoxTreeNode node = (CheckBoxTreeNode) obj; 40 | if (!node.isSelected) 41 | node.setSelected(true); 42 | } 43 | } 44 | // 向上检查,如果父结点的所有子结点都被选中,那么将父结点也选中 45 | CheckBoxTreeNode pNode = (CheckBoxTreeNode) parent; 46 | // 开始检查pNode的所有子节点是否都被选中 47 | if (pNode != null) { 48 | int index = 0; 49 | for (; index < pNode.children.size(); ++index) { 50 | CheckBoxTreeNode pChildNode = (CheckBoxTreeNode) pNode.children.get(index); 51 | if (!pChildNode.isSelected) 52 | break; 53 | } 54 | /* 55 | * 表明pNode所有子结点都已经选中,则选中父结点, 56 | * 该方法是一个递归方法,因此在此不需要进行迭代,因为 57 | * 当选中父结点后,父结点本身会向上检查的。 58 | */ 59 | if (index == pNode.children.size()) { 60 | if (!pNode.isSelected) 61 | pNode.setSelected(true); 62 | } 63 | } 64 | } else { 65 | /* 66 | * 如果是取消父结点导致子结点取消,那么此时所有的子结点都应该是选择上的; 67 | * 否则就是子结点取消导致父结点取消,然后父结点取消导致需要取消子结点,但 68 | * 是这时候是不需要取消子结点的。 69 | */ 70 | if (children != null) { 71 | int index = 0; 72 | for (; index < children.size(); ++index) { 73 | CheckBoxTreeNode childNode = (CheckBoxTreeNode) children.get(index); 74 | if (!childNode.isSelected) 75 | break; 76 | } 77 | // 从上向下取消的时候 78 | if (index == children.size()) { 79 | for (Object child : children) { 80 | CheckBoxTreeNode node = (CheckBoxTreeNode) child; 81 | if (node.isSelected) 82 | node.setSelected(false); 83 | } 84 | } 85 | } 86 | 87 | // 向上取消,只要存在一个子节点不是选上的,那么父节点就不应该被选上。 88 | CheckBoxTreeNode pNode = (CheckBoxTreeNode) parent; 89 | if (pNode != null && pNode.isSelected) 90 | pNode.setSelected(false); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/checkboxtree/CheckBoxTreeNodeSelectionListener.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui.checkboxtree; 2 | 3 | import javax.swing.*; 4 | import javax.swing.tree.DefaultTreeModel; 5 | import javax.swing.tree.TreePath; 6 | import java.awt.event.MouseAdapter; 7 | import java.awt.event.MouseEvent; 8 | 9 | public class CheckBoxTreeNodeSelectionListener extends MouseAdapter { 10 | @Override 11 | public void mouseClicked(MouseEvent event) { 12 | JTree tree = (JTree) event.getSource(); 13 | int x = event.getX(); 14 | int y = event.getY(); 15 | int row = tree.getRowForLocation(x, y); 16 | TreePath path = tree.getPathForRow(row); 17 | if (path != null) { 18 | CheckBoxTreeNode node = (CheckBoxTreeNode) path.getLastPathComponent(); 19 | if (node != null) { 20 | boolean isSelected = !node.isSelected(); 21 | node.setSelected(isSelected); 22 | ((DefaultTreeModel) tree.getModel()).nodeStructureChanged(node); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/fileobjectbrowser/FileObjectBrowser.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui.fileobjectbrowser; 2 | 3 | import github.kasuminova.balloonserver.BalloonServer; 4 | import github.kasuminova.balloonserver.gui.checkboxtree.CheckBoxTreeCellRenderer; 5 | import github.kasuminova.balloonserver.gui.checkboxtree.CheckBoxTreeNode; 6 | import github.kasuminova.balloonserver.gui.checkboxtree.CheckBoxTreeNodeSelectionListener; 7 | import github.kasuminova.balloonserver.gui.layoutmanager.VFlowLayout; 8 | import github.kasuminova.balloonserver.updatechecker.ApplicationVersion; 9 | import github.kasuminova.balloonserver.utils.fileobject.AbstractSimpleFileObject; 10 | import github.kasuminova.balloonserver.utils.fileobject.SimpleDirectoryObject; 11 | import github.kasuminova.balloonserver.utils.fileobject.SimpleFileObject; 12 | 13 | import javax.swing.*; 14 | import javax.swing.border.TitledBorder; 15 | import javax.swing.tree.DefaultTreeModel; 16 | import javax.swing.tree.TreePath; 17 | import java.awt.*; 18 | import java.util.ArrayList; 19 | 20 | /** 21 | * @author Kasumi_Nova 22 | */ 23 | public class FileObjectBrowser extends JDialog { 24 | public static final ApplicationVersion VERSION = new ApplicationVersion("1.0.0-BETA"); 25 | public static final String TITLE = "FileObjectBrowser " + VERSION; 26 | public static final int WINDOW_WIDTH = 750; 27 | public static final int WINDOW_HEIGHT = 600; 28 | 29 | public FileObjectBrowser(SimpleDirectoryObject directoryObject) { 30 | setTitle(TITLE); 31 | setIconImage(BalloonServer.ICON.getImage()); 32 | setSize(WINDOW_WIDTH, WINDOW_HEIGHT); 33 | setResizable(false); 34 | setLocationRelativeTo(null); 35 | 36 | Container contentPane = getContentPane(); 37 | contentPane.setLayout(new VFlowLayout()); 38 | 39 | JPanel treePanel = new JPanel(new VFlowLayout(VFlowLayout.TOP, VFlowLayout.MIDDLE, 5, 5, 5, 5, true, false)); 40 | treePanel.setBorder(new TitledBorder("服务端文件列表")); 41 | 42 | JTree tree = new JTree(); 43 | CheckBoxTreeNode rootNode = new CheckBoxTreeNode("服务端文件夹"); 44 | DefaultTreeModel model = new DefaultTreeModel(rootNode); 45 | 46 | tree.addMouseListener(new CheckBoxTreeNodeSelectionListener()); 47 | tree.setModel(model); 48 | tree.setCellRenderer(new CheckBoxTreeCellRenderer()); 49 | 50 | for (CheckBoxTreeNode checkBoxTreeNode : scanDirAndBuildTree(directoryObject)) { 51 | rootNode.add(checkBoxTreeNode); 52 | } 53 | 54 | tree.expandPath(new TreePath(rootNode.getPath())); 55 | 56 | JScrollPane treeScroll = new JScrollPane(tree); 57 | treePanel.add(treeScroll); 58 | 59 | contentPane.add(treePanel); 60 | 61 | setLocationRelativeTo(null); 62 | } 63 | 64 | private static ArrayList scanDirAndBuildTree(SimpleDirectoryObject directoryObject) { 65 | ArrayList treeNodes = new ArrayList<>(0); 66 | 67 | ArrayList fileObjects = directoryObject.getChildren(); 68 | fileObjects.forEach((obj -> { 69 | if (obj instanceof SimpleFileObject fileObject) { 70 | CheckBoxTreeNode file = new CheckBoxTreeNode(fileObject.getName()); 71 | file.setAllowsChildren(false); 72 | 73 | treeNodes.add(file); 74 | } else if (obj instanceof SimpleDirectoryObject dirObject){ 75 | CheckBoxTreeNode dir = new CheckBoxTreeNode(dirObject.getName()); 76 | dir.setAllowsChildren(true); 77 | scanDirAndBuildTree(dirObject).forEach(dir::add); 78 | 79 | treeNodes.add(dir); 80 | } 81 | })); 82 | 83 | return treeNodes; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/fileobjectbrowser/ImageListCellRenderer.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui.fileobjectbrowser; 2 | 3 | import github.kasuminova.balloonserver.utils.ModernColors; 4 | import github.kasuminova.balloonserver.utils.SvgIcons; 5 | import github.kasuminova.balloonserver.utils.fileobject.AbstractSimpleFileObject; 6 | import github.kasuminova.balloonserver.utils.fileobject.SimpleDirectoryObject; 7 | import github.kasuminova.balloonserver.utils.fileobject.SimpleFileObject; 8 | 9 | import java.awt.*; 10 | import javax.swing.*; 11 | 12 | /** 13 | * @author Kasumi_Nova 14 | */ 15 | public class ImageListCellRenderer extends DefaultListCellRenderer { 16 | public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 17 | if (value instanceof AbstractSimpleFileObject abstractFileObject) { 18 | setText(abstractFileObject.getName()); //设置文字 19 | if (abstractFileObject instanceof SimpleFileObject) { 20 | setIcon(SvgIcons.FILE_ICON); 21 | } else if (abstractFileObject instanceof SimpleDirectoryObject) { 22 | setIcon(SvgIcons.DIR_ICON); 23 | } 24 | } else { 25 | setText(value.toString()); //设置文字 26 | setIcon(SvgIcons.FILE_ICON); 27 | } 28 | 29 | if (isSelected) { //当某个元素被选中时 30 | setForeground(Color.WHITE); //设置前景色(文字颜色)为白色 31 | setBackground(ModernColors.BLUE); //设置背景色为蓝色 32 | } else { //某个元素未被选中时(取消选中) 33 | setForeground(list.getForeground()); //设置前景色(文字颜色)为黑色 34 | setBackground(list.getBackground()); //设置背景色为白色 35 | } 36 | return this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/panels/AboutPanel.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui.panels; 2 | 3 | import cn.hutool.core.img.ImgUtil; 4 | import github.kasuminova.balloonserver.BalloonServer; 5 | import github.kasuminova.balloonserver.gui.layoutmanager.VFlowLayout; 6 | import github.kasuminova.balloonserver.utils.MiscUtils; 7 | 8 | import javax.swing.*; 9 | import javax.swing.border.EmptyBorder; 10 | import java.awt.*; 11 | 12 | /** 13 | * @author Kasumi_Nova 14 | */ 15 | public class AboutPanel { 16 | private static final Dimension ABOUT_BUTTON_SIZE = new Dimension(170, 30); 17 | private static final float TITLE_FONT_SIZE = 36F; 18 | private static final float LICENSE_LABEL_FONT_SIZE = 18F; 19 | 20 | public static JPanel createPanel() { 21 | //主面板 22 | JPanel aboutPanel = new JPanel(new BorderLayout()); 23 | Box descBox = Box.createVerticalBox(); 24 | //标题容器 25 | Box titleBox = Box.createHorizontalBox(); 26 | titleBox.setBorder(new EmptyBorder(10,0,0,0)); 27 | //LOGO, 并缩放图标 28 | titleBox.add(new JLabel(new ImageIcon(ImgUtil.scale(BalloonServer.ICON.getImage(), 80F / BalloonServer.ICON.getIconWidth())))); 29 | //标题 30 | JLabel title = new JLabel("BalloonServer " + BalloonServer.VERSION); 31 | title.setBorder(new EmptyBorder(0,10,0,0)); 32 | //设置字体 33 | title.setFont(title.getFont().deriveFont(TITLE_FONT_SIZE)); 34 | titleBox.add(title); 35 | //描述 36 | JPanel descPanel = new JPanel(new VFlowLayout(0, VFlowLayout.MIDDLE, 5, 5, 5, 5, false, false)); 37 | descPanel.setBorder(new EmptyBorder(10,0,0,0)); 38 | descPanel.add(new JLabel("BalloonServer 是 LittleServer 的衍生图形化版本, 基于 Netty-IO 的增强实现.", JLabel.CENTER)); 39 | descPanel.add(new JLabel("提示: BalloonServer 内嵌了可视化更新规则编辑器, 你可以通过右键更新模式列表打开.", JLabel.CENTER)); 40 | descPanel.add(new JLabel("提示: BalloonServer 支持启动多个服务端, 你可以使用窗口左上角菜单来管理多个实例.", JLabel.CENTER)); 41 | //链接 42 | JPanel linkPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10,5)); 43 | //仓库链接 44 | JButton openProjectLink = new JButton("点击打开仓库链接"); 45 | openProjectLink.addActionListener(e -> MiscUtils.openLinkInBrowser("https://github.com/BalloonUpdate/BalloonServer")); 46 | openProjectLink.setPreferredSize(ABOUT_BUTTON_SIZE); 47 | linkPanel.add(openProjectLink); 48 | //项目链接 49 | JButton openOrganizationLink = new JButton("点击打开项目链接"); 50 | openOrganizationLink.addActionListener(e -> MiscUtils.openLinkInBrowser("https://github.com/BalloonUpdate")); 51 | openOrganizationLink.setPreferredSize(ABOUT_BUTTON_SIZE); 52 | linkPanel.add(openOrganizationLink); 53 | //Issues 链接 54 | JButton openIssuesLink = new JButton("戳我提交 Issue!"); 55 | openIssuesLink.addActionListener(e -> MiscUtils.openLinkInBrowser("https://github.com/BalloonUpdate/BalloonServer/issues/new")); 56 | openIssuesLink.setPreferredSize(ABOUT_BUTTON_SIZE); 57 | linkPanel.add(openIssuesLink); 58 | descPanel.add(linkPanel); 59 | 60 | descPanel.add(new JLabel("BalloonServer 的诞生离不开这些贡献: ", JLabel.CENTER)); 61 | descPanel.add(new JLabel("Netty 为 BalloonServer 提供了高性能的并发网络框架;", JLabel.CENTER)); 62 | descPanel.add(new JLabel("Alibaba FastJson2 为 BalloonServer 提供了高性能的 JSON 解析功能;", JLabel.CENTER)); 63 | descPanel.add(new JLabel("FlatLaf, FlatLaf-Extra 为 BalloonServer 提供了一套完美的用户界面体验;", JLabel.CENTER)); 64 | descPanel.add(new JLabel("Hutools 为 BalloonServer 提供了一系列的实用工具;实现了实时文件监听器的功能;", JLabel.CENTER)); 65 | descPanel.add(new JLabel("以及任何积极使用该软件和为此软件出谋划策的用户和开发者们~", JLabel.CENTER)); 66 | 67 | //协议 68 | JLabel licenseLabel = new JLabel("本软件使用 AGPLv3 协议.", JLabel.RIGHT); 69 | licenseLabel.setFont(licenseLabel.getFont().deriveFont(LICENSE_LABEL_FONT_SIZE)); 70 | licenseLabel.setBorder(new EmptyBorder(0,0,10,10)); 71 | 72 | descBox.add(titleBox); 73 | descBox.add(descPanel); 74 | aboutPanel.add(descBox); 75 | aboutPanel.add(licenseLabel, BorderLayout.SOUTH); 76 | return aboutPanel; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/gui/ruleeditor/RuleEditorActionListener.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.gui.ruleeditor; 2 | 3 | import cn.hutool.core.io.IORuntimeException; 4 | import cn.hutool.core.io.file.FileReader; 5 | import com.alibaba.fastjson2.JSONArray; 6 | import github.kasuminova.balloonserver.BalloonServer; 7 | import github.kasuminova.balloonserver.servers.GUIServerInterface; 8 | import github.kasuminova.balloonserver.servers.localserver.IntegratedServerInterface; 9 | import github.kasuminova.balloonserver.servers.remoteserver.RemoteClientInterface; 10 | import github.kasuminova.balloonserver.utils.GUILogger; 11 | import github.kasuminova.balloonserver.utils.HashCalculator; 12 | import github.kasuminova.balloonserver.utils.filecacheutils.JsonCacheUtils; 13 | import github.kasuminova.messages.RequestMessage; 14 | 15 | import javax.swing.*; 16 | import java.awt.event.ActionEvent; 17 | import java.awt.event.ActionListener; 18 | import java.io.File; 19 | import java.util.List; 20 | 21 | import static github.kasuminova.balloonserver.BalloonServer.*; 22 | 23 | public class RuleEditorActionListener implements ActionListener { 24 | protected final JList ruleList; 25 | protected final List rules; 26 | protected final GUIServerInterface serverInterface; 27 | protected final GUILogger logger; 28 | 29 | public RuleEditorActionListener(JList ruleList, List rules, GUIServerInterface serverInterface, GUILogger logger) { 30 | this.ruleList = ruleList; 31 | this.rules = rules; 32 | this.serverInterface = serverInterface; 33 | this.logger = logger; 34 | } 35 | 36 | @Override 37 | public void actionPerformed(ActionEvent e) { 38 | if (serverInterface.isGenerating().get()) { 39 | JOptionPane.showMessageDialog(MAIN_FRAME, 40 | "当前正在生成资源缓存,请稍后再试。", 41 | BalloonServer.TITLE, 42 | JOptionPane.WARNING_MESSAGE); 43 | return; 44 | } 45 | 46 | if (serverInterface instanceof IntegratedServerInterface) { 47 | File file = new File(String.format("./%s.%s.json", serverInterface.getServerName(), serverInterface.getResJsonFileExtensionName())); 48 | if (file.exists()) { 49 | int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, 50 | "检测到本地 JSON 缓存,是否以 JSON 缓存启动规则编辑器?", 51 | BalloonServer.TITLE, JOptionPane.YES_NO_OPTION); 52 | if (!(selection == JOptionPane.YES_OPTION)) return; 53 | 54 | try { 55 | String json = new FileReader(file).readString(); 56 | showRuleEditorDialog(JSONArray.parseArray(json), ruleList, rules, logger); 57 | } catch (IORuntimeException ex) { 58 | logger.error("无法读取本地 JSON 缓存\n", ex); 59 | } 60 | return; 61 | } 62 | 63 | int selection = JOptionPane.showConfirmDialog(MAIN_FRAME, 64 | "未检测到 JSON 缓存,是否立即生成 JSON 缓存并启动规则编辑器?", 65 | BalloonServer.TITLE, JOptionPane.YES_NO_OPTION); 66 | if (!(selection == JOptionPane.YES_OPTION)) return; 67 | 68 | GLOBAL_THREAD_POOL.execute(() -> { 69 | new JsonCacheUtils((IntegratedServerInterface) serverInterface, null, null).updateDirCache(null, HashCalculator.CRC32); 70 | if (file.exists()) { 71 | try { 72 | String json = new FileReader(file).readString(); 73 | showRuleEditorDialog(JSONArray.parseArray(json), ruleList, rules, logger); 74 | } catch (IORuntimeException ex) { 75 | logger.error("无法读取本地 JSON 缓存\n", ex); 76 | } 77 | } 78 | }); 79 | } else if (serverInterface instanceof RemoteClientInterface remoteClientInterface) { 80 | remoteClientInterface.getChannel().writeAndFlush(new RequestMessage( 81 | "GetJsonCache", List.of(new String[]{"RuleEditor"}))); 82 | } 83 | } 84 | 85 | public static void showRuleEditorDialog(JSONArray jsonArray, JList ruleList, List rules, GUILogger logger) { 86 | GLOBAL_THREAD_POOL.execute(() -> { 87 | //锁定窗口,防止用户误操作 88 | MAIN_FRAME.setEnabled(false); 89 | RuleEditor editorDialog = new RuleEditor(jsonArray, rules, logger); 90 | editorDialog.setModal(true); 91 | 92 | MAIN_FRAME.setEnabled(true); 93 | editorDialog.setVisible(true); 94 | 95 | ruleList.setListData(rules.toArray(new String[0])); 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/httpserver/ContentRanges.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.httpserver; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | 5 | import java.util.List; 6 | 7 | public class ContentRanges { 8 | private final long start; 9 | private final long end; 10 | 11 | public long getStart() { 12 | return start; 13 | } 14 | 15 | public long getEnd() { 16 | return end; 17 | } 18 | 19 | /** 20 | * 解析 range 字符串 21 | * @param rangeContent 要解析的字符串 22 | * @param type range 的类型:如 "bytes" 23 | * @param fileRange 文件长度 24 | */ 25 | public ContentRanges(String rangeContent, String type, long fileRange) { 26 | if (rangeContent == null) { 27 | start = 0; 28 | end = fileRange; 29 | return; 30 | } 31 | 32 | String trueRangeContent = StrUtil.removePrefix(rangeContent, type + "="); 33 | if (trueRangeContent.startsWith("-")) { 34 | long last = Long.parseLong(trueRangeContent); 35 | start = fileRange + last; 36 | end = fileRange; 37 | return; 38 | } 39 | 40 | if ("0-0,-1".equals(trueRangeContent)) { 41 | start = 0; 42 | end = fileRange; 43 | return; 44 | } 45 | 46 | List ranges = StrUtil.split(trueRangeContent, '-', 2); 47 | start = Long.parseLong(ranges.get(0)); 48 | 49 | end = ranges.get(1).isEmpty() ? fileRange : Long.parseLong(ranges.get(1)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/httpserver/DecodeProxy.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.httpserver; 2 | 3 | import github.kasuminova.balloonserver.utils.GUILogger; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.handler.codec.ByteToMessageDecoder; 7 | import io.netty.util.Attribute; 8 | import io.netty.util.AttributeKey; 9 | 10 | import java.nio.charset.StandardCharsets; 11 | import java.util.List; 12 | 13 | /** 14 | * @Description nginx代理 netty tcp服务端负载均衡,nginx stream要打开 proxy_protocol on; 配置 15 | */ 16 | public class DecodeProxy extends ByteToMessageDecoder { 17 | /** 18 | * 保存客户端IP 19 | */ 20 | public static final AttributeKey key = AttributeKey.valueOf("IP"); 21 | final GUILogger logger; 22 | 23 | public DecodeProxy(GUILogger logger) { 24 | this.logger = logger; 25 | } 26 | 27 | /** 28 | * decode() 会根据接收的数据,被调用多次,直到确定没有新的元素添加到list, 29 | * 或者是 ByteBuf 没有更多的可读字节为止。 30 | * 如果 list 不为空,就会将 list 的内容传递给下一个 handler 31 | * 32 | * @param ctx 上下文对象 33 | * @param byteBuf 入站后的 ByteBuf 34 | * @param out 将解码后的数据传递给下一个 handler 35 | */ 36 | @Override 37 | protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List out) { 38 | byte[] bytes = printSz(byteBuf); 39 | String message = new String(bytes, StandardCharsets.UTF_8); 40 | 41 | if (bytes.length > 0) { 42 | //判断是否有代理 43 | if (message.contains("PROXY")) { 44 | logger.info("PROXY MSG: " + message.substring(0, message.length() - 2)); 45 | if (message.contains("\n")) { 46 | String[] str = message.split("\n")[0].split(" "); 47 | logger.info("Real Client IP: " + str[2]); 48 | Attribute channelAttr = ctx.channel().attr(key); 49 | //基于channel的属性 50 | if (null == channelAttr.get()) { 51 | channelAttr.set(str[2]); 52 | } 53 | } 54 | 55 | //清空数据,重要不能省略 56 | byteBuf.clear(); 57 | } 58 | 59 | if (byteBuf.readableBytes() > 0) { 60 | out.add(byteBuf.readBytes(byteBuf.readableBytes())); 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * 打印 byte 数组 67 | */ 68 | public static byte[] printSz(ByteBuf newBuf) { 69 | ByteBuf copy = newBuf.copy(); 70 | byte[] bytes = new byte[copy.readableBytes()]; 71 | copy.readBytes(bytes); 72 | return bytes; 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/httpserver/HttpServerInitializer.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.httpserver; 2 | 3 | import github.kasuminova.balloonserver.configurations.IntegratedServerConfig; 4 | import github.kasuminova.balloonserver.servers.localserver.IntegratedServerInterface; 5 | import github.kasuminova.balloonserver.utils.GUILogger; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.ChannelPipeline; 8 | import io.netty.channel.socket.SocketChannel; 9 | import io.netty.handler.codec.http.*; 10 | import io.netty.handler.ssl.SslHandler; 11 | import io.netty.handler.stream.ChunkedWriteHandler; 12 | 13 | import javax.net.ssl.SSLEngine; 14 | import java.io.File; 15 | import java.io.InputStream; 16 | import java.nio.file.Files; 17 | 18 | /** 19 | * @author Kasumi_Nova 20 | */ 21 | public class HttpServerInitializer extends ChannelInitializer { 22 | private final boolean useSsl; 23 | private final GUILogger logger; 24 | private final IntegratedServerInterface serverInterface; 25 | File jks; 26 | char[] jksPasswd; 27 | 28 | public HttpServerInitializer(IntegratedServerInterface serverInterface) { 29 | this.serverInterface = serverInterface; 30 | 31 | IntegratedServerConfig config = serverInterface.getIntegratedServerConfig(); 32 | this.logger = serverInterface.getLogger(); 33 | 34 | if (config.getJksFilePath().isEmpty() || config.getJksSslPassword().isEmpty()) { 35 | useSsl = false; 36 | return; 37 | } 38 | 39 | this.jks = new File(config.getJksFilePath()); 40 | this.jksPasswd = config.getJksSslPassword().toCharArray(); 41 | 42 | if (jks.exists()) { 43 | if (jksPasswd != null && jksPasswd.length != 0) { 44 | useSsl = true; 45 | logger.info("成功载入 JKS 证书与密码, 使用 HTTPS 协议。"); 46 | } else { 47 | useSsl = false; 48 | logger.warn("检测到 JKS 证书, 但是 JKS 证书密码为空, 使用 HTTP 协议。"); 49 | } 50 | } else { 51 | useSsl = false; 52 | logger.info("未检测到 JKS 证书, 使用 HTTP 协议。"); 53 | } 54 | } 55 | 56 | public boolean isUseSsl() { 57 | return useSsl; 58 | } 59 | 60 | @Override 61 | protected void initChannel(SocketChannel channel) throws Exception { 62 | ChannelPipeline pipeline = channel.pipeline(); 63 | 64 | //SSL, 使用 JKS 格式证书 65 | if (useSsl) { 66 | InputStream jksInputStream = Files.newInputStream(jks.toPath()); 67 | SSLEngine engine = SslContextFactoryOne.getServerContext(jksInputStream, jksPasswd).createSSLEngine(); 68 | //设置服务端模式 69 | engine.setUseClientMode(false); 70 | //单向认证 71 | engine.setNeedClientAuth(false); 72 | pipeline.addFirst("ssl", new SslHandler(engine)); 73 | } 74 | 75 | pipeline.addLast("http-codec", new HttpServerCodec()); 76 | //反向代理适配器 77 | pipeline.addLast("proxy-decoder", new DecodeProxy(logger)); 78 | pipeline.addLast("http-chunked", new ChunkedWriteHandler()); 79 | pipeline.addLast("http-aggregator", new HttpObjectAggregator(65536)); 80 | //gzip 压缩 81 | // pipeline.addLast("http-compressor",new HttpContentCompressor()); 82 | //请求处理器 83 | pipeline.addLast("http-handler", new HttpRequestHandler(serverInterface)); 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/httpserver/HttpServerInterface.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.httpserver; 2 | 3 | /** 4 | * HTTP 服务器的接口,用于控制启动状态 5 | *

TODO:更多功能待更新

6 | */ 7 | public interface HttpServerInterface { 8 | 9 | /** 10 | * 启动服务器 11 | * @return 是否启动成功 12 | */ 13 | boolean startServer(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/httpserver/SslContextFactoryOne.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.httpserver; 2 | 3 | import javax.net.ssl.KeyManagerFactory; 4 | import javax.net.ssl.SSLContext; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.security.KeyStore; 8 | 9 | import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_LOGGER; 10 | 11 | /** 12 | * 用于加载 SSL 证书的类(网上抄的) 13 | * 14 | * @author Kasumi_Nova 15 | */ 16 | public class SslContextFactoryOne { 17 | private static final String PROTOCOL = "TLS"; 18 | 19 | /** 20 | * 服务器安全套接字协议 21 | */ 22 | private static SSLContext serverContext = null; 23 | 24 | // 使用KeyTool生成密钥库和密钥时配置的密码 25 | 26 | public static SSLContext getServerContext(InputStream jks, char[] pass) { 27 | if (serverContext != null) { 28 | return serverContext; 29 | } 30 | try { 31 | //密钥库KeyStore 32 | KeyStore ks = KeyStore.getInstance("JKS"); 33 | //加载服务端证书 34 | //加载服务端的 KeyStore, KeyStore 是生成仓库时设置的密码,用于检查密钥库完整性的密码 35 | ks.load(jks, pass); 36 | 37 | //密钥管理器 38 | KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); 39 | //初始化密钥管理器 40 | kmf.init(ks, pass); 41 | //获取安全套接字协议(TLS协议)的对象 42 | serverContext = SSLContext.getInstance(PROTOCOL); 43 | //初始化此上下文 44 | //参数一:认证的密钥 参数二:对等信任认证 参数三:伪随机数生成器。由于单向认证,服务端不用验证客户端,所以第二个参数为 null 45 | serverContext.init(kmf.getKeyManagers(), null, null); 46 | } catch (Exception e) { 47 | throw new Error("Failed to initialize the server-side SSLContext", e); 48 | } finally { 49 | try { 50 | jks.close(); 51 | } catch (IOException e) { 52 | GLOBAL_LOGGER.error(e); 53 | } 54 | } 55 | return serverContext; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/remoteclient/AbstractRemoteClientChannel.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.remoteclient; 2 | 3 | import github.kasuminova.balloonserver.BalloonServer; 4 | import github.kasuminova.balloonserver.configurations.RemoteClientConfig; 5 | import github.kasuminova.balloonserver.servers.remoteserver.RemoteClientInterface; 6 | import github.kasuminova.balloonserver.utils.GUILogger; 7 | import github.kasuminova.messages.MessageProcessor; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.SimpleChannelInboundHandler; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public abstract class AbstractRemoteClientChannel extends SimpleChannelInboundHandler { 15 | protected static final int TIMEOUT = 5000; 16 | protected final RemoteClientInterface serverInterface; 17 | protected final GUILogger logger; 18 | protected final RemoteClientConfig config; 19 | protected final Map, MessageProcessor> messageProcessors = new HashMap<>(4); 20 | protected ChannelHandlerContext ctx; 21 | 22 | public AbstractRemoteClientChannel(GUILogger logger, RemoteClientInterface serverInterface) { 23 | this.logger = logger; 24 | this.serverInterface = serverInterface; 25 | config = serverInterface.getRemoteClientConfig(); 26 | } 27 | 28 | /** 29 | * 处理接收到的消息 30 | */ 31 | @Override 32 | public void channelRead0(ChannelHandlerContext ctx, Object msg) { 33 | MessageProcessor processor = (MessageProcessor) messageProcessors.get(msg.getClass()); 34 | if (processor != null) { 35 | processor.process(msg); 36 | } else { 37 | ctx.fireChannelRead(msg); 38 | } 39 | } 40 | 41 | @Override 42 | public final void channelRegistered(ChannelHandlerContext ctx) throws Exception { 43 | onRegisterMessages(); 44 | 45 | super.channelRegistered(ctx); 46 | } 47 | 48 | @Override 49 | public final void channelActive(ChannelHandlerContext ctx) throws Exception { 50 | this.ctx = ctx; 51 | 52 | channelActive0(); 53 | 54 | super.channelActive(ctx); 55 | } 56 | 57 | @Override 58 | public final void channelInactive(ChannelHandlerContext ctx) throws Exception { 59 | channelInactive0(); 60 | 61 | super.channelInactive(ctx); 62 | } 63 | 64 | @Override 65 | public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 66 | exceptionCaught0(cause); 67 | 68 | ctx.close(); 69 | } 70 | 71 | /** 72 | * 注册消息以及对应的处理器 73 | * @param clazz 消息类型 74 | * @param processor 消息处理函数 75 | */ 76 | public void registerMessage(Class clazz, MessageProcessor processor) { 77 | messageProcessors.put(clazz, processor); 78 | if (BalloonServer.CONFIG.isDebugMode()) { 79 | logger.debug("Registered Message {}", clazz.getName()); 80 | } 81 | } 82 | 83 | /** 84 | * 开始注册消息事件 85 | */ 86 | protected void onRegisterMessages() { 87 | 88 | } 89 | /** 90 | * 通道启用事件 91 | */ 92 | protected void channelActive0() { 93 | 94 | } 95 | 96 | /** 97 | * 通道关闭事件 98 | */ 99 | protected void channelInactive0() { 100 | 101 | } 102 | 103 | /** 104 | * 通道出现问题时 105 | * @param cause 错误 106 | */ 107 | protected void exceptionCaught0(Throwable cause) { 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/remoteclient/LastChannel.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.remoteclient; 2 | 3 | import github.kasuminova.balloonserver.utils.GUILogger; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.SimpleChannelInboundHandler; 6 | 7 | public final class LastChannel extends SimpleChannelInboundHandler { 8 | private final GUILogger logger; 9 | 10 | public LastChannel(GUILogger logger) { 11 | this.logger = logger; 12 | } 13 | 14 | @Override 15 | protected void channelRead0(ChannelHandlerContext ctx, Object msg) { 16 | logger.warn("Invalid Message: {}", msg.toString()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/remoteclient/RemoteClient.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.remoteclient; 2 | 3 | import github.kasuminova.balloonserver.configurations.RemoteClientConfig; 4 | import github.kasuminova.balloonserver.servers.remoteserver.RemoteClientInterface; 5 | import github.kasuminova.balloonserver.utils.GUILogger; 6 | import io.netty.bootstrap.Bootstrap; 7 | import io.netty.channel.ChannelFuture; 8 | import io.netty.channel.EventLoopGroup; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.channel.socket.nio.NioSocketChannel; 11 | 12 | public class RemoteClient { 13 | private final GUILogger logger; 14 | private final RemoteClientInterface serverInterface; 15 | private final RemoteClientConfig config; 16 | EventLoopGroup group; 17 | ChannelFuture future; 18 | 19 | public RemoteClient(GUILogger logger, RemoteClientInterface serverInterface) { 20 | this.logger = logger; 21 | this.serverInterface = serverInterface; 22 | config = serverInterface.getRemoteClientConfig(); 23 | } 24 | 25 | public void connect() throws Exception { 26 | group = new NioEventLoopGroup(); 27 | 28 | Bootstrap bootstrap = new Bootstrap(); 29 | bootstrap.group(group) 30 | .channel(NioSocketChannel.class) 31 | .handler(new RemoteClientInitializer(logger, serverInterface)); 32 | 33 | logger.info("连接中..."); 34 | future = bootstrap.connect(config.getIp(), config.getPort()).sync(); 35 | } 36 | 37 | public void disconnect() { 38 | try { 39 | group.shutdownGracefully(); 40 | 41 | future.channel().closeFuture().sync(); 42 | } catch (Exception e) { 43 | logger.error(e); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/remoteclient/RemoteClientChannel.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.remoteclient; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import github.kasuminova.balloonserver.BalloonServer; 5 | import github.kasuminova.balloonserver.gui.fileobjectbrowser.FileObjectBrowser; 6 | import github.kasuminova.balloonserver.utils.fileobject.SimpleDirectoryObject; 7 | import github.kasuminova.messages.*; 8 | import github.kasuminova.balloonserver.servers.remoteserver.RemoteClientInterface; 9 | import github.kasuminova.balloonserver.utils.GUILogger; 10 | 11 | import java.util.Timer; 12 | import java.util.TimerTask; 13 | 14 | public class RemoteClientChannel extends AbstractRemoteClientChannel { 15 | private final Timer timeOutListener = new Timer(); 16 | 17 | public RemoteClientChannel(GUILogger logger, RemoteClientInterface serverInterface) { 18 | super(logger, serverInterface); 19 | } 20 | 21 | @Override 22 | public void onRegisterMessages() { 23 | //认证消息 24 | registerMessage(AuthSuccessMessage.class, (MessageProcessor) this::onAuthSuccess); 25 | //日志消息 26 | registerMessage(LogMessage.class, (MessageProcessor) this::processLogMsg); 27 | //状态消息 28 | registerMessage(StatusMessage.class, (MessageProcessor) this::updateStatus); 29 | //文件夹消息 30 | registerMessage(SimpleDirectoryObject.class, (MessageProcessor) RemoteClientChannel::showFileObjectBrowser); 31 | } 32 | 33 | @Override 34 | protected void channelActive0() { 35 | logger.info("已连接至服务器, 认证中..."); 36 | timeOutListener.schedule(new TimerTask() { 37 | @Override 38 | public void run() { 39 | logger.warn("认证超时."); 40 | timeOutListener.cancel(); 41 | ctx.close(); 42 | } 43 | }, TIMEOUT, TIMEOUT); 44 | ctx.writeAndFlush(new TokenMessage(config.getToken(), BalloonServer.VERSION)); 45 | } 46 | 47 | @Override 48 | protected void channelInactive0() { 49 | serverInterface.onDisconnected(); 50 | logger.info("已从服务器断开连接."); 51 | } 52 | 53 | @Override 54 | protected void exceptionCaught0(Throwable cause) { 55 | logger.warn("出现问题, 已断开连接: {}", cause); 56 | } 57 | 58 | private void onAuthSuccess(AuthSuccessMessage message) { 59 | serverInterface.onConnected(message.getConfig()); 60 | serverInterface.setChannel(ctx); 61 | timeOutListener.cancel(); 62 | } 63 | 64 | private void processLogMsg(LogMessage message) { 65 | switch (message.getLevel()) { 66 | case "INFO" -> logger.info(message.getMessage()); 67 | case "WARN" -> logger.warn(message.getMessage()); 68 | case "ERROR" -> logger.error(message.getMessage()); 69 | case "DEBUG" -> logger.debug(message.getMessage()); 70 | } 71 | } 72 | 73 | private void updateStatus(StatusMessage statusMessage) { 74 | serverInterface.updateStatus( 75 | StrUtil.format("{} M / {} M - Max: {} M", 76 | statusMessage.getUsed(), statusMessage.getTotal(), statusMessage.getMax()), 77 | (int) ((double) (statusMessage.getUsed() * 500) / statusMessage.getTotal()), 78 | statusMessage.getRunningThreadCount(), 79 | statusMessage.getClientIP()); 80 | } 81 | 82 | private static void showFileObjectBrowser(SimpleDirectoryObject directoryObject) { 83 | FileObjectBrowser objectBrowser = new FileObjectBrowser(directoryObject); 84 | objectBrowser.setVisible(true); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/remoteclient/RemoteClientInitializer.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.remoteclient; 2 | 3 | import github.kasuminova.balloonserver.servers.remoteserver.RemoteClientInterface; 4 | import github.kasuminova.balloonserver.utils.GUILogger; 5 | import io.netty.channel.ChannelInitializer; 6 | import io.netty.channel.ChannelPipeline; 7 | import io.netty.channel.socket.SocketChannel; 8 | import io.netty.handler.codec.serialization.ClassResolvers; 9 | import io.netty.handler.codec.serialization.ObjectDecoder; 10 | import io.netty.handler.codec.serialization.ObjectEncoder; 11 | 12 | public class RemoteClientInitializer extends ChannelInitializer { 13 | private final GUILogger logger; 14 | private final RemoteClientInterface serverInterface; 15 | public RemoteClientInitializer(GUILogger logger, RemoteClientInterface serverInterface) { 16 | this.logger = logger; 17 | this.serverInterface = serverInterface; 18 | } 19 | 20 | @Override 21 | protected void initChannel(SocketChannel channel) { 22 | ChannelPipeline pipeline = channel.pipeline(); 23 | 24 | //编码器 25 | pipeline.addFirst(new ObjectEncoder()); 26 | //解码器 27 | pipeline.addFirst(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.weakCachingResolver(null))); 28 | 29 | pipeline.addLast("mainChannel", new RemoteClientChannel(logger, serverInterface)); 30 | pipeline.addLast("fileObjectChannel", new RemoteClientFileChannel(logger, serverInterface)); 31 | pipeline.addLast("lastChannel", new LastChannel(logger)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/AbstractServer.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers; 2 | 3 | import github.kasuminova.balloonserver.utils.GUILogger; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | 9 | public abstract class AbstractServer { 10 | protected final long start = System.currentTimeMillis(); 11 | protected final List commonModeList = new ArrayList<>(0); 12 | protected final List onceModeList = new ArrayList<>(0); 13 | //服务器启动状态 14 | protected final AtomicBoolean isStarted = new AtomicBoolean(false); 15 | protected final AtomicBoolean isStarting = new AtomicBoolean(false); 16 | //服务端是否在生成缓存,防止同一时间多个线程生成缓存导致程序混乱 17 | protected final AtomicBoolean isGenerating = new AtomicBoolean(false); 18 | protected final GUILogger logger; 19 | protected final String serverName; 20 | 21 | protected AbstractServer(String serverName) { 22 | this.serverName = serverName; 23 | 24 | //设置 Logger,主体为 logPanel 25 | logger = new GUILogger(serverName); 26 | } 27 | 28 | protected abstract ServerInterface getServerInterface(); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/GUIServerInterface.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers; 2 | 3 | import github.kasuminova.balloonserver.gui.SmoothProgressBar; 4 | 5 | import java.awt.*; 6 | 7 | public interface GUIServerInterface extends ServerInterface { 8 | String getResJsonFileExtensionName(); 9 | 10 | String getLegacyResJsonFileExtensionName(); 11 | 12 | //获取状态栏进度条 13 | SmoothProgressBar getStatusProgressBar(); 14 | 15 | void setStatusLabelText(String text, Color fg); 16 | 17 | void resetStatusProgressBar(); 18 | } -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/LogPaneMouseAdapter.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers; 2 | 3 | import github.kasuminova.balloonserver.utils.MiscUtils; 4 | 5 | import javax.swing.*; 6 | import java.awt.event.MouseAdapter; 7 | import java.awt.event.MouseEvent; 8 | 9 | public class LogPaneMouseAdapter extends MouseAdapter { 10 | private final JPopupMenu logPaneMenu; 11 | private final JTextPane logPane; 12 | 13 | public LogPaneMouseAdapter(JPopupMenu logPaneMenu, JTextPane logPane) { 14 | this.logPaneMenu = logPaneMenu; 15 | this.logPane = logPane; 16 | } 17 | 18 | @Override 19 | public void mouseReleased(MouseEvent e) { 20 | if (e.isPopupTrigger()) MiscUtils.showPopupMenu(logPaneMenu, logPane, e); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/ServerInterface.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers; 2 | 3 | import github.kasuminova.balloonserver.configurations.IntegratedServerConfig; 4 | 5 | import java.util.concurrent.atomic.AtomicBoolean; 6 | 7 | public interface ServerInterface { 8 | IntegratedServerConfig getIntegratedServerConfig(); 9 | 10 | String getServerName(); 11 | 12 | /** 13 | * 重新生成缓存 14 | */ 15 | void regenCache(); 16 | 17 | /** 18 | * 关闭服务器 19 | * 20 | * @return 是否关闭成功 21 | */ 22 | boolean stopServer(); 23 | 24 | /** 25 | * 保存配置 26 | */ 27 | void saveConfig(); 28 | 29 | //设置新的旧版文件结构 JSON 30 | void setLegacyResJson(String newLegacyResJson); 31 | 32 | //获取旧版文件结构 JSON 33 | String getLegacyResJson(); 34 | 35 | AtomicBoolean isGenerating(); 36 | 37 | AtomicBoolean isStarted(); 38 | 39 | //获取文件结构 JSON 40 | String getResJson(); 41 | 42 | //设置新的文件结构 JSON 43 | void setResJson(String newResJson); 44 | 45 | //获取 index.json 字符串 46 | String getIndexJson(); 47 | } -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/localserver/AddUpdateRule.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers.localserver; 2 | 3 | import github.kasuminova.balloonserver.gui.ruleeditor.RuleEditor; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | import java.util.List; 10 | 11 | public class AddUpdateRule implements ActionListener { 12 | private final JList modeList; 13 | private final List rules; 14 | private final Container container; 15 | 16 | public AddUpdateRule(JList modeList, List rules, Container container) { 17 | this.modeList = modeList; 18 | this.rules = rules; 19 | this.container = container; 20 | } 21 | 22 | @Override 23 | public void actionPerformed(ActionEvent e) { 24 | String newRule = JOptionPane.showInputDialog(container, 25 | "请输入更新规则: ", RuleEditor.TITLE, 26 | JOptionPane.INFORMATION_MESSAGE); 27 | if (newRule != null && !newRule.isEmpty()) { 28 | //防止插入相同内容 29 | if (rules.contains(newRule)) { 30 | JOptionPane.showMessageDialog(container, 31 | "重复的更新规则", RuleEditor.TITLE, 32 | JOptionPane.ERROR_MESSAGE); 33 | } else { 34 | rules.add(newRule); 35 | modeList.setListData(rules.toArray(new String[0])); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/localserver/DeleteUpdateRule.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers.localserver; 2 | 3 | import github.kasuminova.balloonserver.gui.ruleeditor.RuleEditor; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | import java.util.List; 10 | 11 | public class DeleteUpdateRule implements ActionListener { 12 | private final JList modeList; 13 | private final List rules; 14 | private final Container container; 15 | 16 | public DeleteUpdateRule(JList modeList, List rules, Container container) { 17 | this.modeList = modeList; 18 | this.rules = rules; 19 | this.container = container; 20 | } 21 | 22 | @Override 23 | public void actionPerformed(ActionEvent e) { 24 | List selected = modeList.getSelectedValuesList(); 25 | 26 | if (!selected.isEmpty()) { 27 | rules.removeAll(selected); 28 | modeList.setListData(rules.toArray(new String[0])); 29 | } else { 30 | JOptionPane.showMessageDialog(container, 31 | "请选择一个规则后再删除.", RuleEditor.TITLE, 32 | JOptionPane.ERROR_MESSAGE); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/localserver/IntegratedServerInterface.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers.localserver; 2 | 3 | import github.kasuminova.balloonserver.servers.GUIServerInterface; 4 | import github.kasuminova.balloonserver.utils.GUILogger; 5 | 6 | import javax.swing.*; 7 | 8 | /** 9 | * LittleServer 面板向外开放的接口,大部分内容都在此处交互。 10 | */ 11 | public interface IntegratedServerInterface extends GUIServerInterface { 12 | GUILogger getLogger(); 13 | 14 | JPanel getRequestListPanel(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/localserver/ShowOrHideComponentActionListener.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers.localserver; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.ActionEvent; 6 | import java.awt.event.ActionListener; 7 | 8 | public class ShowOrHideComponentActionListener implements ActionListener { 9 | private final Component component; 10 | private final String componentName; 11 | private final JButton button; 12 | 13 | public ShowOrHideComponentActionListener(Component component, String componentName, JButton button) { 14 | this.component = component; 15 | this.componentName = componentName; 16 | this.button = button; 17 | } 18 | 19 | @Override 20 | public void actionPerformed(ActionEvent e) { 21 | if (component.isVisible()) { 22 | component.setVisible(false); 23 | button.setText(String.format("显示%s", componentName)); 24 | } else { 25 | component.setVisible(true); 26 | button.setText(String.format("隐藏%s", componentName)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/servers/remoteserver/RemoteClientInterface.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.servers.remoteserver; 2 | 3 | import com.alibaba.fastjson2.JSONArray; 4 | import github.kasuminova.balloonserver.configurations.IntegratedServerConfig; 5 | import github.kasuminova.balloonserver.configurations.RemoteClientConfig; 6 | import github.kasuminova.balloonserver.servers.ServerInterface; 7 | import io.netty.channel.ChannelHandlerContext; 8 | 9 | public interface RemoteClientInterface extends ServerInterface { 10 | ChannelHandlerContext getChannel(); 11 | void setChannel(ChannelHandlerContext ctx); 12 | 13 | RemoteClientConfig getRemoteClientConfig(); 14 | 15 | void updateStatus(String memBarText, int value, int runningThreadCount, String clientIP); 16 | 17 | void onDisconnected(); 18 | 19 | void onConnected(IntegratedServerConfig config); 20 | 21 | void showCommonRuleEditorDialog(JSONArray jsonArray); 22 | void showOnceRuleEditorDialog(JSONArray jsonArray); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/updatechecker/ApplicationVersion.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.updatechecker; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | 6 | public class ApplicationVersion implements Serializable { 7 | @Serial 8 | private static final long serialVersionUID = 1; 9 | 10 | public static final String BETA = "BETA"; 11 | public static final String STABLE = "STABLE"; 12 | public static final int BIG_VERSION_WEIGHTS = 100000; 13 | public static final int SUB_VERSION_WEIGHTS = 1000; 14 | private final int bigVersion; 15 | private final int subVersion; 16 | private final int minorVersion; 17 | private final String branch; 18 | 19 | /** 20 | * 程序版本对象 21 | * 22 | * @param version 如 1.1.1-BETA, 1.0.0-STABLE 23 | */ 24 | public ApplicationVersion(String version) { 25 | String[] versionTmp = version.split("-", 2); 26 | String[] versions = versionTmp[0].split("\\.", 3); 27 | 28 | bigVersion = Integer.parseInt(versions[0]); 29 | subVersion = Integer.parseInt(versions[1]); 30 | minorVersion = Integer.parseInt(versions[2]); 31 | branch = versionTmp[1]; 32 | } 33 | 34 | public int getMinorVersion() { 35 | return minorVersion; 36 | } 37 | 38 | public int getSubVersion() { 39 | return subVersion; 40 | } 41 | 42 | public String getBranch() { 43 | return branch; 44 | } 45 | 46 | public int getBigVersion() { 47 | return bigVersion; 48 | } 49 | 50 | public String toString() { 51 | return String.format("%s.%s.%s-%s", bigVersion, subVersion, minorVersion, branch); 52 | } 53 | 54 | public int toInt() { 55 | return (bigVersion * BIG_VERSION_WEIGHTS) + (subVersion * SUB_VERSION_WEIGHTS) + minorVersion; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/BatchUtils.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import java.io.IOException; 4 | 5 | public class BatchUtils { 6 | /** 7 | * 执行一系列 bat 指令 8 | * 9 | * @param commands 将被分成每行一个指令 10 | */ 11 | public static void runBatch(String[] commands) throws IOException { 12 | if (commands == null) { 13 | throw new NullPointerException("Commands is Null!"); 14 | } 15 | if (commands.length == 0) { 16 | throw new IllegalArgumentException("Empty Commands!"); 17 | } 18 | 19 | StringBuilder sb = new StringBuilder(32); 20 | for (String s : commands) { 21 | sb.append(s).append("\n"); 22 | } 23 | 24 | String fileName = "batchTmp.bat"; 25 | 26 | FileUtil.createFile(sb.toString(), "./", fileName); 27 | 28 | Runtime.getRuntime().exec(new String[]{String.format(".\\%s", fileName)}); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/CustomThreadFactory.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | 5 | import java.util.concurrent.ThreadFactory; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | public class CustomThreadFactory implements ThreadFactory { 9 | private final String threadName; 10 | private final ThreadGroup group = Thread.currentThread().getThreadGroup(); 11 | private final AtomicInteger threadCount = new AtomicInteger(1); 12 | 13 | public CustomThreadFactory(String threadName) { 14 | this.threadName = threadName; 15 | } 16 | 17 | @Override 18 | public Thread newThread(Runnable r) { 19 | return new Thread(group, r, StrUtil.format(threadName, threadCount.getAndIncrement())); 20 | } 21 | 22 | /** 23 | * 新建一个自定义线程工厂, 此工厂的线程名可自定义 24 | * @param threadName 线程名, 如 "Thread-{}"; "CustomThread-{}", {} 为线程编号. 25 | */ 26 | public static CustomThreadFactory create(String threadName) { 27 | return new CustomThreadFactory(threadName); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/FileUtil.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import cn.hutool.core.io.IORuntimeException; 4 | import cn.hutool.core.io.file.FileReader; 5 | import cn.hutool.core.io.file.FileWriter; 6 | import com.alibaba.fastjson2.JSONArray; 7 | 8 | import javax.swing.filechooser.FileFilter; 9 | import java.io.File; 10 | 11 | public class FileUtil { 12 | public static final int KB = 1024; 13 | public static final int MB = 1024 * 1024; 14 | public static final int GB = 1024 * 1024 * 1024; 15 | 16 | /** 17 | * 生成.json格式文件 18 | */ 19 | public static void createJsonFile(String jsonString, String filePath, String fileName) throws IORuntimeException { 20 | createFile(jsonString, filePath, fileName + ".json"); 21 | } 22 | 23 | /** 24 | * 生成文件 25 | * @param str 内容 26 | * @param filePath 路径 27 | * @param fileName 文件名 28 | */ 29 | public static void createFile(String str, String filePath, String fileName) throws IORuntimeException { 30 | //拼接文件完整路径 31 | File target = new File(filePath + fileName); 32 | //保证创建一个新文件 33 | cn.hutool.core.io.FileUtil.touch(target); 34 | 35 | //写入文件 36 | FileWriter writer = new FileWriter(target); 37 | writer.write(str); 38 | } 39 | 40 | /** 41 | * 根据传入大小返回合适的 int 大小 42 | * 43 | * @param size 文件大小 44 | * @return 根据大小适应的 int 大小 45 | */ 46 | public static int formatFileSizeInt(long size) { 47 | if (size <= KB) { 48 | return (int) size; 49 | } else if (size <= MB) { 50 | return KB * 8; 51 | } else if (size <= MB * 128) { 52 | return KB * 64; 53 | } else if (size <= MB * 512) { 54 | return MB; 55 | } else { 56 | return MB * 8; 57 | } 58 | } 59 | 60 | /** 61 | * 根据传入大小返回合适的 int 大小 62 | * 63 | * @param size 文件大小 64 | * @return 根据大小适应的 int 大小 65 | */ 66 | public static int formatFileSizeSmallInt(long size) { 67 | if (size <= KB) { 68 | return (int) size; 69 | } else if (size <= MB) { 70 | return KB * 8; 71 | } else if (size <= MB * 128) { 72 | return KB * 16; 73 | } else { 74 | return KB * 32; 75 | } 76 | } 77 | 78 | /** 79 | * 根据传入大小返回合适的格式化文本 80 | * 81 | * @param size 文件大小 82 | * @return Byte 或 KB 或 MB 或 GB 83 | */ 84 | public static String formatFileSizeToStr(long size) { 85 | if (size <= KB) { 86 | return size + " Byte"; 87 | } else if (size <= MB) { 88 | return String.format("%.2f", (double) size / KB) + " KB"; 89 | } else if (size <= GB) { 90 | return String.format("%.2f", (double) size / MB) + " MB"; 91 | } else { 92 | return String.format("%.2f", (double) size / GB) + " GB"; 93 | } 94 | } 95 | 96 | /** 97 | * 从指定名称 JSON 文件中读取 JSONArray 98 | * 99 | * @param fileName 文件名 100 | * @param resJsonFileExtensionName 自定义扩展名 101 | * @param logger Logger 102 | * @return JSONArray, 如果文件不存在或出现问题, 则返回 null 103 | */ 104 | public static JSONArray loadJsonArrayFromFile(String fileName, String resJsonFileExtensionName, GUILogger logger) { 105 | File jsonCache = new File(String.format("./%s.%s.json", fileName, resJsonFileExtensionName)); 106 | 107 | JSONArray jsonArray = null; 108 | 109 | if (jsonCache.exists()) { 110 | try { 111 | String jsonString = new FileReader(jsonCache).readString(); 112 | jsonArray = JSONArray.parseArray(jsonString); 113 | } catch (IORuntimeException e) { 114 | if (logger != null) { 115 | logger.error("读取缓存文件的时候出现了一些问题...", e); 116 | logger.warn("缓存文件读取失败, 重新生成缓存..."); 117 | } 118 | } 119 | } 120 | 121 | return jsonArray; 122 | } 123 | 124 | public static class SimpleFileFilter extends FileFilter { 125 | private final String[] ext; 126 | private final String[] blackList; 127 | private final String des; 128 | 129 | /** 130 | * 一个简易的多文件扩展名过滤器 131 | * 132 | * @param ext 扩展名数组,如 png,jpg 133 | * @param blackList 黑名单 134 | * @param des 扩展描述 135 | */ 136 | public SimpleFileFilter(String[] ext, String[] blackList, String des) { 137 | this.ext = ext; 138 | this.blackList = blackList; 139 | this.des = des; 140 | } 141 | 142 | public boolean accept(File file) { 143 | //如果是文件夹则显示文件夹 144 | if (file.isDirectory()) return true; 145 | String fileName = file.getName(); 146 | //黑名单检查 147 | if (blackList != null) { 148 | for (String s : blackList) { 149 | if (fileName.contains(s)) return false; 150 | } 151 | } 152 | for (String extension : ext) { 153 | if (file.getName().endsWith(String.format(".%s", extension))) return true; 154 | } 155 | return false; 156 | } 157 | 158 | public String getDescription() { 159 | return des; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/HashCalculator.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import cn.hutool.core.util.HexUtil; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.FileChannel; 9 | import java.nio.file.Paths; 10 | import java.nio.file.StandardOpenOption; 11 | import java.security.MessageDigest; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.util.concurrent.atomic.AtomicLong; 14 | import java.util.zip.CRC32; 15 | 16 | /** 17 | * 公用 Hash 计算类 18 | */ 19 | public class HashCalculator { 20 | public static final String SHA1 = "sha1"; 21 | public static final String CRC32 = "crc32"; 22 | 23 | /** 24 | * 获取文件 SHA1 25 | * BalloonUpdate 的默认方法 26 | * 27 | * @param file 目标文件 28 | * @return SHA1 值 29 | **/ 30 | public static String getSHA1(File file) throws IOException, NoSuchAlgorithmException { 31 | return getSHA1(file, null); 32 | } 33 | 34 | /** 35 | * 获取文件 CRC32 36 | * 37 | * @param file 目标文件 38 | * @return CRC32 值 39 | */ 40 | public static String getCRC32(File file) throws IOException { 41 | return getCRC32(file, null); 42 | } 43 | 44 | /** 45 | * 获取文件 SHA1 46 | * BalloonUpdate 的默认方法 47 | * 48 | * @param file 目标文件 49 | * @param progress 进度变量 50 | * @return SHA1 值 51 | **/ 52 | public static String getSHA1(File file, AtomicLong progress) throws IOException, NoSuchAlgorithmException { 53 | FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); 54 | ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); 55 | int len; 56 | MessageDigest md = MessageDigest.getInstance("SHA1"); 57 | while ((len = fc.read(byteBuffer)) > 0) { 58 | md.update(byteBuffer.array(), 0, len); 59 | byteBuffer.clear(); 60 | if (progress != null) progress.getAndAdd(len); 61 | } 62 | fc.close(); 63 | 64 | //转换为 16 进制 65 | return HexUtil.encodeHexStr(md.digest()); 66 | } 67 | 68 | /** 69 | * 获取文件 CRC32 70 | * 71 | * @param file 目标文件 72 | * @param progress 进度变量 73 | * @return CRC32 值 74 | */ 75 | public static String getCRC32(File file, AtomicLong progress) throws IOException { 76 | FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); 77 | ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); 78 | int len; 79 | CRC32 crc32 = new CRC32(); 80 | while ((len = fc.read(byteBuffer)) > 0) { 81 | crc32.update(byteBuffer.array(), 0, len); 82 | byteBuffer.clear(); 83 | if (progress != null) progress.getAndAdd(len); 84 | } 85 | fc.close(); 86 | 87 | //转换为 16 进制 88 | return HexUtil.toHex(crc32.getValue()); 89 | } 90 | 91 | /** 92 | * 获取文件 CRC32 和 SHA1 93 | * 94 | * @param file 目标文件 95 | * @param progress 进度变量 96 | * @return CRC32 和 SHA1 值 97 | */ 98 | public static HashStrings getCRC32AndSHA1(File file, AtomicLong progress) throws IOException, NoSuchAlgorithmException { 99 | FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); 100 | ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); 101 | int len; 102 | 103 | CRC32 crc32 = new CRC32(); 104 | MessageDigest md = MessageDigest.getInstance("SHA1"); 105 | 106 | while ((len = fc.read(byteBuffer)) > 0) { 107 | crc32.update(byteBuffer.array(), 0, len); 108 | md.update(byteBuffer.array(), 0, len); 109 | byteBuffer.clear(); 110 | progress.getAndAdd(len); 111 | } 112 | fc.close(); 113 | 114 | //转为 16 进制 115 | String CRC32String = HexUtil.toHex(crc32.getValue()); 116 | String SHA1String = HexUtil.encodeHexStr(md.digest()); 117 | 118 | return new HashStrings(CRC32String, SHA1String); 119 | } 120 | 121 | /** 122 | * 获取文件 CRC32 和 SHA1 123 | * 124 | * @param file 目标文件 125 | * @return CRC32 和 SHA1 值 126 | */ 127 | public static HashStrings getCRC32AndSHA1(File file) throws IOException, NoSuchAlgorithmException { 128 | FileChannel fc = FileChannel.open(Paths.get(file.toURI()), StandardOpenOption.READ); 129 | ByteBuffer byteBuffer = ByteBuffer.allocate(FileUtil.formatFileSizeInt(file.length())); 130 | int len; 131 | 132 | CRC32 crc32 = new CRC32(); 133 | MessageDigest md = MessageDigest.getInstance("SHA1"); 134 | 135 | while ((len = fc.read(byteBuffer)) > 0) { 136 | crc32.update(byteBuffer.array(), 0, len); 137 | md.update(byteBuffer.array(), 0, len); 138 | byteBuffer.clear(); 139 | } 140 | fc.close(); 141 | 142 | //转为 16 进制 143 | String CRC32String = HexUtil.toHex(crc32.getValue()); 144 | String SHA1String = HexUtil.encodeHexStr(md.digest()); 145 | 146 | return new HashStrings(CRC32String, SHA1String); 147 | } 148 | } -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/HashStrings.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | public class HashStrings { 4 | private final String crc32; 5 | private final String sha1; 6 | 7 | public HashStrings(String CRC32, String SHA1) { 8 | this.crc32 = CRC32; 9 | this.sha1 = SHA1; 10 | } 11 | 12 | public HashStrings(String CRC32) { 13 | this.crc32 = CRC32; 14 | this.sha1 = null; 15 | } 16 | 17 | public String getCrc32() { 18 | return crc32; 19 | } 20 | 21 | public String getSha1() { 22 | return sha1; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/IPAddressUtil.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import java.net.Inet4Address; 4 | import java.net.Inet6Address; 5 | import java.net.InetAddress; 6 | import java.net.UnknownHostException; 7 | 8 | public class IPAddressUtil { 9 | /** 10 | * 判断地址是 IPv4 还是 IPv6. 11 | * 12 | * @param address 要验证的 IP 地址 13 | * @return "v6" "v4" 如果两者都不是,返回 null 14 | */ 15 | public static String checkAddress(String address) { 16 | try { 17 | InetAddress iNetAddress = InetAddress.getByName(address); 18 | if (iNetAddress instanceof Inet6Address) return "v6"; 19 | if (iNetAddress instanceof Inet4Address) return "v4"; 20 | } catch (UnknownHostException ignored) {} 21 | return null; 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/MiscUtils.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.MouseEvent; 6 | import java.io.PrintWriter; 7 | import java.io.StringWriter; 8 | import java.net.URI; 9 | 10 | import static github.kasuminova.balloonserver.BalloonServer.*; 11 | 12 | public class MiscUtils { 13 | /** 14 | * 使用系统默认浏览器打开指定链接 15 | * 16 | * @param url 链接 17 | */ 18 | public static void openLinkInBrowser(String url) { 19 | Desktop desktop = Desktop.getDesktop(); 20 | if (desktop.isSupported(Desktop.Action.BROWSE)) { 21 | try { 22 | desktop.browse(URI.create(url)); 23 | } catch (Exception ex) { 24 | GLOBAL_LOGGER.error(ex); 25 | } 26 | } else { 27 | JOptionPane.showMessageDialog(MAIN_FRAME, 28 | "当前系统没有默认浏览器或不支持!", TITLE, 29 | JOptionPane.ERROR_MESSAGE); 30 | } 31 | } 32 | 33 | /** 34 | *

35 | * 将错误打印成字符串。 36 | *

37 | *

38 | * 类似 printStackTrace()。 39 | *

40 | * 41 | * @param e Exception 42 | * @return 字符串 43 | */ 44 | public static String stackTraceToString(Throwable e) { 45 | StringWriter sw = new StringWriter(); 46 | PrintWriter pw = new PrintWriter(sw); 47 | e.printStackTrace(pw); 48 | pw.flush(); 49 | return sw.toString(); 50 | } 51 | 52 | public static String formatTime(long time) { 53 | if (time < 1000 * 10) { 54 | return String.format("%.3fs", (double) time / 1000); 55 | } else if (time < 1000 * 100) { 56 | return String.format("%.2fs", (double) time / 1000); 57 | } else if (time < 1000 * 1000) { 58 | return String.format("%.1fs", (double) time / 1000); 59 | } else { 60 | return String.format("%ss", time / 1000); 61 | } 62 | } 63 | 64 | /** 65 | * 显示弹出式菜单 66 | * 修复默认方法的几个奇怪的 BUG. 67 | * @param menu 菜单 68 | * @param invoker 调用者 69 | * @param e 鼠标事件 70 | */ 71 | public static void showPopupMenu(JPopupMenu menu, Component invoker, MouseEvent e) { 72 | menu.setInvoker(invoker); 73 | menu.setLocation(e.getLocationOnScreen().x + 5, e.getLocationOnScreen().y + 5); 74 | menu.setVisible(true); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/ModernColors.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import java.awt.*; 4 | 5 | public class ModernColors { 6 | public static final Color BLUE = new Color(30, 150,255); 7 | public static final Color GREEN = new Color(50, 235,110); 8 | public static final Color YELLOW = new Color(255,200,0); 9 | public static final Color RED = new Color(255,85, 75); 10 | public static final Color PURPLE = new Color(180,85, 255); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/NextFileListener.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import cn.hutool.core.io.watch.WatchMonitor; 4 | import cn.hutool.core.io.watch.Watcher; 5 | 6 | import java.nio.file.Path; 7 | import java.nio.file.WatchEvent; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | 10 | /** 11 | * 文件监听器 12 | */ 13 | public class NextFileListener { 14 | private final WatchMonitor monitor; 15 | 16 | public NextFileListener(String path, AtomicBoolean isFileChanged, GUILogger logger, int maxDepth) { 17 | monitor = WatchMonitor.createAll(path, new FileWatcher(logger, isFileChanged)); 18 | monitor.setMaxDepth(maxDepth); 19 | } 20 | 21 | public void start() { 22 | monitor.start(); 23 | } 24 | 25 | public void stop() { 26 | monitor.close(); 27 | monitor.interrupt(); 28 | } 29 | 30 | private record FileWatcher(GUILogger logger, AtomicBoolean isFileChanged) implements Watcher { 31 | @Override 32 | public void onCreate(WatchEvent event, Path currentPath) { 33 | Object obj = event.context(); 34 | logger.info(String.format("创建: %s -> %s", currentPath, obj)); 35 | isFileChanged.set(true); 36 | } 37 | 38 | @Override 39 | public void onModify(WatchEvent event, Path currentPath) { 40 | Object obj = event.context(); 41 | logger.info(String.format("修改: %s -> %s", currentPath, obj)); 42 | isFileChanged.set(true); 43 | } 44 | 45 | @Override 46 | public void onDelete(WatchEvent event, Path currentPath) { 47 | Object obj = event.context(); 48 | logger.info(String.format("删除: %s -> %s", currentPath, obj)); 49 | isFileChanged.set(true); 50 | } 51 | 52 | @Override 53 | public void onOverflow(WatchEvent event, Path currentPath) { 54 | Object obj = event.context(); 55 | logger.info(String.format("Overflow: %s -> %s", currentPath, obj)); 56 | isFileChanged.set(true); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/NextHashCalculator.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import cn.hutool.core.util.HexUtil; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.RandomAccessFile; 8 | import java.security.MessageDigest; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | import java.util.zip.CRC32; 12 | 13 | /** 14 | * 公用 Hash 计算类 15 | */ 16 | public class NextHashCalculator { 17 | public static final String SHA1 = "sha1"; 18 | public static final String CRC32 = "crc32"; 19 | 20 | /** 21 | * 获取文件 SHA1 22 | * BalloonUpdate 的默认方法 23 | * 24 | * @param file 目标文件 25 | * @return SHA1 值 26 | **/ 27 | public static String getSHA1(File file) throws IOException, NoSuchAlgorithmException { 28 | return getSHA1(file, null); 29 | } 30 | 31 | /** 32 | * 获取文件 CRC32 33 | * 34 | * @param file 目标文件 35 | * @return CRC32 值 36 | */ 37 | public static String getCRC32(File file) throws IOException { 38 | return getCRC32(file, null); 39 | } 40 | 41 | /** 42 | * 获取文件 SHA1 43 | * BalloonUpdate 的默认方法 44 | * 45 | * @param file 目标文件 46 | * @param progress 进度变量 47 | * @return SHA1 值 48 | **/ 49 | public static String getSHA1(File file, AtomicLong progress) throws IOException, NoSuchAlgorithmException { 50 | RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); 51 | byte[] data = new byte[FileUtil.formatFileSizeInt(randomAccessFile.length())]; 52 | int len; 53 | MessageDigest md = MessageDigest.getInstance("SHA1"); 54 | while ((len = randomAccessFile.read(data)) > 0) { 55 | md.update(data, 0, len); 56 | if (progress != null) progress.getAndAdd(len); 57 | } 58 | randomAccessFile.close(); 59 | 60 | //转换为 16 进制 61 | return HexUtil.encodeHexStr(md.digest()); 62 | } 63 | 64 | /** 65 | * 获取文件 CRC32 66 | * 67 | * @param file 目标文件 68 | * @param progress 进度变量 69 | * @return CRC32 值 70 | */ 71 | public static String getCRC32(File file, AtomicLong progress) throws IOException { 72 | RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); 73 | byte[] data = new byte[FileUtil.formatFileSizeInt(randomAccessFile.length())]; 74 | int len; 75 | CRC32 crc32 = new CRC32(); 76 | while ((len = randomAccessFile.read(data)) > 0) { 77 | crc32.update(data, 0, len); 78 | if (progress != null) progress.getAndAdd(len); 79 | } 80 | randomAccessFile.close(); 81 | 82 | //转换为 16 进制 83 | return HexUtil.toHex(crc32.getValue()); 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/Security.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import github.kasuminova.balloonserver.BalloonServer; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.util.HashSet; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | public class Security { 12 | /** 13 | * 检查字符串是否存在非法字符 14 | * 15 | * @param container 对话框父窗口 16 | * @param str 要检查的对象 17 | * @param customUnavailableStrings 自定义非法字符列表 18 | * @return 未通过返回 true, 通过返回 false 19 | */ 20 | public static boolean stringIsUnsafe(Container container, String str, String[] customUnavailableStrings) { 21 | //空字符检查 22 | if (str == null || str.isEmpty()) { 23 | return true; 24 | } 25 | 26 | //非法字符检查 27 | Set unavailableStrList = new HashSet<>(List.of( 28 | ":", "*", "?", "<", ">", "|", 29 | "CON", "AUX", 30 | "COM1", "COM2", "COM3", "COM4", 31 | "LPT1", "LPT2", "LPT3", 32 | "PRN", "NUL")); 33 | 34 | //自定义非法字符 35 | if (customUnavailableStrings != null) unavailableStrList.addAll(List.of(customUnavailableStrings)); 36 | 37 | if (unavailableStrList.contains(str)) { 38 | JOptionPane.showMessageDialog(container, "名称包含非法字符.", BalloonServer.TITLE, JOptionPane.ERROR_MESSAGE); 39 | return true; 40 | } 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/SvgIcons.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils; 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | 9 | /** 10 | * SVG 格式贴图集合 11 | */ 12 | public class SvgIcons { 13 | public static final Map svgIconMap = new HashMap<>(64); 14 | public static void load() { 15 | 16 | } 17 | public static final FlatSVGIcon SERVER_LIST_ICON = new FlatSVGIcon(Objects.requireNonNull( 18 | SvgIcons.class.getResource("/icons/serverList.svg"))); 19 | public static final FlatSVGIcon DEFAULT_SERVER_ICON = new FlatSVGIcon(Objects.requireNonNull( 20 | SvgIcons.class.getResource("/icons/default_server.svg"))); 21 | public static final FlatSVGIcon CUSTOM_SERVER_ICON = new FlatSVGIcon(Objects.requireNonNull( 22 | SvgIcons.class.getResource("/icons/custom_server.svg"))); 23 | public static final FlatSVGIcon SETTINGS_ICON = new FlatSVGIcon(Objects.requireNonNull( 24 | SvgIcons.class.getResource("/icons/settings.svg"))); 25 | public static final FlatSVGIcon ABOUT_ICON = new FlatSVGIcon(Objects.requireNonNull( 26 | SvgIcons.class.getResource("/icons/info.svg"))); 27 | public static final FlatSVGIcon EDIT_ICON = new FlatSVGIcon(Objects.requireNonNull( 28 | SvgIcons.class.getResource("/icons/edit.svg"))); 29 | public static final FlatSVGIcon PLUS_ICON = new FlatSVGIcon(Objects.requireNonNull( 30 | SvgIcons.class.getResource("/icons/plus.svg"))); 31 | public static final FlatSVGIcon REMOVE_ICON = new FlatSVGIcon(Objects.requireNonNull( 32 | SvgIcons.class.getResource("/icons/remove.svg"))); 33 | public static final FlatSVGIcon DELETE_ICON = new FlatSVGIcon(Objects.requireNonNull( 34 | SvgIcons.class.getResource("/icons/delete.svg"))); 35 | public static final FlatSVGIcon RELOAD_ICON = new FlatSVGIcon(Objects.requireNonNull( 36 | SvgIcons.class.getResource("/icons/reload.svg"))); 37 | public static final FlatSVGIcon TERMINAL_ICON = new FlatSVGIcon(Objects.requireNonNull( 38 | SvgIcons.class.getResource("/icons/terminal.svg"))); 39 | public static final FlatSVGIcon STOP_ICON = new FlatSVGIcon(Objects.requireNonNull( 40 | SvgIcons.class.getResource("/icons/stop.svg"))); 41 | public static final FlatSVGIcon RESOURCE_ICON = new FlatSVGIcon(Objects.requireNonNull( 42 | SvgIcons.class.getResource("/icons/resource.svg"))); 43 | public static final FlatSVGIcon PLAY_ICON = new FlatSVGIcon(Objects.requireNonNull( 44 | SvgIcons.class.getResource("/icons/play.svg"))); 45 | public static final FlatSVGIcon DIR_ICON = new FlatSVGIcon(Objects.requireNonNull( 46 | SvgIcons.class.getResource("/icons/file_types/dir.svg"))); 47 | public static final FlatSVGIcon FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 48 | SvgIcons.class.getResource("/icons/file_types/file_default.svg"))); 49 | public static final FlatSVGIcon CLASS_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 50 | SvgIcons.class.getResource("/icons/file_types/class.svg"))); 51 | public static final FlatSVGIcon DOC_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 52 | SvgIcons.class.getResource("/icons/file_types/doc_docx.svg"))); 53 | public static final FlatSVGIcon EXE_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 54 | SvgIcons.class.getResource("/icons/file_types/exe.svg"))); 55 | public static final FlatSVGIcon JAR_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 56 | SvgIcons.class.getResource("/icons/file_types/jar.svg"))); 57 | public static final FlatSVGIcon JAVA_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 58 | SvgIcons.class.getResource("/icons/file_types/java.svg"))); 59 | public static final FlatSVGIcon JPG_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 60 | SvgIcons.class.getResource("/icons/file_types/jpg.svg"))); 61 | public static final FlatSVGIcon JSON_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 62 | SvgIcons.class.getResource("/icons/file_types/json.svg"))); 63 | public static final FlatSVGIcon MD_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 64 | SvgIcons.class.getResource("/icons/file_types/md.svg"))); 65 | public static final FlatSVGIcon PPT_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 66 | SvgIcons.class.getResource("/icons/file_types/ppt_pptx.svg"))); 67 | public static final FlatSVGIcon TXT_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 68 | SvgIcons.class.getResource("/icons/file_types/txt.svg"))); 69 | public static final FlatSVGIcon XLS_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 70 | SvgIcons.class.getResource("/icons/file_types/xls_xlsx.svg"))); 71 | public static final FlatSVGIcon XML_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 72 | SvgIcons.class.getResource("/icons/file_types/xml.svg"))); 73 | public static final FlatSVGIcon YML_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 74 | SvgIcons.class.getResource("/icons/file_types/yml.svg"))); 75 | public static final FlatSVGIcon ZIP_FILE_ICON = new FlatSVGIcon(Objects.requireNonNull( 76 | SvgIcons.class.getResource("/icons/file_types/zip.svg"))); 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/filecacheutils/DirSizeCalculatorThread.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils.filecacheutils; 2 | 3 | import cn.hutool.core.thread.ThreadUtil; 4 | 5 | import java.io.File; 6 | import java.util.ArrayList; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | record DirSizeCalculatorThread(File dir, AtomicLong totalSize, AtomicLong totalFiles) 10 | implements Runnable { 11 | 12 | @Override 13 | public void run() { 14 | File[] fileList = dir.listFiles(); 15 | ArrayList threadList = new ArrayList<>(4); 16 | if (fileList != null) { 17 | for (File value : fileList) { 18 | if (!value.isDirectory()) { 19 | //计算大小 20 | totalSize.getAndAdd(value.length()); 21 | //计算文件 22 | totalFiles.getAndIncrement(); 23 | } else { 24 | Thread thread = new Thread(new DirSizeCalculatorThread(value, totalSize, totalFiles)); 25 | threadList.add(thread); 26 | thread.start(); 27 | } 28 | } 29 | } 30 | for (Thread thread : threadList) { 31 | ThreadUtil.waitForDie(thread); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/filecacheutils/FileCacheCalculator.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils.filecacheutils; 2 | 3 | import github.kasuminova.balloonserver.gui.SmoothProgressBar; 4 | import github.kasuminova.balloonserver.utils.fileobject.*; 5 | import github.kasuminova.balloonserver.utils.FileUtil; 6 | 7 | import javax.swing.*; 8 | import java.io.File; 9 | import java.util.ArrayList; 10 | import java.util.concurrent.*; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.concurrent.atomic.AtomicLong; 13 | 14 | import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_THREAD_POOL; 15 | import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_IO_THREAD_POOL; 16 | 17 | /** 18 | * 计算资源缓存的公用类 19 | */ 20 | public class FileCacheCalculator { 21 | private final AtomicLong completedBytes = new AtomicLong(0); 22 | private final AtomicInteger completedFiles = new AtomicInteger(0); 23 | private final String hashAlgorithm; 24 | public FileCacheCalculator(String hashAlgorithm) { 25 | this.hashAlgorithm = hashAlgorithm; 26 | } 27 | 28 | /** 29 | * 扫描目标文件夹内的文件与文件夹 30 | * 31 | * @param directory 目标文件夹 32 | * @return ArrayList, 如果文件夹内容为空则返回空 ArrayList 33 | */ 34 | public ArrayList scanDir(File directory) { 35 | File[] fileList = directory.listFiles(); 36 | if (fileList == null) { 37 | return new ArrayList<>(0); 38 | } 39 | ArrayList> fileCounterTaskList = new ArrayList<>(32); 40 | ArrayList> direCounterTaskList = new ArrayList<>(8); 41 | ArrayList abstractSimpleFileObjectList = new ArrayList<>(32); 42 | 43 | for (File file : fileList) { 44 | if (file.isFile()) { 45 | FutureTask fileInfoTask = new FutureTask<>(new FileInfoTask(file, hashAlgorithm, completedBytes, completedFiles)); 46 | fileCounterTaskList.add(fileInfoTask); 47 | GLOBAL_IO_THREAD_POOL.execute(fileInfoTask); 48 | } else { 49 | FutureTask dirCounterTask = new FutureTask<>(new DirInfoTask(file, hashAlgorithm, completedBytes, completedFiles)); 50 | direCounterTaskList.add(dirCounterTask); 51 | GLOBAL_THREAD_POOL.execute(dirCounterTask); 52 | } 53 | } 54 | 55 | for (FutureTask simpleDirectoryObjectFutureTask : direCounterTaskList) { 56 | try { 57 | abstractSimpleFileObjectList.add(simpleDirectoryObjectFutureTask.get()); 58 | } catch (Exception ignored) {} 59 | } 60 | 61 | for (FutureTask simpleFileObjectFutureTask : fileCounterTaskList) { 62 | try { 63 | abstractSimpleFileObjectList.add(simpleFileObjectFutureTask.get()); 64 | } catch (Exception ignored) {} 65 | } 66 | 67 | return abstractSimpleFileObjectList; 68 | } 69 | 70 | /** 71 | * 计算文件夹内容大小 72 | */ 73 | private static class FileCounter { 74 | private final AtomicLong totalSize = new AtomicLong(0); 75 | private final AtomicLong totalFiles = new AtomicLong(); 76 | 77 | private long[] getFiles(File dir, SmoothProgressBar statusProgressBar) { 78 | statusProgressBar.setString("扫描文件夹内容... (0 Byte, 0 文件)"); 79 | Timer timer = new Timer(250, e -> statusProgressBar.setString( 80 | String.format("扫描文件夹内容... (%s, %s 文件)", 81 | FileUtil.formatFileSizeToStr(totalSize.get()), 82 | totalFiles.get()))); 83 | timer.start(); 84 | 85 | statusProgressBar.setVisible(true); 86 | statusProgressBar.setIndeterminate(true); 87 | 88 | new DirSizeCalculatorThread(dir, totalSize, totalFiles).run(); 89 | timer.stop(); 90 | 91 | return new long[]{totalSize.get(), totalFiles.get()}; 92 | } 93 | } 94 | 95 | /** 96 | *

97 | * 统计目标文件夹内包含的 文件/文件夹 大小. 98 | *

99 | * 100 | *

101 | * 并将其大小整合在一起至一个变量, 用于轮询线程的查询. 102 | *

103 | * 104 | *

105 | * size[0] 为总大小 106 | *

107 | * 108 | *

109 | * size[1] 为总文件数量 110 | *

111 | */ 112 | public static long[] getDirSize(File dir, SmoothProgressBar statusProgressBar) { 113 | return new FileCounter().getFiles(dir, statusProgressBar); 114 | } 115 | 116 | public long getCompletedBytes() { 117 | return completedBytes.get(); 118 | } 119 | 120 | public int getCompletedFiles() { 121 | return completedFiles.get(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/fileobject/AbstractSimpleFileObject.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils.fileobject; 2 | 3 | import com.alibaba.fastjson2.JSONArray; 4 | import com.alibaba.fastjson2.JSONObject; 5 | 6 | import java.io.Serial; 7 | import java.io.Serializable; 8 | import java.util.ArrayList; 9 | 10 | public abstract class AbstractSimpleFileObject implements Serializable { 11 | @Serial 12 | private static final long serialVersionUID = 1L; 13 | 14 | String name; 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | /** 25 | * 将 JSONObject 转为 AbstractSimpleFileObject 26 | * 27 | * @param obj 要转换的 JSONObject 28 | * @return SimpleFileObject 或 SimpleDirectoryObject 29 | */ 30 | public static AbstractSimpleFileObject jsonObjectToFileObject(JSONObject obj) { 31 | if (fileObjIsDirectoryObj(obj)) { 32 | return new SimpleDirectoryObject(obj.getString("name"), jsonArrToFileObjArr(obj.getJSONArray("children"))); 33 | } else { 34 | return new SimpleFileObject( 35 | obj.getString("name"), 36 | obj.getLong("length"), 37 | obj.getString("hash"), 38 | obj.getLong("modified")); 39 | } 40 | } 41 | 42 | /** 43 | * 将 JSONArray 转为 ArrayList 44 | * 45 | * @param arr 要转换的 JSONArray 46 | * @return SimpleFileObject 或 SimpleDirectoryObject 47 | */ 48 | public static ArrayList jsonArrToFileObjArr(JSONArray arr) { 49 | ArrayList fileObjList = new ArrayList<>(0); 50 | arr.toList(JSONObject.class).forEach(jsonObject -> { 51 | if (fileObjIsDirectoryObj(jsonObject)) { 52 | fileObjList.add(jsonObjectToFileObject(jsonObject)); 53 | } else { 54 | fileObjList.add(new SimpleFileObject( 55 | jsonObject.getString("name"), 56 | jsonObject.getLong("length"), 57 | jsonObject.getString("hash"), 58 | jsonObject.getLong("modified"))); 59 | } 60 | }); 61 | 62 | return fileObjList; 63 | } 64 | 65 | /** 66 | * 此文件对象是否为文件夹 67 | * 68 | * @param obj 要检查的 JSON 对象 69 | */ 70 | private static boolean fileObjIsDirectoryObj(JSONObject obj) { 71 | return obj.getLong("length") == null || obj.getLong("modified") == null || obj.getString("hash") == null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/fileobject/DirInfoTask.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils.fileobject; 2 | 3 | import java.io.File; 4 | import java.util.ArrayList; 5 | import java.util.concurrent.*; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_THREAD_POOL; 10 | import static github.kasuminova.balloonserver.BalloonServer.GLOBAL_IO_THREAD_POOL; 11 | 12 | /** 13 | * 获取文件夹内所有文件信息的线程 14 | * @param directory 目标文件夹 15 | * @param hashAlgorithm Hash 算法类型 16 | * @param completedBytes 进度(可空) 17 | * @param completedFiles 已完成的文件数量(可空) 18 | */ 19 | public record DirInfoTask(File directory, String hashAlgorithm, AtomicLong completedBytes, AtomicInteger completedFiles) 20 | implements Callable { 21 | 22 | @Override 23 | public SimpleDirectoryObject call() { 24 | File[] fileList = directory.listFiles(); 25 | if (fileList == null || fileList.length == 0) { 26 | return new SimpleDirectoryObject(directory.getName(), new ArrayList<>(0)); 27 | } 28 | 29 | ArrayList> fileCounterTaskList = new ArrayList<>(0); 30 | ArrayList> direCounterTaskList = new ArrayList<>(0); 31 | ArrayList abstractSimpleFileObjectList = new ArrayList<>(fileList.length); 32 | 33 | for (File file : fileList) { 34 | if (file.isFile()) { 35 | FutureTask fileCounterTask = new FutureTask<>( 36 | new FileInfoTask(file, hashAlgorithm, completedBytes, completedFiles)); 37 | fileCounterTaskList.add(fileCounterTask); 38 | GLOBAL_IO_THREAD_POOL.execute(fileCounterTask); 39 | } else { 40 | FutureTask dirCounterTask = new FutureTask<>( 41 | new DirInfoTask(file, hashAlgorithm, completedBytes, completedFiles)); 42 | direCounterTaskList.add(dirCounterTask); 43 | GLOBAL_THREAD_POOL.execute(dirCounterTask); 44 | } 45 | } 46 | 47 | for (FutureTask simpleDirectoryObjectFutureTask : direCounterTaskList) { 48 | try { 49 | abstractSimpleFileObjectList.add(simpleDirectoryObjectFutureTask.get()); 50 | } catch (Exception ignored) {} 51 | } 52 | 53 | for (FutureTask simpleFileObjectFutureTask : fileCounterTaskList) { 54 | try { 55 | abstractSimpleFileObjectList.add(simpleFileObjectFutureTask.get()); 56 | } catch (Exception ignored) {} 57 | } 58 | 59 | return new SimpleDirectoryObject(directory.getName(), abstractSimpleFileObjectList); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/fileobject/FileInfoTask.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils.fileobject; 2 | 3 | import github.kasuminova.balloonserver.utils.HashCalculator; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.util.concurrent.Callable; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | import static github.kasuminova.balloonserver.utils.HashCalculator.getCRC32; 13 | import static github.kasuminova.balloonserver.utils.HashCalculator.getSHA1; 14 | 15 | public record FileInfoTask(File file, String hashAlgorithm, AtomicLong completedBytes, AtomicInteger completedFiles) 16 | implements Callable { 17 | @Override 18 | public SimpleFileObject call() throws IOException, NoSuchAlgorithmException { 19 | String hash; 20 | if (hashAlgorithm.equals(HashCalculator.SHA1)) { 21 | hash = completedBytes == null ? getSHA1(file) : getSHA1(file, completedBytes); 22 | } else { 23 | hash = completedBytes == null ? getCRC32(file) : getCRC32(file, completedBytes); 24 | } 25 | if (completedFiles != null) completedFiles.getAndIncrement(); 26 | return new SimpleFileObject( 27 | file.getName(), 28 | file.length(), 29 | hash, 30 | file.lastModified() / 1000); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/fileobject/SimpleDirectoryObject.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils.fileobject; 2 | 3 | import com.alibaba.fastjson2.annotation.JSONField; 4 | 5 | import java.util.ArrayList; 6 | 7 | public final class SimpleDirectoryObject extends AbstractSimpleFileObject { 8 | @JSONField(ordinal = 1) 9 | ArrayList children; 10 | 11 | public SimpleDirectoryObject(String name, ArrayList children) { 12 | this.name = name; 13 | this.children = children; 14 | } 15 | 16 | public ArrayList getChildren() { 17 | return children; 18 | } 19 | 20 | public void setChildren(ArrayList children) { 21 | this.children = children; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/balloonserver/utils/fileobject/SimpleFileObject.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.balloonserver.utils.fileobject; 2 | 3 | import com.alibaba.fastjson2.annotation.JSONField; 4 | 5 | public final class SimpleFileObject extends AbstractSimpleFileObject { 6 | @JSONField(ordinal = 1) 7 | long modified; 8 | @JSONField(ordinal = 2) 9 | String hash; 10 | @JSONField(ordinal = 3) 11 | long length; 12 | public SimpleFileObject(String name, long length, String hash, long modified) { 13 | this.name = name; 14 | this.length = length; 15 | this.hash = hash; 16 | this.modified = modified; 17 | } 18 | 19 | public long getLength() { 20 | return length; 21 | } 22 | 23 | public void setLength(long length) { 24 | this.length = length; 25 | } 26 | 27 | public String getHash() { 28 | return hash; 29 | } 30 | 31 | public void setHash(String hash) { 32 | this.hash = hash; 33 | } 34 | 35 | public long getModified() { 36 | return modified; 37 | } 38 | 39 | public void setModified(long modified) { 40 | this.modified = modified; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/AbstractMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | 6 | public abstract class AbstractMessage implements Serializable { 7 | @Serial 8 | private static final long serialVersionUID = 1; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/AuthSuccessMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | import github.kasuminova.balloonserver.configurations.IntegratedServerConfig; 4 | 5 | public class AuthSuccessMessage extends AbstractMessage { 6 | private String clientID; 7 | private IntegratedServerConfig config; 8 | 9 | public AuthSuccessMessage(String clientID, IntegratedServerConfig config) { 10 | this.clientID = clientID; 11 | this.config = config; 12 | } 13 | 14 | public String getClientID() { 15 | return clientID; 16 | } 17 | 18 | public void setClientID(String clientID) { 19 | this.clientID = clientID; 20 | } 21 | 22 | public IntegratedServerConfig getConfig() { 23 | return config; 24 | } 25 | 26 | public void setConfig(IntegratedServerConfig config) { 27 | this.config = config; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/FileListMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | import github.kasuminova.balloonserver.utils.fileobject.AbstractSimpleFileObject; 4 | 5 | public class FileListMessage extends AbstractMessage { 6 | AbstractSimpleFileObject[] fileObjects; 7 | 8 | public FileListMessage(AbstractSimpleFileObject[] fileObjects) { 9 | this.fileObjects = fileObjects; 10 | } 11 | 12 | public AbstractSimpleFileObject[] getFileObjects() { 13 | return fileObjects; 14 | } 15 | 16 | public void setFileObjects(AbstractSimpleFileObject[] fileObjects) { 17 | this.fileObjects = fileObjects; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/LogMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | public class LogMessage extends AbstractMessage { 4 | private String message; 5 | private String level; 6 | 7 | public LogMessage(String level, String message) { 8 | this.level = level; 9 | this.message = message; 10 | } 11 | 12 | public String getMessage() { 13 | return message; 14 | } 15 | 16 | public void setMessage(String message) { 17 | this.message = message; 18 | } 19 | 20 | public String getLevel() { 21 | return level; 22 | } 23 | 24 | public void setLevel(String level) { 25 | this.level = level; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/MessageProcessor.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | public interface MessageProcessor { 4 | void process(T message0); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/MethodMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | @Deprecated 4 | public class MethodMessage extends AbstractMessage { 5 | private String className; 6 | private String methodName; 7 | private String[] params; 8 | 9 | public MethodMessage(String className, String methodName) { 10 | this(className, methodName, null); 11 | } 12 | 13 | public MethodMessage(String className, String methodName, String[] methods) { 14 | this.className = className; 15 | this.methodName = methodName; 16 | this.params = methods; 17 | } 18 | 19 | public String getMethodName() { 20 | return methodName; 21 | } 22 | 23 | public void setMethodName(String methodName) { 24 | this.methodName = methodName; 25 | } 26 | 27 | public String getClassName() { 28 | return className; 29 | } 30 | 31 | public void setClassName(String className) { 32 | this.className = className; 33 | } 34 | 35 | public String[] getParams() { 36 | return params; 37 | } 38 | 39 | public MethodMessage setParams(String[] params) { 40 | this.params = params; 41 | return this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/RequestMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * 请求信息 8 | */ 9 | public class RequestMessage extends AbstractMessage { 10 | /** 11 | * 消息类型 12 | */ 13 | private String requestType; 14 | 15 | /** 16 | * 消息参数 17 | */ 18 | private List requestParams; 19 | 20 | /** 21 | * 新建一个请求信息 22 | * @param requestType 请求类型 23 | */ 24 | public RequestMessage(String requestType, List requestParams) { 25 | this.requestType = requestType; 26 | this.requestParams = requestParams; 27 | } 28 | 29 | public RequestMessage(String requestType) { 30 | this(requestType, new ArrayList<>(1)); 31 | } 32 | 33 | public List getRequestParams() { 34 | return requestParams; 35 | } 36 | 37 | public RequestMessage setRequestParams(List requestParams) { 38 | this.requestParams = requestParams; 39 | return this; 40 | } 41 | 42 | public String getRequestType() { 43 | return requestType; 44 | } 45 | 46 | public RequestMessage setRequestType(String requestType) { 47 | this.requestType = requestType; 48 | return this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/StatusMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | public class StatusMessage extends AbstractMessage { 4 | private int used; 5 | private int total; 6 | private int max; 7 | private int runningThreadCount; 8 | private String clientID; 9 | 10 | public StatusMessage(int used, int total, int max, int runningThreadCount, String clientID) { 11 | this.used = used; 12 | this.total = total; 13 | this.max = max; 14 | this.runningThreadCount = runningThreadCount; 15 | this.clientID = clientID; 16 | } 17 | 18 | public int getUsed() { 19 | return used; 20 | } 21 | 22 | public void setUsed(int used) { 23 | this.used = used; 24 | } 25 | 26 | public int getTotal() { 27 | return total; 28 | } 29 | 30 | public void setTotal(int total) { 31 | this.total = total; 32 | } 33 | 34 | public int getMax() { 35 | return max; 36 | } 37 | 38 | public void setMax(int max) { 39 | this.max = max; 40 | } 41 | 42 | public int getRunningThreadCount() { 43 | return runningThreadCount; 44 | } 45 | 46 | public void setRunningThreadCount(int runningThreadCount) { 47 | this.runningThreadCount = runningThreadCount; 48 | } 49 | 50 | public String getClientIP() { 51 | return clientID; 52 | } 53 | 54 | public void setClientIP(String clientIP) { 55 | this.clientID = clientIP; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/TokenMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages; 2 | 3 | import github.kasuminova.balloonserver.updatechecker.ApplicationVersion; 4 | 5 | public class TokenMessage extends AbstractMessage { 6 | private String token; 7 | private ApplicationVersion clientVersion; 8 | 9 | public TokenMessage(String token, ApplicationVersion clientVersion) { 10 | this.token = token; 11 | this.clientVersion = clientVersion; 12 | } 13 | 14 | public ApplicationVersion getClientVersion() { 15 | return clientVersion; 16 | } 17 | 18 | public void setClientVersion(ApplicationVersion clientVersion) { 19 | this.clientVersion = clientVersion; 20 | } 21 | 22 | public String getToken() { 23 | return token; 24 | } 25 | 26 | public void setToken(String token) { 27 | this.token = token; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/filemessages/FileInfoMsg.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages.filemessages; 2 | 3 | public class FileInfoMsg extends FileMessage { 4 | private long size; 5 | 6 | public FileInfoMsg(String filePath, String fileName, long size) { 7 | super(filePath, fileName); 8 | this.size = size; 9 | } 10 | 11 | public long getSize() { 12 | return size; 13 | } 14 | 15 | public void setSize(long size) { 16 | this.size = size; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/filemessages/FileMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages.filemessages; 2 | 3 | import github.kasuminova.messages.AbstractMessage; 4 | 5 | import java.io.Serial; 6 | 7 | public abstract class FileMessage extends AbstractMessage { 8 | @Serial 9 | private static final long serialVersionUID = 1L; 10 | String filePath; 11 | String fileName; 12 | 13 | public FileMessage(String filePath, String fileName) { 14 | this.filePath = filePath; 15 | this.fileName = fileName; 16 | } 17 | 18 | public String getFilePath() { 19 | return filePath; 20 | } 21 | 22 | public void setFilePath(String filePath) { 23 | this.filePath = filePath; 24 | } 25 | 26 | public String getFileName() { 27 | return fileName; 28 | } 29 | 30 | public void setFileName(String fileName) { 31 | this.fileName = fileName; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/filemessages/FileObjMessage.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages.filemessages; 2 | 3 | public class FileObjMessage extends FileMessage { 4 | private long offset; 5 | private long length; 6 | private long total; 7 | private byte[] data; 8 | 9 | public FileObjMessage(String filePath, String fileName, long offset, long length, long total, byte[] data) { 10 | super(filePath, fileName); 11 | this.offset = offset; 12 | this.length = length; 13 | this.total = total; 14 | this.data = data; 15 | } 16 | 17 | public long getOffset() { 18 | return offset; 19 | } 20 | 21 | public void setOffset(long offset) { 22 | this.offset = offset; 23 | } 24 | 25 | public long getLength() { 26 | return length; 27 | } 28 | 29 | public void setLength(long length) { 30 | this.length = length; 31 | } 32 | 33 | public long getTotal() { 34 | return total; 35 | } 36 | 37 | public void setTotal(long total) { 38 | this.total = total; 39 | } 40 | 41 | public byte[] getData() { 42 | return data; 43 | } 44 | 45 | public void setData(byte[] data) { 46 | this.data = data; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/filemessages/FileRequestMsg.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages.filemessages; 2 | 3 | public class FileRequestMsg extends FileMessage { 4 | private long offset; 5 | private long length; 6 | 7 | public FileRequestMsg(String filePath, String fileName, long offset, long length) { 8 | super(filePath, fileName); 9 | this.offset = offset; 10 | this.length = length; 11 | } 12 | 13 | public FileRequestMsg(String filePath, String fileName) { 14 | super(filePath, fileName); 15 | offset = 0; 16 | length = -1; 17 | } 18 | 19 | public long getOffset() { 20 | return offset; 21 | } 22 | 23 | public void setOffset(long offset) { 24 | this.offset = offset; 25 | } 26 | 27 | public long getLength() { 28 | return length; 29 | } 30 | 31 | public void setLength(long length) { 32 | this.length = length; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/github/kasuminova/messages/processor/MessageProcessor.java: -------------------------------------------------------------------------------- 1 | package github.kasuminova.messages.processor; 2 | 3 | public class MessageProcessor { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/font/HarmonyOS_Sans_SC+JetBrains_Mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/src/main/resources/font/HarmonyOS_Sans_SC+JetBrains_Mono.ttf -------------------------------------------------------------------------------- /src/main/resources/font/HarmonyOS_Sans_SC+Saira.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/src/main/resources/font/HarmonyOS_Sans_SC+Saira.ttf -------------------------------------------------------------------------------- /src/main/resources/font/JetBrains_Mono_OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | 5 | This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/main/resources/font/Saira_OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Saira Project Authors (https://github.com/Omnibus-Type/Saira) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/main/resources/icons/custom_server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/default_server.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/class.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/dir.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/doc_docx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/exe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/file_default.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/jar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/java.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/jpg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/json.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/md.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/ppt_pptx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/txt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/xls_xlsx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/xml.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/yml.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/file_types/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/reload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/remove.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/resource.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/serverList.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/terminal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/image/icon_16x16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/src/main/resources/image/icon_16x16.ico -------------------------------------------------------------------------------- /src/main/resources/image/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/src/main/resources/image/icon_16x16.png -------------------------------------------------------------------------------- /src/main/resources/image/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BalloonUpdate/BalloonServer/076a8c2a58eba255f3c915a2c9c1f199f2f02e6a/src/main/resources/image/splash.png --------------------------------------------------------------------------------