diff --git a/src/main/java/common/PluginDownloader.java b/src/main/java/common/PluginDownloader.java index f4ade3d..6c87384 100644 --- a/src/main/java/common/PluginDownloader.java +++ b/src/main/java/common/PluginDownloader.java @@ -21,6 +21,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.*; +import java.nio.file.InvalidPathException; import java.nio.file.attribute.BasicFileAttributes; import java.security.DigestInputStream; import java.security.MessageDigest; @@ -126,6 +127,10 @@ public X509Certificate[] getAcceptedIssuers() { } public boolean downloadPlugin(String link, String fileName, String githubToken) throws IOException { + return downloadPlugin(link, fileName, githubToken, null); + } + + public boolean downloadPlugin(String link, String fileName, String githubToken, String customPath) throws IOException { String host = null; Semaphore hostSem = null; try { @@ -145,7 +150,7 @@ public boolean downloadPlugin(String link, String fileName, String githubToken) String tempBase = UpdateOptions.tempPath != null && !UpdateOptions.tempPath.isEmpty() ? ensureDir(UpdateOptions.tempPath) : "plugins/"; String rawTempPath = tempBase + fileName + ".download.tmp"; - String outputFilePath = getString(fileName); + String outputFilePath = resolveOutputPath(fileName, customPath); String outputTempPath = outputFilePath + ".temp"; try { @@ -287,22 +292,17 @@ private String getString(String fileName) { String configuredFilePath = UpdateOptions.filePath; String configuredUpdatePath = UpdateOptions.updatePath; - if (configuredFilePath != null && !configuredFilePath.isEmpty()) { return ensureDir(configuredFilePath) + fileName + ".jar"; } - File mainJar = new File(basePlugins + fileName + ".jar"); - if (UpdateOptions.useUpdateFolder) { - String updateDir = (configuredUpdatePath != null && !configuredUpdatePath.isEmpty()) ? ensureDir(configuredUpdatePath) : ensureDir(basePlugins + "update/"); - if (mainJar.exists()) { return updateDir + fileName + ".jar"; } else { @@ -310,14 +310,61 @@ private String getString(String fileName) { } } - return basePlugins + fileName + ".jar"; } + private String resolveOutputPath(String fileName, String customPath) { + if (customPath != null && !customPath.trim().isEmpty()) { + String cp = sanitizeCustomPath(customPath); + if (cp != null && !cp.isEmpty()) { + String dir = ensureDir(cp); + return dir + fileName + ".jar"; + } + } + return getString(fileName); + } + + private String sanitizeCustomPath(String cp) { + if (cp == null) return null; + cp = cp.trim(); + if (cp.isEmpty()) return null; + cp = expandUserHome(cp); + try { + Path path = Paths.get(cp).normalize(); + String normalized = path.toString(); + return normalized.isEmpty() ? null : normalized; + } catch (InvalidPathException ex) { + if (UpdateOptions.debug) { + logger.info("[DEBUG] Ignoring custom path '" + cp + "' due to invalid path: " + ex.getMessage()); + } + return null; + } + } + + private String expandUserHome(String path) { + if (path == null || !path.startsWith("~")) { + return path; + } + String home = System.getProperty("user.home"); + if (home == null || home.isEmpty()) { + return path; + } + if (path.equals("~")) { + return home; + } + if (path.startsWith("~/") || path.startsWith("~\\")) { + return home + path.substring(1); + } + return path; + } + private String ensureDir(String dir) { - if (!dir.endsWith("/") && !dir.endsWith("\\")) dir = dir + "/"; - new File(dir).mkdirs(); - return dir; + if (dir == null || dir.isEmpty()) return dir; + File directory = new File(dir); + directory.mkdirs(); + String path = directory.getPath(); + if (!path.endsWith(File.separator)) path = path + File.separator; + return path; } private boolean downloadPluginToFile(String outputFilePath, HttpURLConnection connection) throws IOException { @@ -354,9 +401,13 @@ private boolean extractFirstJarFromZip(String zipFilePath, String outputFilePath } public boolean downloadJenkinsPlugin(String link, String fileName) { + return downloadJenkinsPlugin(link, fileName, null); + } + + public boolean downloadJenkinsPlugin(String link, String fileName, String customPath) { String tempBase = UpdateOptions.tempPath != null && !UpdateOptions.tempPath.isEmpty() ? ensureDir(UpdateOptions.tempPath) : "plugins/"; String rawTempPath = tempBase + fileName + ".download.tmp"; - String outputFilePath = getString(fileName); + String outputFilePath = resolveOutputPath(fileName, customPath); String outputTempPath = outputFilePath + ".temp"; HttpURLConnection seedConnection; try { @@ -867,6 +918,10 @@ private boolean downloadLenient(File outFile, HttpURLConnection connection) { public boolean buildFromGitHubRepo(String repoPath, String fileName, String key) throws IOException { + return buildFromGitHubRepo(repoPath, fileName, key, null); + } + + public boolean buildFromGitHubRepo(String repoPath, String fileName, String key, String customPath) throws IOException { if (repoPath == null || repoPath.isEmpty()) throw new IOException("Invalid repo path"); if (UpdateOptions.debug) logger.info("[DEBUG] Starting GitHub build for " + repoPath); @@ -982,7 +1037,8 @@ public boolean buildFromGitHubRepo(String repoPath, String fileName, String key) } - File out = new File("plugins/update/" + fileName + ".jar"); + String outputFilePath = resolveOutputPath(fileName, customPath); + File out = new File(outputFilePath); if (UpdateOptions.debug) logger.info("[DEBUG] Built jar selected: " + jar.getAbsolutePath()); copyFile(jar, out); return true; diff --git a/src/main/java/common/PluginUpdater.java b/src/main/java/common/PluginUpdater.java index 7c151df..7b825bd 100644 --- a/src/main/java/common/PluginUpdater.java +++ b/src/main/java/common/PluginUpdater.java @@ -24,6 +24,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.security.SecureRandom; @@ -163,6 +164,21 @@ public boolean stopUpdates() { } private static Path decideInstallPath(String pluginName) { + return decideInstallPath(pluginName, null); + } + + private static Path decideInstallPath(String pluginName, String customPath) { + if (customPath != null && !customPath.trim().isEmpty()) { + String cp = sanitizeCustomPath(customPath); + if (cp != null && !cp.isEmpty()) { + Path dir = Paths.get(cp); + try { + Files.createDirectories(dir); + } catch (Exception ignored) { + } + return dir.resolve(pluginName + ".jar"); + } + } Path pluginsDir = Paths.get("plugins"); Path mainJar = pluginsDir.resolve(pluginName + ".jar"); @@ -181,6 +197,52 @@ private static Path decideInstallPath(String pluginName) { } } + private static String extractCustomPath(String raw) { + if (raw == null) return null; + int i = raw.indexOf('|'); + if (i < 0) return null; + String tail = raw.substring(i + 1).trim(); + return tail.isEmpty() ? null : tail; + } + + private static String stripLinkPart(String raw) { + if (raw == null) return null; + int i = raw.indexOf('|'); + return i < 0 ? raw.trim() : raw.substring(0, i).trim(); + } + + private static String sanitizeCustomPath(String cp) { + if (cp == null) return null; + cp = cp.trim(); + if (cp.isEmpty()) return null; + cp = expandUserHome(cp); + try { + Path path = Paths.get(cp).normalize(); + String normalized = path.toString(); + return normalized.isEmpty() ? null : normalized; + } catch (InvalidPathException ex) { + return null; + } + } + + private static String expandUserHome(String path) { + if (path == null || !path.startsWith("~")) { + return path; + } + String home = System.getProperty("user.home"); + if (home == null || home.isEmpty()) { + return path; + } + if (path.equals("~")) { + return home; + } + if (path.startsWith("~/") || path.startsWith("~\\")) { + return home + path.substring(1); + } + return path; + } + + private ExecutorService createExecutor(int parallelism) { try { Class execs = Class.forName("java.util.concurrent.Executors"); @@ -198,7 +260,16 @@ private ExecutorService createExecutor(int parallelism) { private boolean handleUpdateEntry(String platform, String key, Map.Entry entry) throws IOException { try { logger.info(entry.getKey() + " ---- " + entry.getValue()); - String value = entry.getValue().replace("dev.bukkit.org/projects", "www.curseforge.com/minecraft/bukkit-plugins"); + String rawValue = entry.getValue(); + String customPath = null; + String linkPart = rawValue; + int pipe = rawValue != null ? rawValue.indexOf('|') : -1; + if (pipe >= 0) { + linkPart = rawValue.substring(0, pipe).trim(); + String tail = rawValue.substring(pipe + 1).trim(); + if (!tail.isEmpty()) customPath = tail; + } + String value = linkPart.replace("dev.bukkit.org/projects", "www.curseforge.com/minecraft/bukkit-plugins"); if (value.contains("blob.build")) { return handleBlobBuild(value, key, entry); @@ -222,7 +293,7 @@ private boolean handleUpdateEntry(String platform, String key, Map.Entry(entry.getKey(), href)); + String cp = extractCustomPath(entry.getValue()); + String forward = (cp != null && !cp.isEmpty()) ? (href + " | " + cp) : href; + return handleUpdateEntry("paper", key, new AbstractMap.SimpleEntry<>(entry.getKey(), forward)); } } return false; @@ -704,14 +787,16 @@ public X509Certificate[] getAcceptedIssuers() { String downloadUrl = latest.has("downloadUrl") ? latest.get("downloadUrl").asText() : null; if (downloadUrl != null && !downloadUrl.isEmpty()) { logger.info("[CF] Using LAST file downloadUrl from servermods: " + downloadUrl); - return pluginDownloader.downloadPlugin(downloadUrl, entry.getKey(), key); + String cp = extractCustomPath(entry.getValue()); + return pluginDownloader.downloadPlugin(downloadUrl, entry.getKey(), key, cp); } String fileId = latest.has("id") ? latest.get("id").asText() : null; if (fileId != null && !fileId.isEmpty()) { String fallback = "https://www.curseforge.com/minecraft/bukkit-plugins/" + slug + "/download/" + fileId + "/file"; logger.info("[CF] Constructed redirect URL for LAST file: " + fallback); - return pluginDownloader.downloadPlugin(fallback, entry.getKey(), key); + String cp = extractCustomPath(entry.getValue()); + return pluginDownloader.downloadPlugin(fallback, entry.getKey(), key, cp); } logger.info("[CF] Could not determine a usable download URL from LAST file."); @@ -741,7 +826,8 @@ private boolean handleGenericPageDownload(String value, String key, Map.Entry= 200 && code < 300 && indicatesBinary) { - return pluginDownloader.downloadPlugin(value, entry.getKey(), key); + String cp = extractCustomPath(entry.getValue()); + return pluginDownloader.downloadPlugin(value, entry.getKey(), key, cp); } } catch (IOException ignored) { } @@ -750,10 +836,13 @@ private boolean handleGenericPageDownload(String value, String key, Map.Entry(entry.getKey(), href)); + String cp = extractCustomPath(entry.getValue()); + String forward = (cp != null && !cp.isEmpty()) ? (href + " | " + cp) : href; + return handleUpdateEntry("paper", key, new AbstractMap.SimpleEntry<>(entry.getKey(), forward)); } } return false; @@ -817,7 +906,8 @@ private boolean handleJenkinsDownload(String key, Map.Entry entr String artifactUrl = jenkinsLink + "lastSuccessfulBuild/artifact/" + artifactName; try { - return pluginDownloader.downloadPlugin(artifactUrl, entry.getKey(), key); + String cp = extractCustomPath(entry.getValue()); + return pluginDownloader.downloadPlugin(artifactUrl, entry.getKey(), key, cp); } catch (IOException e) { logger.info("Failed to download plugin from jenkins, " + value + " , are you sure link is correct and in right " + "format?" + e.getMessage()); return false; @@ -936,7 +1026,8 @@ private boolean handleGitHubDownload(String key, Map.Entry entry } try { - boolean ok = pluginDownloader.downloadPlugin(downloadUrl, entry.getKey(), key); + String cp = extractCustomPath(entry.getValue()); + boolean ok = pluginDownloader.downloadPlugin(downloadUrl, entry.getKey(), key, cp); if (!ok) { if (UpdateOptions.debug) { if (UpdateOptions.autoCompileEnable) { @@ -1031,7 +1122,8 @@ private boolean handleGitHubDevDownload(String key, Map.Entry en } try { - return pluginDownloader.downloadPlugin(downloadUrl, entry.getKey(), key); + String cp = extractCustomPath(entry.getValue()); + return pluginDownloader.downloadPlugin(downloadUrl, entry.getKey(), key, cp); } catch (IOException e) { logger.info("Failed to download plugin from github, " + value + " , are you sure the link is correct and in the right format? " + e.getMessage()); return false; @@ -1042,7 +1134,8 @@ private boolean handleSpigotDownload(String key, Map.Entry entry try { String pluginId = extractPluginIdFromLink(value); String downloadUrl = "https://api.spiget.org/v2/resources/" + pluginId + "/download"; - return pluginDownloader.downloadPlugin(downloadUrl, entry.getKey(), key); + String cp = extractCustomPath(entry.getValue()); + return pluginDownloader.downloadPlugin(downloadUrl, entry.getKey(), key, cp); } catch (Exception e) { logger.info("Failed to download plugin from spigot, " + value + " , are you sure link is correct and in right format?" + e.getMessage()); return false; @@ -1052,7 +1145,8 @@ private boolean handleSpigotDownload(String key, Map.Entry entry private boolean handleAlternateJenkinsDownload(String key, Map.Entry entry, String value) { try { String downloadUrl = value + "lastSuccessfulBuild/artifact/*zip*/archive.zip"; - return pluginDownloader.downloadJenkinsPlugin(downloadUrl, entry.getKey()); + String cp = extractCustomPath(entry.getValue()); + return pluginDownloader.downloadJenkinsPlugin(downloadUrl, entry.getKey(), cp); } catch (Exception e) { logger.info("Failed to download plugin from jenkins, " + value + " , are you sure link is correct and in right format?" + e.getMessage()); return false; @@ -1152,7 +1246,8 @@ private boolean attemptSourceBuild(String repoPath, Map.Entry en if (noJarAsset && !UpdateOptions.autoCompileWhenNoJarAsset) return false; } try { - Path out = decideInstallPath(entry.getKey()); + String cp = extractCustomPath(entry.getValue()); + Path out = decideInstallPath(entry.getKey(), cp); try { Files.createDirectories(out.getParent()); } catch (Exception ignored) { @@ -1164,7 +1259,8 @@ private boolean attemptSourceBuild(String repoPath, Map.Entry en } if (repoPath == null || repoPath.isEmpty()) return false; try { - return pluginDownloader.buildFromGitHubRepo(repoPath, entry.getKey(), key); + String cp = extractCustomPath(entry.getValue()); + return pluginDownloader.buildFromGitHubRepo(repoPath, entry.getKey(), key, cp); } catch (IOException e) { if (UpdateOptions.debug) { logger.info("[DEBUG] Source build failed for " + repoPath + ": " + e.getMessage());