diff --git a/app/Http/Controllers/Client/Servers/CurseForge/ModpackController.php b/app/Http/Controllers/Client/Servers/CurseForge/ModpackController.php index 7ebdda7..9230f3c 100644 --- a/app/Http/Controllers/Client/Servers/CurseForge/ModpackController.php +++ b/app/Http/Controllers/Client/Servers/CurseForge/ModpackController.php @@ -5,17 +5,21 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers\CurseForge; use GuzzleHttp\Client; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Log; use Nette\NotImplementedException; use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Jobs\ModInstall; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; +use Pterodactyl\Repositories\Wings\DaemonFileRepository; class ModpackController extends ClientApiController { private $http_client; private $minecraft_game_id; private $modpack_class_id; + private $api_key; + private DaemonFileRepository $fileRepository; - public function __construct() { + + public function __construct(DaemonFileRepository $fileRepository) { parent::__construct(); if(!config('curseforge.api_key')) { @@ -29,8 +33,11 @@ class ModpackController extends ClientApiController { ] ]); + $this->api_key = config('curseforge.api_key'); $this->minecraft_game_id = config('curseforge.minecraft_game_id'); $this->modpack_class_id = config('curseforge.minecraft_modpack_class_id'); + + $this->fileRepository = $fileRepository; } public function index(Request $request) { @@ -68,7 +75,55 @@ class ModpackController extends ClientApiController { throw new NotImplementedException(); } - public function install() { - throw new NotImplementedException(); + public function install($server, $modId, $fileId) { + $this->fileRepository->setServer($server)->deleteFiles('/', ['mods']); + $this->fileRepository->setServer($server)->deleteFiles('/', ['uninstallable.txt']); + + $modpackFile = json_decode($this->getFileById($modId, $fileId)); + + $data = $modpackFile->data; + $modpackFileUrl = $this->traceUrl($data->downloadUrl); + + $this->fileRepository->setServer($server)->pull( + $modpackFileUrl, + '/', + ['filename' => 'modpack.zip', 'foreground' => true] + ); + + $this->fileRepository->setServer($server)->decompressFile('', 'modpack.zip'); + $modpackManifest = json_decode($this->fileRepository->setServer($server)->getContent('manifest.json')); + + $job = ModInstall::dispatch($modpackManifest, $server, $this->api_key); + + return response()->json([ + 'success' => true, + ]); + } + + private function traceUrl(string $url): string { + $httpCode = ''; + while (!str_contains($httpCode, '200 OK')) { + $headers = get_headers($url, 5); + $httpCode = $headers[0]; + if (array_key_exists('Location', $headers) && !empty($headers['Location'])) { + $url = $headers['Location']; + } else { + break; + } + } + + return $url; + } + + private function getModById(int $modId): string { + $result = $this->http_client->get("mods/$modId"); + + return $result->getBody()->getContents(); + } + + private function getFileById(int $modId, int $fileId): string { + $result = $this->http_client->get("mods/$modId/files/$fileId"); + + return $result->getBody()->getContents(); } } diff --git a/app/Jobs/ModInstall.php b/app/Jobs/ModInstall.php new file mode 100644 index 0000000..8f8977f --- /dev/null +++ b/app/Jobs/ModInstall.php @@ -0,0 +1,76 @@ +api_key = $api_key; + $this->modpackManifest = $modpackManifest; + $this->server = $server; + } + + public function handle(DaemonFileRepository $fileRepository) { + $this->http_client = new Client([ + 'base_uri' => 'https://api.curseforge.com/v1/', + 'headers' => [ + 'x-api-key' => $this->api_key, + ] + ]); + + $uninstallable = []; + + foreach($this->modpackManifest->files as $file) { + $modFile = json_decode($this->getFileById($file->projectID, $file->fileID)); + $modFileUrl = $this->traceUrl($modFile->data->downloadUrl); + + if($modFileUrl == null) { + $uninstallable[] = $modFile->data->fileName; + continue; + } + + $fileRepository->setServer($this->server)->pull( + $modFileUrl, + 'mods', + ['filename' => $modFile->data->fileName, 'foreground' => true] + ); + } + + $fileRepository->putContent('uninstallable.txt', print_r($uninstallable)); + $fileRepository->deleteFiles('/', ['modpack.zip', 'manifest.json', $this->modpackManifest->overrides]); + } + + private function getFileById(int $modId, int $fileId): string { + $result = $this->http_client->get("mods/$modId/files/$fileId"); + + return $result->getBody()->getContents(); + } + + private function traceUrl(string $url): string { + $httpCode = ''; + while (!str_contains($httpCode, '200 OK')) { + $headers = get_headers($url, 5); + $httpCode = $headers[0]; + if (array_key_exists('Location', $headers) && !empty($headers['Location'])) { + $url = $headers['Location']; + } else { + break; + } + } + + return $url; + } +} diff --git a/app/config/curseforge.php b/app/config/curseforge.php new file mode 100644 index 0000000..39a902f --- /dev/null +++ b/app/config/curseforge.php @@ -0,0 +1,7 @@ + env('CURSEFORGE_API_KEY'), + 'minecraft_game_id' => '432', + 'minecraft_modpack_class_id' => '4471', +]; diff --git a/resources/scripts/api/server/modpacks/installModpack.ts b/resources/scripts/api/server/modpacks/installModpack.ts new file mode 100644 index 0000000..726cd87 --- /dev/null +++ b/resources/scripts/api/server/modpacks/installModpack.ts @@ -0,0 +1,12 @@ +import http from "@/api/http"; + +export default (server: string, modpackId: number, fileId: number): Promise => { + return new Promise((resolve, reject) => { + http.get(`/api/client/servers/${server}/modpacks/${modpackId}/install/${fileId}`) + .then(({ data }) => { + console.log(data); + resolve(data) + }) + .catch(reject); + }); +} diff --git a/resources/scripts/components/server/modpacks/ModpackItem.tsx b/resources/scripts/components/server/modpacks/ModpackItem.tsx index fd1a7f3..2480fd0 100644 --- a/resources/scripts/components/server/modpacks/ModpackItem.tsx +++ b/resources/scripts/components/server/modpacks/ModpackItem.tsx @@ -8,20 +8,38 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faDownload, faGamepad } from '@fortawesome/free-solid-svg-icons'; import getModpackDescription from '@/api/server/modpacks/getModpackDescription'; import { ServerContext } from '@/state/server'; -import { get } from 'http'; -import pullFile from '@/api/server/files/pullFile'; -import getFileExists from '@/api/server/files/getFileExists'; import Spinner from '@/components/elements/Spinner'; -import decompressFiles from '@/api/server/files/decompressFiles'; +import installModpack from '@/api/server/modpacks/installModpack'; +import getFileExists from '@/api/server/files/getFileExists'; interface Props { modpack: Modpack; } +interface ModpackManifest { + author: string; + files: ModpackFile[] + manifestType: string; + manifestVersion: number; + minecraft: {}; + name: string; + overrides: string; + version: string; +} + +interface ModpackFile { + projectID: number; + fileID: number; + required: boolean; +} + +const timer = (ms: number) => new Promise(res => setTimeout(res, ms)) + export default ({ modpack }: Props) => { const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); const [visible, setVisible] = React.useState(false); const [installVisible, setInstallVisible] = React.useState(false); + const [installed, setInstalled] = React.useState(false); const [modpackDesc, setModpackDesc] = React.useState(''); const [installing, setInstalling] = React.useState(false); @@ -35,22 +53,17 @@ export default ({ modpack }: Props) => { const install = async (latestFile: File): Promise => { setInstalling(true); - pullFile(uuid, { - url: latestFile.downloadUrl, - directory: '/home/container', - filename: 'modpack.zip', - use_header: true, - foreground: true, - }); - let modpackDownloaded = await getFileExists(uuid, '/home/container/modpack.zip'); - while (!modpackDownloaded) { - modpackDownloaded = await getFileExists(uuid, '/home/container/modpack.zip'); + await installModpack(uuid, modpack.id, latestFile.id); + + let uninstallableExists = false; + while (!uninstallableExists) { + uninstallableExists = await getFileExists(uuid, 'uninstallable.txt') as unknown as boolean; + await timer(6500); } - decompressFiles(uuid, '/home/container/modpack', '/home/container/modpack.zip'); - setInstalling(false); + setInstalled(true); }; const getDescription = (): void => { @@ -94,26 +107,31 @@ export default ({ modpack }: Props) => {
{installing ? (
-

Installing...

+

Installing... This can take a while, sit back, grab a drink and relax

+ ) : (<>{installed ? ( +

Modpack insatlled, if any mods couldn't be manually installed you can find them in the file 'uninstallable.txt', install them manually before playing.

) : ( - modpack.latestFiles.map((file) => ( -
-

{file.displayName}

-
- - {file.gameVersions[0]} -
- -
- )))} + modpack.latestFiles.map((file) => ( +
+

{file.displayName}

+
+ + {file.gameVersions[0]} +
+ +
+ )) + )} + + )}
diff --git a/resources/scripts/components/server/modpacks/ModpackModal.tsx b/resources/scripts/components/server/modpacks/ModpackModal.tsx index 3b107a2..e3e5bb4 100644 --- a/resources/scripts/components/server/modpacks/ModpackModal.tsx +++ b/resources/scripts/components/server/modpacks/ModpackModal.tsx @@ -1,16 +1,150 @@ -import { Modpack } from "@/api/server/modpacks/Modpack"; -import PortaledModal, { ModalProps } from "@/components/elements/Modal"; -import React, { PropsWithChildren } from "react"; +) => { +/* +|-------------------------------------------------------------------------- +| Client Control API +|-------------------------------------------------------------------------- +| +| Endpoint: /api/client +| +*/ +Route::get('/', [Client\ClientController::class, 'index'])->name('api:client.index'); +Route::get('/permissions', [Client\ClientController::class, 'permissions']); - return ( - - {props.children} - - ) -} +Route::prefix('/account')->middleware(AccountSubject::class)->group(function () { + Route::prefix('/')->withoutMiddleware(RequireTwoFactorAuthentication::class)->group(function () { + Route::get('/', [Client\AccountController::class, 'index'])->name('api:client.account'); + Route::get('/two-factor', [Client\TwoFactorController::class, 'index']); + Route::post('/two-factor', [Client\TwoFactorController::class, 'store']); + Route::delete('/two-factor', [Client\TwoFactorController::class, 'delete']); + }); + + Route::put('/email', [Client\AccountController::class, 'updateEmail'])->name('api:client.account.update-email'); + Route::put('/password', [Client\AccountController::class, 'updatePassword'])->name('api:client.account.update-password'); + + Route::get('/activity', Client\ActivityLogController::class)->name('api:client.account.activity'); + + Route::get('/api-keys', [Client\ApiKeyController::class, 'index']); + Route::post('/api-keys', [Client\ApiKeyController::class, 'store']); + Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']); + + Route::prefix('/ssh-keys')->group(function () { + Route::get('/', [Client\SSHKeyController::class, 'index']); + Route::post('/', [Client\SSHKeyController::class, 'store']); + Route::post('/remove', [Client\SSHKeyController::class, 'delete']); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Client Control API +|-------------------------------------------------------------------------- +| +| Endpoint: /api/client/servers/{server} +| +*/ +Route::group([ + 'prefix' => '/servers/{server}', + 'middleware' => [ + ServerSubject::class, + AuthenticateServerAccess::class, + ResourceBelongsToServer::class, + ], +], function () { + Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('api:client:server.view'); + Route::get('/websocket', Client\Servers\WebsocketController::class)->name('api:client:server.ws'); + Route::get('/resources', Client\Servers\ResourceUtilizationController::class)->name('api:client:server.resources'); + Route::get('/activity', Client\Servers\ActivityLogController::class)->name('api:client:server.activity'); + + Route::post('/command', [Client\Servers\CommandController::class, 'index']); + Route::post('/power', [Client\Servers\PowerController::class, 'index']); + + Route::group(['prefix' => '/modpacks'], function () { + Route::get('/', [Client\Servers\CurseForge\ModpackController::class, 'index']); + Route::get('/{modpack}', [Client\Servers\CurseForge\ModpackController::class, 'show']); + Route::get('/{modpack}/install/{file}', [Client\Servers\CurseForge\ModpackController::class, 'install']); + Route::get('/{modpack}/description', [Client\Servers\CurseForge\ModpackController::class, 'description']); + }); + + Route::group(['prefix' => '/databases'], function () { + Route::get('/', [Client\Servers\DatabaseController::class, 'index']); + Route::post('/', [Client\Servers\DatabaseController::class, 'store']); + Route::post('/{database}/rotate-password', [Client\Servers\DatabaseController::class, 'rotatePassword']); + Route::delete('/{database}', [Client\Servers\DatabaseController::class, 'delete']); + }); + + Route::group(['prefix' => '/files'], function () { + Route::get('/list', [Client\Servers\FileController::class, 'directory']); + Route::get('/contents', [Client\Servers\FileController::class, 'contents']); + Route::get('/exists', [Client\Servers\FileController::class, 'exists']); + Route::get('/download', [Client\Servers\FileController::class, 'download']); + Route::put('/rename', [Client\Servers\FileController::class, 'rename']); + Route::post('/copy', [Client\Servers\FileController::class, 'copy']); + Route::post('/write', [Client\Servers\FileController::class, 'write']); + Route::post('/compress', [Client\Servers\FileController::class, 'compress']); + Route::post('/decompress', [Client\Servers\FileController::class, 'decompress']); + Route::post('/delete', [Client\Servers\FileController::class, 'delete']); + Route::post('/create-folder', [Client\Servers\FileController::class, 'create']); + Route::post('/chmod', [Client\Servers\FileController::class, 'chmod']); + Route::post('/pull', [Client\Servers\FileController::class, 'pull'])->middleware(['throttle:10,5']); + Route::get('/upload', Client\Servers\FileUploadController::class); + }); + + Route::group(['prefix' => '/schedules'], function () { + Route::get('/', [Client\Servers\ScheduleController::class, 'index']); + Route::post('/', [Client\Servers\ScheduleController::class, 'store']); + Route::get('/{schedule}', [Client\Servers\ScheduleController::class, 'view']); + Route::post('/{schedule}', [Client\Servers\ScheduleController::class, 'update']); + Route::post('/{schedule}/execute', [Client\Servers\ScheduleController::class, 'execute']); + Route::delete('/{schedule}', [Client\Servers\ScheduleController::class, 'delete']); + + Route::post('/{schedule}/tasks', [Client\Servers\ScheduleTaskController::class, 'store']); + Route::post('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'update']); + Route::delete('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'delete']); + }); + + Route::group(['prefix' => '/network'], function () { + Route::get('/allocations', [Client\Servers\NetworkAllocationController::class, 'index']); + Route::post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']); + Route::post('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']); + Route::post('/allocations/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']); + Route::delete('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']); + }); + + Route::group(['prefix' => '/users'], function () { + Route::get('/', [Client\Servers\SubuserController::class, 'index']); + Route::post('/', [Client\Servers\SubuserController::class, 'store']); + Route::get('/{user}', [Client\Servers\SubuserController::class, 'view']); + Route::post('/{user}', [Client\Servers\SubuserController::class, 'update']); + Route::delete('/{user}', [Client\Servers\SubuserController::class, 'delete']); + }); + + Route::group(['prefix' => '/backups'], function () { + Route::get('/', [Client\Servers\BackupController::class, 'index']); + Route::post('/', [Client\Servers\BackupController::class, 'store']); + Route::get('/{backup}', [Client\Servers\BackupController::class, 'view']); + Route::get('/{backup}/download', [Client\Servers\BackupController::class, 'download']); + Route::post('/{backup}/lock', [Client\Servers\BackupController::class, 'toggleLock']); + Route::post('/{backup}/restore', [Client\Servers\BackupController::class, 'restore']); + Route::delete('/{backup}', [Client\Servers\BackupController::class, 'delete']); + }); + + Route::group(['prefix' => '/startup'], function () { + Route::get('/', [Client\Servers\StartupController::class, 'index']); + Route::put('/variable', [Client\Servers\StartupController::class, 'update']); + }); + + Route::group(['prefix' => '/settings'], function () { + Route::post('/rename', [Client\Servers\SettingsController::class, 'rename']); + Route::post('/reinstall', [Client\Servers\SettingsController::class, 'reinstall']); + Route::put('/docker-image', [Client\Servers\SettingsController::class, 'dockerImage']); + }); +});