5 Commits

Author SHA1 Message Date
72bd880950 added SpigotMC!
Well its technically its Spiget, but same diff
2025-12-14 00:05:09 -08:00
aee24d9492 The Big Three! (V1.1.1)
Added concurrency control for downloads using p-limit (max 6 simultaneous).

Removed cli-progress and progress bars from downloads, as it shits the bed with concurrency.

Simplified axios requests in downloadJar (removed custom headers as it was used only for cli-progress).

Refactored Jenkins handling to use API JSON instead of HTML scraping. (as i had weird failing with a few jenkins servers, and this seemed to fix it completely)

Removed cli-progress dependency; added p-limit dependency.
2025-12-13 20:04:37 -08:00
da7a8c5f3e Updated to 1.1.0 and changes :P
- Fixed PaperMC hanger
- added PaperMC server downloader
- made it so that you set your remote path as the root of your minecraft server, and the script figures out where the files need to go
- Fixed Modrinth so that it also handles server distros like how Jenkins handler does it
- Added minecraft distro and version to confing.json.example, this is used right now, but i will be wrighting it into the script soon
- added LINCENSE file
2025-07-23 12:42:26 -07:00
e4210aa9db fix jenkins download, version bump to 1.0.5
When i was pulling some downloads, it was grabbing all versions
2025-07-02 14:48:19 -07:00
ec1b8a4929 fix remote delete 2025-05-18 18:05:55 -07:00
5 changed files with 218 additions and 85 deletions

21
LICENSE Normal file
View File

@@ -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.

View File

@@ -5,7 +5,8 @@
## Features ## 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 - Skips already downloaded files
- Password encryption for SFTP (AES-256-CBC) - Password encryption for SFTP (AES-256-CBC)
- Optional SFTP upload support - Optional SFTP upload support

View File

@@ -2,6 +2,9 @@
"global": { "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", "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" "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": [ "urls": [
"https://github.com/McPlugin/SoCool", "https://github.com/McPlugin/SoCool",
@@ -12,7 +15,7 @@
"port": 22, "port": 22,
"username": "", "username": "",
"password": "", // This will be encrypted by the script and removed from the file "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 "privateKeyPath": "" // Leave blank if using password
} }
} }

268
index.js
View File

@@ -2,10 +2,10 @@ const axios = require("axios");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const { pipeline } = require("stream/promises"); const { pipeline } = require("stream/promises");
const cliProgress = require("cli-progress");
const cheerio = require("cheerio"); const cheerio = require("cheerio");
const SftpClient = require("ssh2-sftp-client"); const SftpClient = require("ssh2-sftp-client");
const crypto = require("crypto"); const crypto = require("crypto");
const pLimit = require("p-limit").default;
// --- Config & Secrets --- // --- Config & Secrets ---
const CONFIG_PATH = "config.json"; const CONFIG_PATH = "config.json";
@@ -61,44 +61,51 @@ const ensureDir = (dir) => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir); if (!fs.existsSync(dir)) fs.mkdirSync(dir);
}; };
const DOWNLOAD_PATH = config.global.downloadPath || "downloads"; const DOWNLOAD_PATH = config.global.downloadPath || "downloads";
const isDownloaded = (filename) => fs.existsSync(path.join(DOWNLOAD_PATH, filename)); 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 downloadJar = async (url, name) => { 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}`); console.log(`🟡 Skipped (already exists): ${name}`);
return; return;
} }
const USER_AGENT = config.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";
const response = await axios.get(url, {
responseType: "stream",
headers: {
"User-Agent":
USER_AGENT,
},
});
const total = parseInt(response.headers["content-length"] || "0", 10); const response = await axios.get(url, { responseType: "stream" });
const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
bar.start(total, 0);
response.data.on("data", (chunk) => bar.increment(chunk.length)); await pipeline(
response.data.on("end", () => bar.stop()); response.data,
fs.createWriteStream(filePath)
);
const filePath = path.join(DOWNLOAD_PATH, name);
await pipeline(response.data, fs.createWriteStream(filePath));
console.log(`✔️ Saved: ${name}`); console.log(`✔️ Saved: ${name}`);
}; };
// --- Source handlers ---
// --- Handle Jenkins ---
const handleJenkins = async (url) => { const handleJenkins = async (url) => {
const html = await axios.get(url).then((res) => res.data); const apiURL = url.endsWith("/")
const $ = cheerio.load(html); ? url + "api/json?tree=artifacts[fileName,relativePath]"
const links = $("a[href$='.jar']").map((_, el) => $(el).attr("href")).get(); : url + "/api/json?tree=artifacts[fileName,relativePath]";
const data = await axios.get(apiURL).then((res) => res.data);
const links = data.artifacts
.filter((a) => a.fileName.endsWith(".jar"))
.map((a) => a.relativePath);
if (!links.length) throw new Error("No .jar files found on page"); if (!links.length) throw new Error("No .jar files found on page");
const base = new URL(url); const base = new URL(url + (url.endsWith("/") ? "" : "/") + "artifact/");
const preferred = ["paper", "spigot", "bukkit"]; const preferred = ["paper", "spigot", "bukkit"];
const skip = ["javadoc", "sources", "cli", "bootstrap", "mojangapi", "nashorn", "remapper"]; const skip = ["javadoc", "sources", "cli", "bootstrap", "mojangapi", "nashorn", "remapper", "fabric", "neoforge"];
const essentialsOK = ["EssentialsX", "EssentialsXChat", "EssentialsXSpawn", "EssentialsXGeoIP"]; const essentialsOK = ["EssentialsX", "EssentialsXChat", "EssentialsXSpawn", "EssentialsXGeoIP"];
const valid = links const valid = links
@@ -107,8 +114,8 @@ const handleJenkins = async (url) => {
const lower = fileName.toLowerCase(); const lower = fileName.toLowerCase();
if (skip.some((term) => lower.includes(term))) return null; if (skip.some((term) => lower.includes(term))) return null;
if (fileName.startsWith("EssentialsX")) { if (fileName.startsWith("EssentialsX")) {
const base = fileName.split("-")[0]; const baseName = fileName.split("-")[0];
if (!essentialsOK.includes(base)) return null; if (!essentialsOK.includes(baseName)) return null;
} }
return { href, fileName: lower }; return { href, fileName: lower };
}) })
@@ -125,11 +132,11 @@ const handleJenkins = async (url) => {
// Fallback: all valid jars // Fallback: all valid jars
for (const f of valid) { for (const f of valid) {
const finalURL = new URL(f.href, base).href; await downloadJar(new URL(f.href, base).href, path.basename(f.href));
await downloadJar(finalURL, path.basename(f.href));
} }
}; };
// --- Handle GitHub ---
const handleGitHub = async (url) => { const handleGitHub = async (url) => {
const match = url.match(/github\.com\/([^/]+\/[^/]+)/); const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
if (!match) throw new Error("Invalid GitHub URL format"); if (!match) throw new Error("Invalid GitHub URL format");
@@ -146,6 +153,7 @@ const handleGitHub = async (url) => {
await downloadJar(chosen.browser_download_url, chosen.name); await downloadJar(chosen.browser_download_url, chosen.name);
}; };
// --- Handle Modrinth ---
const handleModrinth = async (url) => { const handleModrinth = async (url) => {
const match = url.match(/modrinth\.com\/plugin\/([^/]+)/); const match = url.match(/modrinth\.com\/plugin\/([^/]+)/);
if (!match) throw new Error("Invalid Modrinth URL format"); if (!match) throw new Error("Invalid Modrinth URL format");
@@ -155,13 +163,55 @@ const handleModrinth = async (url) => {
.get(`https://api.modrinth.com/v2/project/${project}/version`) .get(`https://api.modrinth.com/v2/project/${project}/version`)
.then((res) => res.data); .then((res) => res.data);
const latest = versions[0]; // Filter to only versions compatible with Spigot/Paper/etc
const file = latest.files.find((f) => f.filename.endsWith(".jar")); const compatible = versions.find((v) =>
if (!file) throw new Error("No .jar file in latest version"); (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); await downloadJar(file.url, file.filename);
}; };
// --- Handle SpigotMC ---
const handleSpigotMC = async (url) => {
const match = url.match(/spigotmc\.org\/resources\/.*\.(\d+)/);
if (!match) throw new Error("Invalid SpigotMC resource URL");
const resourceId = match[1];
const resource = await axios
.get(`https://api.spiget.org/v2/resources/${resourceId}`)
.then(res => res.data);
const versionData = await axios
.get(`https://api.spiget.org/v2/resources/${resourceId}/versions/latest`)
.then(res => res.data);
const baseName = resource.name
.split("[")[0]
.trim()
.split(/\s+/)
.slice(0, 2)
.join("-");
const rawVersion = versionData?.name ?? "latest";
const version = rawVersion.replace(/^v/i, "");
const fileName = `${baseName}-${version}.jar`;
const downloadURL = `https://api.spiget.org/v2/resources/${resourceId}/download`;
console.log(`Spiget download: ${downloadURL}`);
await downloadJar(downloadURL, fileName);
};
// --- Handle Direct (Mainly for Floodgate) ---
const handleDirect = async (url) => { const handleDirect = async (url) => {
let name = path.basename(url.split("?")[0]); let name = path.basename(url.split("?")[0]);
@@ -182,16 +232,21 @@ const handleDirect = async (url) => {
// --- Handle PaperMC --- // --- Handle PaperMC ---
const handlePaperMC = async (url) => { const handlePaperMC = async (url) => {
const versionMatch = url.match(/papermc.io\/download\/(.*?)(?:\/|$)/); if (url.includes("hangar.papermc.io")) {
if (!versionMatch) throw new Error("Invalid PaperMC URL format"); const { data: html } = await axios.get(url);
const version = versionMatch[1]; const $ = cheerio.load(html);
const apiURL = `https://api.papermc.io/v2/projects/paper/versions/${version}/builds`; const jarLink = $('a[href$=".jar"]').first().attr("href");
const builds = await axios.get(apiURL).then((res) => res.data.builds); if (!jarLink) throw new Error("❌ No .jar link found on Hangar page");
const latestBuild = builds[0];
const downloadURL = latestBuild.downloads.application.url; const downloadURL = jarLink.startsWith("http")
await downloadJar(downloadURL, `paper-${version}-${latestBuild.build}.jar`); ? jarLink
: `https://hangar.papermc.io${jarLink}`;
const fileName = path.basename(downloadURL);
await downloadJar(downloadURL, fileName);
return;
}
}; };
// --- Handle dev.bukkit.org --- // --- Handle dev.bukkit.org ---
@@ -235,6 +290,26 @@ const handleBukkit = async (url) => {
await downloadJar(fullDownloadLink, filename); 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 --- // --- Upload to SFTP ---
const uploadToSFTP = async () => { const uploadToSFTP = async () => {
@@ -245,8 +320,6 @@ const uploadToSFTP = async () => {
const sftp = new SftpClient(); const sftp = new SftpClient();
const remote = sftpConfig.remotePath || "/"; const remote = sftpConfig.remotePath || "/";
const files = fs.readdirSync(DOWNLOAD_PATH).filter((f) => f.endsWith(".jar"));
const connectOptions = { const connectOptions = {
host: sftpConfig.host, host: sftpConfig.host,
port: sftpConfig.port || 22, port: sftpConfig.port || 22,
@@ -261,28 +334,62 @@ const uploadToSFTP = async () => {
throw new Error("Missing SFTP password or private key."); throw new Error("Missing SFTP password or private key.");
} }
const extractBaseName = (filename) => {
return filename.replace(/[-_.](v?\d.*)?\.jar$/, "").trim();
};
try { try {
await sftp.connect(connectOptions); await sftp.connect(connectOptions);
const remoteFiles = await sftp.list(remote);
for (const file of remoteFiles) {
if (file.name.endsWith(".jar") && files.includes(file.name)) {
const remoteFilePath = path.posix.join(remote, file.name);
console.log(`🗑️ Deleting remote file: ${remoteFilePath}`);
await sftp.delete(remoteFilePath);
}
}
// 🚀 Upload new files const uploadFolder = async (localDir, remoteDir) => {
for (const file of files) { const files = fs.readdirSync(localDir).filter(f => f.endsWith(".jar"));
const local = path.join(DOWNLOAD_PATH, file); const remoteFiles = await sftp.list(remoteDir);
const remoteFile = path.posix.join(remote, file); const remoteJars = remoteFiles.filter(f => f.name.endsWith(".jar"));
console.log(`🚀 Uploading ${file}${remoteFile}`);
await sftp.put(local, remoteFile); for (const file of files) {
} const baseName = extractBaseName(file);
await sftp.end(); const toDelete = remoteJars.filter(r => extractBaseName(r.name) === baseName);
console.log("✅ SFTP upload finished."); 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}`);
}
};
await uploadFolder(PLUGIN_PATH, path.posix.join(remote, "plugins"));
await uploadFolder(SERVEREXEC_PATH, remote);
} catch (err) { } catch (err) {
console.error("❌ SFTP upload failed:", err.message); console.error("❌ SFTP Error:", err.message);
} finally {
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);
}
} }
}; };
@@ -296,28 +403,29 @@ const uploadToSFTP = async () => {
console.log(`🗑️ Deleted local file: ${file}`); console.log(`🗑️ Deleted local file: ${file}`);
} }
for (const url of config.urls) { await downloadLatestPaperMC();
console.log(`\n📥 ${url}`);
try { console.log("\n🔍 Starting plugin downloads from configured URLs...");
if (url.includes("github.com")) {
await handleGitHub(url); const limit = pLimit(6);
} else if (url.includes("modrinth.com")) {
await handleModrinth(url); await Promise.all(
} else if (url.includes("papermc.io")) { config.urls.map(url =>
await handlePaperMC(url); limit(async () => {
} else if (url.includes("dev.bukkit.org")) { try {
await handleBukkit(url); if (url.includes("github.com")) return handleGitHub(url);
} else if (url.includes("/job/")) { if (url.includes("modrinth.com")) return handleModrinth(url);
await handleJenkins(url); if (url.includes("papermc.io")) return handlePaperMC(url);
} else if (url.endsWith(".jar") || url.includes("download.geysermc.org")) { if (url.includes("dev.bukkit.org")) return handleBukkit(url);
await handleDirect(url); if (url.includes("spigotmc.org")) return handleSpigotMC(url);
} else { if (url.includes("/job/")) return handleJenkins(url);
console.warn("⚠️ Skipping unknown URL format."); if (url.endsWith(".jar") || url.includes("download.geysermc.org")) return handleDirect(url);
} catch (err) {
console.error(`❌ Failed: ${err.message}`);
} }
} catch (err) { })
console.error(`❌ Failed: ${err.message}`); )
} );
}
await uploadToSFTP(); await uploadToSFTP();
})(); })();

View File

@@ -1,6 +1,6 @@
{ {
"name": "download-plugs", "name": "super-cool-jar-downloader",
"version": "1.0.4", "version": "1.2.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"run": "node index.js" "run": "node index.js"
@@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"cli-progress": "^3.12.0", "p-limit": "^7.2.0",
"ssh2-sftp-client": "^12.0.0" "ssh2-sftp-client": "^12.0.0"
} }
} }