4 Commits

Author SHA1 Message Date
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 183 additions and 84 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
- 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

View File

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

224
index.js
View File

@@ -2,10 +2,10 @@ const axios = require("axios");
const fs = require("fs");
const path = require("path");
const { pipeline } = require("stream/promises");
const cliProgress = require("cli-progress");
const cheerio = require("cheerio");
const SftpClient = require("ssh2-sftp-client");
const crypto = require("crypto");
const pLimit = require("p-limit").default;
// --- Config & Secrets ---
const CONFIG_PATH = "config.json";
@@ -61,44 +61,51 @@ const ensureDir = (dir) => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
};
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) => {
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;
}
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 bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
bar.start(total, 0);
const response = await axios.get(url, { responseType: "stream" });
response.data.on("data", (chunk) => bar.increment(chunk.length));
response.data.on("end", () => bar.stop());
await pipeline(
response.data,
fs.createWriteStream(filePath)
);
const filePath = path.join(DOWNLOAD_PATH, name);
await pipeline(response.data, fs.createWriteStream(filePath));
console.log(`✔️ Saved: ${name}`);
};
// --- Source handlers ---
// --- Handle Jenkins ---
const handleJenkins = async (url) => {
const html = await axios.get(url).then((res) => res.data);
const $ = cheerio.load(html);
const links = $("a[href$='.jar']").map((_, el) => $(el).attr("href")).get();
const apiURL = url.endsWith("/")
? url + "api/json?tree=artifacts[fileName,relativePath]"
: 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");
const base = new URL(url);
const base = new URL(url + (url.endsWith("/") ? "" : "/") + "artifact/");
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 valid = links
@@ -107,8 +114,8 @@ const handleJenkins = async (url) => {
const lower = fileName.toLowerCase();
if (skip.some((term) => lower.includes(term))) return null;
if (fileName.startsWith("EssentialsX")) {
const base = fileName.split("-")[0];
if (!essentialsOK.includes(base)) return null;
const baseName = fileName.split("-")[0];
if (!essentialsOK.includes(baseName)) return null;
}
return { href, fileName: lower };
})
@@ -125,11 +132,11 @@ const handleJenkins = async (url) => {
// Fallback: all valid jars
for (const f of valid) {
const finalURL = new URL(f.href, base).href;
await downloadJar(finalURL, path.basename(f.href));
await downloadJar(new URL(f.href, base).href, path.basename(f.href));
}
};
// --- Handle GitHub ---
const handleGitHub = async (url) => {
const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
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);
};
// --- Handle Modrinth ---
const handleModrinth = async (url) => {
const match = url.match(/modrinth\.com\/plugin\/([^/]+)/);
if (!match) throw new Error("Invalid Modrinth URL format");
@@ -155,13 +163,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]);
@@ -182,16 +199,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 ---
@@ -235,6 +257,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 () => {
@@ -245,8 +287,6 @@ const uploadToSFTP = async () => {
const sftp = new SftpClient();
const remote = sftpConfig.remotePath || "/";
const files = fs.readdirSync(DOWNLOAD_PATH).filter((f) => f.endsWith(".jar"));
const connectOptions = {
host: sftpConfig.host,
port: sftpConfig.port || 22,
@@ -261,28 +301,62 @@ const uploadToSFTP = async () => {
throw new Error("Missing SFTP password or private key.");
}
const extractBaseName = (filename) => {
return filename.replace(/[-_.](v?\d.*)?\.jar$/, "").trim();
};
try {
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);
}
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}`);
}
// 🚀 Upload new files
for (const file of files) {
const local = path.join(DOWNLOAD_PATH, file);
const remoteFile = path.posix.join(remote, file);
console.log(`🚀 Uploading ${file}${remoteFile}`);
await sftp.put(local, remoteFile);
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 sftp.end();
console.log("✅ SFTP upload finished.");
};
await uploadFolder(PLUGIN_PATH, path.posix.join(remote, "plugins"));
await uploadFolder(SERVEREXEC_PATH, remote);
} 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 +370,28 @@ const uploadToSFTP = async () => {
console.log(`🗑️ Deleted local file: ${file}`);
}
for (const url of config.urls) {
console.log(`\n📥 ${url}`);
await downloadLatestPaperMC();
console.log("\n🔍 Starting plugin downloads from configured URLs...");
const limit = pLimit(6);
await Promise.all(
config.urls.map(url =>
limit(async () => {
try {
if (url.includes("github.com")) {
await handleGitHub(url);
} else if (url.includes("modrinth.com")) {
await handleModrinth(url);
} else if (url.includes("papermc.io")) {
await handlePaperMC(url);
} else if (url.includes("dev.bukkit.org")) {
await handleBukkit(url);
} else if (url.includes("/job/")) {
await handleJenkins(url);
} else if (url.endsWith(".jar") || url.includes("download.geysermc.org")) {
await handleDirect(url);
} else {
console.warn("⚠️ Skipping unknown URL format.");
}
if (url.includes("github.com")) return handleGitHub(url);
if (url.includes("modrinth.com")) return handleModrinth(url);
if (url.includes("papermc.io")) return handlePaperMC(url);
if (url.includes("dev.bukkit.org")) return handleBukkit(url);
if (url.includes("/job/")) return handleJenkins(url);
if (url.endsWith(".jar") || url.includes("download.geysermc.org")) return handleDirect(url);
} catch (err) {
console.error(`❌ Failed: ${err.message}`);
}
}
})
)
);
await uploadToSFTP();
})();

View File

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