diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..97ec8ae --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sophia Atkinson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2c7b26c..525fa1e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ ## Features -- Auto-detects download source (GitHub / Jenkins / Modrinth / Bukkit / Direct) +- Downloads the Latest PaperMC server version (may add more soon) +- Auto-detects download source (GitHub / Jenkins / Modrinth / Bukkit / Paper Hanger / Direct) - Skips already downloaded files - Password encryption for SFTP (AES-256-CBC) - Optional SFTP upload support diff --git a/config.json.example b/config.json.example index fc19cba..eb707b5 100644 --- a/config.json.example +++ b/config.json.example @@ -2,6 +2,9 @@ "global": { "useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", "downloadPath": "downloads" + // Not in use but for future use + "MinecraftDistro": "Paper", + "MinecraftVersion": "latest" // Set to "latest" to always download the latest version, or specify a version like "1.20.4" }, "urls": [ "https://github.com/McPlugin/SoCool", @@ -12,7 +15,7 @@ "port": 22, "username": "", "password": "", // This will be encrypted by the script and removed from the file - "remotePath": "/minecraft/plugins", + "remotePath": "/minecraft/", // as of 1.1.0 you need to set this as the root of your minecraft server! "privateKeyPath": "" // Leave blank if using password } } diff --git a/index.js b/index.js index 6a627ae..90008b9 100644 --- a/index.js +++ b/index.js @@ -61,12 +61,18 @@ const ensureDir = (dir) => { if (!fs.existsSync(dir)) fs.mkdirSync(dir); }; const DOWNLOAD_PATH = config.global.downloadPath || "downloads"; -ensureDir(DOWNLOAD_PATH); +const PLUGIN_PATH = path.join(DOWNLOAD_PATH, "Plugins"); +const SERVEREXEC_PATH = path.join(DOWNLOAD_PATH, "Serverexec"); + +[DOWNLOAD_PATH, PLUGIN_PATH, SERVEREXEC_PATH].forEach(ensureDir); -const isDownloaded = (filename) => fs.existsSync(path.join(DOWNLOAD_PATH, filename)); const downloadJar = async (url, name) => { - if (isDownloaded(name)) { + const isServerJar = name === "server.jar"; + const destDir = isServerJar ? SERVEREXEC_PATH : PLUGIN_PATH; + const filePath = path.join(destDir, name); + + if (fs.existsSync(filePath)) { console.log(`๐ŸŸก Skipped (already exists): ${name}`); return; } @@ -86,12 +92,12 @@ const downloadJar = async (url, name) => { response.data.on("data", (chunk) => bar.increment(chunk.length)); response.data.on("end", () => bar.stop()); - const filePath = path.join(DOWNLOAD_PATH, name); await pipeline(response.data, fs.createWriteStream(filePath)); - console.log(`โœ”๏ธ Saved: ${name}`); + console.log(`โœ”๏ธ Saved: ${filePath}`); }; -// --- Source handlers --- + +// --- Handle Jenkins --- const handleJenkins = async (url) => { const html = await axios.get(url).then((res) => res.data); const $ = cheerio.load(html); @@ -131,6 +137,7 @@ const handleJenkins = async (url) => { } }; +// --- Handle GitHub --- const handleGitHub = async (url) => { const match = url.match(/github\.com\/([^/]+\/[^/]+)/); if (!match) throw new Error("Invalid GitHub URL format"); @@ -147,6 +154,7 @@ const handleGitHub = async (url) => { await downloadJar(chosen.browser_download_url, chosen.name); }; +// --- Handle Modrinth --- const handleModrinth = async (url) => { const match = url.match(/modrinth\.com\/plugin\/([^/]+)/); if (!match) throw new Error("Invalid Modrinth URL format"); @@ -156,13 +164,22 @@ const handleModrinth = async (url) => { .get(`https://api.modrinth.com/v2/project/${project}/version`) .then((res) => res.data); - const latest = versions[0]; - const file = latest.files.find((f) => f.filename.endsWith(".jar")); - if (!file) throw new Error("No .jar file in latest version"); + // Filter to only versions compatible with Spigot/Paper/etc + const compatible = versions.find((v) => + (v.loaders || []).some((loader) => + ["spigot", "paper", "bukkit", "purpur", "folia"].includes(loader.toLowerCase()) + ) + ); + + if (!compatible) throw new Error("No compatible Spigot/Paper version found"); + + const file = compatible.files.find((f) => f.filename.endsWith(".jar")); + if (!file) throw new Error("No .jar file in compatible version"); await downloadJar(file.url, file.filename); }; +// --- Handle Direct (Mainly for Floodgate) --- const handleDirect = async (url) => { let name = path.basename(url.split("?")[0]); @@ -183,16 +200,21 @@ const handleDirect = async (url) => { // --- Handle PaperMC --- const handlePaperMC = async (url) => { - const versionMatch = url.match(/papermc.io\/download\/(.*?)(?:\/|$)/); - if (!versionMatch) throw new Error("Invalid PaperMC URL format"); - const version = versionMatch[1]; + if (url.includes("hangar.papermc.io")) { + const { data: html } = await axios.get(url); + const $ = cheerio.load(html); - const apiURL = `https://api.papermc.io/v2/projects/paper/versions/${version}/builds`; - const builds = await axios.get(apiURL).then((res) => res.data.builds); - const latestBuild = builds[0]; + const jarLink = $('a[href$=".jar"]').first().attr("href"); + if (!jarLink) throw new Error("โŒ No .jar link found on Hangar page"); - const downloadURL = latestBuild.downloads.application.url; - await downloadJar(downloadURL, `paper-${version}-${latestBuild.build}.jar`); + const downloadURL = jarLink.startsWith("http") + ? jarLink + : `https://hangar.papermc.io${jarLink}`; + + const fileName = path.basename(downloadURL); + await downloadJar(downloadURL, fileName); + return; + } }; // --- Handle dev.bukkit.org --- @@ -236,6 +258,26 @@ const handleBukkit = async (url) => { await downloadJar(fullDownloadLink, filename); }; +// --- Handle PaperMC Server DL --- +const downloadLatestPaperMC = async () => { + console.log("๐Ÿ“ฅ Downloading latest PaperMC server jar..."); + + const projectURL = 'https://api.papermc.io/v2/projects/paper'; + const { data: versionsData } = await axios.get(projectURL); + const latestVersion = versionsData.versions[versionsData.versions.length - 1]; + + const buildsURL = `${projectURL}/versions/${latestVersion}`; + const { data: buildsData } = await axios.get(buildsURL); + const latestBuild = buildsData.builds[buildsData.builds.length - 1]; + + const jarInfoURL = `${projectURL}/versions/${latestVersion}/builds/${latestBuild}`; + const { data: jarInfo } = await axios.get(jarInfoURL); + + const jarPath = jarInfo.downloads.application.name; + const downloadURL = `https://api.papermc.io/v2/projects/paper/versions/${latestVersion}/builds/${latestBuild}/downloads/${jarPath}`; + + await downloadJar(downloadURL, "server.jar"); +}; // --- Upload to SFTP --- const uploadToSFTP = async () => { @@ -246,8 +288,6 @@ const uploadToSFTP = async () => { const sftp = new SftpClient(); const remote = sftpConfig.remotePath || "/"; - const localFiles = fs.readdirSync(DOWNLOAD_PATH).filter(f => f.endsWith(".jar")); - const connectOptions = { host: sftpConfig.host, port: sftpConfig.port || 22, @@ -268,29 +308,56 @@ const uploadToSFTP = async () => { try { await sftp.connect(connectOptions); - const remoteFiles = await sftp.list(remote); - const remoteJars = remoteFiles.filter(f => f.name.endsWith(".jar")); - for (const localFile of localFiles) { - const baseName = extractBaseName(localFile); - const toDelete = remoteJars.filter(remoteFile => - extractBaseName(remoteFile.name) === baseName - ); - for (const file of toDelete) { - const fullPath = path.posix.join(remote, file.name); - await sftp.delete(fullPath); - console.log(`๐Ÿ—‘๏ธ Deleted remote: ${file.name}`); + const uploadFolder = async (localDir, remoteDir) => { + const files = fs.readdirSync(localDir).filter(f => f.endsWith(".jar")); + const remoteFiles = await sftp.list(remoteDir); + const remoteJars = remoteFiles.filter(f => f.name.endsWith(".jar")); + + for (const file of files) { + const baseName = extractBaseName(file); + const toDelete = remoteJars.filter(r => extractBaseName(r.name) === baseName); + for (const del of toDelete) { + const fullPath = path.posix.join(remoteDir, del.name); + await sftp.delete(fullPath); + console.log(`๐Ÿ—‘๏ธ Deleted remote: ${fullPath}`); + } + + const localPath = path.join(localDir, file); + const remotePath = path.posix.join(remoteDir, file); + await sftp.fastPut(localPath, remotePath); + console.log(`โฌ†๏ธ Uploaded to ${remoteDir}: ${file}`); } - // ๐Ÿš€ Upload new files - const localPath = path.join(DOWNLOAD_PATH, localFile); - const remotePath = path.posix.join(remote, localFile); - await sftp.fastPut(localPath, remotePath); - console.log(`โฌ†๏ธ Uploaded: ${localFile}`); - } + }; + + await uploadFolder(PLUGIN_PATH, path.posix.join(remote, "plugins")); + await uploadFolder(SERVEREXEC_PATH, remote); + } catch (err) { console.error("โŒ SFTP Error:", err.message); } finally { - sftp.end(); + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout closing SFTP connection")), 5000) + ); + try { + await Promise.race([sftp.end(), timeout]); + console.log("๐Ÿ”Œ SFTP connection closed."); + // Clean up local files after upload + const deleteFilesInDir = (dir) => { + fs.readdirSync(dir).forEach(file => { + const filePath = path.join(dir, file); + if (fs.lstatSync(filePath).isFile()) { + fs.unlinkSync(filePath); + console.log(`๐Ÿงน Deleted: ${filePath}`); + } + }); + }; + + deleteFilesInDir(PLUGIN_PATH); + deleteFilesInDir(SERVEREXEC_PATH); + } catch (e) { + console.warn("โš ๏ธ Error or timeout closing SFTP:", e.message); + } } }; @@ -304,6 +371,9 @@ const uploadToSFTP = async () => { console.log(`๐Ÿ—‘๏ธ Deleted local file: ${file}`); } + await downloadLatestPaperMC(); + + console.log("\n๐Ÿ” Starting plugin downloads from configured URLs..."); for (const url of config.urls) { console.log(`\n๐Ÿ“ฅ ${url}`); try { diff --git a/package.json b/package.json index c0f6e26..188de81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "download-plugs", - "version": "1.0.5", + "version": "1.1.0", "main": "index.js", "scripts": { "run": "node index.js"