better code: less duplication
Massimo Melina committed
Oct 2, 2023 at 15:46 UTC
c9bc8b266ca1a8faf80e15f252920b2221ab1aea
9 files changed
+58
-122
admin/src/FileForm.ts
+4
-7
@@ -14,11 +14,9 @@ import {
14
StringField
15
} from '@hfs/mui-grid-form'
16
import { apiCall, useApiEx } from './api'
17
-import {
18
- basename, Btn, formatBytes, formatTimestamp, IconBtn, isEqualLax, LinkBtn, modifiedSx, newDialog, objSameKeys,
19
- onlyTruthy, prefix, wikiLink
20
-} from './misc'
21
-import { reloadVfs, VfsNode, VfsPerms, Who } from './VfsPage'
17
+import { basename, Btn, defaultPerms, formatBytes, formatTimestamp, IconBtn, isEqualLax, LinkBtn, modifiedSx,
18
+ newDialog, objSameKeys, onlyTruthy, prefix, useBreakpoint, Who, wikiLink } from './misc'
19
+import { reloadVfs, VfsNode } from './VfsPage'
20
import md from './md'
21
import _ from 'lodash'
22
import FileField from './FileField'
@@ -31,14 +29,13 @@ interface Account { username: string }
29
interface FileFormProps {
30
file: VfsNode
31
anyMask?: boolean
34
- defaultPerms: VfsPerms
32
addToBar?: ReactNode
33
statusApi: any
34
}
35
36
const ACCEPT_LINK = "https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept"
37
41
-export default function FileForm({ file, anyMask, defaultPerms, addToBar, statusApi }: FileFormProps) {
38
+export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFormProps) {
39
const { parent, children, isRoot, byMasks, ...rest } = file
40
const [values, setValues] = useState(rest)
41
useEffect(() => {
admin/src/VfsPage.ts
+2
-30
@@ -2,26 +2,17 @@
2
3
import { createElement as h, Fragment, useEffect, useMemo, useState } from 'react'
4
import { apiCall, useApiEx } from './api'
5
-import {
6
- Alert,
7
- Button,
8
- Card, CardContent,
9
- Grid,
10
- Link,
11
- List, ListItem, ListItemText,
12
- Typography
13
-} from '@mui/material'
5
+import { Alert, Button, Card, CardContent, 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'
17
-import { IconBtn, newDialog, onlyTruthy, prefix, useBreakpoint } from './misc'
9
+import { Flex, IconBtn, newDialog, onlyTruthy, prefix, useBreakpoint, VfsPerms } 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
import { Close, Delete } from '@mui/icons-material'
15
import { alertDialog, confirmDialog } from './dialog'
24
-import { Flex } from './misc'
16
17
let selectOnReload: string[] | undefined
18
@@ -56,7 +47,6 @@ export default function VfsPage() {
47
title: "Close",
48
onClick: selectNone
49
}),
59
- defaultPerms: data?.defaultPerms as VfsPerms,
50
anyMask,
51
statusApi,
52
file: selectedFiles[0] as VfsNode // it's actually Snapshot<VfsNode> but it's easier this way
@@ -159,13 +149,6 @@ export async function deleteFiles() {
149
}
150
}
151
162
-export interface VfsPerms {
163
- can_see?: Who
164
- can_read?: Who
165
- can_list?: Who
166
- can_upload?: Who
167
- can_delete?: Who
168
-}
152
export interface VfsNode extends VfsPerms {
153
id: string
154
name: string
@@ -183,14 +166,3 @@ export interface VfsNode extends VfsPerms {
166
isRoot?: true
167
accept?: string
168
}
186
-
187
-const WHO_ANYONE = true
188
-const WHO_NO_ONE = false
189
-const WHO_ANY_ACCOUNT = '*'
190
-type AccountList = string[]
191
-export type Who = SimpleWho | { this?: SimpleWho, children?: SimpleWho }
192
-export type SimpleWho = typeof WHO_ANYONE
193
- | typeof WHO_NO_ONE
194
- | typeof WHO_ANY_ACCOUNT
195
- | AccountList
196
- | null
\ No newline at end of file
admin/src/VfsTree.ts
+2
-2
@@ -8,8 +8,8 @@ import {
8
InsertDriveFileOutlined, Lock, RemoveRedEye, Web, Upload, Cloud, Delete, HighlightOff
9
} from '@mui/icons-material'
10
import { Box } from '@mui/material'
11
-import { reloadVfs, VfsNode, Who } from './VfsPage'
12
-import { iconTooltip, onlyTruthy } from './misc'
11
+import { reloadVfs, VfsNode } from './VfsPage'
12
+import { iconTooltip, onlyTruthy, Who } from './misc'
13
import { apiCall } from './api'
14
import { alertDialog, confirmDialog } from './dialog'
15
mui-grid-form/index.ts
+1
-1
@@ -16,7 +16,7 @@ import {
16
import { Box, BoxProps, Button, Grid } from '@mui/material'
17
import { Save } from '@mui/icons-material'
18
import { LoadingButton } from '@mui/lab'
19
-import * as _ from 'lodash'
19
+import _ from 'lodash'
20
import { StringField } from './StringField'
21
import { GridProps } from '@mui/material/Grid/Grid'
22
import { useDebounce } from 'usehooks-ts'
src/api.file_list.ts
+3
-13
@@ -1,21 +1,11 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import {
4
- applyParentToChild,
5
- getNodeName,
6
- hasPermission,
7
- masksCouldGivePermission,
8
- nodeIsDirectory,
9
- statusCodeForMissingPerm,
10
- urlToNode,
11
- VfsNode,
12
- walkNode,
13
- WHO_NO_ONE
14
-} from './vfs'
3
+import { applyParentToChild, getNodeName, hasPermission, masksCouldGivePermission, nodeIsDirectory,
4
+ statusCodeForMissingPerm, urlToNode, VfsNode, walkNode } from './vfs'
5
import { ApiError, ApiHandler, SendListReadable } from './apiMiddleware'
6
import { stat } from 'fs/promises'
7
import { mapPlugins } from './plugins'
18
-import { asyncGeneratorToArray, dirTraversal, pattern2filter } from './misc'
8
+import { asyncGeneratorToArray, dirTraversal, pattern2filter, WHO_NO_ONE } from './misc'
9
import _ from 'lodash'
10
import { HTTP_FOOL, HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND } from './const'
11
import Koa from 'koa'
src/api.vfs.ts
+3
-17
@@ -1,22 +1,11 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import {
4
- defaultPerms,
5
- getNodeName,
6
- isSameFilenameAs,
7
- nodeIsDirectory,
8
- saveVfs,
9
- urlToNode,
10
- vfs,
11
- VfsNode,
12
- PERM_KEYS,
13
- applyParentToChild
14
-} from './vfs'
3
+import { getNodeName, isSameFilenameAs, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode, applyParentToChild } from './vfs'
4
import _ from 'lodash'
5
import { stat } from 'fs/promises'
6
import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
7
import { dirname, extname, join, resolve } from 'path'
19
-import { dirStream, isDirectory, isWindowsDrive, makeMatcher } from './misc'
8
+import { dirStream, isDirectory, isWindowsDrive, makeMatcher, defaultPerms,PERM_KEYS } from './misc'
9
import {
10
IS_WINDOWS,
11
HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE,
@@ -43,10 +32,7 @@ async function urlToNodeOriginal(uri: string) {
32
const apis: ApiHandlers = {
33
34
async get_vfs() {
46
- return {
47
- root: await recur(),
48
- defaultPerms,
49
- }
35
+ return { root: await recur() }
36
37
async function recur(node=vfs): Promise<VfsAdmin> {
38
const { source } = node
src/apiMiddleware.ts
+1
-1
@@ -5,7 +5,7 @@ import createSSE from './sse'
5
import { Readable } from 'stream'
6
import { asyncGeneratorToReadable, onOff, removeStarting } from './misc'
7
import events from './events'
8
-import { HTTP_BAD_REQUEST, HTTP_FOOL, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED } from './const'
8
+import { HTTP_BAD_REQUEST, HTTP_FOOL, HTTP_NOT_FOUND } from './const'
9
import _ from 'lodash'
10
import { defineConfig } from './config'
11
src/cross.ts
+33
@@ -35,6 +35,39 @@ export interface GetNat {
35
externalPort?: number
36
}
37
38
+export interface VfsPerms {
39
+ can_see?: Who
40
+ can_read?: Who
41
+ can_list?: Who
42
+ can_upload?: Who
43
+ can_delete?: Who
44
+}
45
+export const WHO_ANYONE = true
46
+export const WHO_NO_ONE = false
47
+export const WHO_ANY_ACCOUNT = '*'
48
+type AccountList = string[]
49
+export type Who = typeof WHO_ANYONE
50
+ | typeof WHO_NO_ONE
51
+ | typeof WHO_ANY_ACCOUNT
52
+ | keyof VfsPerms
53
+ | WhoObject
54
+ | AccountList // use false instead of empty array to keep the type boolean-able
55
+interface WhoObject { this?: Who, children?: Who }
56
+
57
+export const defaultPerms: Required<VfsPerms> = {
58
+ can_see: 'can_read',
59
+ can_read: WHO_ANYONE,
60
+ can_list: 'can_read',
61
+ can_upload: WHO_NO_ONE,
62
+ can_delete: WHO_NO_ONE,
63
+}
64
+
65
+export const PERM_KEYS = typedKeys(defaultPerms)
66
+
67
+export function isWhoObject(v: undefined | Who): v is WhoObject {
68
+ return v !== null && typeof v === 'object' && !Array.isArray(v)
69
+}
70
+
71
const MULTIPLIERS = ['', 'K', 'M', 'G', 'T']
72
export function formatBytes(n: number, { post='B', k=1024, digits=NaN }={}) {
73
if (isNaN(Number(n)) || n < 0)
src/vfs.ts
+9
-51
@@ -3,16 +3,8 @@
3
import fs from 'fs/promises'
4
import { basename, dirname, join, resolve } from 'path'
5
import {
6
- dirStream,
7
- dirTraversal,
8
- enforceFinal,
9
- getOrSet,
10
- isDirectory,
11
- typedKeys,
12
- makeMatcher,
13
- setHidden,
14
- onlyTruthy,
15
- typedEntries, throw_
6
+ dirStream, dirTraversal, enforceFinal, getOrSet, isDirectory, typedKeys, makeMatcher, setHidden, onlyTruthy,
7
+ typedEntries, throw_, VfsPerms, Who, isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS
8
} from './misc'
9
import Koa from 'koa'
10
import _ from 'lodash'
@@ -21,29 +13,9 @@ import { HTTP_FOOL, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED } from './const'
13
import events from './events'
14
import { expandUsername, getCurrentUsername } from './perm'
15
24
-export const WHO_ANYONE = true
25
-export const WHO_NO_ONE = false
26
-export const WHO_ANY_ACCOUNT = '*'
27
-type AccountList = string[]
28
-export type Who = typeof WHO_ANYONE
29
- | typeof WHO_NO_ONE
30
- | typeof WHO_ANY_ACCOUNT
31
- | keyof VfsPerm
32
- | WhoObject
33
- | AccountList // empty array shouldn't be used to keep the type boolean-able
34
-interface WhoObject { this?: Who, children?: Who }
35
-
36
-export interface VfsPerm {
37
- can_read: Who
38
- can_see: Who
39
- can_list: Who
40
- can_upload: Who
41
- can_delete: Who
42
-}
43
-
16
type Masks = Record<string, VfsNode & { maskOnly?: 'files' | 'folders' }>
17
46
-export interface VfsNode extends Partial<VfsPerm> {
18
+export interface VfsNode extends VfsPerms {
19
name?: string
20
source?: string
21
children?: VfsNode[]
@@ -52,7 +24,7 @@ export interface VfsNode extends Partial<VfsPerm> {
24
rename?: Record<string, string>
25
masks?: Masks // express fields for descendants that are not in the tree
26
accept?: string
55
- propagate?: Record<keyof VfsPerm, boolean> // legacy pre-0.47
27
+ propagate?: Record<keyof VfsPerms, boolean> // legacy pre-0.47
28
// fields that are only filled at run-time
29
isTemp?: true // this node doesn't belong to the tree and was created by necessity
30
original?: VfsNode // if this is a temp node but reflecting an existing node
@@ -60,20 +32,10 @@ export interface VfsNode extends Partial<VfsPerm> {
32
isFolder?: boolean
33
}
34
63
-export const defaultPerms: VfsPerm = {
64
- can_see: 'can_read',
65
- can_read: WHO_ANYONE,
66
- can_list: 'can_read',
67
- can_upload: WHO_NO_ONE,
68
- can_delete: WHO_NO_ONE,
69
-}
70
-
71
-export const PERM_KEYS = typedKeys(defaultPerms)
72
-
35
export const MIME_AUTO = 'auto'
36
37
function inheritFromParent(parent: VfsNode, child: VfsNode) {
76
- for (const k of typedKeys(defaultPerms)) {
38
+ for (const k of PERM_KEYS) {
39
let p: VfsNode | undefined = parent
40
let inheritedPerm: Who | undefined
41
while (p) {
@@ -95,10 +57,6 @@ function inheritFromParent(parent: VfsNode, child: VfsNode) {
57
return child
58
}
59
98
-function isWhoObject(v: undefined | Who): v is WhoObject {
99
- return v !== null && typeof v === 'object' && !Array.isArray(v)
100
-}
101
-
60
export function isSameFilenameAs(name: string) {
61
const lc = name.toLowerCase()
62
return (other: string | VfsNode) =>
@@ -215,11 +173,11 @@ export async function nodeIsDirectory(node: VfsNode) {
173
return isFolder
174
}
175
218
-export function hasPermission(node: VfsNode, perm: keyof VfsPerm, ctx: Koa.Context): boolean {
176
+export function hasPermission(node: VfsNode, perm: keyof VfsPerms, ctx: Koa.Context): boolean {
177
return !statusCodeForMissingPerm(node, perm, ctx, false)
178
}
179
222
-export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerm, ctx: Koa.Context, assign=true) {
180
+export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerms, ctx: Koa.Context, assign=true) {
181
const ret = getCode()
182
if (ret && assign)
183
ctx.status = ret
@@ -256,7 +214,7 @@ export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerm, ctx
214
215
// it's responsibility of the caller to verify you have list permission on parent, as callers have different needs.
216
// Too many parameters: consider object, but benchmark against degraded recursion on huge folders.
259
-export async function* walkNode(parent:VfsNode, ctx?: Koa.Context, depth:number=0, prefixPath:string='', requiredPerm?: keyof VfsPerm): AsyncIterableIterator<VfsNode> {
217
+export async function* walkNode(parent:VfsNode, ctx?: Koa.Context, depth:number=0, prefixPath:string='', requiredPerm?: keyof VfsPerms): AsyncIterableIterator<VfsNode> {
218
const { children, source } = parent
219
const took = prefixPath ? undefined : new Set()
220
const maskApplier = parentMaskApplier(parent)
@@ -327,7 +285,7 @@ export async function* walkNode(parent:VfsNode, ctx?: Koa.Context, depth:number=
285
}
286
}
287
330
-export function masksCouldGivePermission(masks: Masks | undefined, perm: keyof VfsPerm): boolean {
288
+export function masksCouldGivePermission(masks: Masks | undefined, perm: keyof VfsPerms): boolean {
289
return masks !== undefined && Object.values(masks).some(props =>
290
props[perm] || masksCouldGivePermission(props.masks, perm))
291
}