mirror of
https://github.com/noothenoot/crafty-mod-installer.git
synced 2026-07-21 21:37:00 +00:00
222 lines
8.3 KiB
JavaScript
222 lines
8.3 KiB
JavaScript
const axios = require('axios');
|
|
const https = require('https');
|
|
const FormData = require('form-data');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
|
|
class CraftyClient {
|
|
constructor(baseUrl, token) {
|
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
this.token = token;
|
|
this.client = axios.create({
|
|
baseURL: `${this.baseUrl}/api/v2`,
|
|
headers: {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
httpsAgent: new https.Agent({
|
|
rejectUnauthorized: false
|
|
})
|
|
});
|
|
}
|
|
|
|
async testConnection() {
|
|
try {
|
|
const resp = await this.client.get('/servers');
|
|
return resp.status === 200;
|
|
} catch (err) {
|
|
if (err.response) {
|
|
console.error(`Crafty Connection Error: ${err.response.status} - ${JSON.stringify(err.response.data)}`);
|
|
} else if (err.request) {
|
|
console.error(`Crafty Connection Error: No response received. Check if your CRAFTY_URL is correct and reachable.`);
|
|
} else {
|
|
console.error('Crafty Connection Error:', err.message);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async listServers() {
|
|
const resp = await this.client.get('/servers');
|
|
return resp.data.data;
|
|
}
|
|
|
|
async getServerDetails(serverId) {
|
|
const resp = await this.client.get(`/servers/${serverId}`);
|
|
return resp.data.data;
|
|
}
|
|
|
|
async listFiles(serverId, remotePath = 'mods') {
|
|
try {
|
|
const targetPath = remotePath.replace(/^\//, '');
|
|
// In Crafty 4, file listing is often just GET /servers/{id}/files
|
|
// or /servers/{id}/files/list. We'll try the most standard one first.
|
|
const resp = await this.client.get(`/servers/${serverId}/files?path=${encodeURIComponent(targetPath)}`);
|
|
return resp.data.data || [];
|
|
} catch (err) {
|
|
// Fallback for different Crafty versions
|
|
try {
|
|
const resp = await this.client.get(`/servers/${serverId}/files/list?path=${encodeURIComponent(remotePath)}`);
|
|
return resp.data.data || [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
|
|
async createServer(name, serverType = 'paper', memory = 2048, port = 25565, version = '1.20.1') {
|
|
const memGB = Math.floor(memory / 1024) || 1;
|
|
const payload = {
|
|
"name": name,
|
|
"monitoring_type": "minecraft_java",
|
|
"minecraft_java_monitoring_data": {
|
|
"host": "127.0.0.1",
|
|
"port": port
|
|
},
|
|
"create_type": "minecraft_java",
|
|
"minecraft_java_create_data": {
|
|
"create_type": "download_jar",
|
|
"download_jar_create_data": {
|
|
"category": "mc_java_servers",
|
|
"type": serverType,
|
|
"version": version,
|
|
"mem_min": memGB,
|
|
"mem_max": memGB,
|
|
"server_properties_port": port
|
|
}
|
|
}
|
|
};
|
|
const resp = await this.client.post('/servers', payload);
|
|
const data = resp.data.data;
|
|
|
|
// Handle variations in ID property name
|
|
if (data && !data.server_id) {
|
|
data.server_id = data.new_server_id || data.id || data.serverID || data.uuid;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
async installServer(serverId) {
|
|
const resp = await this.client.post(`/servers/${serverId}/install`);
|
|
return resp.data;
|
|
}
|
|
|
|
async runAction(serverId, action) {
|
|
// Common actions: start_server, stop_server, restart_server, kill_server
|
|
const resp = await this.client.post(`/servers/${serverId}/action/${action}`);
|
|
return resp.data;
|
|
}
|
|
|
|
async getServerStatus(serverId) {
|
|
const resp = await this.client.get(`/servers/${serverId}`);
|
|
return resp.data.data;
|
|
}
|
|
|
|
async createRemoteFile(serverId, fileName, content, remotePath = '') {
|
|
const tempPath = path.join(process.cwd(), `temp_${fileName}`);
|
|
await fs.writeFile(tempPath, content);
|
|
try {
|
|
await this.uploadFile(serverId, tempPath, remotePath);
|
|
} finally {
|
|
await fs.remove(tempPath);
|
|
}
|
|
}
|
|
|
|
async ensureDirectory(serverId, remotePath) {
|
|
try {
|
|
const targetPath = remotePath.replace(/^\//, '');
|
|
// Attempt to create folder using both common endpoints
|
|
await this.client.post(`/servers/${serverId}/files/folder`, { path: targetPath }).catch(() => {
|
|
return this.client.post(`/servers/${serverId}/files`, { action: 'mkdir', path: targetPath });
|
|
});
|
|
} catch (err) {
|
|
// Ignore errors if directory already exists
|
|
}
|
|
}
|
|
|
|
async uploadFile(serverId, localFilePath, remotePath = 'mods') {
|
|
const stats = fs.statSync(localFilePath);
|
|
const fileName = path.basename(localFilePath);
|
|
const fileId = Math.random().toString(36).substring(2, 15);
|
|
const targetLocation = remotePath.replace(/^\//, '');
|
|
|
|
await this.ensureDirectory(serverId, targetLocation);
|
|
|
|
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
|
|
const totalChunks = Math.ceil(stats.size / CHUNK_SIZE);
|
|
|
|
console.log(` Syncing ${fileName} to Crafty (${(stats.size / 1024 / 1024).toFixed(2)} MB)...`);
|
|
|
|
if (stats.size > CHUNK_SIZE) {
|
|
console.log(` [Chunked Upload] Splitting into ${totalChunks} parts...`);
|
|
|
|
// Initial handshake request (no chunkId)
|
|
await this.client.post(`/servers/${serverId}/files/upload`, null, {
|
|
headers: {
|
|
'chunked': 'true',
|
|
'fileId': fileId,
|
|
'fileName': fileName,
|
|
'fileSize': stats.size.toString(),
|
|
'totalChunks': totalChunks.toString(),
|
|
'location': targetLocation
|
|
}
|
|
});
|
|
|
|
for (let i = 0; i < totalChunks; i++) {
|
|
const start = i * CHUNK_SIZE;
|
|
const end = Math.min(start + CHUNK_SIZE, stats.size);
|
|
const chunkBody = await this.readChunk(localFilePath, start, end);
|
|
|
|
// Calculate SHA-256 hash of the chunk
|
|
const hash = crypto.createHash('sha256').update(chunkBody).digest('hex');
|
|
|
|
process.stdout.write(` Uploading part ${i + 1}/${totalChunks}... `);
|
|
await this.client.post(`/servers/${serverId}/files/upload`, chunkBody, {
|
|
headers: {
|
|
'Content-Type': 'application/octet-stream',
|
|
'location': targetLocation,
|
|
'fileName': fileName,
|
|
'fileSize': stats.size.toString(),
|
|
'fileId': fileId,
|
|
'chunked': 'true',
|
|
'chunkId': i.toString(),
|
|
'totalChunks': totalChunks.toString(),
|
|
'chunkHash': hash
|
|
},
|
|
maxContentLength: Infinity,
|
|
maxBodyLength: Infinity
|
|
});
|
|
console.log('✅');
|
|
}
|
|
} else {
|
|
// Small file - single upload
|
|
await this.client.post(`/servers/${serverId}/files/upload`, fs.createReadStream(localFilePath), {
|
|
headers: {
|
|
'Content-Type': 'application/octet-stream',
|
|
'location': targetLocation,
|
|
'fileName': fileName,
|
|
'fileSize': stats.size.toString(),
|
|
'fileId': fileId
|
|
},
|
|
maxContentLength: Infinity,
|
|
maxBodyLength: Infinity
|
|
});
|
|
}
|
|
return { status: 'ok' };
|
|
}
|
|
|
|
async readChunk(filePath, start, end) {
|
|
return new Promise((resolve, reject) => {
|
|
const stream = fs.createReadStream(filePath, { start, end: end - 1 });
|
|
const chunks = [];
|
|
stream.on('data', (chunk) => chunks.push(chunk));
|
|
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
stream.on('error', (err) => reject(err));
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = CraftyClient;
|