From 3bbf0b519949a9b192ba531bc06e0121764f3684 Mon Sep 17 00:00:00 2001 From: Job Rapati Date: Mon, 25 Sep 2023 16:40:44 +0200 Subject: [PATCH] #5 #8 #2 #9 #11 --- .../Servers/CurseForge/ModpackController.php | 11 + .../Client/Servers/FileController.php | 284 ++++++++++++++++++ .../scripts/api/server/files/getFileExists.ts | 11 + .../scripts/api/server/files/pullFile.ts | 23 ++ .../scripts/api/server/modpacks/Modpack.ts | 2 +- .../server/modpacks/getModpackDescription.ts | 14 + .../api/server/modpacks/getModpacks.ts | 2 - .../server/modpacks/ModpackItem.tsx | 126 +++++++- .../server/modpacks/ModpackModal.tsx | 16 + routes/api-client.php | 4 +- 10 files changed, 476 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/Client/Servers/FileController.php create mode 100644 resources/scripts/api/server/files/getFileExists.ts create mode 100644 resources/scripts/api/server/files/pullFile.ts create mode 100644 resources/scripts/api/server/modpacks/getModpackDescription.ts create mode 100644 resources/scripts/components/server/modpacks/ModpackModal.tsx diff --git a/app/Http/Controllers/Client/Servers/CurseForge/ModpackController.php b/app/Http/Controllers/Client/Servers/CurseForge/ModpackController.php index 0091b4c..7ebdda7 100644 --- a/app/Http/Controllers/Client/Servers/CurseForge/ModpackController.php +++ b/app/Http/Controllers/Client/Servers/CurseForge/ModpackController.php @@ -53,6 +53,17 @@ class ModpackController extends ClientApiController { 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() { throw new NotImplementedException(); } diff --git a/app/Http/Controllers/Client/Servers/FileController.php b/app/Http/Controllers/Client/Servers/FileController.php new file mode 100644 index 0000000..0f04a74 --- /dev/null +++ b/app/Http/Controllers/Client/Servers/FileController.php @@ -0,0 +1,284 @@ +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); + } +} diff --git a/resources/scripts/api/server/files/getFileExists.ts b/resources/scripts/api/server/files/getFileExists.ts new file mode 100644 index 0000000..3003692 --- /dev/null +++ b/resources/scripts/api/server/files/getFileExists.ts @@ -0,0 +1,11 @@ +import http from '@/api/http'; + +export default (server: string, file: string): Promise => { + return new Promise((resolve, reject) => { + http.get(`/api/client/servers/${server}/files/exists`, { + params: { file }, + }) + .then(({ data }) => resolve(data)) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/server/files/pullFile.ts b/resources/scripts/api/server/files/pullFile.ts new file mode 100644 index 0000000..833bafd --- /dev/null +++ b/resources/scripts/api/server/files/pullFile.ts @@ -0,0 +1,23 @@ +import http from '@/api/http'; + +export default (server: string, pullFileRequest: PullFileRequest): Promise => { + 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; +} diff --git a/resources/scripts/api/server/modpacks/Modpack.ts b/resources/scripts/api/server/modpacks/Modpack.ts index 24b2f31..7fdc594 100644 --- a/resources/scripts/api/server/modpacks/Modpack.ts +++ b/resources/scripts/api/server/modpacks/Modpack.ts @@ -77,7 +77,7 @@ export interface File { fileLength: number; downloadCount: number; downloadUrl: string; - gameVersion: string[]; + gameVersions: string[]; sortableGameVersion: GameVersion[]; dependencies: any[]; alternateFileId: number; diff --git a/resources/scripts/api/server/modpacks/getModpackDescription.ts b/resources/scripts/api/server/modpacks/getModpackDescription.ts new file mode 100644 index 0000000..1fbf323 --- /dev/null +++ b/resources/scripts/api/server/modpacks/getModpackDescription.ts @@ -0,0 +1,14 @@ +import http from "@/api/http"; + +export default (uuid: string, id: number): Promise => { + 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); + }); +} diff --git a/resources/scripts/api/server/modpacks/getModpacks.ts b/resources/scripts/api/server/modpacks/getModpacks.ts index ca61a22..ef0f81e 100644 --- a/resources/scripts/api/server/modpacks/getModpacks.ts +++ b/resources/scripts/api/server/modpacks/getModpacks.ts @@ -11,8 +11,6 @@ export default (uuid: string, pageIndex: number = 0, filters: ModpackSearchFilte filterQuery += `modloader=0`; } - console.log(filterQuery); - http.get(`/api/client/servers/${uuid}/modpacks?pageindex=${pageIndex}${filterQuery}`) .then((response) => { resolve([(response.data.data || []).map((item: any) => rawDataToModpackData(item)), rawDataToModpackPaginationData(response.data.pagination), {modloaderType: filters.modloaderType}]) diff --git a/resources/scripts/components/server/modpacks/ModpackItem.tsx b/resources/scripts/components/server/modpacks/ModpackItem.tsx index 975a296..fd1a7f3 100644 --- a/resources/scripts/components/server/modpacks/ModpackItem.tsx +++ b/resources/scripts/components/server/modpacks/ModpackItem.tsx @@ -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 TitledGreyBox from '@/components/elements/TitledGreyBox'; 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 { modpack: Modpack; } -export default({modpack}: Props) => { - return ( -
- - - -

{modpack.summary}

-
-
-
- ); -} +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(''); + const [installing, setInstalling] = React.useState(false); + + const openModal = (): void => { + setVisible(true); + }; + + const openInstallModal = (): void => { + setInstallVisible(true); + }; + + 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'); + } + + 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 ( +
+ + + +

{modpack.summary}

+
+
+ + setVisible(false)} modpack={modpack}> +
+ +

{modpack.name}

+ +

{modpack.summary}

+ {modpack.screenshots.map((screenshot) => ( + + ))} +
+ + {modpack.downloadCount} +
+
+ +
+
+ + setInstallVisible(false)} modpack={modpack}> +
+ {installing ? ( +
+

Installing...

+ +
+ ) : ( + 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 new file mode 100644 index 0000000..3b107a2 --- /dev/null +++ b/resources/scripts/components/server/modpacks/ModpackModal.tsx @@ -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) => { + + return ( + + {props.children} + + ) +} diff --git a/routes/api-client.php b/routes/api-client.php index e0bdee1..5de80cd 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -70,7 +70,8 @@ Route::group([ 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', [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 () { @@ -83,6 +84,7 @@ Route::group([ 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']);