commit 7677a03ec848fa2fff9440d8c5faba02344c99e2 Author: Sophia Atkinson Date: Mon May 5 00:37:43 2025 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..029742e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +secret.key +config.json +downloads/ +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bd61f2 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Super Cool Jar Downloader / Uploader :3 + +> Highly customized for my use case. Not intended for the lay-person use. +> Im Assuming you're experienced, can read the code and understand it. + +## Features + +- Auto-detects download source (GitHub / Jenkins / Modrinth / Direct) +- Skips already downloaded files +- Password encryption for SFTP (AES-256-CBC) +- Optional SFTP upload support +- CLI progress bar for downloads + +## Requirements + +- Node.js v20+ +- `config.json` in project root +- Generates `secret.key` on first run (For SFTP/SSH connections to copy the jars to your server) diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..f70c84f --- /dev/null +++ b/config.json.example @@ -0,0 +1,14 @@ +{ + "urls": [ + "https://github.com/McPlugin/SoCool", + ], + "sftp": { + "enabled": true, + "host": "", + "port": 22, + "username": "", + "password": "", // This will be encrypted by the script and removed from the file + "remotePath": "/minecraft/plugins", + "privateKeyPath": "" // Leave blank if using password + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..034ab5e --- /dev/null +++ b/index.js @@ -0,0 +1,247 @@ +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"); + +// --- Config & Secrets --- +const CONFIG_PATH = "config.json"; +const SECRET_PATH = "secret.key"; + +// Load or create encryption key +let secret; +if (!fs.existsSync(SECRET_PATH)) { + secret = crypto.randomBytes(32); + fs.writeFileSync(SECRET_PATH, secret.toString("hex")); + console.log("šŸ” Generated and saved a new secret key."); +} else { + secret = Buffer.from(fs.readFileSync(SECRET_PATH, "utf-8"), "hex"); +} + +function encryptPassword(password) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv("aes-256-cbc", secret, iv); + const encrypted = Buffer.concat([cipher.update(password, "utf8"), cipher.final()]); + return { + iv: iv.toString("hex"), + password: encrypted.toString("hex"), + }; +} + +function decryptPassword(encrypted) { + const decipher = crypto.createDecipheriv( + "aes-256-cbc", + secret, + Buffer.from(encrypted.iv, "hex") + ); + const decrypted = Buffer.concat([ + decipher.update(Buffer.from(encrypted.password, "hex")), + decipher.final(), + ]); + return decrypted.toString("utf8"); +} + +// Load config and auto-convert plaintext SFTP password +let config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")); +const sftpConfig = config.sftp || { enabled: false }; + +if (sftpConfig.password && !sftpConfig.encryptedPassword) { + sftpConfig.encryptedPassword = encryptPassword(sftpConfig.password); + delete sftpConfig.password; + config.sftp = sftpConfig; + fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); + console.log("šŸ” Encrypted SFTP password saved to config.json."); +} + +// --- Utilities --- +const ensureDir = (dir) => { + if (!fs.existsSync(dir)) fs.mkdirSync(dir); +}; + +const isDownloaded = (filename) => fs.existsSync(path.join("downloads", filename)); + +const downloadJar = async (url, name) => { + if (isDownloaded(name)) { + console.log(`🟔 Skipped (already exists): ${name}`); + return; + } + + const response = await axios.get(url, { + responseType: "stream", + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", + }, + }); + + const total = parseInt(response.headers["content-length"] || "0", 10); + const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); + bar.start(total, 0); + + response.data.on("data", (chunk) => bar.increment(chunk.length)); + response.data.on("end", () => bar.stop()); + + const filePath = path.join("downloads", name); + await pipeline(response.data, fs.createWriteStream(filePath)); + console.log(`āœ”ļø Saved: ${name}`); +}; + +// --- Source handlers --- +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(); + if (!links.length) throw new Error("No .jar files found on page"); + + const base = new URL(url); + const preferred = ["paper", "spigot", "bukkit"]; + const skip = ["javadoc", "sources", "cli", "bootstrap", "mojangapi", "nashorn", "remapper"]; + const essentialsOK = ["EssentialsX", "EssentialsXChat", "EssentialsXSpawn", "EssentialsXGeoIP"]; + + const valid = links + .map((href) => { + const fileName = path.basename(href); + 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; + } + return { href, fileName: lower }; + }) + .filter(Boolean); + + for (const tag of preferred) { + const pick = valid.find((f) => f.fileName.includes(tag)); + if (pick) { + const finalURL = new URL(pick.href, base).href; + await downloadJar(finalURL, path.basename(pick.href)); + return; + } + } + + // Fallback: all valid jars + for (const f of valid) { + const finalURL = new URL(f.href, base).href; + await downloadJar(finalURL, path.basename(f.href)); + } +}; + +const handleGitHub = async (url) => { + const match = url.match(/github\.com\/([^/]+\/[^/]+)/); + if (!match) throw new Error("Invalid GitHub URL format"); + const repo = match[1]; + const apiURL = `https://api.github.com/repos/${repo}/releases/latest`; + const { data } = await axios.get(apiURL); + + const preferred = data.assets.find((a) => a.name === "GeyserSkinManager-Spigot.jar"); + const fallback = data.assets.find((a) => a.name.endsWith(".jar")); + + const chosen = preferred || fallback; + if (!chosen) throw new Error("No .jar assets found"); + + await downloadJar(chosen.browser_download_url, chosen.name); +}; + +const handleModrinth = async (url) => { + const match = url.match(/modrinth\.com\/plugin\/([^/]+)/); + if (!match) throw new Error("Invalid Modrinth URL format"); + const project = match[1]; + + const versions = await axios + .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"); + + await downloadJar(file.url, file.filename); +}; + +const handleDirect = async (url) => { + let name = path.basename(url.split("?")[0]); + + if (url.includes("download.geysermc.org")) { + name = "floodgate-spigot.jar"; + } else { + try { + const head = await axios.head(url); + const disp = head.headers["content-disposition"]; + const match = disp?.match(/filename[^=]*=(?:UTF-8'')?["']?([^"';]+)/i); + if (match) name = decodeURIComponent(match[1]); + } catch {} + } + + name = name.replace(/[<>:"/\\|?*\x00-\x1F]/g, ""); + await downloadJar(url, name); +}; + +// --- Upload to SFTP --- +const uploadToSFTP = async () => { + if (!sftpConfig.enabled) { + console.log("šŸ“¦ SFTP is disabled in config."); + return; + } + + const sftp = new SftpClient(); + const remote = sftpConfig.remotePath || "/"; + const files = fs.readdirSync("downloads").filter((f) => f.endsWith(".jar")); + + const connectOptions = { + host: sftpConfig.host, + port: sftpConfig.port || 22, + username: sftpConfig.username, + }; + + if (sftpConfig.privateKeyPath && fs.existsSync(sftpConfig.privateKeyPath)) { + connectOptions.privateKey = fs.readFileSync(sftpConfig.privateKeyPath); + } else if (sftpConfig.encryptedPassword) { + connectOptions.password = decryptPassword(sftpConfig.encryptedPassword); + } else { + throw new Error("Missing SFTP password or private key."); + } + + try { + await sftp.connect(connectOptions); + for (const file of files) { + const local = path.join("downloads", file); + const remoteFile = path.join(remote, file).replace(/\\/g, "/"); + console.log(`šŸš€ Uploading ${file} → ${remoteFile}`); + await sftp.put(local, remoteFile); + } + await sftp.end(); + console.log("āœ… SFTP upload finished."); + } catch (err) { + console.error("āŒ SFTP upload failed:", err.message); + } +}; + +// --- Main --- +(async () => { + ensureDir("downloads"); + + for (const url of config.urls) { + console.log(`\nšŸ“„ ${url}`); + try { + if (url.includes("github.com")) { + await handleGitHub(url); + } else if (url.includes("modrinth.com")) { + await handleModrinth(url); + } else if (url.includes("/lastSuccessfulBuild/")) { + await handleJenkins(url); + } else if (url.endsWith(".jar") || url.includes("download.geysermc.org")) { + await handleDirect(url); + } else { + console.warn("āš ļø Skipping unknown URL format."); + } + } catch (err) { + console.error(`āŒ Failed: ${err.message}`); + } + } + + await uploadToSFTP(); +})(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..a295312 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "download-plugs", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "run": "node index.js" + }, + "keywords": [], + "author": "Sophia Atkinson", + "license": "MIT?", + "description": "", + "dependencies": { + "axios": "^1.9.0", + "cheerio": "^1.0.0", + "cli-progress": "^3.12.0", + "ssh2-sftp-client": "^12.0.0" + } +}