diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php
index 470adc2ce..b6e2eed56 100644
--- a/app/Http/Controllers/Api/Client/Servers/FileController.php
+++ b/app/Http/Controllers/Api/Client/Servers/FileController.php
@@ -216,7 +216,7 @@ class FileController extends ClientApiController
}
/**
- * Deletes a file or folder from the server.
+ * Deletes files or folders for the server in the given root directory.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest $request
* @param \Pterodactyl\Models\Server $server
@@ -224,9 +224,10 @@ class FileController extends ClientApiController
*/
public function delete(DeleteFileRequest $request, Server $server): JsonResponse
{
- $this->fileRepository
- ->setServer($server)
- ->deleteFile($request->input('location'));
+ $this->fileRepository->setServer($server)
+ ->deleteFiles(
+ $request->input('root'), $request->input('files')
+ );
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
diff --git a/app/Http/Requests/Api/Client/Servers/Files/DeleteFileRequest.php b/app/Http/Requests/Api/Client/Servers/Files/DeleteFileRequest.php
index dcf1d2a95..7f5ccbf72 100644
--- a/app/Http/Requests/Api/Client/Servers/Files/DeleteFileRequest.php
+++ b/app/Http/Requests/Api/Client/Servers/Files/DeleteFileRequest.php
@@ -22,7 +22,9 @@ class DeleteFileRequest extends ClientApiRequest implements ClientPermissionsReq
public function rules(): array
{
return [
- 'location' => 'required|string',
+ 'root' => 'required|nullable|string',
+ 'files' => 'required|array',
+ 'files.*' => 'string',
];
}
}
diff --git a/app/Repositories/Wings/DaemonFileRepository.php b/app/Repositories/Wings/DaemonFileRepository.php
index e69af982f..3f506d149 100644
--- a/app/Repositories/Wings/DaemonFileRepository.php
+++ b/app/Repositories/Wings/DaemonFileRepository.php
@@ -151,10 +151,11 @@ class DaemonFileRepository extends DaemonRepository
/**
* Delete a file or folder for the server.
*
- * @param string $location
+ * @param string|null $root
+ * @param array $files
* @return \Psr\Http\Message\ResponseInterface
*/
- public function deleteFile(string $location): ResponseInterface
+ public function deleteFiles(?string $root, array $files): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
@@ -162,7 +163,8 @@ class DaemonFileRepository extends DaemonRepository
sprintf('/api/servers/%s/files/delete', $this->server->uuid),
[
'json' => [
- 'location' => $location,
+ 'root' => $root,
+ 'files' => $files,
],
]
);
diff --git a/resources/scripts/api/server/files/deleteFile.ts b/resources/scripts/api/server/files/deleteFiles.ts
similarity index 65%
rename from resources/scripts/api/server/files/deleteFile.ts
rename to resources/scripts/api/server/files/deleteFiles.ts
index cb8a59e02..1250463ed 100644
--- a/resources/scripts/api/server/files/deleteFile.ts
+++ b/resources/scripts/api/server/files/deleteFiles.ts
@@ -1,8 +1,8 @@
import http from '@/api/http';
-export default (uuid: string, location: string): Promise => {
+export default (uuid: string, directory: string, files: string[]): Promise => {
return new Promise((resolve, reject) => {
- http.post(`/api/client/servers/${uuid}/files/delete`, { location })
+ http.post(`/api/client/servers/${uuid}/files/delete`, { root: directory, files })
.then(() => resolve())
.catch(reject);
});
diff --git a/resources/scripts/components/elements/Checkbox.tsx b/resources/scripts/components/elements/Checkbox.tsx
index ae7548dcf..790536489 100644
--- a/resources/scripts/components/elements/Checkbox.tsx
+++ b/resources/scripts/components/elements/Checkbox.tsx
@@ -5,13 +5,14 @@ import Input from '@/components/elements/Input';
interface Props {
name: string;
value: string;
+ className?: string;
}
type OmitFields = 'ref' | 'name' | 'value' | 'type' | 'checked' | 'onClick' | 'onChange';
type InputProps = Omit;
-const Checkbox = ({ name, value, ...props }: Props & InputProps) => (
+const Checkbox = ({ name, value, className, ...props }: Props & InputProps) => (
{({ field, form }: FieldProps) => {
if (!Array.isArray(field.value)) {
@@ -24,6 +25,7 @@ const Checkbox = ({ name, value, ...props }: Props & InputProps) => (
form.setFieldTouched(field.name, true)}
diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx
index 5d4e14fdc..e372ac09d 100644
--- a/resources/scripts/components/server/files/FileDropdownMenu.tsx
+++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx
@@ -13,7 +13,7 @@ import {
import RenameFileModal from '@/components/server/files/RenameFileModal';
import { ServerContext } from '@/state/server';
import { join } from 'path';
-import deleteFile from '@/api/server/files/deleteFile';
+import deleteFiles from '@/api/server/files/deleteFiles';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import copyFile from '@/api/server/files/copyFile';
import Can from '@/components/elements/Can';
@@ -71,7 +71,7 @@ export default ({ file }: { file: FileObject }) => {
// If the delete actually fails, we'll fetch the current directory contents again automatically.
mutate(files => files.filter(f => f.uuid !== file.uuid), false);
- deleteFile(uuid, join(directory, file.name)).catch(error => {
+ deleteFiles(uuid, directory, [ file.name ]).catch(error => {
mutate();
clearAndAddHttpError({ key: 'files', error });
});
diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx
index eeed24078..8ff6fe603 100644
--- a/resources/scripts/components/server/files/FileManagerContainer.tsx
+++ b/resources/scripts/components/server/files/FileManagerContainer.tsx
@@ -15,6 +15,8 @@ import Button from '@/components/elements/Button';
import useServer from '@/plugins/useServer';
import { ServerContext } from '@/state/server';
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
+import { Form, Formik } from 'formik';
+import MassActionsBar from '@/components/server/files/MassActionsBar';
const sortFiles = (files: FileObject[]): FileObject[] => {
return files.sort((a, b) => a.name.localeCompare(b.name))
@@ -55,23 +57,29 @@ export default () => {
:
-
-
- {files.length > 250 &&
-
-
- This directory is too large to display in the browser,
- limiting the output to the first 250 files.
-
-
- }
- {
- sortFiles(files.slice(0, 250)).map(file => (
-
- ))
- }
-
-
+
+
undefined}
+ initialValues={{ selectedFiles: [] }}
+ >
+
+
+
}
diff --git a/resources/scripts/components/server/files/FileObjectRow.tsx b/resources/scripts/components/server/files/FileObjectRow.tsx
index 2f097e1b9..13b329d2b 100644
--- a/resources/scripts/components/server/files/FileObjectRow.tsx
+++ b/resources/scripts/components/server/files/FileObjectRow.tsx
@@ -10,11 +10,22 @@ import { NavLink, useHistory, useRouteMatch } from 'react-router-dom';
import tw from 'twin.macro';
import isEqual from 'react-fast-compare';
import styled from 'styled-components/macro';
+import FormikCheckbox from '@/components/elements/Checkbox';
const Row = styled.div`
${tw`flex bg-neutral-700 rounded-sm mb-px text-sm hover:text-neutral-100 cursor-pointer items-center no-underline hover:bg-neutral-600`};
`;
+const Checkbox = styled(FormikCheckbox)`
+ && {
+ ${tw`border-neutral-500`};
+
+ &:not(:checked) {
+ ${tw`hover:border-neutral-300`};
+ }
+ }
+`;
+
const FileObjectRow = ({ file }: { file: FileObject }) => {
const directory = ServerContext.useStoreState(state => state.files.directory);
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
@@ -44,12 +55,15 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.uuid}`, { detail: e.clientX }));
}}
>
+
-
+
{file.isFile ?
:
diff --git a/resources/scripts/components/server/files/MassActionsBar.tsx b/resources/scripts/components/server/files/MassActionsBar.tsx
new file mode 100644
index 000000000..1d8e5c9cf
--- /dev/null
+++ b/resources/scripts/components/server/files/MassActionsBar.tsx
@@ -0,0 +1,87 @@
+import React, { useState } from 'react';
+import tw from 'twin.macro';
+import Button from '@/components/elements/Button';
+import { useFormikContext } from 'formik';
+import Fade from '@/components/elements/Fade';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faFileArchive, faLevelUpAlt, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
+import useFileManagerSwr from '@/plugins/useFileManagerSwr';
+import useFlash from '@/plugins/useFlash';
+import compressFiles from '@/api/server/files/compressFiles';
+import useServer from '@/plugins/useServer';
+import { ServerContext } from '@/state/server';
+import ConfirmationModal from '@/components/elements/ConfirmationModal';
+import deleteFiles from '@/api/server/files/deleteFiles';
+
+const MassActionsBar = () => {
+ const { uuid } = useServer();
+ const { mutate } = useFileManagerSwr();
+ const { clearFlashes, clearAndAddHttpError } = useFlash();
+ const [ loading, setLoading ] = useState(false);
+ const [ showConfirm, setShowConfirm ] = useState(false);
+ const { values, setFieldValue } = useFormikContext<{ selectedFiles: string[] }>();
+ const directory = ServerContext.useStoreState(state => state.files.directory);
+
+ const onClickCompress = () => {
+ setLoading(true);
+ clearFlashes('files');
+
+ compressFiles(uuid, directory, values.selectedFiles)
+ .then(() => mutate())
+ .then(() => setFieldValue('selectedFiles', []))
+ .catch(error => clearAndAddHttpError({ key: 'files', error }))
+ .then(() => setLoading(false));
+ };
+
+ const onClickConfirmDeletion = () => {
+ setLoading(true);
+ setShowConfirm(false);
+ clearFlashes('files');
+
+ deleteFiles(uuid, directory, values.selectedFiles)
+ .then(() => {
+ mutate(files => files.filter(f => values.selectedFiles.indexOf(f.name) < 0), false);
+ setFieldValue('selectedFiles', []);
+ })
+ .catch(error => {
+ mutate();
+ clearAndAddHttpError({ key: 'files', error });
+ })
+ .then(() => setLoading(false));
+ };
+
+ return (
+
0} unmountOnExit>
+
+
+
setShowConfirm(false)}
+ >
+ Deleting files is a permanent operation, you cannot undo this action.
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MassActionsBar;