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 }