Skip to content

Commit 7b9a8c8

Browse files
authored
Merge pull request pterodactyl#2768 from pterodactyl/feature/chmod-files
Chmod Files from the File Manager
2 parents 360d93b + e2be4e5 commit 7b9a8c8

File tree

13 files changed

+230
-11
lines changed

13 files changed

+230
-11
lines changed

app/Http/Controllers/Api/Client/Servers/FileController.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest;
1717
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest;
1818
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest;
19+
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ChmodFilesRequest;
1920
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest;
2021
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CompressFilesRequest;
2122
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest;
@@ -263,6 +264,25 @@ public function delete(DeleteFileRequest $request, Server $server): JsonResponse
263264
return new JsonResponse([], Response::HTTP_NO_CONTENT);
264265
}
265266

267+
/**
268+
* Updates file permissions for file(s) in the given root directory.
269+
*
270+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\ChmodFilesRequest $request
271+
* @param \Pterodactyl\Models\Server $server
272+
* @return \Illuminate\Http\JsonResponse
273+
*
274+
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
275+
*/
276+
public function chmod(ChmodFilesRequest $request, Server $server): JsonResponse
277+
{
278+
$this->fileRepository->setServer($server)
279+
->chmodFiles(
280+
$request->input('root'), $request->input('files')
281+
);
282+
283+
return new JsonResponse([], Response::HTTP_NO_CONTENT);
284+
}
285+
266286
/**
267287
* Encodes a given file name & path in a format that should work for a good majority
268288
* of file names without too much confusing logic.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
4+
5+
use Pterodactyl\Models\Permission;
6+
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
7+
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
8+
9+
class ChmodFilesRequest extends ClientApiRequest implements ClientPermissionsRequest
10+
{
11+
/**
12+
* @return string
13+
*/
14+
public function permission(): string
15+
{
16+
return Permission::ACTION_FILE_UPDATE;
17+
}
18+
19+
/**
20+
* @return array
21+
*/
22+
public function rules(): array
23+
{
24+
return [
25+
'root' => 'required|nullable|string',
26+
'files' => 'required|array',
27+
'files.*.file' => 'required|string',
28+
'files.*.mode' => 'required|numeric',
29+
];
30+
}
31+
}

app/Repositories/Wings/DaemonFileRepository.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,32 @@ public function decompressFile(?string $root, string $file): ResponseInterface
269269
throw new DaemonConnectionException($exception);
270270
}
271271
}
272+
273+
/**
274+
* Chmods the given files.
275+
*
276+
* @param string|null $root
277+
* @param array $files
278+
* @return \Psr\Http\Message\ResponseInterface
279+
*
280+
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
281+
*/
282+
public function chmodFiles(?string $root, array $files): ResponseInterface
283+
{
284+
Assert::isInstanceOf($this->server, Server::class);
285+
286+
try {
287+
return $this->getHttpClient()->post(
288+
sprintf('/api/servers/%s/files/chmod', $this->server->uuid),
289+
[
290+
'json' => [
291+
'root' => $root ?? '/',
292+
'files' => $files,
293+
],
294+
]
295+
);
296+
} catch (TransferException $exception) {
297+
throw new DaemonConnectionException($exception);
298+
}
299+
}
272300
}

app/Transformers/Daemon/FileObjectTransformer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function transform(array $item)
2525
return [
2626
'name' => Arr::get($item, 'name'),
2727
'mode' => Arr::get($item, 'mode'),
28+
'mode_bits' => Arr::get($item, 'mode_bits'),
2829
'size' => Arr::get($item, 'size'),
2930
'is_file' => Arr::get($item, 'file', true),
3031
'is_symlink' => Arr::get($item, 'symlink', false),
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import http from '@/api/http';
2+
3+
interface Data {
4+
file: string;
5+
mode: string;
6+
}
7+
8+
export default (uuid: string, directory: string, files: Data[]): Promise<void> => {
9+
return new Promise((resolve, reject) => {
10+
http.post(`/api/client/servers/${uuid}/files/chmod`, { root: directory, files })
11+
.then(() => resolve())
12+
.catch(reject);
13+
});
14+
};

resources/scripts/api/server/files/loadDirectory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface FileObject {
55
key: string;
66
name: string;
77
mode: string;
8+
modeBits: string,
89
size: number;
910
isFile: boolean;
1011
isSymlink: boolean;

resources/scripts/api/transformers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
1616
key: `${data.attributes.is_file ? 'file' : 'dir'}_${data.attributes.name}`,
1717
name: data.attributes.name,
1818
mode: data.attributes.mode,
19+
modeBits: data.attributes.mode_bits,
1920
size: Number(data.attributes.size),
2021
isFile: data.attributes.is_file,
2122
isSymlink: data.attributes.is_symlink,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { fileBitsToString } from '@/helpers';
2+
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
3+
import React from 'react';
4+
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
5+
import { Form, Formik, FormikHelpers } from 'formik';
6+
import Field from '@/components/elements/Field';
7+
import chmodFiles from '@/api/server/files/chmodFiles';
8+
import { ServerContext } from '@/state/server';
9+
import tw from 'twin.macro';
10+
import Button from '@/components/elements/Button';
11+
import useFlash from '@/plugins/useFlash';
12+
13+
interface FormikValues {
14+
mode: string;
15+
}
16+
17+
interface File {
18+
file: string,
19+
mode: string,
20+
}
21+
22+
type OwnProps = RequiredModalProps & { files: File[] };
23+
24+
const ChmodFileModal = ({ files, ...props }: OwnProps) => {
25+
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
26+
const { mutate } = useFileManagerSwr();
27+
const { clearFlashes, clearAndAddHttpError } = useFlash();
28+
const directory = ServerContext.useStoreState(state => state.files.directory);
29+
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
30+
31+
const submit = ({ mode }: FormikValues, { setSubmitting }: FormikHelpers<FormikValues>) => {
32+
clearFlashes('files');
33+
34+
mutate(data => data.map(f => f.name === files[0].file ? { ...f, mode: fileBitsToString(mode, !f.isFile), modeBits: mode } : f), false);
35+
36+
const data = files.map(f => ({ file: f.file, mode: mode }));
37+
38+
chmodFiles(uuid, directory, data)
39+
.then((): Promise<any> => files.length > 0 ? mutate() : Promise.resolve())
40+
.then(() => setSelectedFiles([]))
41+
.catch(error => {
42+
mutate();
43+
setSubmitting(false);
44+
clearAndAddHttpError({ key: 'files', error });
45+
})
46+
.then(() => props.onDismissed());
47+
};
48+
49+
return (
50+
<Formik onSubmit={submit} initialValues={{ mode: files.length > 1 ? '' : (files[0].mode || '') }}>
51+
{({ isSubmitting }) => (
52+
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
53+
<Form css={tw`m-0`}>
54+
<div css={tw`flex flex-wrap items-end`}>
55+
<div css={tw`w-full sm:flex-1 sm:mr-4`}>
56+
<Field
57+
type={'string'}
58+
id={'file_mode'}
59+
name={'mode'}
60+
label={'File Mode'}
61+
autoFocus
62+
/>
63+
</div>
64+
<div css={tw`w-full sm:w-auto mt-4 sm:mt-0`}>
65+
<Button css={tw`w-full`}>Update</Button>
66+
</div>
67+
</div>
68+
</Form>
69+
</Modal>
70+
)}
71+
</Formik>
72+
);
73+
};
74+
75+
export default ChmodFileModal;

resources/scripts/components/server/files/FileDropdownMenu.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
faCopy,
66
faEllipsisH,
77
faFileArchive,
8+
faFileCode,
89
faFileDownload,
910
faLevelUpAlt,
1011
faPencilAlt,
@@ -30,8 +31,9 @@ import compressFiles from '@/api/server/files/compressFiles';
3031
import decompressFiles from '@/api/server/files/decompressFiles';
3132
import isEqual from 'react-fast-compare';
3233
import ConfirmationModal from '@/components/elements/ConfirmationModal';
34+
import ChmodFileModal from '@/components/server/files/ChmodFileModal';
3335

34-
type ModalType = 'rename' | 'move';
36+
type ModalType = 'rename' | 'move' | 'chmod';
3537

3638
const StyledRow = styled.div<{ $danger?: boolean }>`
3739
${tw`p-2 flex items-center rounded`};
@@ -140,14 +142,23 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
140142
renderToggle={onClick => (
141143
<div css={tw`p-3 hover:text-white`} onClick={onClick}>
142144
<FontAwesomeIcon icon={faEllipsisH}/>
143-
{!!modal &&
144-
<RenameFileModal
145-
visible
146-
appear
147-
files={[ file.name ]}
148-
useMoveTerminology={modal === 'move'}
149-
onDismissed={() => setModal(null)}
150-
/>
145+
{modal ?
146+
modal === 'chmod' ?
147+
<ChmodFileModal
148+
visible
149+
appear
150+
files={[ { file: file.name, mode: file.modeBits } ]}
151+
onDismissed={() => setModal(null)}
152+
/>
153+
:
154+
<RenameFileModal
155+
visible
156+
appear
157+
files={[ file.name ]}
158+
useMoveTerminology={modal === 'move'}
159+
onDismissed={() => setModal(null)}
160+
/>
161+
: null
151162
}
152163
<SpinnerOverlay visible={showSpinner} fixed size={'large'}/>
153164
</div>
@@ -156,6 +167,7 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => {
156167
<Can action={'file.update'}>
157168
<Row onClick={() => setModal('rename')} icon={faPencilAlt} title={'Rename'}/>
158169
<Row onClick={() => setModal('move')} icon={faLevelUpAlt} title={'Move'}/>
170+
<Row onClick={() => setModal('chmod')} icon={faFileCode} title={'Permissions'}/>
159171
</Can>
160172
{file.isFile &&
161173
<Can action={'file.create'}>

resources/scripts/components/server/files/FileObjectRow.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
6464
>
6565
<SelectFileCheckbox name={file.name}/>
6666
<Clickable file={file}>
67-
<div css={tw`flex-none self-center text-neutral-400 mr-4 text-lg pl-3 ml-6`}>
67+
<div css={tw`w-24 ml-6 pl-3 hidden md:block`}>
68+
{file.mode}
69+
</div>
70+
71+
<div css={tw`flex-none self-center text-neutral-400 ml-6 md:ml-0 mr-4 text-lg pl-3`}>
6872
{file.isFile ?
6973
<FontAwesomeIcon icon={file.isSymlink ? faFileImport : file.isArchiveType() ? faFileArchive : faFileAlt}/>
7074
:

0 commit comments

Comments
 (0)