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