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
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
136
index.js
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user