Skip to content

Commit ed5613e

Browse files
committed
Show file mode on file listing, add ability to change file mode
1 parent 8611ebb commit ed5613e

File tree

9 files changed

+150
-11
lines changed

9 files changed

+150
-11
lines changed

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
:

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const schema = object().shape({
2525
const generateDirectoryData = (name: string): FileObject => ({
2626
key: `dir_${name.split('/', 1)[0] ?? name}`,
2727
name: name.replace(/^(\/*)/, '').split('/', 1)[0] ?? name,
28-
mode: '0644',
28+
mode: 'drwxr-xr-x',
29+
modeBits: '0755',
2930
size: 0,
3031
isFile: false,
3132
isSymlink: false,

resources/scripts/helpers.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,33 @@ export const randomInt = (low: number, high: number) => Math.floor(Math.random()
2020
export const cleanDirectoryPath = (path: string) => path.replace(/(^#\/*)|(\/(\/*))|(^$)/g, '/');
2121

2222
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
23+
24+
export function fileBitsToString (mode: string, directory: boolean): string {
25+
const m = parseInt(mode, 8);
26+
27+
let buf = '';
28+
'dalTLDpSugct?'.split('').forEach((c, i) => {
29+
if ((m & (1 << (32 - 1 - i))) !== 0) {
30+
buf = buf + c;
31+
}
32+
});
33+
34+
if (buf.length === 0) {
35+
// If the file is directory, make sure it has the directory flag.
36+
if (directory) {
37+
buf = 'd';
38+
} else {
39+
buf = '-';
40+
}
41+
}
42+
43+
'rwxrwxrwx'.split('').forEach((c, i) => {
44+
if ((m & (1 << (9 - 1 - i))) !== 0) {
45+
buf = buf + c;
46+
} else {
47+
buf = buf + '-';
48+
}
49+
});
50+
51+
return buf;
52+
}

0 commit comments

Comments
 (0)