This commit is contained in:
Job Rapati 2023-09-25 16:40:44 +02:00
parent 85d197a526
commit 3bbf0b5199
10 changed files with 476 additions and 17 deletions

View File

@ -53,6 +53,17 @@ class ModpackController extends ClientApiController {
return $result->getBody()->getContents(); return $result->getBody()->getContents();
} }
public function description(Request $request, $server, $modpack) {
$result = $this->http_client->get("mods/$modpack/description");
if($result->getStatusCode() !== 200) {
throw new DisplayException('Failed to fetch modpack description from CurseForge.');
}
return $result->getBody()->getContents();
}
public function show() { public function show() {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -0,0 +1,284 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Carbon\CarbonImmutable;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Facades\Activity;
use Pterodactyl\Services\Nodes\NodeJWTService;
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
use Pterodactyl\Transformers\Api\Client\FileObjectTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\PullFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ChmodFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CompressFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest;
class FileController extends ClientApiController
{
/**
* FileController constructor.
*/
public function __construct(
private NodeJWTService $jwtService,
private DaemonFileRepository $fileRepository
) {
parent::__construct();
}
/**
* Returns a listing of files in a given directory.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function directory(ListFilesRequest $request, Server $server): array
{
$contents = $this->fileRepository
->setServer($server)
->getDirectory($request->get('directory') ?? '/');
return $this->fractal->collection($contents)
->transformWith($this->getTransformer(FileObjectTransformer::class))
->toArray();
}
/**
* Return the contents of a specified file for the user.
*
* @throws \Throwable
*/
public function contents(GetFileContentsRequest $request, Server $server): Response
{
$response = $this->fileRepository->setServer($server)->getContent(
$request->get('file'),
config('pterodactyl.files.max_edit_size')
);
Activity::event('server:file.read')->property('file', $request->get('file'))->log();
return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
}
/**
* Returns true if file exists, false if it does not.
*/
public function exists(GetFileContentsRequest $request, Server $server): JsonResponse
{
try {
$response = $this->fileRepository->setServer($server)->getContent(
$request->get('file'),
config('pterodactyl.files.max_edit_size')
);
Activity::event('server:file.exists')->property('file', $request->get('file'))->log();
return new JsonResponse($response !== '');
} catch (\Exception $exception) {
return new JsonResponse(false);
}
}
/**
* Generates a one-time token with a link that the user can use to
* download a given file.
*
* @throws \Throwable
*/
public function download(GetFileContentsRequest $request, Server $server): array
{
$token = $this->jwtService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setUser($request->user())
->setClaims([
'file_path' => rawurldecode($request->get('file')),
'server_uuid' => $server->uuid,
])
->handle($server->node, $request->user()->id . $server->uuid);
Activity::event('server:file.download')->property('file', $request->get('file'))->log();
return [
'object' => 'signed_url',
'attributes' => [
'url' => sprintf(
'%s/download/file?token=%s',
$server->node->getConnectionAddress(),
$token->toString()
),
],
];
}
/**
* Writes the contents of the specified file to the server.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function write(WriteFileContentRequest $request, Server $server): JsonResponse
{
$this->fileRepository->setServer($server)->putContent($request->get('file'), $request->getContent());
Activity::event('server:file.write')->property('file', $request->get('file'))->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* Creates a new folder on the server.
*
* @throws \Throwable
*/
public function create(CreateFolderRequest $request, Server $server): JsonResponse
{
$this->fileRepository
->setServer($server)
->createDirectory($request->input('name'), $request->input('root', '/'));
Activity::event('server:file.create-directory')
->property('name', $request->input('name'))
->property('directory', $request->input('root'))
->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* Renames a file on the remote machine.
*
* @throws \Throwable
*/
public function rename(RenameFileRequest $request, Server $server): JsonResponse
{
$this->fileRepository
->setServer($server)
->renameFiles($request->input('root'), $request->input('files'));
Activity::event('server:file.rename')
->property('directory', $request->input('root'))
->property('files', $request->input('files'))
->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* Copies a file on the server.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function copy(CopyFileRequest $request, Server $server): JsonResponse
{
$this->fileRepository
->setServer($server)
->copyFile($request->input('location'));
Activity::event('server:file.copy')->property('file', $request->input('location'))->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function compress(CompressFilesRequest $request, Server $server): array
{
$file = $this->fileRepository->setServer($server)->compressFiles(
$request->input('root'),
$request->input('files')
);
Activity::event('server:file.compress')
->property('directory', $request->input('root'))
->property('files', $request->input('files'))
->log();
return $this->fractal->item($file)
->transformWith($this->getTransformer(FileObjectTransformer::class))
->toArray();
}
/**
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse
{
set_time_limit(300);
$this->fileRepository->setServer($server)->decompressFile(
$request->input('root'),
$request->input('file')
);
Activity::event('server:file.decompress')
->property('directory', $request->input('root'))
->property('files', $request->input('file'))
->log();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Deletes files or folders for the server in the given root directory.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function delete(DeleteFileRequest $request, Server $server): JsonResponse
{
$this->fileRepository->setServer($server)->deleteFiles(
$request->input('root'),
$request->input('files')
);
Activity::event('server:file.delete')
->property('directory', $request->input('root'))
->property('files', $request->input('files'))
->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* Updates file permissions for file(s) in the given root directory.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function chmod(ChmodFilesRequest $request, Server $server): JsonResponse
{
$this->fileRepository->setServer($server)->chmodFiles(
$request->input('root'),
$request->input('files')
);
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* Requests that a file be downloaded from a remote location by Wings.
*
* @throws \Throwable
*/
public function pull(PullFileRequest $request, Server $server): JsonResponse
{
$this->fileRepository->setServer($server)->pull(
$request->input('url'),
$request->input('directory'),
$request->safe(['filename', 'use_header', 'foreground'])
);
Activity::event('server:file.pull')
->property('directory', $request->input('directory'))
->property('url', $request->input('url'))
->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
}

View File

@ -0,0 +1,11 @@
import http from '@/api/http';
export default (server: string, file: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/files/exists`, {
params: { file },
})
.then(({ data }) => resolve(data))
.catch(reject);
});
};

View File

@ -0,0 +1,23 @@
import http from '@/api/http';
export default (server: string, pullFileRequest: PullFileRequest): Promise<string> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${server}/files/pull`, {
url: encodeURI(pullFileRequest.url),
directory: pullFileRequest.directory,
filename: pullFileRequest.filename,
use_header: pullFileRequest.use_header,
foreground: pullFileRequest.foreground,
})
.then(({ data }) => resolve(data))
.catch(reject);
});
};
export interface PullFileRequest {
url: string;
directory: string;
filename: string;
use_header: boolean;
foreground: boolean;
}

View File

@ -77,7 +77,7 @@ export interface File {
fileLength: number; fileLength: number;
downloadCount: number; downloadCount: number;
downloadUrl: string; downloadUrl: string;
gameVersion: string[]; gameVersions: string[];
sortableGameVersion: GameVersion[]; sortableGameVersion: GameVersion[];
dependencies: any[]; dependencies: any[];
alternateFileId: number; alternateFileId: number;

View File

@ -0,0 +1,14 @@
import http from "@/api/http";
export default (uuid: string, id: number): Promise<string> => {
return new Promise((resolve, reject) => {
console.log(uuid, id);
http.get(`/api/client/servers/${uuid}/modpacks/${id}/description`)
.then((response) => {
resolve(response.data.data)
})
.catch(reject);
});
}

View File

@ -11,8 +11,6 @@ export default (uuid: string, pageIndex: number = 0, filters: ModpackSearchFilte
filterQuery += `modloader=0`; filterQuery += `modloader=0`;
} }
console.log(filterQuery);
http.get(`/api/client/servers/${uuid}/modpacks?pageindex=${pageIndex}${filterQuery}`) http.get(`/api/client/servers/${uuid}/modpacks?pageindex=${pageIndex}${filterQuery}`)
.then((response) => { .then((response) => {
resolve([(response.data.data || []).map((item: any) => rawDataToModpackData(item)), rawDataToModpackPaginationData(response.data.pagination), {modloaderType: filters.modloaderType}]) resolve([(response.data.data || []).map((item: any) => rawDataToModpackData(item)), rawDataToModpackPaginationData(response.data.pagination), {modloaderType: filters.modloaderType}])

View File

@ -1,21 +1,121 @@
import { Modpack } from '@/api/server/modpacks/Modpack'; import { File, Modpack } from '@/api/server/modpacks/Modpack';
import GreyRowBox from '@/components/elements/GreyRowBox'; import GreyRowBox from '@/components/elements/GreyRowBox';
import TitledGreyBox from '@/components/elements/TitledGreyBox'; import TitledGreyBox from '@/components/elements/TitledGreyBox';
import React from 'react'; import React from 'react';
import ModpackModal from './ModpackModal';
import Button from '@/components/elements/Button';
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';
interface Props { interface Props {
modpack: Modpack; modpack: Modpack;
} }
export default ({ modpack }: Props) => { 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 [modpackDesc, setModpackDesc] = React.useState<string>('');
const [installing, setInstalling] = React.useState(false);
const openModal = (): void => {
setVisible(true);
};
const openInstallModal = (): void => {
setInstallVisible(true);
};
const install = async (latestFile: File): Promise<void> => {
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');
}
decompressFiles(uuid, '/home/container/modpack', '/home/container/modpack.zip');
setInstalling(false);
};
const getDescription = (): void => {
getModpackDescription(uuid, modpack.id).then((description) => setModpackDesc(description));
};
const openSite = (): void => {
throw new Error('Function not implemented.');
};
return ( return (
<div> <div>
<TitledGreyBox title={modpack.name}> <TitledGreyBox title={modpack.name}>
<GreyRowBox> <GreyRowBox onClick={openModal}>
<img style={{ width: '50px', marginRight: '10px' }} src={modpack.logo.url} /> <img style={{ width: '50px', marginRight: '10px' }} src={modpack.logo.url} />
<p>{modpack.summary}</p> <p>{modpack.summary}</p>
</GreyRowBox> </GreyRowBox>
</TitledGreyBox> </TitledGreyBox>
<ModpackModal visible={visible} onDismissed={() => setVisible(false)} modpack={modpack}>
<div className='grid grid-cols-3 gap-2' style={{ maxHeight: '70vh' }}>
<img src={modpack.logo.url} style={{ width: '150px' }} />
<h1 style={{ fontSize: '1.5em' }}>{modpack.name}</h1>
<Button style={{ height: '50px' }} onClick={openInstallModal}>
Install
</Button>
<p className='col-span-3'>{modpack.summary}</p>
{modpack.screenshots.map((screenshot) => (
<img src={screenshot.url} />
))}
<div className='flex items-center'>
<FontAwesomeIcon icon={faDownload} style={{ marginRight: '10px' }} />
{modpack.downloadCount}
</div>
<div className='flex items-center'></div>
<Button onClick={openSite}>Website</Button>
</div>
</ModpackModal>
<ModpackModal visible={installVisible} onDismissed={() => setInstallVisible(false)} modpack={modpack}>
<div style={{ maxHeight: '50vh' }}>
{installing ? (
<div>
<p>Installing...</p>
<Spinner centered size='base' />
</div>
) : (
modpack.latestFiles.map((file) => (
<div className='grid grid-cols-3 gap-4'>
<p>{file.displayName}</p>
<div className='flex items-center'>
<FontAwesomeIcon icon={faGamepad} style={{ marginRight: '10px' }} />
{file.gameVersions[0]}
</div>
<Button
onClick={() => {
install(file);
}}
>
Install
</Button>
</div>
)))}
</div>
</ModpackModal>
</div> </div>
); );
} };

View File

@ -0,0 +1,16 @@
import { Modpack } from "@/api/server/modpacks/Modpack";
import PortaledModal, { ModalProps } from "@/components/elements/Modal";
import React, { PropsWithChildren } from "react";
export interface ModpackModalProps extends ModalProps {
modpack: Modpack;
}
export default(props: PropsWithChildren<ModpackModalProps>) => {
return (
<PortaledModal visible={props.visible} onDismissed={props.onDismissed} appear={props.appear} top={props.top}>
{props.children}
</PortaledModal>
)
}

View File

@ -70,7 +70,8 @@ Route::group([
Route::group(['prefix' => '/modpacks'], function () { Route::group(['prefix' => '/modpacks'], function () {
Route::get('/', [Client\Servers\CurseForge\ModpackController::class, 'index']); Route::get('/', [Client\Servers\CurseForge\ModpackController::class, 'index']);
Route::get('/{modpack}', [Client\Servers\CurseForge\ModpackController::class, 'show']); Route::get('/{modpack}', [Client\Servers\CurseForge\ModpackController::class, 'show']);
Route::get('/{modpack}/install', [Client\Servers\CurseForge\CurseForgeModpackController::class, 'install']); Route::get('/{modpack}/install', [Client\Servers\CurseForge\ModpackController::class, 'install']);
Route::get('/{modpack}/description', [Client\Servers\CurseForge\ModpackController::class, 'description']);
}); });
Route::group(['prefix' => '/databases'], function () { Route::group(['prefix' => '/databases'], function () {
@ -83,6 +84,7 @@ Route::group([
Route::group(['prefix' => '/files'], function () { Route::group(['prefix' => '/files'], function () {
Route::get('/list', [Client\Servers\FileController::class, 'directory']); Route::get('/list', [Client\Servers\FileController::class, 'directory']);
Route::get('/contents', [Client\Servers\FileController::class, 'contents']); 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::get('/download', [Client\Servers\FileController::class, 'download']);
Route::put('/rename', [Client\Servers\FileController::class, 'rename']); Route::put('/rename', [Client\Servers\FileController::class, 'rename']);
Route::post('/copy', [Client\Servers\FileController::class, 'copy']); Route::post('/copy', [Client\Servers\FileController::class, 'copy']);