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/misc.ts
DELETED
|
@@ -1,141 +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, FC, ReactNode } from 'react'
|
|
4
|
-
import { Box, Breakpoint, CircularProgress, IconButton, Link, Tooltip, useMediaQuery } from '@mui/material'
|
|
5
|
-
import { Link as RouterLink } from 'react-router-dom'
|
|
6
|
-
import { SxProps } from '@mui/system'
|
|
7
|
-
import { SvgIconComponent } from '@mui/icons-material'
|
|
8
|
-
import { alertDialog, confirmDialog } from './dialog'
|
|
9
|
-
import { apiCall } from './api'
|
|
10
|
-
import { onlyTruthy, useStateMounted } from '@hfs/shared'
|
|
11
|
-
export * from '@hfs/shared'
|
|
12
|
-
|
|
13
|
-
export function spinner() {
|
|
14
|
-
return h(CircularProgress)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function isWindowsDrive(s?: string) {
|
|
18
|
-
return s && /^[a-zA-Z]:$/.test(s)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function isEqualLax(a: any,b: any): boolean {
|
|
22
|
-
return a == b //eslint-disable-line
|
|
23
|
-
|| (a && b && typeof a === 'object' && typeof b === 'object'
|
|
24
|
-
&& Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function modifiedSx(is: boolean) {
|
|
28
|
-
return is ? { outline: '2px solid' } : undefined
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface IconBtnProps {
|
|
32
|
-
title?: ReactNode
|
|
33
|
-
icon: SvgIconComponent
|
|
34
|
-
disabled?: boolean | string
|
|
35
|
-
progress?: boolean | number
|
|
36
|
-
link?: string
|
|
37
|
-
confirm?: string
|
|
38
|
-
[rest: string]: any
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function IconBtn({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: IconBtnProps) {
|
|
42
|
-
const [loading, setLoading] = useStateMounted(false)
|
|
43
|
-
if (typeof disabled === 'string')
|
|
44
|
-
title = disabled
|
|
45
|
-
if (link)
|
|
46
|
-
onClick = () => window.open(link)
|
|
47
|
-
let ret: ReturnType<FC> = h(IconButton, {
|
|
48
|
-
disabled: Boolean(loading || progress || disabled),
|
|
49
|
-
...rest,
|
|
50
|
-
async onClick() {
|
|
51
|
-
if (confirm && !await confirmDialog(confirm)) return
|
|
52
|
-
const ret = onClick?.apply(this,arguments)
|
|
53
|
-
if (ret && ret instanceof Promise) {
|
|
54
|
-
setLoading(true)
|
|
55
|
-
ret.catch(alertDialog).finally(()=> setLoading(false))
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}, h(icon))
|
|
59
|
-
if ((progress || loading) && progress !== false) // false is also useful to inhibit behavior with loading
|
|
60
|
-
ret = h(Box, { position:'relative', display: 'inline-block' },
|
|
61
|
-
h(CircularProgress, {
|
|
62
|
-
...(typeof progress === 'number' ? { value: progress*100, variant: 'determinate' } : null),
|
|
63
|
-
style: { position:'absolute', top: 4, left: 4, width: 32, height: 32 }
|
|
64
|
-
}),
|
|
65
|
-
ret
|
|
66
|
-
)
|
|
67
|
-
if (title)
|
|
68
|
-
ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
|
|
69
|
-
return ret
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function iconTooltip(icon: SvgIconComponent, tooltip: string, sx?: SxProps) {
|
|
73
|
-
return h(Tooltip, { title: tooltip, children: h(icon, { sx }) })
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function InLink(props:any) {
|
|
77
|
-
return h(Link, { component: RouterLink, ...props })
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function Center(props: any) {
|
|
81
|
-
return h(Box, { display:'flex', height:'100%', width:'100%', justifyContent:'center', alignItems:'center', ...props })
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function manipulateConfig(k: string, work:(data:any) => any) {
|
|
85
|
-
const cfg = await apiCall('get_config', { only: [k] })
|
|
86
|
-
const was = cfg[k]
|
|
87
|
-
const will = await work(was)
|
|
88
|
-
if (JSON.stringify(was) !== JSON.stringify(will))
|
|
89
|
-
await apiCall('set_config', { values: { [k]: will } })
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function typedKeys<T extends {}>(o: T) {
|
|
93
|
-
return Object.keys(o) as (keyof T)[]
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function dirname(s: string) {
|
|
97
|
-
let i = s.lastIndexOf('/')
|
|
98
|
-
if (i < 0)
|
|
99
|
-
i = s.lastIndexOf('\\')
|
|
100
|
-
return i < 0 ? '' : s.slice(0, i)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function isAbsolutePath(s: string) {
|
|
104
|
-
return s && (s[0] === '/' || isWindowsDrive(s.slice(0,2)))
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function pathJoin(...args: any[]) {
|
|
108
|
-
const delimiter = findFirst(args, x => /\\|\//.exec('\\a/b')?.[0])
|
|
109
|
-
const good = onlyTruthy(args.map(x => x == null ? '' : String(x)))
|
|
110
|
-
return good.map((x, i) => i === good.length-1 || x.endsWith('\\') || x.endsWith('/') ? x : x + delimiter)
|
|
111
|
-
.join('')
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function findFirst<I=any, O=any>(a: I[], cb:(v:I)=>O): any {
|
|
115
|
-
for (const x of a) {
|
|
116
|
-
const ret = cb(x)
|
|
117
|
-
if (ret !== undefined)
|
|
118
|
-
return ret
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function xlate(input: any, table: Record<string, any>) {
|
|
123
|
-
return table[input] ?? input
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// return true if same size or larger
|
|
127
|
-
export function useBreakpoint(breakpoint: Breakpoint) {
|
|
128
|
-
return useMediaQuery((theme: any) => theme.breakpoints.up(breakpoint), { noSsr:true }) // without noSsr, first execution always returns false
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function err2msg(code: string) {
|
|
132
|
-
return {
|
|
133
|
-
ENOENT: "Not found",
|
|
134
|
-
ENOTDIR: "Not a folder",
|
|
135
|
-
}[code] || code
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function wantArray<T>(x?: void | T | T[]) {
|
|
139
|
-
return x == null ? [] : Array.isArray(x) ? x : [x]
|
|
140
|
-
}
|
|
141
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/// <reference types="react-scripts" />
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { ReportHandler } from 'web-vitals';
|
|
2
|
-
|
|
3
|
-
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
4
|
-
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
5
|
-
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
6
|
-
getCLS(onPerfEntry);
|
|
7
|
-
getFID(onPerfEntry);
|
|
8
|
-
getFCP(onPerfEntry);
|
|
9
|
-
getLCP(onPerfEntry);
|
|
10
|
-
getTTFB(onPerfEntry);
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export default reportWebVitals;
|
package/admin/src/setupTests.ts
DELETED
package/admin/src/state.ts
DELETED
|
@@ -1,40 +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 { proxy, useSnapshot } from 'valtio'
|
|
4
|
-
import { Dict } from './misc'
|
|
5
|
-
import { VfsNode } from './VfsPage'
|
|
6
|
-
import _ from 'lodash'
|
|
7
|
-
import { subscribeKey } from 'valtio/utils'
|
|
8
|
-
|
|
9
|
-
const STORAGE_KEY = 'admin_state'
|
|
10
|
-
export const state = proxy<{
|
|
11
|
-
title: string
|
|
12
|
-
config: Dict
|
|
13
|
-
vfs: VfsNode | undefined
|
|
14
|
-
selectedFiles: VfsNode[]
|
|
15
|
-
loginRequired: boolean
|
|
16
|
-
username: string
|
|
17
|
-
onlinePluginsColumns: Dict<boolean>
|
|
18
|
-
}>(Object.assign({
|
|
19
|
-
title: '',
|
|
20
|
-
config: {},
|
|
21
|
-
selectedFiles: [],
|
|
22
|
-
vfs: undefined,
|
|
23
|
-
loginRequired: false,
|
|
24
|
-
username: '',
|
|
25
|
-
onlinePluginsColumns: {
|
|
26
|
-
version: false,
|
|
27
|
-
pushed_at: false,
|
|
28
|
-
license: false,
|
|
29
|
-
}
|
|
30
|
-
}, JSON.parse(localStorage[STORAGE_KEY]||null)))
|
|
31
|
-
|
|
32
|
-
const SETTINGS_TO_STORE: (keyof typeof state)[] = ['onlinePluginsColumns']
|
|
33
|
-
const storeSettings = _.debounce(() =>
|
|
34
|
-
localStorage[STORAGE_KEY] = JSON.stringify(_.pick(state, SETTINGS_TO_STORE)), 500, { maxWait: 1000 })
|
|
35
|
-
for (const k of SETTINGS_TO_STORE)
|
|
36
|
-
subscribeKey(state, k, storeSettings)
|
|
37
|
-
|
|
38
|
-
export function useSnapState() {
|
|
39
|
-
return useSnapshot(state)
|
|
40
|
-
}
|
package/admin/src/theme.ts
DELETED
|
@@ -1,37 +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 { createTheme, useMediaQuery } from '@mui/material'
|
|
4
|
-
import { useMemo } from 'react'
|
|
5
|
-
|
|
6
|
-
const EMPTY = {}
|
|
7
|
-
export function useMyTheme() {
|
|
8
|
-
const lightMode = useMediaQuery('(prefers-color-scheme: dark)') ? null : EMPTY
|
|
9
|
-
return useMemo(() => createTheme({
|
|
10
|
-
palette: lightMode || {
|
|
11
|
-
mode: 'dark',
|
|
12
|
-
text: { primary: '#bbb' },
|
|
13
|
-
primary: { main: '#469' },
|
|
14
|
-
},
|
|
15
|
-
typography: {
|
|
16
|
-
fontFamily: 'Roboto, "Noto sans", "Segoe UI", "San Francisco", "Helvetica Neue", Arial, sans-serif'
|
|
17
|
-
},
|
|
18
|
-
components: {
|
|
19
|
-
MuiTextField: {
|
|
20
|
-
defaultProps: { variant: 'filled' },
|
|
21
|
-
styleOverrides: lightMode || {
|
|
22
|
-
root: { '& label.Mui-focused': { color: '#ccc' } } // our primary.main is too dark for mui's dark theme, and when input element is :-webkit-autofill it will make not enough contrast
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
MuiButton: {
|
|
26
|
-
defaultProps: { variant: 'outlined' },
|
|
27
|
-
styleOverrides: lightMode || {
|
|
28
|
-
root({ ownerState }) {
|
|
29
|
-
return ownerState.color === 'primary' && {
|
|
30
|
-
color: ownerState.variant === 'contained' ? '#ddd' : '#68c'
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}), [lightMode])
|
|
37
|
-
}
|
package/admin/tsconfig.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "es2017",
|
|
4
|
-
"lib": [
|
|
5
|
-
"dom",
|
|
6
|
-
"dom.iterable",
|
|
7
|
-
"esnext"
|
|
8
|
-
],
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"esModuleInterop": true,
|
|
12
|
-
"allowSyntheticDefaultImports": true,
|
|
13
|
-
"strict": true,
|
|
14
|
-
"forceConsistentCasingInFileNames": true,
|
|
15
|
-
"noFallthroughCasesInSwitch": true,
|
|
16
|
-
"module": "esnext",
|
|
17
|
-
"moduleResolution": "node",
|
|
18
|
-
"resolveJsonModule": true,
|
|
19
|
-
"isolatedModules": true,
|
|
20
|
-
"noEmit": true,
|
|
21
|
-
"jsx": "react-jsx"
|
|
22
|
-
},
|
|
23
|
-
"include": [
|
|
24
|
-
"src"
|
|
25
|
-
]
|
|
26
|
-
}
|
package/admin/vite.config.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite'
|
|
2
|
-
import vitePluginImport from 'vite-plugin-babel-import';
|
|
3
|
-
|
|
4
|
-
// https://vitejs.dev/config/
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
build: {
|
|
7
|
-
outDir: '../dist/admin',
|
|
8
|
-
emptyOutDir: true,
|
|
9
|
-
target: "es2015",
|
|
10
|
-
},
|
|
11
|
-
plugins: [
|
|
12
|
-
vitePluginImport([] || [
|
|
13
|
-
{ // this is (currently) speeding up build process, by bringing "modules transformed" from 11k+ down to 1.5k+
|
|
14
|
-
libraryName: '@mui/icons-material',
|
|
15
|
-
libraryDirectory: '',
|
|
16
|
-
libraryChangeCase: "camelCase",
|
|
17
|
-
ignoreStyles: [],
|
|
18
|
-
},
|
|
19
|
-
])
|
|
20
|
-
],
|
|
21
|
-
server: {
|
|
22
|
-
port: 3006,
|
|
23
|
-
proxy: {
|
|
24
|
-
'/~/': {
|
|
25
|
-
target: 'http://localhost',
|
|
26
|
-
proxyTimeout: 2000,
|
|
27
|
-
changeOrigin: true,
|
|
28
|
-
ws: true,
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
})
|
package/frontend/.DS_Store
DELETED
|
Binary file
|
package/frontend/.eslintrc
DELETED
package/frontend/.gitignore
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
-
|
|
3
|
-
# dependencies
|
|
4
|
-
/node_modules
|
|
5
|
-
/.pnp
|
|
6
|
-
.pnp.js
|
|
7
|
-
|
|
8
|
-
# testing
|
|
9
|
-
/coverage
|
|
10
|
-
|
|
11
|
-
# production
|
|
12
|
-
/build
|
|
13
|
-
|
|
14
|
-
# misc
|
|
15
|
-
.DS_Store
|
|
16
|
-
.env.local
|
|
17
|
-
.env.development.local
|
|
18
|
-
.env.test.local
|
|
19
|
-
.env.production.local
|
|
20
|
-
|
|
21
|
-
npm-debug.log*
|
|
22
|
-
yarn-debug.log*
|
|
23
|
-
yarn-error.log*
|
package/frontend/package.json
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@hfs/frontend",
|
|
3
|
-
"private": true,
|
|
4
|
-
"proxy": "http://localhost",
|
|
5
|
-
"scripts": {
|
|
6
|
-
"start": "vite",
|
|
7
|
-
"build": "tsc && vite build",
|
|
8
|
-
"preview": "vite preview",
|
|
9
|
-
"test-dep": "npm audit --production"
|
|
10
|
-
},
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"@hfs/shared": "*",
|
|
13
|
-
"js-sha512": "^0.8.0",
|
|
14
|
-
"lodash": "^4.17.21",
|
|
15
|
-
"react": "^18.2.0",
|
|
16
|
-
"react-dom": "^18.2.0",
|
|
17
|
-
"react-router-dom": "^6.1.1",
|
|
18
|
-
"tssrp6a": "^3.0.0",
|
|
19
|
-
"use-debounce": "^7.0.1",
|
|
20
|
-
"usehooks-ts": "^2.6.0",
|
|
21
|
-
"valtio": "^1.2.7",
|
|
22
|
-
"web-vitals": "^2.1.4"
|
|
23
|
-
},
|
|
24
|
-
"devDependencies": {
|
|
25
|
-
"@types/lodash": "^4.14.178",
|
|
26
|
-
"@types/node": "^16.11.21",
|
|
27
|
-
"@types/react": "^18.0.15",
|
|
28
|
-
"@types/react-dom": "^18.0.6",
|
|
29
|
-
"cross-env": "^7.0.3",
|
|
30
|
-
"sass": "^1.54.5",
|
|
31
|
-
"vite": "^3.0.0"
|
|
32
|
-
},
|
|
33
|
-
"eslintConfig": {
|
|
34
|
-
"extends": [
|
|
35
|
-
"react-app",
|
|
36
|
-
"react-app/jest"
|
|
37
|
-
]
|
|
38
|
-
},
|
|
39
|
-
"browserslist": {
|
|
40
|
-
"production": [
|
|
41
|
-
">0.2%",
|
|
42
|
-
"not dead",
|
|
43
|
-
"not op_mini all"
|
|
44
|
-
],
|
|
45
|
-
"development": [
|
|
46
|
-
"last 1 chrome version",
|
|
47
|
-
"last 1 firefox version",
|
|
48
|
-
"last 1 safari version"
|
|
49
|
-
]
|
|
50
|
-
}
|
|
51
|
-
}
|
package/frontend/src/App.ts
DELETED
|
@@ -1,25 +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 { BrowserRouter, Route, Routes } from "react-router-dom"
|
|
4
|
-
import { createElement as h, Fragment } from 'react'
|
|
5
|
-
import { BrowseFiles } from "./BrowseFiles"
|
|
6
|
-
import { Dialogs } from './dialog'
|
|
7
|
-
import useTheme from "./useTheme"
|
|
8
|
-
import { useSnapState } from './state'
|
|
9
|
-
|
|
10
|
-
function App() {
|
|
11
|
-
useTheme()
|
|
12
|
-
const { messageOnly } = useSnapState()
|
|
13
|
-
if (messageOnly)
|
|
14
|
-
return h('h1', { style: { textAlign: 'center'} }, messageOnly)
|
|
15
|
-
return h(Fragment, {},
|
|
16
|
-
h(BrowserRouter, {},
|
|
17
|
-
h(Routes, {},
|
|
18
|
-
h(Route, { path:'*', element: h(BrowseFiles) })
|
|
19
|
-
)
|
|
20
|
-
),
|
|
21
|
-
h(Dialogs)
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export default App;
|
|
@@ -1,43 +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 { Link, useLocation } from 'react-router-dom'
|
|
4
|
-
import { createElement as h, Fragment, ReactElement } from 'react'
|
|
5
|
-
import { hIcon } from './misc'
|
|
6
|
-
import { state } from './state'
|
|
7
|
-
import { reloadList } from './useFetchList'
|
|
8
|
-
|
|
9
|
-
export function Breadcrumbs() {
|
|
10
|
-
const currentPath = useLocation().pathname.slice(1,-1)
|
|
11
|
-
let prev = ''
|
|
12
|
-
const parent = currentPath.split('/').slice(0,-1).join('/')+'/'
|
|
13
|
-
const breadcrumbs = currentPath ? currentPath.split('/').map(x => [prev = prev + x + '/', decodeURIComponent(x)]) : []
|
|
14
|
-
return h(Fragment, {},
|
|
15
|
-
h(Breadcrumb, { label: hIcon('parent', { alt:'parent folder' }), path: parent }),
|
|
16
|
-
h(Breadcrumb, { current: !currentPath, label: hIcon('home', { alt:'home' }) }),
|
|
17
|
-
breadcrumbs.map(([path,label]) =>
|
|
18
|
-
h(Breadcrumb, {
|
|
19
|
-
key: path,
|
|
20
|
-
path,
|
|
21
|
-
label,
|
|
22
|
-
current: path === currentPath+'/',
|
|
23
|
-
}) )
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function Breadcrumb({ path, label, current }:{ current?: boolean, path?: string, label?: string | ReactElement }) {
|
|
28
|
-
const PAD = '\u00A0\u00A0' // make small elements easier to tap. Don't use min-width 'cause it requires display-inline that breaks word-wrapping
|
|
29
|
-
if (typeof label === 'string' && label.length < 3)
|
|
30
|
-
label = PAD+label+PAD
|
|
31
|
-
return h(Link, {
|
|
32
|
-
className: 'breadcrumb',
|
|
33
|
-
to: path || '/',
|
|
34
|
-
async onClick() {
|
|
35
|
-
if (current) {
|
|
36
|
-
state.remoteSearch = ''
|
|
37
|
-
state.stopSearch?.()
|
|
38
|
-
reloadList()
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}, label)
|
|
42
|
-
}
|
|
43
|
-
|
|
@@ -1,141 +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 { Link, useLocation } from 'react-router-dom'
|
|
4
|
-
import { createElement as h, Fragment, memo, useEffect, useMemo, useState } from 'react'
|
|
5
|
-
import { formatBytes, hError, hfsEvent, hIcon, isMobile } from './misc'
|
|
6
|
-
import { Checkbox, Html, Spinner } from './components'
|
|
7
|
-
import { Head } from './Head'
|
|
8
|
-
import { state, useSnapState } from './state'
|
|
9
|
-
import { alertDialog } from './dialog'
|
|
10
|
-
import useFetchList from './useFetchList'
|
|
11
|
-
import useAuthorized from './useAuthorized'
|
|
12
|
-
|
|
13
|
-
export function usePath() {
|
|
14
|
-
return decodeURI(useLocation().pathname)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface DirEntry { n:string, s?:number, m?:string, c?:string,
|
|
18
|
-
ext:string, isFolder:boolean, t?:Date } // we memoize these value for speed
|
|
19
|
-
export type DirList = DirEntry[]
|
|
20
|
-
|
|
21
|
-
export function BrowseFiles() {
|
|
22
|
-
useFetchList()
|
|
23
|
-
const { error, list, serverConfig } = useSnapState()
|
|
24
|
-
return useAuthorized() && h(Fragment, {},
|
|
25
|
-
h(Html, { code: serverConfig?.custom_header }),
|
|
26
|
-
h(Head),
|
|
27
|
-
hError(error)
|
|
28
|
-
|| h(list ? FilesList : Spinner))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function FilesList() {
|
|
32
|
-
const { filteredList, list, loading, stoppedSearch } = useSnapState()
|
|
33
|
-
const midnight = useMidnight() // as an optimization we calculate this only once per list and pass it down
|
|
34
|
-
const pageSize = 100
|
|
35
|
-
const [page, setPage] = useState(0)
|
|
36
|
-
const offset = page * pageSize
|
|
37
|
-
const theList = filteredList || list
|
|
38
|
-
const total = theList.length
|
|
39
|
-
|
|
40
|
-
useEffect(() => setPage(0), [theList])
|
|
41
|
-
useEffect(() => document.scrollingElement?.scrollTo(0,0), [page])
|
|
42
|
-
|
|
43
|
-
return h(Fragment, {},
|
|
44
|
-
h('ul', { className: 'dir' },
|
|
45
|
-
!list.length ? (!loading && (stoppedSearch ? "Stopped before finding anything" : "Nothing here"))
|
|
46
|
-
: filteredList && !filteredList.length ? "No match for this filter"
|
|
47
|
-
: theList.slice(offset, offset + pageSize).map((entry: DirEntry) =>
|
|
48
|
-
h(Entry, { key: entry.n, midnight, ...entry })),
|
|
49
|
-
loading && h(Spinner),
|
|
50
|
-
),
|
|
51
|
-
total > pageSize && h(Paging, { total, current:page, pageSize, pageChange:setPage })
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface PagingProps {
|
|
56
|
-
total: number
|
|
57
|
-
current: number
|
|
58
|
-
pageSize: number
|
|
59
|
-
pageChange:(newPage:number) => void
|
|
60
|
-
}
|
|
61
|
-
function Paging({ total, current, pageSize, pageChange }: PagingProps) {
|
|
62
|
-
const nPages = Math.ceil(total / pageSize)
|
|
63
|
-
const pages = []
|
|
64
|
-
for (let i=0; i<nPages; i++)
|
|
65
|
-
pages.push(h('button', {
|
|
66
|
-
...i===current && { className:'toggled' },
|
|
67
|
-
onClick(){
|
|
68
|
-
pageChange(i)
|
|
69
|
-
}
|
|
70
|
-
}, i*pageSize || 1))
|
|
71
|
-
return h('div', { id:'paging' }, ...pages)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function useMidnight() {
|
|
75
|
-
const [midnight, setMidnight] = useState(calcMidnight)
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
setTimeout(()=> setMidnight(calcMidnight()), 10 * 60_000) // refresh every 10 minutes
|
|
78
|
-
}, [])
|
|
79
|
-
return midnight
|
|
80
|
-
|
|
81
|
-
function calcMidnight() {
|
|
82
|
-
const recent = new Date()
|
|
83
|
-
recent.setHours(recent.getHours() - 6)
|
|
84
|
-
const midnight = new Date()
|
|
85
|
-
midnight.setHours(0,0,0,0)
|
|
86
|
-
return recent < midnight ? recent : midnight
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const Entry = memo(function(entry: DirEntry & { midnight: Date }) {
|
|
91
|
-
let { n: relativePath, isFolder } = entry
|
|
92
|
-
const base = usePath()
|
|
93
|
-
const { showFilter, selected } = useSnapState()
|
|
94
|
-
const href = fixUrl(relativePath)
|
|
95
|
-
const containerDir = isFolder ? '' : relativePath.substring(0, relativePath.lastIndexOf('/')+1)
|
|
96
|
-
const name = relativePath.substring(containerDir.length)
|
|
97
|
-
return h('li', { className: isFolder ? 'folder' : 'file' },
|
|
98
|
-
showFilter && h(Checkbox, {
|
|
99
|
-
value: selected[relativePath],
|
|
100
|
-
onChange(v){
|
|
101
|
-
if (v)
|
|
102
|
-
return state.selected[relativePath] = true
|
|
103
|
-
delete state.selected[relativePath]
|
|
104
|
-
},
|
|
105
|
-
}),
|
|
106
|
-
isFolder ? h(Link, { to: base+href }, hIcon('folder'), relativePath)
|
|
107
|
-
: h(Fragment, {},
|
|
108
|
-
containerDir && h(Link, { to: base+fixUrl(containerDir), className:'container-folder' }, hIcon('file'), containerDir ),
|
|
109
|
-
h('a', { href }, !containerDir && hIcon('file'), name)
|
|
110
|
-
),
|
|
111
|
-
h(EntryProps, entry),
|
|
112
|
-
h('div', { style:{ clear:'both' } })
|
|
113
|
-
)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
function fixUrl(s:string) {
|
|
117
|
-
return s.replace(/#/g, encodeURIComponent)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const EntryProps = memo(function(entry: DirEntry & { midnight: Date }) {
|
|
121
|
-
const { t, s } = entry
|
|
122
|
-
const today = t && t > entry.midnight
|
|
123
|
-
const shortTs = isMobile()
|
|
124
|
-
const code = useMemo(()=> hfsEvent('additionalEntryProps', { entry }).join(''),
|
|
125
|
-
[entry])
|
|
126
|
-
return h('div', { className:'entry-props' },
|
|
127
|
-
h(Html, { code, className:'add-props' }),
|
|
128
|
-
s !== undefined && h(Fragment, {},
|
|
129
|
-
h('span', { className:'entry-size' }, formatBytes(s)),
|
|
130
|
-
" — ",
|
|
131
|
-
),
|
|
132
|
-
t && h('span', {
|
|
133
|
-
className: 'entry-ts',
|
|
134
|
-
title: today || !shortTs ? null : t.toLocaleString(),
|
|
135
|
-
onClick() { // mobile has no hover
|
|
136
|
-
if (shortTs)
|
|
137
|
-
alertDialog("Full timestamp:\n" + t.toLocaleString()).then()
|
|
138
|
-
}
|
|
139
|
-
}, !shortTs ? t.toLocaleString() : today ? t.toLocaleTimeString() : t.toLocaleDateString()),
|
|
140
|
-
)
|
|
141
|
-
})
|
package/frontend/src/Head.ts
DELETED
|
@@ -1,45 +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, useMemo} from 'react'
|
|
4
|
-
import { formatBytes, hIcon, prefix } from './misc'
|
|
5
|
-
import { Spinner } from './components'
|
|
6
|
-
import { useSnapState } from './state'
|
|
7
|
-
import { MenuPanel } from './menu'
|
|
8
|
-
import { Breadcrumbs } from './Breadcrumbs'
|
|
9
|
-
|
|
10
|
-
export function Head() {
|
|
11
|
-
return h('header', {},
|
|
12
|
-
h(MenuPanel),
|
|
13
|
-
h(Breadcrumbs),
|
|
14
|
-
h(FolderStats),
|
|
15
|
-
h('div', { style:{ clear:'both' }}),
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function FolderStats() {
|
|
20
|
-
const { list, loading, filteredList, selected, stoppedSearch } = useSnapState()
|
|
21
|
-
const stats = useMemo(() =>{
|
|
22
|
-
let files = 0, folders = 0, size = 0
|
|
23
|
-
for (const x of list) {
|
|
24
|
-
if (x.isFolder)
|
|
25
|
-
++folders
|
|
26
|
-
else
|
|
27
|
-
++files
|
|
28
|
-
size += x.s||0
|
|
29
|
-
}
|
|
30
|
-
return { files, folders, size }
|
|
31
|
-
}, [list])
|
|
32
|
-
const sel = Object.keys(selected).length
|
|
33
|
-
const fil = filteredList?.length
|
|
34
|
-
return h('div', { id:'folder-stats' },
|
|
35
|
-
stoppedSearch ? hIcon('interrupted', { title:'Search was interrupted' })
|
|
36
|
-
: list.length>0 && loading && h(Spinner),
|
|
37
|
-
[
|
|
38
|
-
prefix('', stats.files,' file(s)'),
|
|
39
|
-
prefix('', stats.folders, ' folder(s)'),
|
|
40
|
-
stats.size ? formatBytes(stats.size) : '',
|
|
41
|
-
sel && sel+' selected',
|
|
42
|
-
fil !== undefined && fil < list.length && fil+' displayed',
|
|
43
|
-
].filter(Boolean).join(', ')
|
|
44
|
-
)
|
|
45
|
-
}
|