1 Commits
1.0.5 ... 1.1.0

Author SHA1 Message Date
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
5 changed files with 135 additions and 40 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
} }
} }

136
index.js
View File

@@ -61,12 +61,18 @@ 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";
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) => { 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;
} }
@@ -86,12 +92,12 @@ const downloadJar = async (url, name) => {
response.data.on("data", (chunk) => bar.increment(chunk.length)); response.data.on("data", (chunk) => bar.increment(chunk.length));
response.data.on("end", () => bar.stop()); response.data.on("end", () => bar.stop());
const filePath = path.join(DOWNLOAD_PATH, name);
await pipeline(response.data, fs.createWriteStream(filePath)); await pipeline(response.data, fs.createWriteStream(filePath));
console.log(`✔️ Saved: ${name}`); console.log(`✔️ Saved: ${filePath}`);
}; };
// --- Source handlers ---
// --- Handle Jenkins ---
const handleJenkins = async (url) => { const handleJenkins = async (url) => {
const html = await axios.get(url).then((res) => res.data); const html = await axios.get(url).then((res) => res.data);
const $ = cheerio.load(html); const $ = cheerio.load(html);
@@ -131,6 +137,7 @@ const handleJenkins = async (url) => {
} }
}; };
// --- 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");
@@ -147,6 +154,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");
@@ -156,13 +164,22 @@ 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 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]);
@@ -183,16 +200,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 ---
@@ -236,6 +258,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 () => {
@@ -246,8 +288,6 @@ const uploadToSFTP = async () => {
const sftp = new SftpClient(); const sftp = new SftpClient();
const remote = sftpConfig.remotePath || "/"; const remote = sftpConfig.remotePath || "/";
const localFiles = 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,
@@ -268,29 +308,56 @@ const uploadToSFTP = async () => {
try { try {
await sftp.connect(connectOptions); await sftp.connect(connectOptions);
const remoteFiles = await sftp.list(remote);
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")); const remoteJars = remoteFiles.filter(f => f.name.endsWith(".jar"));
for (const localFile of localFiles) { for (const file of files) {
const baseName = extractBaseName(localFile); const baseName = extractBaseName(file);
const toDelete = remoteJars.filter(remoteFile => const toDelete = remoteJars.filter(r => extractBaseName(r.name) === baseName);
extractBaseName(remoteFile.name) === baseName for (const del of toDelete) {
); const fullPath = path.posix.join(remoteDir, del.name);
for (const file of toDelete) {
const fullPath = path.posix.join(remote, file.name);
await sftp.delete(fullPath); await sftp.delete(fullPath);
console.log(`🗑️ Deleted remote: ${file.name}`); console.log(`🗑️ Deleted remote: ${fullPath}`);
} }
// 🚀 Upload new files
const localPath = path.join(DOWNLOAD_PATH, localFile); const localPath = path.join(localDir, file);
const remotePath = path.posix.join(remote, localFile); const remotePath = path.posix.join(remoteDir, file);
await sftp.fastPut(localPath, remotePath); await sftp.fastPut(localPath, remotePath);
console.log(`⬆️ Uploaded: ${localFile}`); 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 Error:", err.message); console.error("❌ SFTP Error:", err.message);
} finally { } 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}`); console.log(`🗑️ Deleted local file: ${file}`);
} }
await downloadLatestPaperMC();
console.log("\n🔍 Starting plugin downloads from configured URLs...");
for (const url of config.urls) { for (const url of config.urls) {
console.log(`\n📥 ${url}`); console.log(`\n📥 ${url}`);
try { try {

View File

@@ -1,6 +1,6 @@
{ {
"name": "download-plugs", "name": "download-plugs",
"version": "1.0.5", "version": "1.1.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"run": "node index.js" "run": "node index.js"