diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index c4e4a035f..77f672f44 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -231,6 +231,24 @@ class FileController extends ClientApiController ->toArray(); } + /** + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse + { + // Allow up to five minutes for this request to process before timing out. + set_time_limit(300); + + $this->fileRepository->setServer($server) + ->decompressFile($request->input('root'), $request->input('file')); + + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + } + /** * Deletes files or folders for the server in the given root directory. * diff --git a/app/Http/Requests/Api/Client/Servers/Files/DecompressFilesRequest.php b/app/Http/Requests/Api/Client/Servers/Files/DecompressFilesRequest.php new file mode 100644 index 000000000..f8493ec4a --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/DecompressFilesRequest.php @@ -0,0 +1,32 @@ + 'sometimes|nullable|string', + 'file' => 'required|string', + ]; + } +} diff --git a/resources/scripts/api/server/files/decompressFiles.ts b/resources/scripts/api/server/files/decompressFiles.ts new file mode 100644 index 000000000..d674eadb0 --- /dev/null +++ b/resources/scripts/api/server/files/decompressFiles.ts @@ -0,0 +1,8 @@ +import http from '@/api/http'; + +export default async (uuid: string, directory: string, file: string): Promise => { + await http.post(`/api/client/servers/${uuid}/files/decompress`, { root: directory, file }, { + timeout: 300000, + timeoutErrorMessage: 'It looks like this archive is taking a long time to be unarchived. Once completed the unarchived files will appear.', + }); +}; diff --git a/resources/scripts/api/server/files/loadDirectory.ts b/resources/scripts/api/server/files/loadDirectory.ts index 85f290689..7899d2216 100644 --- a/resources/scripts/api/server/files/loadDirectory.ts +++ b/resources/scripts/api/server/files/loadDirectory.ts @@ -12,6 +12,7 @@ export interface FileObject { mimetype: string; createdAt: Date; modifiedAt: Date; + isArchiveType: () => boolean; } export default async (uuid: string, directory?: string): Promise => { diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index bd59a7e84..c8676bdac 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -23,4 +23,12 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({ mimetype: data.attributes.mimetype, createdAt: new Date(data.attributes.created_at), modifiedAt: new Date(data.attributes.modified_at), + + isArchiveType: function () { + return this.isFile && [ + 'application/zip', + 'application/gzip', + 'application/x-tar', + ].indexOf(this.mimetype) >= 0; + }, }); diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx index e75f8e16b..19fbe522a 100644 --- a/resources/scripts/components/server/files/FileDropdownMenu.tsx +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -1,6 +1,7 @@ import React, { useRef, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { + faBoxOpen, faCopy, faEllipsisH, faFileArchive, @@ -27,6 +28,7 @@ import DropdownMenu from '@/components/elements/DropdownMenu'; import styled from 'styled-components/macro'; import useEventListener from '@/plugins/useEventListener'; import compressFiles from '@/api/server/files/compressFiles'; +import decompressFiles from '@/api/server/files/decompressFiles'; type ModalType = 'rename' | 'move'; @@ -43,7 +45,7 @@ interface RowProps extends React.HTMLAttributes { const Row = ({ icon, title, ...props }: RowProps) => ( - + {title} ); @@ -110,6 +112,16 @@ export default ({ file }: { file: FileObject }) => { .then(() => setShowSpinner(false)); }; + const doUnarchive = () => { + setShowSpinner(true); + clearFlashes('files'); + + decompressFiles(uuid, directory, file.name) + .then(() => mutate()) + .catch(error => clearAndAddHttpError({ key: 'files', error })) + .then(() => setShowSpinner(false)); + }; + return ( { } - - - + {file.isArchiveType() ? + + + + : + + + + } diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index d1f23c022..9adbf57dd 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -34,6 +34,7 @@ const generateDirectoryData = (name: string): FileObject => ({ mimetype: '', createdAt: new Date(), modifiedAt: new Date(), + isArchiveType: () => false, }); export default () => { diff --git a/routes/api-client.php b/routes/api-client.php index 9b57cf0c4..f92ba6ed9 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -60,6 +60,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::post('/copy', 'Servers\FileController@copy'); Route::post('/write', 'Servers\FileController@write'); Route::post('/compress', 'Servers\FileController@compress'); + Route::post('/decompress', 'Servers\FileController@decompress'); Route::post('/delete', 'Servers\FileController@delete'); Route::post('/create-folder', 'Servers\FileController@create'); });