Initial commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
secret.key
|
||||
config.json
|
||||
downloads/
|
||||
package-lock.json
|
18
README.md
Normal file
18
README.md
Normal file
@ -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)
|
14
config.json.example
Normal file
14
config.json.example
Normal file
@ -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
|
||||
}
|
||||
}
|
247
index.js
Normal file
247
index.js
Normal file
@ -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();
|
||||
})();
|
18
package.json
Normal file
18
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user