hfs 0.26.8 → 0.27.2
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/README.md +15 -2
- package/admin/assets/index-509bb1d6.js +415 -0
- package/admin/assets/index-60a380a7.css +1 -0
- package/admin/assets/sha512-738f0943.js +8 -0
- package/admin/index.html +3 -1
- package/admin/{public/logo.svg → logo.svg} +0 -0
- package/frontend/assets/index-6e178dfd.css +1 -0
- package/frontend/assets/index-aea7654e.js +85 -0
- package/frontend/assets/sha512-bf915587.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 +4 -2
- package/package.json +2 -6
- package/plugins/vhosting/plugin.js +23 -20
- 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 +110 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +104 -0
- package/src/api.plugins.js +128 -0
- package/src/api.vfs.js +167 -0
- package/src/apiMiddleware.js +123 -0
- package/src/block.js +34 -0
- package/src/commands.js +125 -0
- package/src/config.js +168 -0
- package/src/connections.js +57 -0
- package/src/const.js +94 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +49 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +104 -0
- package/src/index.js +57 -0
- package/src/listen.js +235 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +195 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +183 -0
- package/src/plugins.js +343 -0
- package/src/serveFile.js +105 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +30 -0
- package/src/throttler.js +91 -0
- package/src/update.js +70 -0
- package/src/util-files.js +163 -0
- package/src/util-generators.js +31 -0
- package/src/util-http.js +32 -0
- package/src/vfs.js +232 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +73 -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/MonitorPage.ts
DELETED
|
@@ -1,200 +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 _ from "lodash"
|
|
4
|
-
import { createElement as h, useMemo, Fragment, useState } from "react"
|
|
5
|
-
import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
|
|
6
|
-
import { PauseCircle, PlayCircle, Delete, Lock, Block, FolderZip } from '@mui/icons-material'
|
|
7
|
-
import { Alert, Box, Chip, ChipProps } from '@mui/material'
|
|
8
|
-
import { DataGrid } from "@mui/x-data-grid"
|
|
9
|
-
import { formatBytes, IconBtn, iconTooltip, manipulateConfig, useBreakpoint } from "./misc"
|
|
10
|
-
import { Field, SelectField } from '@hfs/mui-grid-form'
|
|
11
|
-
import { GridColumns } from '@mui/x-data-grid/models/colDef/gridColDef'
|
|
12
|
-
import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
|
|
13
|
-
|
|
14
|
-
export default function MonitorPage() {
|
|
15
|
-
return h(Fragment, {},
|
|
16
|
-
h(MoreInfo),
|
|
17
|
-
h(Connections),
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const isoDateRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
22
|
-
|
|
23
|
-
function MoreInfo() {
|
|
24
|
-
const { data: status, element } = useApiEx('get_status')
|
|
25
|
-
const { data: connections } = useApiEvents('get_connection_stats')
|
|
26
|
-
if (status && connections)
|
|
27
|
-
Object.assign(status, connections)
|
|
28
|
-
const md = useBreakpoint('md')
|
|
29
|
-
const sm = useBreakpoint('sm')
|
|
30
|
-
return element || h(Box, { display: 'flex', flexWrap: 'wrap', gap: '1em', mb: 2 },
|
|
31
|
-
md && pair('started'),
|
|
32
|
-
md && pair('http', { label: "HTTP", render: port }),
|
|
33
|
-
md && pair('https', { label: "HTTPS", render: port }),
|
|
34
|
-
sm && pair('connections'),
|
|
35
|
-
pair('sent', { render: formatBytes, minWidth: '4em' }),
|
|
36
|
-
pair('outSpeed', { label: "Output speed", render: formatSpeed }),
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
type Color = ChipProps['color']
|
|
40
|
-
type Render = (v: any) => [string, Color?] | string
|
|
41
|
-
interface PairOptions {
|
|
42
|
-
label?: string
|
|
43
|
-
render?: Render
|
|
44
|
-
minWidth?: StandardCSSProperties['minWidth']
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function pair(k: string, { label, minWidth, render }: PairOptions={}) {
|
|
48
|
-
let v = _.get(status, k)
|
|
49
|
-
if (v === undefined)
|
|
50
|
-
return null
|
|
51
|
-
if (typeof v === 'string' && isoDateRe.test(v))
|
|
52
|
-
v = new Date(v).toLocaleString()
|
|
53
|
-
let color: Color = undefined
|
|
54
|
-
if (render) {
|
|
55
|
-
v = render(v)
|
|
56
|
-
if (Array.isArray(v))
|
|
57
|
-
[v, color] = v
|
|
58
|
-
}
|
|
59
|
-
if (!label)
|
|
60
|
-
label = _.capitalize(k.replaceAll('_', ' '))
|
|
61
|
-
return h(Chip, {
|
|
62
|
-
variant: 'filled',
|
|
63
|
-
color,
|
|
64
|
-
label: h(Fragment, {},
|
|
65
|
-
h('b',{},label),
|
|
66
|
-
': ',
|
|
67
|
-
h('span', { style:{ display: 'inline-block', minWidth } }, v),
|
|
68
|
-
),
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function port(v: any): ReturnType<Render> {
|
|
73
|
-
return v.listening ? ["port " + v.port, 'success']
|
|
74
|
-
: v.error ? [v.error, 'error']
|
|
75
|
-
: "off"
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function Connections() {
|
|
81
|
-
const { list, error } = useApiList('get_connections')
|
|
82
|
-
const [filtered, setFiltered] = useState(true)
|
|
83
|
-
const [paused, setPaused] = useState(false)
|
|
84
|
-
const rows = useMemo(() =>
|
|
85
|
-
list?.filter((x: any) => !filtered || x.path).map((x: any, id: number) => ({ id, ...x })),
|
|
86
|
-
[!paused && list, filtered]) //eslint-disable-line
|
|
87
|
-
// if I don't memo 'columns', it won't keep hiding status
|
|
88
|
-
const columns = useMemo<GridColumns<any>>(() => [
|
|
89
|
-
{
|
|
90
|
-
field: 'ip',
|
|
91
|
-
headerName: "Address",
|
|
92
|
-
flex: 1,
|
|
93
|
-
maxWidth: 400,
|
|
94
|
-
valueGetter: ({ row, value }) => (row.v === 6 ? `[${value}]` : value) + ' :' + row.port
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
field: 'user',
|
|
98
|
-
headerName: "User",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
field: 'started',
|
|
102
|
-
headerName: "Started",
|
|
103
|
-
type: 'dateTime',
|
|
104
|
-
width: 130,
|
|
105
|
-
valueFormatter: ({ value }) => new Date(value as string).toLocaleTimeString()
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
field: 'path',
|
|
109
|
-
headerName: "File",
|
|
110
|
-
flex: 1,
|
|
111
|
-
renderCell({ value, row }) {
|
|
112
|
-
if (!value) return
|
|
113
|
-
if (row.archive)
|
|
114
|
-
return h(Fragment, {},
|
|
115
|
-
h(FolderZip, { sx: { mr: 1 } }),
|
|
116
|
-
row.archive,
|
|
117
|
-
h(Box, { ml: 2, color: 'text.secondary' }, value)
|
|
118
|
-
)
|
|
119
|
-
const i = value?.lastIndexOf('/')
|
|
120
|
-
return h(Fragment, {}, value.slice(i + 1),
|
|
121
|
-
i > 0 && h(Box, { ml: 2, color: 'text.secondary' }, value.slice(0, i)))
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
field: 'v',
|
|
126
|
-
headerName: "Protocol",
|
|
127
|
-
align: 'center',
|
|
128
|
-
hide: true,
|
|
129
|
-
renderCell: ({ value, row }) => h(Fragment, {},
|
|
130
|
-
"IPv" + value,
|
|
131
|
-
row.secure && iconTooltip(Lock, "HTTPS", { opacity: .5 })
|
|
132
|
-
)
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
field: 'outSpeed',
|
|
136
|
-
headerName: "Speed",
|
|
137
|
-
type: 'number',
|
|
138
|
-
valueFormatter: ({ value }) => formatSpeed(value)
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
field: 'sent',
|
|
142
|
-
headerName: "Total",
|
|
143
|
-
type: 'number',
|
|
144
|
-
valueFormatter: ({ value }) => formatBytes(value as number)
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
field: "Actions",
|
|
148
|
-
width: 80,
|
|
149
|
-
align: 'center',
|
|
150
|
-
hideSortIcons: true,
|
|
151
|
-
disableColumnMenu: true,
|
|
152
|
-
renderCell({ row }) {
|
|
153
|
-
return h('div', {},
|
|
154
|
-
h(IconBtn, {
|
|
155
|
-
icon: Delete,
|
|
156
|
-
title: "Disconnect",
|
|
157
|
-
onClick: () => apiCall('disconnect', _.pick(row, ['ip', 'port'])),
|
|
158
|
-
}),
|
|
159
|
-
h(IconBtn, {
|
|
160
|
-
icon: Block,
|
|
161
|
-
title: "Block IP",
|
|
162
|
-
onClick: () => blockIp(row.ip),
|
|
163
|
-
}),
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
], [])
|
|
168
|
-
return h(Fragment, {},
|
|
169
|
-
h(Box, { display: 'flex', alignItems: 'center' },
|
|
170
|
-
h(SelectField as Field<boolean>, {
|
|
171
|
-
fullWidth: false,
|
|
172
|
-
value: filtered,
|
|
173
|
-
onChange: setFiltered as any,
|
|
174
|
-
options: { "Show only downloads": true, "Show all connections": false }
|
|
175
|
-
}),
|
|
176
|
-
|
|
177
|
-
h(Box, { flex: 1 }),
|
|
178
|
-
h(IconBtn, {
|
|
179
|
-
title: paused ? "Resume" : "Pause",
|
|
180
|
-
icon: paused ? PlayCircle : PauseCircle,
|
|
181
|
-
sx: { mr: 1 },
|
|
182
|
-
onClick() {
|
|
183
|
-
setPaused(!paused)
|
|
184
|
-
}
|
|
185
|
-
}),
|
|
186
|
-
),
|
|
187
|
-
error ? h(Alert, { severity: 'error' }, error)
|
|
188
|
-
: h(DataGrid, { rows, columns,
|
|
189
|
-
localeText: filtered ? { noRowsLabel: "No downloads at the moment" } : undefined,
|
|
190
|
-
})
|
|
191
|
-
)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function blockIp(ip: string) {
|
|
195
|
-
return manipulateConfig('block', data => [...data, { ip }])
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function formatSpeed(value: number) {
|
|
199
|
-
return !value ? '' : formatBytes(value * 1000, { post: "B/s", k: 1000, digits: 1 })
|
|
200
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { apiCall, useApiList } from './api'
|
|
2
|
-
import { Fragment, createElement as h, useState } from 'react'
|
|
3
|
-
import { DataGrid } from '@mui/x-data-grid'
|
|
4
|
-
import { IconBtn } from './misc'
|
|
5
|
-
import { Download, Search } from '@mui/icons-material'
|
|
6
|
-
import { confirmDialog } from './dialog'
|
|
7
|
-
import { StringField } from '@hfs/mui-grid-form'
|
|
8
|
-
import { useDebounce } from 'use-debounce'
|
|
9
|
-
import { repoLink, showError, startPlugin, UpdateButton } from './InstalledPlugins'
|
|
10
|
-
import { state, useSnapState } from './state'
|
|
11
|
-
import _ from 'lodash'
|
|
12
|
-
import md from './md'
|
|
13
|
-
|
|
14
|
-
export default function OnlinePlugins() {
|
|
15
|
-
const [search, setSearch] = useState('')
|
|
16
|
-
const [debouncedSearch] = useDebounce(search, 1000)
|
|
17
|
-
const { list, error, initializing, updateList } = useApiList('search_online_plugins', { text: debouncedSearch })
|
|
18
|
-
const snap = useSnapState()
|
|
19
|
-
if (error)
|
|
20
|
-
return showError(error)
|
|
21
|
-
return h(Fragment, {},
|
|
22
|
-
h(StringField, {
|
|
23
|
-
value: search,
|
|
24
|
-
onChange: setSearch as any,
|
|
25
|
-
start: h(Search),
|
|
26
|
-
typing: true,
|
|
27
|
-
label: "Search text"
|
|
28
|
-
}),
|
|
29
|
-
h(DataGrid, {
|
|
30
|
-
rows: list.length ? list : [], // workaround for DataGrid bug causing 'no rows' message to be not displayed after 'loading' was also used
|
|
31
|
-
localeText: { noRowsLabel: "No compatible plugins have been found" },
|
|
32
|
-
loading: initializing,
|
|
33
|
-
columnVisibilityModel: snap.onlinePluginsColumns,
|
|
34
|
-
onColumnVisibilityModelChange: newModel => Object.assign(state.onlinePluginsColumns, newModel),
|
|
35
|
-
columns: [
|
|
36
|
-
{
|
|
37
|
-
field: 'id',
|
|
38
|
-
headerName: "name",
|
|
39
|
-
flex: 1,
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
field: 'version',
|
|
43
|
-
width: 70,
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
field: 'pushed_at',
|
|
47
|
-
headerName: "last update",
|
|
48
|
-
valueGetter: ({ value }) => new Date(value).toLocaleDateString(),
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
field: 'license',
|
|
52
|
-
width: 80,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
field: 'description',
|
|
56
|
-
flex: 3,
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
field: 'stargazers_count',
|
|
60
|
-
width: 50,
|
|
61
|
-
headerName: "stars",
|
|
62
|
-
align: 'center',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
field: "actions",
|
|
66
|
-
width: 80,
|
|
67
|
-
align: 'center',
|
|
68
|
-
hideSortIcons: true,
|
|
69
|
-
disableColumnMenu: true,
|
|
70
|
-
hideable: false,
|
|
71
|
-
renderCell({ row }) {
|
|
72
|
-
const { id, branch } = row
|
|
73
|
-
return h('div', {},
|
|
74
|
-
repoLink(id),
|
|
75
|
-
row.update ? h(UpdateButton, {
|
|
76
|
-
id,
|
|
77
|
-
then() {
|
|
78
|
-
updateList(list =>
|
|
79
|
-
_.find(list, { id }).update = false )
|
|
80
|
-
}
|
|
81
|
-
}) : h(IconBtn, {
|
|
82
|
-
icon: Download,
|
|
83
|
-
title: "Install",
|
|
84
|
-
progress: row.downloading,
|
|
85
|
-
disabled: row.installed && "Already installed",
|
|
86
|
-
tooltipProps: { placement:'bottom-end' }, // workaround problem with horizontal scrolling by moving the tooltip leftward
|
|
87
|
-
confirm: "WARNING - Proceed only if you trust this author and this plugin",
|
|
88
|
-
async onClick() {
|
|
89
|
-
const { id: installedId } = await apiCall('download_plugin', { id, branch })
|
|
90
|
-
if (await confirmDialog(md(`Plugin /${id}/ installed.\nDo you want to start it now?`)))
|
|
91
|
-
await startPlugin(installedId)
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
]
|
|
98
|
-
})
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
|
package/admin/src/PermField.ts
DELETED
|
@@ -1,80 +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 { Dict, useStateMounted } from './misc'
|
|
4
|
-
import { createElement as h, Fragment } from 'react'
|
|
5
|
-
import { Button, Grid } from '@mui/material'
|
|
6
|
-
import { Field, FieldProps, SelectField } from '@hfs/mui-grid-form'
|
|
7
|
-
import _ from 'lodash'
|
|
8
|
-
import { useApiEx } from './api'
|
|
9
|
-
|
|
10
|
-
export default function PermField({ label, value, onChange }: FieldProps<Dict<string> | null> & { keyLabel:string }) {
|
|
11
|
-
const [temp, setTemp] = useStateMounted<string|undefined>(undefined)
|
|
12
|
-
const { data, element } = useApiEx('get_usernames')
|
|
13
|
-
const usernames = data?.list || []
|
|
14
|
-
|
|
15
|
-
const permOptions = [{ label:'read', value:'r' }, { label:'none', value:'' }]
|
|
16
|
-
const usernamesLeft = _.difference(usernames, Object.keys(value||{}))
|
|
17
|
-
|
|
18
|
-
return h(Grid, { container: true },
|
|
19
|
-
label && h(Grid, { item: true, xs: 12, pl: 2, py: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between' },
|
|
20
|
-
label,
|
|
21
|
-
!element && h(Button, {
|
|
22
|
-
onClick(event){
|
|
23
|
-
setTemp(undefined)
|
|
24
|
-
onChange(null, { event, was: value })
|
|
25
|
-
}
|
|
26
|
-
}, 'Clear')),
|
|
27
|
-
element || h(Fragment, {},
|
|
28
|
-
// existing entries
|
|
29
|
-
Object.entries(value||{}).map(([username, perm]) => [
|
|
30
|
-
h(Grid, { key:'k', item: true, xs: 6 },
|
|
31
|
-
h(SelectField as Field<string>, {
|
|
32
|
-
label: 'Username',
|
|
33
|
-
options: usernames,
|
|
34
|
-
value: username,
|
|
35
|
-
onChange(v, { was, ...rest }){
|
|
36
|
-
const copy: any = { ...value, [v]: value![was!] }
|
|
37
|
-
delete copy[was!]
|
|
38
|
-
onChange(copy, { was:value, ...rest })
|
|
39
|
-
}
|
|
40
|
-
})),
|
|
41
|
-
h(Grid, { key:'v', item: true, xs: 6 },
|
|
42
|
-
h(SelectField as Field<string>, {
|
|
43
|
-
label: 'Access',
|
|
44
|
-
options: permOptions,
|
|
45
|
-
value: perm,
|
|
46
|
-
onChange(v, { was, ...rest }){
|
|
47
|
-
const copy = { ...value }
|
|
48
|
-
if (v)
|
|
49
|
-
copy[username] = v
|
|
50
|
-
else
|
|
51
|
-
delete copy[username]
|
|
52
|
-
onChange( _.isEmpty(copy) ? null : copy, { was:value, ...rest })
|
|
53
|
-
}
|
|
54
|
-
})),
|
|
55
|
-
]),
|
|
56
|
-
// row for new entries
|
|
57
|
-
!usernamesLeft.length && h(Grid, { item: true, xs: 12, py: 1, px: 2, color:'text.secondary' }, "No accounts left"),
|
|
58
|
-
usernamesLeft.length>0 && h(Grid, { item: true, xs: 6 },
|
|
59
|
-
h(SelectField as Field<string>, {
|
|
60
|
-
label: value ? "Add access to" : "Restrict access to ",
|
|
61
|
-
value: temp,
|
|
62
|
-
options: usernamesLeft,
|
|
63
|
-
onChange: setTemp as any,
|
|
64
|
-
})),
|
|
65
|
-
usernamesLeft.length>0 && h(Grid, { item: true, xs: 6 },
|
|
66
|
-
h(SelectField as Field<string>, {
|
|
67
|
-
value: undefined,
|
|
68
|
-
label: temp && "Select access type",
|
|
69
|
-
disabled: !temp,
|
|
70
|
-
options: permOptions,
|
|
71
|
-
onChange(v, rest) {
|
|
72
|
-
if (v)
|
|
73
|
-
onChange({ ...value, [temp!]: v }, { ...rest, was: value })
|
|
74
|
-
setTemp(undefined)
|
|
75
|
-
}
|
|
76
|
-
}))
|
|
77
|
-
)
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
package/admin/src/PluginsPage.ts
DELETED
|
@@ -1,27 +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, useState } from "react"
|
|
4
|
-
import { Tab, Tabs } from '@mui/material'
|
|
5
|
-
import InstalledPlugins from "./InstalledPlugins"
|
|
6
|
-
import OnlinePlugins from "./OnlinePlugins"
|
|
7
|
-
|
|
8
|
-
const TABS = {
|
|
9
|
-
"Installed": InstalledPlugins,
|
|
10
|
-
"Search online": OnlinePlugins,
|
|
11
|
-
"Check updates": () => h(InstalledPlugins, { updates: true }),
|
|
12
|
-
}
|
|
13
|
-
const LABELS = Object.keys(TABS)
|
|
14
|
-
const PANES = Object.values(TABS)
|
|
15
|
-
|
|
16
|
-
export default function PluginsPage() {
|
|
17
|
-
const [tab, setTab] = useState(0)
|
|
18
|
-
return h(Fragment, {},
|
|
19
|
-
h(Tabs, {
|
|
20
|
-
value: tab,
|
|
21
|
-
onChange(ev, i) {
|
|
22
|
-
setTab(i)
|
|
23
|
-
}
|
|
24
|
-
}, LABELS.map(label => h(Tab, { label, key: label })) ),
|
|
25
|
-
h(PANES[tab])
|
|
26
|
-
)
|
|
27
|
-
}
|
package/admin/src/VfsMenuBar.ts
DELETED
|
@@ -1,58 +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, useSnapState } from './state'
|
|
4
|
-
import { createElement as h } from 'react'
|
|
5
|
-
import { Box, Button } from '@mui/material'
|
|
6
|
-
import { Add, Delete, Refresh } from '@mui/icons-material'
|
|
7
|
-
import { alertDialog, confirmDialog } from './dialog'
|
|
8
|
-
import { apiCall } from './api'
|
|
9
|
-
import { reloadVfs } from './VfsPage'
|
|
10
|
-
import addFiles, { addVirtual } from './addFiles'
|
|
11
|
-
import MenuButton from './MenuButton'
|
|
12
|
-
import { IconBtn } from './misc'
|
|
13
|
-
|
|
14
|
-
export default function VfsMenuBar() {
|
|
15
|
-
const { selectedFiles } = useSnapState()
|
|
16
|
-
return h(Box, {
|
|
17
|
-
display: 'flex',
|
|
18
|
-
gap: 2,
|
|
19
|
-
mb: 2,
|
|
20
|
-
sx: {
|
|
21
|
-
position: 'sticky',
|
|
22
|
-
top: 0,
|
|
23
|
-
zIndex: 2,
|
|
24
|
-
backgroundColor: 'background.paper',
|
|
25
|
-
width: 'fit-content',
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
h(MenuButton, {
|
|
29
|
-
variant: 'contained',
|
|
30
|
-
startIcon: h(Add),
|
|
31
|
-
items: [
|
|
32
|
-
{ children: "from disk", onClick: addFiles },
|
|
33
|
-
{ children: "virtual folder", onClick: addVirtual }
|
|
34
|
-
]
|
|
35
|
-
}, "Add"),
|
|
36
|
-
h(Button, { onClick: removeFiles, disabled: !selectedFiles.length, startIcon: h(Delete) }, "Remove"),
|
|
37
|
-
h(IconBtn, { icon: Refresh, title: "Reload", onClick(){ reloadVfs() } }),
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function removeFiles() {
|
|
42
|
-
const f = state.selectedFiles
|
|
43
|
-
if (!f.length) return
|
|
44
|
-
if (await confirmDialog(`Remove ${f.length} item(s)?`)) {
|
|
45
|
-
try {
|
|
46
|
-
const uris = f.map(x => x.id)
|
|
47
|
-
const { errors } = await apiCall('del_vfs', { uris })
|
|
48
|
-
const urisThatFailed = uris.filter((uri, idx) => errors[idx])
|
|
49
|
-
if (urisThatFailed.length)
|
|
50
|
-
return alertDialog("Following elements couldn't be removed: " + urisThatFailed.join(', '), 'error')
|
|
51
|
-
reloadVfs()
|
|
52
|
-
}
|
|
53
|
-
catch(e) {
|
|
54
|
-
await alertDialog(e as Error)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
}
|
package/admin/src/VfsPage.ts
DELETED
|
@@ -1,124 +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, useEffect, useMemo, useState } from 'react'
|
|
4
|
-
import { useApi, useApiEx } from './api'
|
|
5
|
-
import { Alert, Grid, Link, List, ListItem, ListItemText, Typography } from '@mui/material'
|
|
6
|
-
import { state, useSnapState } from './state'
|
|
7
|
-
import VfsMenuBar from './VfsMenuBar'
|
|
8
|
-
import VfsTree from './VfsTree'
|
|
9
|
-
import { onlyTruthy, prefix } from './misc'
|
|
10
|
-
import { reactJoin } from '@hfs/shared'
|
|
11
|
-
import _ from 'lodash'
|
|
12
|
-
import { AlertProps } from '@mui/material/Alert/Alert'
|
|
13
|
-
import FileForm from './FileForm'
|
|
14
|
-
|
|
15
|
-
let selectOnReload: string[] | undefined
|
|
16
|
-
|
|
17
|
-
export default function VfsPage() {
|
|
18
|
-
const [id2node] = useState(() => new Map<string, VfsNode>())
|
|
19
|
-
const snap = useSnapState()
|
|
20
|
-
const { data, reload, element } = useApiEx('get_vfs')
|
|
21
|
-
useMemo(() => snap.vfs || reload(), [snap.vfs, reload])
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
state.vfs = undefined
|
|
24
|
-
if (!data) return
|
|
25
|
-
// rebuild id2node
|
|
26
|
-
id2node.clear()
|
|
27
|
-
const { root } = data
|
|
28
|
-
if (!root) return
|
|
29
|
-
recur(root) // this must be done before state change that would cause Tree to render and expecting id2node
|
|
30
|
-
root.isRoot = true
|
|
31
|
-
state.vfs = root
|
|
32
|
-
// refresh objects of selectedFiles
|
|
33
|
-
const ids = selectOnReload || state.selectedFiles.map(x => x.id)
|
|
34
|
-
selectOnReload = undefined
|
|
35
|
-
state.selectedFiles = onlyTruthy(ids.map(id =>
|
|
36
|
-
id2node.get(id)))
|
|
37
|
-
|
|
38
|
-
// calculate id and parent fields, and builds the map id2node
|
|
39
|
-
function recur(node: VfsNode, pre='', parent: VfsNode|undefined=undefined) {
|
|
40
|
-
node.parent = parent
|
|
41
|
-
node.id = prefix(pre, node.name) || '/' // root
|
|
42
|
-
id2node.set(node.id, node)
|
|
43
|
-
if (!node.children) return
|
|
44
|
-
for (const n of node.children)
|
|
45
|
-
recur(n, (pre && node.id) + '/', node)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
}, [data, id2node])
|
|
49
|
-
const [status] = useApi(window.location.host === 'localhost' && 'get_status')
|
|
50
|
-
const urls = useMemo(() =>
|
|
51
|
-
typeof status === 'object'
|
|
52
|
-
&& _.sortBy(
|
|
53
|
-
onlyTruthy(Object.values(status.urls?.https || status.urls?.http || {}).map(u => typeof u === 'string' && u)),
|
|
54
|
-
url => url.includes('[')
|
|
55
|
-
),
|
|
56
|
-
[status])
|
|
57
|
-
if (element) {
|
|
58
|
-
id2node.clear()
|
|
59
|
-
return element
|
|
60
|
-
}
|
|
61
|
-
const anythingShared = !data?.root?.children?.length && !data?.root?.source
|
|
62
|
-
const alert: AlertProps | false = anythingShared ? {
|
|
63
|
-
severity: 'warning',
|
|
64
|
-
children: "Add something to your shared files — click Add"
|
|
65
|
-
} : urls && {
|
|
66
|
-
severity: 'info',
|
|
67
|
-
children: [
|
|
68
|
-
"Your shared files can be browsed from ",
|
|
69
|
-
reactJoin(" or ", urls.slice(0,3).map(href => h(Link, { href }, href)))
|
|
70
|
-
]
|
|
71
|
-
}
|
|
72
|
-
return h(Grid, { container:true, rowSpacing: 1, maxWidth: '80em', columnSpacing: 2 },
|
|
73
|
-
alert && h(Grid, { item: true, mb: 2, xs: 12 }, h(Alert, alert)),
|
|
74
|
-
h(Grid, { item:true, sm: 6, lg: 5 },
|
|
75
|
-
h(Typography, { variant: 'h6', mb:1, }, "Virtual File System"),
|
|
76
|
-
h(VfsMenuBar),
|
|
77
|
-
snap.vfs && h(VfsTree, { id2node })),
|
|
78
|
-
h(Grid, { item:true, sm: 6, lg: 7, maxWidth:'100%' },
|
|
79
|
-
h(SidePanel))
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function SidePanel() {
|
|
84
|
-
const { selectedFiles: files } = useSnapState()
|
|
85
|
-
return files.length === 0 ? null
|
|
86
|
-
: files.length === 1 ? h(FileForm, { file: files[0] as VfsNode }) // it's actually Snapshot<VfsNode> but it's easier this way
|
|
87
|
-
: h(List, {},
|
|
88
|
-
files.length + ' selected',
|
|
89
|
-
files.map(f => h(ListItem, { key: f.name },
|
|
90
|
-
h(ListItemText, { primary: f.name, secondary: f.source }) )))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function reloadVfs(pleaseSelect?: string[]) {
|
|
94
|
-
selectOnReload = pleaseSelect
|
|
95
|
-
state.vfs = undefined
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export type VfsNode = {
|
|
99
|
-
id: string
|
|
100
|
-
name: string
|
|
101
|
-
type?: 'folder'
|
|
102
|
-
source?: string
|
|
103
|
-
size?: number
|
|
104
|
-
ctime?: string
|
|
105
|
-
mtime?: string
|
|
106
|
-
default?: string
|
|
107
|
-
children?: VfsNode[]
|
|
108
|
-
parent?: VfsNode
|
|
109
|
-
can_see: Who
|
|
110
|
-
can_read: Who
|
|
111
|
-
website?: true
|
|
112
|
-
masks?: any
|
|
113
|
-
|
|
114
|
-
isRoot?: true
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const WHO_ANYONE = true
|
|
118
|
-
const WHO_NO_ONE = false
|
|
119
|
-
const WHO_ANY_ACCOUNT = '*'
|
|
120
|
-
type AccountList = string[]
|
|
121
|
-
export type Who = typeof WHO_ANYONE
|
|
122
|
-
| typeof WHO_NO_ONE
|
|
123
|
-
| typeof WHO_ANY_ACCOUNT
|
|
124
|
-
| AccountList
|
package/admin/src/VfsTree.ts
DELETED
|
@@ -1,95 +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, useSnapState } from './state'
|
|
4
|
-
import { createElement as h, ReactElement, useState } from 'react'
|
|
5
|
-
import { TreeItem, TreeView } from '@mui/lab'
|
|
6
|
-
import {
|
|
7
|
-
ChevronRight,
|
|
8
|
-
ExpandMore,
|
|
9
|
-
TheaterComedy,
|
|
10
|
-
Folder,
|
|
11
|
-
Home,
|
|
12
|
-
InsertDriveFileOutlined,
|
|
13
|
-
Lock,
|
|
14
|
-
RemoveRedEye,
|
|
15
|
-
Web
|
|
16
|
-
} from '@mui/icons-material'
|
|
17
|
-
import { Box } from '@mui/material'
|
|
18
|
-
import { VfsNode, Who } from './VfsPage'
|
|
19
|
-
import { iconTooltip, isWindowsDrive, onlyTruthy } from './misc'
|
|
20
|
-
|
|
21
|
-
export const FolderIcon = Folder
|
|
22
|
-
export const FileIcon = InsertDriveFileOutlined
|
|
23
|
-
|
|
24
|
-
export default function VfsTree({ id2node }:{ id2node: Map<string, VfsNode> }) {
|
|
25
|
-
const { vfs, selectedFiles } = useSnapState()
|
|
26
|
-
const [selected, setSelected] = useState<string[]>(selectedFiles.map(x => x.id)) // try to restore selection after reload
|
|
27
|
-
const [expanded, setExpanded] = useState(Array.from(id2node.keys()))
|
|
28
|
-
if (!vfs)
|
|
29
|
-
return null
|
|
30
|
-
return h(TreeView, {
|
|
31
|
-
expanded,
|
|
32
|
-
selected,
|
|
33
|
-
multiSelect: true,
|
|
34
|
-
sx: { overflowX: 'auto' },
|
|
35
|
-
onNodeSelect(ev, ids) {
|
|
36
|
-
setSelected(ids)
|
|
37
|
-
state.selectedFiles = onlyTruthy(ids.map(id => id2node.get(id)))
|
|
38
|
-
}
|
|
39
|
-
}, recur(vfs as Readonly<VfsNode>))
|
|
40
|
-
|
|
41
|
-
function isRestricted(who: Who) {
|
|
42
|
-
return who !== undefined && who !== true
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function recur(node: Readonly<VfsNode>): ReactElement {
|
|
46
|
-
let { id, name, source, isRoot } = node
|
|
47
|
-
if (!id)
|
|
48
|
-
debugger
|
|
49
|
-
const folder = node.type === 'folder'
|
|
50
|
-
if (folder && !isWindowsDrive(source) && source === name) // we need a way to show that the name we are displaying is a source in this ambiguous case, so we add a redundant ./
|
|
51
|
-
source = './' + source
|
|
52
|
-
return h(TreeItem, {
|
|
53
|
-
label: h(Box, {
|
|
54
|
-
sx: {
|
|
55
|
-
display: 'flex',
|
|
56
|
-
gap: '.5em',
|
|
57
|
-
lineHeight: '2em',
|
|
58
|
-
alignItems: 'center',
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
isRoot ? iconTooltip(Home, "home, or root if you like")
|
|
62
|
-
: folder ? iconTooltip(FolderIcon, "Folder")
|
|
63
|
-
: iconTooltip(FileIcon, "File"),
|
|
64
|
-
isRestricted(node.can_see) && iconTooltip(RemoveRedEye, "Restrictions on who can see"),
|
|
65
|
-
isRestricted(node.can_read) && iconTooltip(Lock, "Restrictions on who can download"),
|
|
66
|
-
node.default && iconTooltip(Web, "Act as website"),
|
|
67
|
-
node.masks && iconTooltip(TheaterComedy, "Masks"),
|
|
68
|
-
isRoot ? "Home"
|
|
69
|
-
// special rendering if the whole source is not too long, and the name was not customized
|
|
70
|
-
: source?.length! < 45 && source?.endsWith(name) ? h('span', {},
|
|
71
|
-
h('span', { style: { opacity: .4 } }, source.slice(0,-name.length)),
|
|
72
|
-
h('span', {}, source.slice(-name.length)),
|
|
73
|
-
)
|
|
74
|
-
: name
|
|
75
|
-
),
|
|
76
|
-
key: name,
|
|
77
|
-
collapseIcon: h(ExpandMore, {
|
|
78
|
-
onClick(ev) {
|
|
79
|
-
setExpanded( expanded.filter(x => x !== id) )
|
|
80
|
-
ev.preventDefault()
|
|
81
|
-
ev.stopPropagation()
|
|
82
|
-
}
|
|
83
|
-
}),
|
|
84
|
-
expandIcon: h(ChevronRight, {
|
|
85
|
-
onClick(ev) {
|
|
86
|
-
setExpanded( [...expanded, id] )
|
|
87
|
-
ev.preventDefault()
|
|
88
|
-
ev.stopPropagation()
|
|
89
|
-
}
|
|
90
|
-
}),
|
|
91
|
-
nodeId: id
|
|
92
|
-
}, isRoot && !node.children?.length ? h('i', {}, "nothing here") : node.children?.map(recur))
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
}
|