hfs 0.26.8 → 0.26.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/assets/index.bb5198ec.js +281 -0
- package/admin/assets/index.dcc78777.css +1 -0
- package/admin/assets/sha512.9dfe82e1.js +8 -0
- package/admin/index.html +3 -1
- package/admin/{public/logo.svg → logo.svg} +0 -0
- package/frontend/assets/index.27a78796.js +85 -0
- package/frontend/assets/index.93366732.css +1 -0
- package/frontend/assets/sha512.6af42937.js +8 -0
- package/frontend/{public/fontello.css → fontello.css} +0 -0
- package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
- package/frontend/index.html +3 -1
- package/package.json +1 -1
- package/src/QuickZipStream.js +285 -0
- package/src/ThrottledStream.js +93 -0
- package/src/adminApis.js +169 -0
- package/src/api.accounts.js +59 -0
- package/src/api.auth.js +128 -0
- package/src/api.file_list.js +103 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +102 -0
- package/src/api.plugins.js +127 -0
- package/src/api.vfs.js +164 -0
- package/src/apiMiddleware.js +120 -0
- package/src/block.js +33 -0
- package/src/commands.js +124 -0
- package/src/config.js +168 -0
- package/src/connections.js +57 -0
- package/src/const.js +83 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +48 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +102 -0
- package/src/index.js +56 -0
- package/src/listen.js +235 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +175 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +181 -0
- package/src/plugins.js +343 -0
- package/src/serveFile.js +105 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +29 -0
- package/src/throttler.js +91 -0
- package/src/update.js +69 -0
- package/src/util-files.js +148 -0
- package/src/util-generators.js +30 -0
- package/src/util-http.js +30 -0
- package/src/vfs.js +230 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +72 -0
- package/admin/.DS_Store +0 -0
- package/admin/.eslintrc +0 -8
- package/admin/.gitignore +0 -23
- package/admin/package.json +0 -67
- package/admin/src/AccountForm.ts +0 -92
- package/admin/src/AccountsPage.ts +0 -143
- package/admin/src/App.ts +0 -83
- package/admin/src/ArrayField.ts +0 -84
- package/admin/src/ConfigPage.ts +0 -279
- package/admin/src/FileField.ts +0 -52
- package/admin/src/FileForm.ts +0 -148
- package/admin/src/FilePicker.ts +0 -166
- package/admin/src/HomePage.ts +0 -96
- package/admin/src/InstalledPlugins.ts +0 -158
- package/admin/src/LoginRequired.ts +0 -75
- package/admin/src/LogoutPage.ts +0 -27
- package/admin/src/LogsPage.ts +0 -75
- package/admin/src/MainMenu.ts +0 -74
- package/admin/src/MenuButton.ts +0 -38
- package/admin/src/MonitorPage.ts +0 -200
- package/admin/src/OnlinePlugins.ts +0 -101
- package/admin/src/PermField.ts +0 -80
- package/admin/src/PluginsPage.ts +0 -27
- package/admin/src/VfsMenuBar.ts +0 -58
- package/admin/src/VfsPage.ts +0 -124
- package/admin/src/VfsTree.ts +0 -95
- package/admin/src/addFiles.ts +0 -59
- package/admin/src/api.ts +0 -246
- package/admin/src/dialog.ts +0 -203
- package/admin/src/index.css +0 -21
- package/admin/src/index.ts +0 -10
- package/admin/src/md.ts +0 -31
- package/admin/src/misc.ts +0 -141
- package/admin/src/react-app-env.d.ts +0 -1
- package/admin/src/reportWebVitals.ts +0 -15
- package/admin/src/setupTests.ts +0 -5
- package/admin/src/state.ts +0 -40
- package/admin/src/theme.ts +0 -37
- package/admin/tsconfig.json +0 -26
- package/admin/vite.config.ts +0 -32
- package/frontend/.DS_Store +0 -0
- package/frontend/.eslintrc +0 -8
- package/frontend/.gitignore +0 -23
- package/frontend/package.json +0 -51
- package/frontend/src/App.ts +0 -25
- package/frontend/src/Breadcrumbs.ts +0 -43
- package/frontend/src/BrowseFiles.ts +0 -141
- package/frontend/src/Head.ts +0 -45
- package/frontend/src/UserPanel.ts +0 -52
- package/frontend/src/api.ts +0 -78
- package/frontend/src/components.ts +0 -54
- package/frontend/src/dialog.css +0 -76
- package/frontend/src/dialog.ts +0 -105
- package/frontend/src/icons.ts +0 -46
- package/frontend/src/index.scss +0 -307
- package/frontend/src/index.ts +0 -10
- package/frontend/src/login.ts +0 -50
- package/frontend/src/menu.ts +0 -188
- package/frontend/src/misc.ts +0 -54
- package/frontend/src/options.ts +0 -52
- package/frontend/src/react-app-env.d.ts +0 -1
- package/frontend/src/reportWebVitals.ts +0 -15
- package/frontend/src/setupTests.ts +0 -5
- package/frontend/src/state.ts +0 -82
- package/frontend/src/useAuthorized.ts +0 -17
- package/frontend/src/useFetchList.ts +0 -144
- package/frontend/src/useTheme.ts +0 -23
- package/frontend/tsconfig.json +0 -26
- package/frontend/vite.config.ts +0 -21
- package/src/QuickZipStream.ts +0 -279
- package/src/ThrottledStream.ts +0 -98
- package/src/adminApis.ts +0 -161
- package/src/api.accounts.ts +0 -78
- package/src/api.auth.ts +0 -131
- package/src/api.file_list.ts +0 -102
- package/src/api.helpers.ts +0 -30
- package/src/api.monitor.ts +0 -106
- package/src/api.plugins.ts +0 -139
- package/src/api.vfs.ts +0 -182
- package/src/apiMiddleware.ts +0 -124
- package/src/block.ts +0 -35
- package/src/commands.ts +0 -122
- package/src/config.ts +0 -166
- package/src/connections.ts +0 -60
- package/src/const.ts +0 -57
- package/src/crypt.ts +0 -16
- package/src/debounceAsync.ts +0 -51
- package/src/events.ts +0 -6
- package/src/frontEndApis.ts +0 -17
- package/src/github.ts +0 -102
- package/src/index.ts +0 -53
- package/src/listen.ts +0 -220
- package/src/log.ts +0 -128
- package/src/middlewares.ts +0 -176
- package/src/misc.ts +0 -149
- package/src/pbkdf2.ts +0 -83
- package/src/perm.ts +0 -194
- package/src/plugins.ts +0 -342
- package/src/serveFile.ts +0 -104
- package/src/serveGuiFiles.ts +0 -95
- package/src/sse.ts +0 -29
- package/src/throttler.ts +0 -106
- package/src/update.ts +0 -67
- package/src/util-files.ts +0 -137
- package/src/util-generators.ts +0 -29
- package/src/util-http.ts +0 -29
- package/src/vfs.ts +0 -258
- package/src/watchLoad.ts +0 -75
- package/src/zip.ts +0 -69
package/admin/src/FileForm.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
-
|
|
3
|
-
import { state } from './state'
|
|
4
|
-
import { createElement as h, useEffect, useMemo, useState } from 'react'
|
|
5
|
-
import { Alert, Button } from '@mui/material'
|
|
6
|
-
import { BoolField, DisplayField, Field, FieldProps, Form, MultiSelectField, SelectField } from '@hfs/mui-grid-form'
|
|
7
|
-
import { apiCall, useApiEx } from './api'
|
|
8
|
-
import { formatBytes, isEqualLax, modifiedSx, onlyTruthy } from './misc'
|
|
9
|
-
import { reloadVfs, VfsNode, Who } from './VfsPage'
|
|
10
|
-
import md from './md'
|
|
11
|
-
import _ from 'lodash'
|
|
12
|
-
import FileField from './FileField'
|
|
13
|
-
import { alertDialog } from './dialog'
|
|
14
|
-
|
|
15
|
-
interface Account { username: string }
|
|
16
|
-
|
|
17
|
-
export default function FileForm({ file }: { file: VfsNode }) {
|
|
18
|
-
const { parent, children, isRoot, ...rest } = file
|
|
19
|
-
const [values, setValues] = useState(rest)
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
setValues(Object.assign({ can_see: null, can_read: null }, rest))
|
|
22
|
-
}, [file]) //eslint-disable-line
|
|
23
|
-
|
|
24
|
-
const { source } = file
|
|
25
|
-
const isDir = file.type === 'folder'
|
|
26
|
-
const hasSource = source !== undefined // we need a boolean
|
|
27
|
-
const realFolder = hasSource && isDir
|
|
28
|
-
const inheritedPerms = useMemo(() => {
|
|
29
|
-
const ret = { can_read: true, can_see: true }
|
|
30
|
-
// reconstruct parents backward
|
|
31
|
-
const parents = []
|
|
32
|
-
let run = parent
|
|
33
|
-
while (run) {
|
|
34
|
-
parents.unshift(run)
|
|
35
|
-
run = run.parent
|
|
36
|
-
}
|
|
37
|
-
for (const node of parents)
|
|
38
|
-
Object.assign(ret, node)
|
|
39
|
-
return ret
|
|
40
|
-
}, [parent])
|
|
41
|
-
const showCanSee = (values.can_read ?? inheritedPerms.can_read) === true
|
|
42
|
-
const showTimestamps = hasSource && Boolean(values.ctime)
|
|
43
|
-
|
|
44
|
-
const { data, element } = useApiEx<{ list: Account[] }>('get_accounts')
|
|
45
|
-
if (element || !data)
|
|
46
|
-
return element
|
|
47
|
-
const accounts = data.list
|
|
48
|
-
|
|
49
|
-
return h(Form, {
|
|
50
|
-
values,
|
|
51
|
-
set(v, k) {
|
|
52
|
-
setValues({ ...values, [k]: v })
|
|
53
|
-
},
|
|
54
|
-
addToBar: [
|
|
55
|
-
h(Button, { // not really useful, but users misled in thinking it's a dialog will find satisfaction in dismissing the form
|
|
56
|
-
sx: { ml: 2 },
|
|
57
|
-
onClick(){
|
|
58
|
-
state.selectedFiles = []
|
|
59
|
-
}
|
|
60
|
-
}, "Close")
|
|
61
|
-
],
|
|
62
|
-
onError: alertDialog,
|
|
63
|
-
save: {
|
|
64
|
-
sx: modifiedSx(!isEqualLax(values, file)),
|
|
65
|
-
async onClick() {
|
|
66
|
-
const props = { ...values }
|
|
67
|
-
if (!props.masks)
|
|
68
|
-
props.masks = null // undefined cannot be serialized
|
|
69
|
-
if (!isRoot)
|
|
70
|
-
delete props.source
|
|
71
|
-
await apiCall('set_vfs', {
|
|
72
|
-
uri: values.id,
|
|
73
|
-
props,
|
|
74
|
-
})
|
|
75
|
-
if (props.name !== file.name) // when the name changes, the id of the selected file is changing too, and we have to update it in the state if we want it to be correctly re-selected after reload
|
|
76
|
-
state.selectedFiles[0].id = file.id.slice(0, -file.name.length) + props.name
|
|
77
|
-
reloadVfs()
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
fields: [
|
|
81
|
-
isRoot ? h(Alert,{ severity: 'info' }, "This is Home, the root of your shared files. Options set here will be applied to all files.")
|
|
82
|
-
: { k: 'name', required: true, helperText: source && "You can decide a name that's different from the one on your disk" },
|
|
83
|
-
isRoot ? { k: 'source', comp: FileField, files: false, helperText: "If you specify a folder here, its files will be listed in the home" }
|
|
84
|
-
: (hasSource && { k: 'source', comp: DisplayField, multiline: true }),
|
|
85
|
-
{ k: 'can_read', label:"Who can download", xl: showCanSee && 6, comp: WhoField, parent, accounts, inherit: inheritedPerms.can_read,
|
|
86
|
-
helperText: "Note: who can't download won't see it in the list"
|
|
87
|
-
},
|
|
88
|
-
showCanSee && { k: 'can_see', label:"Who can see", xl: 6, comp: WhoField, parent, accounts, inherit: inheritedPerms.can_see,
|
|
89
|
-
helperText: "If you hide this element it will not be listed, but will still be accessible if you have a direct link"
|
|
90
|
-
},
|
|
91
|
-
hasSource && !realFolder && { k: 'size', comp: DisplayField, toField: formatBytes },
|
|
92
|
-
showTimestamps && { k: 'ctime', comp: DisplayField, lg: 6, label: 'Created', toField: formatTimestamp },
|
|
93
|
-
showTimestamps && { k: 'mtime', comp: DisplayField, lg: 6, label: 'Modified', toField: formatTimestamp },
|
|
94
|
-
file.website && { k: 'default', comp: BoolField, label:"Serve index.html",
|
|
95
|
-
toField: Boolean, fromField: (v:boolean) => v ? 'index.html' : null,
|
|
96
|
-
helperText: md("This folder may be a website because contains `index.html`. Enabling this will show the website instead of the list of files.")
|
|
97
|
-
},
|
|
98
|
-
isDir && { k: 'masks', multiline: true, xl: true, toField: JSON.stringify, fromField: v => v ? JSON.parse(v) : undefined,
|
|
99
|
-
helperText: "This is a special field. Leave it empty unless you know what you are doing." }
|
|
100
|
-
]
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function formatTimestamp(x: string) {
|
|
105
|
-
return x ? new Date(x).toLocaleString() : '-'
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
interface WhoFieldProps extends FieldProps<Who> { accounts: Account[] }
|
|
109
|
-
function WhoField({ value, onChange, parent, inherit, accounts, helperText, ...rest }: WhoFieldProps) {
|
|
110
|
-
const options = useMemo(() =>
|
|
111
|
-
onlyTruthy([
|
|
112
|
-
{ value: null, label: (parent ? "Same as parent: " : "Default: " ) + who2desc(inherit === 0 ? true : inherit) },
|
|
113
|
-
{ value: true },
|
|
114
|
-
{ value: false },
|
|
115
|
-
{ value: '*' },
|
|
116
|
-
{ value: [], label: "Select accounts" },
|
|
117
|
-
].map(x => x && x.value !== inherit // don't offer inherited value twice
|
|
118
|
-
&& { label: _.capitalize(who2desc(x.value)), ...x })), // default label
|
|
119
|
-
[inherit, parent])
|
|
120
|
-
|
|
121
|
-
const arrayMode = Array.isArray(value)
|
|
122
|
-
return h('div', {},
|
|
123
|
-
h(SelectField as Field<Who>, {
|
|
124
|
-
...rest,
|
|
125
|
-
helperText: !arrayMode && helperText,
|
|
126
|
-
value: arrayMode ? [] : value,
|
|
127
|
-
onChange(v, { was, event }) {
|
|
128
|
-
onChange(v, { was , event })
|
|
129
|
-
},
|
|
130
|
-
options
|
|
131
|
-
}),
|
|
132
|
-
arrayMode && h(MultiSelectField as Field<string[]>, {
|
|
133
|
-
label: accounts?.length ? "Choose accounts for " + rest.label : "You didn't create any account yet",
|
|
134
|
-
value,
|
|
135
|
-
onChange,
|
|
136
|
-
helperText,
|
|
137
|
-
options: accounts?.map(a => ({ value: a.username, label: a.username })) || [],
|
|
138
|
-
})
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function who2desc(who: any) {
|
|
143
|
-
return who === false ? "no one"
|
|
144
|
-
: who === true ? "anyone"
|
|
145
|
-
: who === '*' ? "any account (login required)"
|
|
146
|
-
: Array.isArray(who) ? who.join(', ')
|
|
147
|
-
: "*UNKNOWN*" + JSON.stringify(who)
|
|
148
|
-
}
|
package/admin/src/FilePicker.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
-
|
|
3
|
-
import { createElement as h, Fragment, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
-
import { apiCall, useApiList } from './api'
|
|
5
|
-
import _ from 'lodash'
|
|
6
|
-
import {
|
|
7
|
-
Alert,
|
|
8
|
-
Box,
|
|
9
|
-
Button,
|
|
10
|
-
Checkbox,
|
|
11
|
-
ListItemIcon,
|
|
12
|
-
ListItemText,
|
|
13
|
-
MenuItem,
|
|
14
|
-
TextField,
|
|
15
|
-
Typography
|
|
16
|
-
} from '@mui/material'
|
|
17
|
-
import { enforceFinal, formatBytes, isWindowsDrive, spinner, Center, err2msg } from './misc'
|
|
18
|
-
import { ArrowUpward, VerticalAlignTop } from '@mui/icons-material'
|
|
19
|
-
import { StringField } from '@hfs/mui-grid-form'
|
|
20
|
-
import { FileIcon, FolderIcon } from './VfsTree'
|
|
21
|
-
import { FixedSizeList } from 'react-window'
|
|
22
|
-
|
|
23
|
-
export interface DirEntry { n:string, s?:number, m?:string, c?:string, k?:'d' }
|
|
24
|
-
|
|
25
|
-
interface FilePickerProps {
|
|
26
|
-
onSelect:(v:string[])=>void
|
|
27
|
-
multiple?: boolean
|
|
28
|
-
from?: string
|
|
29
|
-
folders?: boolean
|
|
30
|
-
files?: boolean
|
|
31
|
-
fileMask?: string
|
|
32
|
-
}
|
|
33
|
-
export default function FilePicker({ onSelect, multiple=true, files=true, folders=true, fileMask, from='' }: FilePickerProps) {
|
|
34
|
-
const [cwd, setCwd] = useState(from)
|
|
35
|
-
const [ready, setReady] = useState(false)
|
|
36
|
-
const isWindows = useRef(false)
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
apiCall('resolve_path', { path: from, closestFolder: true }).then(res => {
|
|
39
|
-
if (typeof res.path === 'string') {
|
|
40
|
-
setCwd(res.path)
|
|
41
|
-
isWindows.current = res.path[1] === ':'
|
|
42
|
-
}
|
|
43
|
-
}).finally(() => setReady(true))
|
|
44
|
-
}, [from])
|
|
45
|
-
const { list, error, loading } = useApiList<DirEntry>(ready && 'ls', { path: cwd, files, fileMask })
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
setSel([])
|
|
48
|
-
setFilter('')
|
|
49
|
-
}, [cwd])
|
|
50
|
-
|
|
51
|
-
const [sel, setSel] = useState<string[]>([])
|
|
52
|
-
const [filter, setFilter] = useState('')
|
|
53
|
-
const setFilterBounced = useMemo(() => _.debounce((x:string) => setFilter(x)), [])
|
|
54
|
-
const filterMatch = useMemo(() => {
|
|
55
|
-
const re = new RegExp(_.escapeRegExp(filter), 'i')
|
|
56
|
-
return (v:string) => re.test(v)
|
|
57
|
-
}, [filter])
|
|
58
|
-
|
|
59
|
-
const [listHeight, setListHeight] = useState(0)
|
|
60
|
-
const filteredList = useMemo(() => list.filter(it => filterMatch(it.n)), [list,filterMatch])
|
|
61
|
-
if (loading)
|
|
62
|
-
return spinner()
|
|
63
|
-
const root = isWindows.current ? '' : '/'
|
|
64
|
-
const pathDelimiter = isWindows.current ? '\\' : '/'
|
|
65
|
-
const cwdDelimiter = enforceFinal(pathDelimiter, cwd)
|
|
66
|
-
const isRoot = cwd.length < 2
|
|
67
|
-
return h(Fragment, {},
|
|
68
|
-
h(Box, { display: 'flex', gap: 1 },
|
|
69
|
-
h(Button, {
|
|
70
|
-
title: "root",
|
|
71
|
-
disabled: isRoot,
|
|
72
|
-
onClick() {
|
|
73
|
-
setCwd(root)
|
|
74
|
-
}
|
|
75
|
-
}, h(VerticalAlignTop)),
|
|
76
|
-
h(Button, {
|
|
77
|
-
disabled: isRoot,
|
|
78
|
-
title: "parent folder",
|
|
79
|
-
onClick() {
|
|
80
|
-
const cwdND = /[\\/]$/.test(cwd) ? cwd.slice(0,-1) : cwd // exclude final delimiter, if any
|
|
81
|
-
const parent = isWindowsDrive(cwdND) ? root : cwdND.slice(0, cwdND.lastIndexOf(pathDelimiter) || 1)
|
|
82
|
-
setCwd(parent)
|
|
83
|
-
}
|
|
84
|
-
}, h(ArrowUpward)),
|
|
85
|
-
h(StringField, {
|
|
86
|
-
label: "Current path",
|
|
87
|
-
value: cwd,
|
|
88
|
-
InputLabelProps: { shrink: true },
|
|
89
|
-
async onChange(v) {
|
|
90
|
-
if (!v)
|
|
91
|
-
return setCwd(root)
|
|
92
|
-
const res = await apiCall('resolve_path', { path: v })
|
|
93
|
-
setCwd(res.path)
|
|
94
|
-
},
|
|
95
|
-
}),
|
|
96
|
-
),
|
|
97
|
-
error ? h(Alert, { severity: 'error' }, err2msg(error))
|
|
98
|
-
: h(Fragment, {},
|
|
99
|
-
h(Box, {
|
|
100
|
-
ref(x?: HTMLElement){
|
|
101
|
-
if (!x) return
|
|
102
|
-
const h = x?.clientHeight - 1
|
|
103
|
-
if (h - listHeight > 1)
|
|
104
|
-
setListHeight(h)
|
|
105
|
-
},
|
|
106
|
-
sx: { flex: 1, display: 'flex', flexDirection: 'column' }
|
|
107
|
-
},
|
|
108
|
-
!list.length ? h(Center, { flex: 1, mt: '4em' }, "No elements in this folder")
|
|
109
|
-
: h(FixedSizeList, {
|
|
110
|
-
width: '100%', height: listHeight,
|
|
111
|
-
itemSize: 46, itemCount: filteredList.length, overscanCount: 5,
|
|
112
|
-
children({ index, style }) {
|
|
113
|
-
const it: DirEntry = filteredList[index]
|
|
114
|
-
const isFolder = it.k === 'd'
|
|
115
|
-
return h(MenuItem, {
|
|
116
|
-
style: { ...style, padding: 0 },
|
|
117
|
-
key: it.n,
|
|
118
|
-
onClick() {
|
|
119
|
-
if (isFolder)
|
|
120
|
-
setCwd(cwdDelimiter + it.n)
|
|
121
|
-
else
|
|
122
|
-
onSelect([cwdDelimiter + it.n])
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
multiple && h(Checkbox, {
|
|
126
|
-
checked: sel.includes(it.n),
|
|
127
|
-
disabled: !folders && isFolder,
|
|
128
|
-
onClick(ev) {
|
|
129
|
-
const id = it.n
|
|
130
|
-
const removed = sel.filter(x => x !== id)
|
|
131
|
-
setSel(removed.length < sel.length ? removed : [...sel, id])
|
|
132
|
-
ev.stopPropagation()
|
|
133
|
-
},
|
|
134
|
-
}),
|
|
135
|
-
h(ListItemIcon, {}, h(it.k ? FolderIcon : FileIcon)),
|
|
136
|
-
h(ListItemText, { sx: { whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }, it.n),
|
|
137
|
-
!isFolder && it.s !== undefined && h(Typography, {
|
|
138
|
-
variant: 'body2',
|
|
139
|
-
color: 'text.secondary',
|
|
140
|
-
ml: 4, mr: 1,
|
|
141
|
-
}, formatBytes(it.s))
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
),
|
|
146
|
-
h(Box, { display:'flex', gap: 1 },
|
|
147
|
-
(multiple || folders || !files) && h(Button, {
|
|
148
|
-
variant: 'contained',
|
|
149
|
-
disabled: !cwd || !folders && !sel.length && files,
|
|
150
|
-
sx: { minWidth: 'max-content' },
|
|
151
|
-
onClick() {
|
|
152
|
-
onSelect(sel.length ? sel.map(x => cwdDelimiter + x) : [cwd])
|
|
153
|
-
}
|
|
154
|
-
}, files && (sel.length || !folders) ? `Select (${sel.length})` : `Select this folder`),
|
|
155
|
-
h(TextField, {
|
|
156
|
-
value: filter,
|
|
157
|
-
label: `Filter results (${filteredList.length}${filteredList.length < list.length ? '/'+list.length : ''})`,
|
|
158
|
-
onChange(ev) {
|
|
159
|
-
setFilterBounced(ev.target.value)
|
|
160
|
-
},
|
|
161
|
-
sx: { flex: 1 },
|
|
162
|
-
}),
|
|
163
|
-
),
|
|
164
|
-
)
|
|
165
|
-
)
|
|
166
|
-
}
|
package/admin/src/HomePage.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
-
|
|
3
|
-
import { createElement as h } from 'react'
|
|
4
|
-
import { Box, Button, LinearProgress, Link } from '@mui/material'
|
|
5
|
-
import { apiCall, useApi, useApiEx, useApiList } from './api'
|
|
6
|
-
import { Dict, dontBotherWithKeys, InLink, objSameKeys, onlyTruthy } from './misc'
|
|
7
|
-
import { CheckCircle, Error, Info, Launch, Warning } from '@mui/icons-material'
|
|
8
|
-
import md from './md'
|
|
9
|
-
import { useSnapState } from './state'
|
|
10
|
-
import { confirmDialog } from './dialog'
|
|
11
|
-
import { isCertError, isKeyError, makeCertAndSave } from './ConfigPage'
|
|
12
|
-
import { VfsNode } from './VfsPage'
|
|
13
|
-
import { Account } from './AccountsPage'
|
|
14
|
-
|
|
15
|
-
interface ServerStatus { listening: boolean, port: number, error?: string, busy?: string }
|
|
16
|
-
|
|
17
|
-
export default function HomePage() {
|
|
18
|
-
const SOLUTION_SEP = " — "
|
|
19
|
-
const { username } = useSnapState()
|
|
20
|
-
const { data: status, reload: reloadStatus, element: statusEl } = useApiEx<Dict<ServerStatus>>('get_status')
|
|
21
|
-
const { data: vfs } = useApiEx<{ root?: VfsNode }>('get_vfs')
|
|
22
|
-
const [account] = useApi<Account>(username && 'get_account')
|
|
23
|
-
const { data: cfg, reload: reloadCfg } = useApiEx('get_config', { only: ['https_port', 'cert', 'private_key', 'proxies', 'ignore_proxies'] })
|
|
24
|
-
const { list: plugins } = useApiList('get_plugins')
|
|
25
|
-
if (statusEl || !status)
|
|
26
|
-
return statusEl
|
|
27
|
-
const { http, https } = status
|
|
28
|
-
const goSecure = !http?.listening && https?.listening ? 's' : ''
|
|
29
|
-
const srv = goSecure ? https : (http?.listening && http)
|
|
30
|
-
const href = srv && `http${goSecure}://`+window.location.hostname + (srv.port === (goSecure ? 443 : 80) ? '' : ':'+srv.port)
|
|
31
|
-
const errorMap = objSameKeys(status, v =>
|
|
32
|
-
v.busy ? [`port ${v.port} already used by ${v.busy}${SOLUTION_SEP}choose a `, cfgLink('different port'), ` or stop ${v.busy}`]
|
|
33
|
-
: v.error )
|
|
34
|
-
const errors = errorMap && onlyTruthy(Object.entries(errorMap).map(([k,v]) =>
|
|
35
|
-
v && [md(`Protocol _${k}_ cannot work: `), v,
|
|
36
|
-
(isCertError(v) || isKeyError(v)) && [
|
|
37
|
-
SOLUTION_SEP, h(Link, { sx: { cursor: 'pointer' }, onClick() { makeCertAndSave().then(reloadCfg).then(reloadStatus) } }, "make one"),
|
|
38
|
-
" or ", SOLUTION_SEP, cfgLink("provide adequate files")
|
|
39
|
-
]]))
|
|
40
|
-
return h(Box, { display:'flex', gap: 2, flexDirection:'column' },
|
|
41
|
-
username && entry('', "Welcome "+username),
|
|
42
|
-
errors.length ? dontBotherWithKeys(errors.map(msg => entry('error', dontBotherWithKeys(msg))))
|
|
43
|
-
: entry('success', "Server is working"),
|
|
44
|
-
!vfs ? h(LinearProgress)
|
|
45
|
-
: !vfs.root?.children?.length && !vfs.root?.source ? entry('warning', "You have no files shared", SOLUTION_SEP, fsLink("add some"))
|
|
46
|
-
: entry('', md("Here you manage your server. There is a _separated_ interface to access your shared files: "),
|
|
47
|
-
h(Link, { target:'frontend', href: '/' }, "Frontend interface", h(Launch, { sx: { verticalAlign: 'sub', ml: '.2em' } }))),
|
|
48
|
-
!href && entry('warning', "Frontend unreachable: ",
|
|
49
|
-
['http','https'].map(k => k + " " + (errorMap[k] ? "is in error" : "is off")).join(', '),
|
|
50
|
-
!errors.length && [ SOLUTION_SEP, cfgLink("switch http or https on") ]
|
|
51
|
-
),
|
|
52
|
-
plugins.find(x => x.badApi) && entry('warning', "Some plugins may be incompatible"),
|
|
53
|
-
!account?.adminActualAccess && entry('', md("You are accessing on _localhost_ where permission is not required"),
|
|
54
|
-
SOLUTION_SEP, h(InLink, { to:'accounts' }, "give admin access to an account to be able to access from other computers") ),
|
|
55
|
-
proxyWarning(cfg, status) && entry('warning', "A proxy was detected but none is configured",
|
|
56
|
-
SOLUTION_SEP, cfgLink("set the number of proxies"),
|
|
57
|
-
SOLUTION_SEP, "unless you are sure you can ", h(Button, {
|
|
58
|
-
async onClick() {
|
|
59
|
-
if (await confirmDialog("Go on only if you know what you are doing")
|
|
60
|
-
&& await apiCall('set_config', { values: { ignore_proxies: true } }))
|
|
61
|
-
reloadCfg()
|
|
62
|
-
}
|
|
63
|
-
}, "ignore this warning")),
|
|
64
|
-
status.frpDetected && entry('warning', `FRP is detected. It should not be used with "type = tcp" with HFS. Possible solutions are`,
|
|
65
|
-
h('ol',{},
|
|
66
|
-
h('li',{}, `configure FRP with type=http (best solution)`),
|
|
67
|
-
h('li',{}, md(`configure FRP to connect to HFS _not_ with 127.0.0.1 (safe, but you won't see users' IPs)`)),
|
|
68
|
-
h('li',{}, `disable "admin access for localhost" in HFS (safe, but you won't see users' IPs)`),
|
|
69
|
-
))
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
type Color = '' | 'success' | 'warning' | 'error'
|
|
74
|
-
|
|
75
|
-
function entry(color: Color, ...content: any[]) {
|
|
76
|
-
return h(Box, {
|
|
77
|
-
fontSize: 'x-large',
|
|
78
|
-
color: th => color && th.palette[color]?.main,
|
|
79
|
-
},
|
|
80
|
-
h(({ success: CheckCircle, info: Info, '': Info, warning: Warning, error: Error })[color], {
|
|
81
|
-
sx: { mb: '-3px', mr: 1 }
|
|
82
|
-
}),
|
|
83
|
-
...content)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function fsLink(text=`File System page`) {
|
|
87
|
-
return h(InLink, { to:'fs' }, text)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function cfgLink(text=`Configuration page`) {
|
|
91
|
-
return h(InLink, { to:'configuration' }, text)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function proxyWarning(cfg: any, status: any) {
|
|
95
|
-
return cfg && !cfg.proxies && !cfg.ignore_proxies && status?.proxyDetected
|
|
96
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { apiCall, useApiList } from './api'
|
|
2
|
-
import { createElement as h, Fragment } from 'react'
|
|
3
|
-
import { Alert, Box, Tooltip } from '@mui/material'
|
|
4
|
-
import { DataGrid } from '@mui/x-data-grid'
|
|
5
|
-
import { Delete, Error, GitHub, PlayCircle, Settings, StopCircle, Upgrade } from '@mui/icons-material'
|
|
6
|
-
import { IconBtn, xlate } from './misc'
|
|
7
|
-
import { formDialog, toast } from './dialog'
|
|
8
|
-
import _ from 'lodash'
|
|
9
|
-
import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from '@hfs/mui-grid-form'
|
|
10
|
-
import { ArrayField } from './ArrayField'
|
|
11
|
-
import FileField from './FileField'
|
|
12
|
-
|
|
13
|
-
export default function InstalledPlugins({ updates }: { updates?: true }) {
|
|
14
|
-
const { list, setList, error, initializing } = useApiList(updates ? 'get_plugin_updates' : 'get_plugins')
|
|
15
|
-
if (error)
|
|
16
|
-
return showError(error)
|
|
17
|
-
return h(DataGrid, {
|
|
18
|
-
rows: list.length ? list : [], // workaround for DataGrid bug causing 'no rows' message to be not displayed after 'loading' was also used
|
|
19
|
-
loading: initializing,
|
|
20
|
-
disableColumnSelector: true,
|
|
21
|
-
disableColumnMenu: true,
|
|
22
|
-
columnVisibilityModel: {
|
|
23
|
-
started: !updates,
|
|
24
|
-
},
|
|
25
|
-
localeText: updates && { noRowsLabel: "No updates available. Only online plugins are checked." },
|
|
26
|
-
columns: [
|
|
27
|
-
{
|
|
28
|
-
field: 'id',
|
|
29
|
-
headerName: "name",
|
|
30
|
-
flex: .3,
|
|
31
|
-
minWidth: 150,
|
|
32
|
-
renderCell({ row, value }) {
|
|
33
|
-
return h(Fragment, {},
|
|
34
|
-
value,
|
|
35
|
-
typeof row.badApi === 'string' && h(Tooltip, { title: row.badApi, children: h(Error, { color: 'warning', sx: { ml: 1 } }) }),
|
|
36
|
-
repoLink(row.repo),
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
field: 'version',
|
|
42
|
-
width: 70,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
field: 'description',
|
|
46
|
-
flex: 1,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
field: "actions",
|
|
50
|
-
width: 120,
|
|
51
|
-
align: 'center',
|
|
52
|
-
headerAlign: 'center',
|
|
53
|
-
hideSortIcons: true,
|
|
54
|
-
disableColumnMenu: true,
|
|
55
|
-
renderCell({ row }) {
|
|
56
|
-
const { config, id } = row
|
|
57
|
-
if (updates)
|
|
58
|
-
return h(UpdateButton, { id, then: () => setList(list.filter(x => x.id !== id)) })
|
|
59
|
-
return h('div', {},
|
|
60
|
-
h(IconBtn, row.started ? {
|
|
61
|
-
icon: StopCircle,
|
|
62
|
-
title: h(Box, {}, `Stop ${id}`, h('br'), `Started ` + new Date(row.started as string).toLocaleString()),
|
|
63
|
-
color: 'success',
|
|
64
|
-
onClick: () =>
|
|
65
|
-
apiCall('set_plugin', { id, enabled: false }).then(() =>
|
|
66
|
-
toast("Plugin is stopping", h(StopCircle, { color: 'warning' })))
|
|
67
|
-
} : {
|
|
68
|
-
icon: PlayCircle,
|
|
69
|
-
title: `Start ${id}`,
|
|
70
|
-
onClick: () => startPlugin(id),
|
|
71
|
-
}),
|
|
72
|
-
h(IconBtn, {
|
|
73
|
-
icon: Settings,
|
|
74
|
-
title: "Configuration",
|
|
75
|
-
disabled: !config && "No configuration available for this plugin",
|
|
76
|
-
async onClick() {
|
|
77
|
-
const pl = await apiCall('get_plugin', { id })
|
|
78
|
-
const values = await formDialog({
|
|
79
|
-
title: `${id} configuration`,
|
|
80
|
-
fields: [ h(Box, {}, row.description), ...makeFields(config) ],
|
|
81
|
-
values: pl.config,
|
|
82
|
-
...row.configDialog,
|
|
83
|
-
})
|
|
84
|
-
if (!values || _.isEqual(pl.config, values)) return
|
|
85
|
-
await apiCall('set_plugin', { id, config: values })
|
|
86
|
-
toast("Configuration saved")
|
|
87
|
-
}
|
|
88
|
-
}),
|
|
89
|
-
h(IconBtn, {
|
|
90
|
-
icon: Delete,
|
|
91
|
-
title: "Uninstall",
|
|
92
|
-
confirm: "Remove?",
|
|
93
|
-
async onClick() {
|
|
94
|
-
await apiCall('uninstall_plugin', { id })
|
|
95
|
-
toast("Plugin uninstalled")
|
|
96
|
-
}
|
|
97
|
-
}),
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
]
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function makeFields(config: any) {
|
|
106
|
-
return Object.entries(config).map(([k,o]: [string,any]) => {
|
|
107
|
-
if (!_.isPlainObject(o))
|
|
108
|
-
return o
|
|
109
|
-
let { type, defaultValue, fields, $column, $width, ...rest } = o
|
|
110
|
-
const comp = (type2comp as any)[type] as Field<any> | undefined
|
|
111
|
-
if (comp === ArrayField)
|
|
112
|
-
fields = makeFields(fields)
|
|
113
|
-
if (defaultValue !== undefined && type === 'boolean')
|
|
114
|
-
rest.placeholder = `Default value is ${JSON.stringify(defaultValue)}`
|
|
115
|
-
return { k, comp, fields, ...rest }
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const type2comp = {
|
|
120
|
-
string: StringField,
|
|
121
|
-
number: NumberField,
|
|
122
|
-
boolean: BoolField,
|
|
123
|
-
select: SelectField,
|
|
124
|
-
multiselect: MultiSelectField,
|
|
125
|
-
array: ArrayField,
|
|
126
|
-
real_path: FileField,
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function repoLink(repo?: string) {
|
|
130
|
-
return repo && h(IconBtn, {
|
|
131
|
-
icon: GitHub,
|
|
132
|
-
title: "Open web page",
|
|
133
|
-
link: 'https://github.com/' + repo,
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function showError(error: any) {
|
|
138
|
-
return h(Alert, { severity: 'error' }, xlate(error, {
|
|
139
|
-
ENOTFOUND: "Couldn't reach github.com"
|
|
140
|
-
}))
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function UpdateButton({ id, then }: { id: string, then: (id:string)=>void }) {
|
|
144
|
-
return h(IconBtn, {
|
|
145
|
-
icon: Upgrade,
|
|
146
|
-
title: "Update",
|
|
147
|
-
async onClick() {
|
|
148
|
-
await apiCall('update_plugin', { id })
|
|
149
|
-
then?.(id)
|
|
150
|
-
toast("Plugin updated")
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function startPlugin(id: string) {
|
|
156
|
-
return apiCall('set_plugin', { id, enabled: true }).then(() =>
|
|
157
|
-
toast("Plugin is starting", h(PlayCircle, { color: 'success' })))
|
|
158
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { state, useSnapState } from './state'
|
|
2
|
-
import { createElement as h, Fragment, useState } from 'react'
|
|
3
|
-
import { Center } from './misc'
|
|
4
|
-
import { Form } from '@hfs/mui-grid-form'
|
|
5
|
-
import { apiCall } from './api'
|
|
6
|
-
import { srpSequence } from '@hfs/shared'
|
|
7
|
-
import { Alert } from '@mui/material'
|
|
8
|
-
|
|
9
|
-
export function LoginRequired({ children }: any) {
|
|
10
|
-
const { loginRequired } = useSnapState()
|
|
11
|
-
if (loginRequired)
|
|
12
|
-
return h(LoginForm)
|
|
13
|
-
return h(Fragment, {}, children)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function LoginForm() {
|
|
17
|
-
const [values, setValues] = useState({ username: '', password: '' })
|
|
18
|
-
const [error, setError] = useState('')
|
|
19
|
-
return h(Center, {},
|
|
20
|
-
h(Form, {
|
|
21
|
-
values,
|
|
22
|
-
set(v, k) {
|
|
23
|
-
setValues({ ...values, [k]: v })
|
|
24
|
-
},
|
|
25
|
-
fields: [
|
|
26
|
-
{ k: 'username', autoComplete: 'username', autoFocus: true, required: true },
|
|
27
|
-
{ k: 'password', type: 'password', autoComplete: 'current-password', required: true },
|
|
28
|
-
],
|
|
29
|
-
addToBar: [ error && h(Alert, { severity: 'error', sx: { flex: 1 } }, error) ],
|
|
30
|
-
saveOnEnter: true,
|
|
31
|
-
save: {
|
|
32
|
-
children: "Enter",
|
|
33
|
-
startIcon: null,
|
|
34
|
-
async onClick() {
|
|
35
|
-
try {
|
|
36
|
-
setError('')
|
|
37
|
-
await login(values.username, values.password)
|
|
38
|
-
}
|
|
39
|
-
catch(e) {
|
|
40
|
-
setError(String(e))
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function login(username: string, password: string) {
|
|
49
|
-
const res = await srpSequence(username, password, apiCall).catch(err => {
|
|
50
|
-
throw err?.code === 401 ? "Wrong username or password"
|
|
51
|
-
: err === 'trust' ? "Login aborted: server identity cannot be trusted"
|
|
52
|
-
: err?.name === 'AbortError' ? "Server didn't respond"
|
|
53
|
-
: (err?.message || "Unknown error")
|
|
54
|
-
})
|
|
55
|
-
if (!res.adminUrl)
|
|
56
|
-
throw "This account has no Admin access"
|
|
57
|
-
|
|
58
|
-
// login was successful, update state
|
|
59
|
-
state.loginRequired = false
|
|
60
|
-
sessionRefresher(res)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// @ts-ignore
|
|
64
|
-
sessionRefresher(window.SESSION)
|
|
65
|
-
|
|
66
|
-
function sessionRefresher(response: any) {
|
|
67
|
-
if (!response) return
|
|
68
|
-
const { exp, username } = response
|
|
69
|
-
state.username = username
|
|
70
|
-
if (!username || !exp) return
|
|
71
|
-
const delta = new Date(exp).getTime() - Date.now()
|
|
72
|
-
const t = Math.min(delta - 30_000, 600_000)
|
|
73
|
-
console.debug('session refresh in', Math.round(t/1000))
|
|
74
|
-
setTimeout(() => apiCall('refresh_session').then(sessionRefresher), t)
|
|
75
|
-
}
|
package/admin/src/LogoutPage.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { createElement as h } from "react"
|
|
2
|
-
import { Alert, Box, Button } from '@mui/material'
|
|
3
|
-
import { apiCall, useApiEx } from './api'
|
|
4
|
-
import { alertDialog } from "./dialog"
|
|
5
|
-
import { useSnapState } from './state'
|
|
6
|
-
|
|
7
|
-
export default function LogoutPage() {
|
|
8
|
-
const { element } = useApiEx('get_config', { only: [] }) // sort of noop, just to get the 'element' part
|
|
9
|
-
const { username } = useSnapState()
|
|
10
|
-
if (element)
|
|
11
|
-
return element
|
|
12
|
-
if (!username)
|
|
13
|
-
return h(Alert, { severity: 'info' }, "You are not logged in, because authentication is not required on localhost")
|
|
14
|
-
return h(Box, { display: 'flex', flexDirection:'column', gap: 2 },
|
|
15
|
-
"You are logged in as " + username,
|
|
16
|
-
h(Box, {},
|
|
17
|
-
h(Button, {
|
|
18
|
-
size: 'large',
|
|
19
|
-
variant: 'contained',
|
|
20
|
-
onClick() {
|
|
21
|
-
apiCall('logout').catch(err => // we expect 401
|
|
22
|
-
err.code !== 401 && alertDialog(err))
|
|
23
|
-
}
|
|
24
|
-
}, "Yes, I want to logout")
|
|
25
|
-
)
|
|
26
|
-
)
|
|
27
|
-
}
|