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.
Files changed (163) hide show
  1. package/README.md +15 -2
  2. package/admin/assets/index-509bb1d6.js +415 -0
  3. package/admin/assets/index-60a380a7.css +1 -0
  4. package/admin/assets/sha512-738f0943.js +8 -0
  5. package/admin/index.html +3 -1
  6. package/admin/{public/logo.svg → logo.svg} +0 -0
  7. package/frontend/assets/index-6e178dfd.css +1 -0
  8. package/frontend/assets/index-aea7654e.js +85 -0
  9. package/frontend/assets/sha512-bf915587.js +8 -0
  10. package/frontend/{public/fontello.css → fontello.css} +0 -0
  11. package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
  12. package/frontend/index.html +4 -2
  13. package/package.json +2 -6
  14. package/plugins/vhosting/plugin.js +23 -20
  15. package/src/QuickZipStream.js +285 -0
  16. package/src/ThrottledStream.js +93 -0
  17. package/src/adminApis.js +169 -0
  18. package/src/api.accounts.js +59 -0
  19. package/src/api.auth.js +128 -0
  20. package/src/api.file_list.js +110 -0
  21. package/src/api.helpers.js +32 -0
  22. package/src/api.monitor.js +104 -0
  23. package/src/api.plugins.js +128 -0
  24. package/src/api.vfs.js +167 -0
  25. package/src/apiMiddleware.js +123 -0
  26. package/src/block.js +34 -0
  27. package/src/commands.js +125 -0
  28. package/src/config.js +168 -0
  29. package/src/connections.js +57 -0
  30. package/src/const.js +94 -0
  31. package/src/crypt.js +21 -0
  32. package/src/debounceAsync.js +49 -0
  33. package/src/events.js +9 -0
  34. package/src/frontEndApis.js +38 -0
  35. package/src/github.js +104 -0
  36. package/src/index.js +57 -0
  37. package/src/listen.js +235 -0
  38. package/src/log.js +137 -0
  39. package/src/middlewares.js +195 -0
  40. package/src/misc.js +160 -0
  41. package/src/pbkdf2.js +74 -0
  42. package/src/perm.js +183 -0
  43. package/src/plugins.js +343 -0
  44. package/src/serveFile.js +105 -0
  45. package/src/serveGuiFiles.js +113 -0
  46. package/src/sse.js +30 -0
  47. package/src/throttler.js +91 -0
  48. package/src/update.js +70 -0
  49. package/src/util-files.js +163 -0
  50. package/src/util-generators.js +31 -0
  51. package/src/util-http.js +32 -0
  52. package/src/vfs.js +232 -0
  53. package/src/watchLoad.js +73 -0
  54. package/src/zip.js +73 -0
  55. package/admin/.DS_Store +0 -0
  56. package/admin/.eslintrc +0 -8
  57. package/admin/.gitignore +0 -23
  58. package/admin/package.json +0 -67
  59. package/admin/src/AccountForm.ts +0 -92
  60. package/admin/src/AccountsPage.ts +0 -143
  61. package/admin/src/App.ts +0 -83
  62. package/admin/src/ArrayField.ts +0 -84
  63. package/admin/src/ConfigPage.ts +0 -279
  64. package/admin/src/FileField.ts +0 -52
  65. package/admin/src/FileForm.ts +0 -148
  66. package/admin/src/FilePicker.ts +0 -166
  67. package/admin/src/HomePage.ts +0 -96
  68. package/admin/src/InstalledPlugins.ts +0 -158
  69. package/admin/src/LoginRequired.ts +0 -75
  70. package/admin/src/LogoutPage.ts +0 -27
  71. package/admin/src/LogsPage.ts +0 -75
  72. package/admin/src/MainMenu.ts +0 -74
  73. package/admin/src/MenuButton.ts +0 -38
  74. package/admin/src/MonitorPage.ts +0 -200
  75. package/admin/src/OnlinePlugins.ts +0 -101
  76. package/admin/src/PermField.ts +0 -80
  77. package/admin/src/PluginsPage.ts +0 -27
  78. package/admin/src/VfsMenuBar.ts +0 -58
  79. package/admin/src/VfsPage.ts +0 -124
  80. package/admin/src/VfsTree.ts +0 -95
  81. package/admin/src/addFiles.ts +0 -59
  82. package/admin/src/api.ts +0 -246
  83. package/admin/src/dialog.ts +0 -203
  84. package/admin/src/index.css +0 -21
  85. package/admin/src/index.ts +0 -10
  86. package/admin/src/md.ts +0 -31
  87. package/admin/src/misc.ts +0 -141
  88. package/admin/src/react-app-env.d.ts +0 -1
  89. package/admin/src/reportWebVitals.ts +0 -15
  90. package/admin/src/setupTests.ts +0 -5
  91. package/admin/src/state.ts +0 -40
  92. package/admin/src/theme.ts +0 -37
  93. package/admin/tsconfig.json +0 -26
  94. package/admin/vite.config.ts +0 -32
  95. package/frontend/.DS_Store +0 -0
  96. package/frontend/.eslintrc +0 -8
  97. package/frontend/.gitignore +0 -23
  98. package/frontend/package.json +0 -51
  99. package/frontend/src/App.ts +0 -25
  100. package/frontend/src/Breadcrumbs.ts +0 -43
  101. package/frontend/src/BrowseFiles.ts +0 -141
  102. package/frontend/src/Head.ts +0 -45
  103. package/frontend/src/UserPanel.ts +0 -52
  104. package/frontend/src/api.ts +0 -78
  105. package/frontend/src/components.ts +0 -54
  106. package/frontend/src/dialog.css +0 -76
  107. package/frontend/src/dialog.ts +0 -105
  108. package/frontend/src/icons.ts +0 -46
  109. package/frontend/src/index.scss +0 -307
  110. package/frontend/src/index.ts +0 -10
  111. package/frontend/src/login.ts +0 -50
  112. package/frontend/src/menu.ts +0 -188
  113. package/frontend/src/misc.ts +0 -54
  114. package/frontend/src/options.ts +0 -52
  115. package/frontend/src/react-app-env.d.ts +0 -1
  116. package/frontend/src/reportWebVitals.ts +0 -15
  117. package/frontend/src/setupTests.ts +0 -5
  118. package/frontend/src/state.ts +0 -82
  119. package/frontend/src/useAuthorized.ts +0 -17
  120. package/frontend/src/useFetchList.ts +0 -144
  121. package/frontend/src/useTheme.ts +0 -23
  122. package/frontend/tsconfig.json +0 -26
  123. package/frontend/vite.config.ts +0 -21
  124. package/src/QuickZipStream.ts +0 -279
  125. package/src/ThrottledStream.ts +0 -98
  126. package/src/adminApis.ts +0 -161
  127. package/src/api.accounts.ts +0 -78
  128. package/src/api.auth.ts +0 -131
  129. package/src/api.file_list.ts +0 -102
  130. package/src/api.helpers.ts +0 -30
  131. package/src/api.monitor.ts +0 -106
  132. package/src/api.plugins.ts +0 -139
  133. package/src/api.vfs.ts +0 -182
  134. package/src/apiMiddleware.ts +0 -124
  135. package/src/block.ts +0 -35
  136. package/src/commands.ts +0 -122
  137. package/src/config.ts +0 -166
  138. package/src/connections.ts +0 -60
  139. package/src/const.ts +0 -57
  140. package/src/crypt.ts +0 -16
  141. package/src/debounceAsync.ts +0 -51
  142. package/src/events.ts +0 -6
  143. package/src/frontEndApis.ts +0 -17
  144. package/src/github.ts +0 -102
  145. package/src/index.ts +0 -53
  146. package/src/listen.ts +0 -220
  147. package/src/log.ts +0 -128
  148. package/src/middlewares.ts +0 -176
  149. package/src/misc.ts +0 -149
  150. package/src/pbkdf2.ts +0 -83
  151. package/src/perm.ts +0 -194
  152. package/src/plugins.ts +0 -342
  153. package/src/serveFile.ts +0 -104
  154. package/src/serveGuiFiles.ts +0 -95
  155. package/src/sse.ts +0 -29
  156. package/src/throttler.ts +0 -106
  157. package/src/update.ts +0 -67
  158. package/src/util-files.ts +0 -137
  159. package/src/util-generators.ts +0 -29
  160. package/src/util-http.ts +0 -29
  161. package/src/vfs.ts +0 -258
  162. package/src/watchLoad.ts +0 -75
  163. package/src/zip.ts +0 -69
@@ -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
-
@@ -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
-
@@ -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
- }
@@ -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
- }
@@ -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
@@ -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
- }