@samitouri / QOSami-HFS / commits / b3b80989

fix: admin/fs: different-children-permissions were displayed incorrectly on children

Massimo Melina committed Oct 2, 2023 at 16:52 UTC b3b80989af1b51cf89ad043d9ea7446e24d960b1
5 files changed +47 -55
admin/src/FileForm.ts
+11 -21
@@ -15,7 +15,7 @@ import {
15 } from '@hfs/mui-grid-form'
16 import { apiCall, useApiEx } from './api'
17 import { basename, Btn, defaultPerms, formatBytes, formatTimestamp, IconBtn, isEqualLax, LinkBtn, modifiedSx,
18 - newDialog, objSameKeys, onlyTruthy, prefix, useBreakpoint, Who, wikiLink } from './misc'
18 + newDialog, objSameKeys, onlyTruthy, prefix, useBreakpoint, VfsPerms, Who, wikiLink } from './misc'
19 import { reloadVfs, VfsNode } from './VfsPage'
20 import md from './md'
21 import _ from 'lodash'
@@ -46,19 +46,10 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
46 const isDir = file.type === 'folder'
47 const hasSource = source !== undefined // we need a boolean
48 const realFolder = hasSource && isDir
49 - const inheritedPerms = useMemo(() => {
50 - const ret = {}
51 - let run = parent
52 - while (run) {
53 - _.defaults(ret, run, run.byMasks)
54 - run = run.parent
55 - }
56 - return _.defaults(ret, defaultPerms)
57 - }, [parent])
49 const lg = useBreakpoint('lg')
50 const showTimestamps = lg || hasSource
51 const showSize = lg || (hasSource && !realFolder)
61 - const showAccept = file.accept! > '' || isDir && (file.can_upload ?? inheritedPerms.can_upload)
52 + const showAccept = file.accept! > '' || isDir && (file.can_upload ?? file.inherited?.can_upload)
53 const barColors = useDialogBarColors()
54
55 const { data, element } = useApiEx<{ list: Account[] }>('get_accounts')
@@ -72,7 +63,7 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
63 set(v, k) {
64 if (k === 'link') return
65 setValues(values => {
75 - const nameIsVirtual = k === 'source' && values.source?.endsWith(values.name)
66 + const nameIsVirtual = k === 'source' && values.name && values.source?.endsWith(values.name)
67 const name = nameIsVirtual ? basename(v) : values.name // update name if virtual
68 return { ...values, name, [k]: v }
69 })
@@ -93,8 +84,7 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
84 sx: modifiedSx(!isEqualLax(values, rest)),
85 async onClick() {
86 const props = _.omit(values, ['ctime','mtime','size','id'])
96 - if (!props.masks)
97 - props.masks = null // undefined cannot be serialized
87 + ;(props as any).masks ||= null // undefined cannot be serialized
88 await apiCall('set_vfs', { uri: values.id, props })
89 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
90 state.selectedFiles[0].id = file.parent!.id + props.name + (isDir ? '/' : '')
@@ -131,13 +121,13 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
121 ]
122 })
123
134 - function perm(perm: keyof typeof inheritedPerms, helperText?: ReactNode, props: Partial<WhoFieldProps>={}) {
124 + function perm(perm: keyof VfsPerms, helperText?: ReactNode, props: Partial<WhoFieldProps>={}) {
125 return {
126 showInherited: anyMask, // with masks, you may need to set a permission to override the mask
127 otherPerms: _.without(Object.keys(defaultPerms), perm).map(x => ({ value: x, label: "As " +perm2word(x) })),
128 k: perm, lg: 6, xl: 4, comp: WhoField, parent, accounts, helperText,
129 label: "Who can " + perm2word(perm),
140 - inherit: inheritedPerms[perm],
130 + inherit: file.inherited?.[perm] ?? defaultPerms[perm],
131 byMasks: byMasks?.[perm],
132 isDir,
133 ...props
@@ -183,11 +173,11 @@ function WhoField({ value, onChange, parent, inherit, accounts, helperText, show
173 const arrayMode = Array.isArray(thisValue)
174 // a large side band will convey union across the fields
175 return h(Box, { sx: { borderRight: objectMode ? '8px solid #8884' : undefined, transition: `all ${timeout}ms` } },
186 - h(SelectField as typeof SelectField<typeof thisValue>, {
176 + h(SelectField as typeof SelectField<typeof thisValue | null>, {
177 ...rest,
188 - value: arrayMode ? [] : thisValue,
178 + value: arrayMode ? [] : thisValue ?? null,
179 onChange(v, { event }) {
190 - onChange(objectMode ? { this: v, children: childrenValue } : v, { was: value, event })
180 + onChange(objectMode ? { this: v ?? undefined, children: childrenValue } : v ?? undefined, { was: value, event })
181 },
182 options,
183 }),
@@ -213,9 +203,9 @@ function WhoField({ value, onChange, parent, inherit, accounts, helperText, show
203 label: "Permission for " + contentText,
204 parent, inherit, accounts, showInherited, otherPerms, isDir,
205 isChildren: true,
216 - value: childrenValue ?? null,
206 + value: childrenValue ?? undefined,
207 onChange(v, { event }) {
218 - onChange({ this: thisValue, children: v as any }, { was: value, event })
208 + onChange({ this: thisValue ?? undefined, children: v }, { was: value, event })
209 }
210 })
211 ),
admin/src/VfsPage.ts
+2 -10
@@ -6,7 +6,7 @@ import { Alert, Button, Card, CardContent, Grid, Link, List, ListItem, ListItemT
6 import { state, useSnapState } from './state'
7 import VfsMenuBar from './VfsMenuBar'
8 import VfsTree from './VfsTree'
9 -import { Flex, IconBtn, newDialog, onlyTruthy, prefix, useBreakpoint, VfsPerms } from './misc'
9 +import { Flex, IconBtn, newDialog, onlyTruthy, prefix, useBreakpoint, VfsNodeAdminSend } from './misc'
10 import { reactJoin } from '@hfs/shared'
11 import _ from 'lodash'
12 import { AlertProps } from '@mui/material/Alert/Alert'
@@ -149,20 +149,12 @@ export async function deleteFiles() {
149 }
150 }
151
152 -export interface VfsNode extends VfsPerms {
152 +export interface VfsNode extends Omit<VfsNodeAdminSend, 'ctime' | 'mtime' | 'children'> {
153 id: string
154 - name: string
155 - type?: 'folder'
156 - source?: string
157 - size?: number
154 ctime?: string
155 mtime?: string
156 default?: string
157 children?: VfsNode[]
158 parent?: VfsNode
163 - website?: true
164 - masks?: any
165 - byMasks?: VfsPerms
159 isRoot?: true
167 - accept?: string
160 }
src/api.vfs.ts
+11 -20
@@ -1,11 +1,12 @@
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 { getNodeName, isSameFilenameAs, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode, applyParentToChild } from './vfs'
3 +import { getNodeName, isSameFilenameAs, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode, applyParentToChild,
4 + permsFromParent } from './vfs'
5 import _ from 'lodash'
6 import { stat } from 'fs/promises'
7 import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
8 import { dirname, extname, join, resolve } from 'path'
8 -import { dirStream, isDirectory, isWindowsDrive, makeMatcher, defaultPerms,PERM_KEYS } from './misc'
9 +import { dirStream, isDirectory, isWindowsDrive, makeMatcher, PERM_KEYS, VfsNodeAdminSend } from './misc'
10 import {
11 IS_WINDOWS,
12 HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE,
@@ -13,16 +14,6 @@ import {
14 import { getDrives } from './util-os'
15 import { Stats } from 'fs'
16
16 -type VfsAdmin = {
17 - type?: string,
18 - size?: number,
19 - ctime?: Date,
20 - mtime?: Date,
21 - website?: true,
22 - byMasks?: any
23 - children?: VfsAdmin[]
24 -} & Omit<VfsNode, 'type' | 'children'>
25 -
17 // to manipulate the tree we need the original node
18 async function urlToNodeOriginal(uri: string) {
19 const n = await urlToNode(uri)
@@ -34,28 +25,28 @@ const apis: ApiHandlers = {
25 async get_vfs() {
26 return { root: await recur() }
27
37 - async function recur(node=vfs): Promise<VfsAdmin> {
28 + async function recur(node=vfs): Promise<VfsNodeAdminSend> {
29 const { source } = node
30 const stats: false | Stats = Boolean(source) && await stat(source!).catch(() => false)
31 const isDir = !source || stats && stats.isDirectory()
41 - const copyStats: Pick<VfsAdmin, 'size' | 'ctime' | 'mtime'> = stats ? _.pick(stats, ['size', 'ctime', 'mtime'])
32 + const copyStats: Pick<VfsNodeAdminSend, 'size' | 'ctime' | 'mtime'> = stats ? _.pick(stats, ['size', 'ctime', 'mtime'])
33 : { size: source ? -1 : undefined }
34 if (copyStats.mtime && Number(copyStats.mtime) === Number(copyStats.ctime))
35 delete copyStats.mtime
45 - let byMasks = node.original && _.pickBy(node, (v,k) =>
36 + const inherited = node.parent && permsFromParent(node.parent, node.original || node)
37 + const byMasks = node.original && _.pickBy(node, (v,k) =>
38 v !== (node.original as any)[k] // something is changing me...
47 - && v !== (node.parent as any)[k] // ...and it's not inheritance...
39 + && !(inherited && k in inherited) // ...and it's not inheritance...
40 && PERM_KEYS.includes(k as any)) // ...must be masks. Please limit this to perms
49 - if (_.isEmpty(byMasks))
50 - byMasks = undefined
41 return {
42 ...copyStats,
43 ...node.original || node,
54 - byMasks,
44 + inherited,
45 + byMasks: _.isEmpty(byMasks) ? undefined : byMasks,
46 website: Boolean(node.children?.find(isSameFilenameAs('index.html')))
47 || isDir && source && await stat(join(source, 'index.html')).then(() => true, () => undefined)
48 || undefined,
58 - name: node === vfs ? undefined : getNodeName(node),
49 + name: node === vfs ? '' : getNodeName(node),
50 type: isDir ? 'folder' : undefined,
51 children: node.children && await Promise.all(node.children.map(child =>
52 recur(applyParentToChild(child, node)) ))
src/cross.ts
+13
@@ -1,6 +1,7 @@
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 // all content here is shared between client and server
3 import _ from 'lodash'
4 +import { VfsNode } from './vfs'
5
6 export const REPO_URL = 'https://github.com/rejetto/hfs/'
7 export const WIKI_URL = REPO_URL + 'wiki/'
@@ -64,6 +65,18 @@ export const defaultPerms: Required<VfsPerms> = {
65 can_archive: 'can_read'
66 }
67
68 +export type VfsNodeAdminSend = {
69 + name: string
70 + type?: 'folder'
71 + size?: number
72 + ctime?: Date
73 + mtime?: Date
74 + website?: true
75 + byMasks?: VfsPerms
76 + inherited?: VfsPerms
77 + children?: VfsNodeAdminSend[]
78 +} & Omit<VfsNode, 'type' | 'children'>
79 +
80 export const PERM_KEYS = typedKeys(defaultPerms)
81
82 export function isWhoObject(v: undefined | Who): v is WhoObject {
src/vfs.ts
+10 -4
@@ -34,21 +34,27 @@ export interface VfsNode extends VfsPerms {
34
35 export const MIME_AUTO = 'auto'
36
37 -function inheritFromParent(parent: VfsNode, child: VfsNode) {
37 +export function permsFromParent(parent: VfsNode, child: VfsNode) {
38 + const ret: VfsPerms = {}
39 for (const k of PERM_KEYS) {
40 let p: VfsNode | undefined = parent
41 let inheritedPerm: Who | undefined
42 while (p) {
43 inheritedPerm = p[k]
43 - // // in case of object without children, parent is skipped in favor of the parent's parent
44 + // in case of object without children, parent is skipped in favor of the parent's parent
45 if (!isWhoObject(inheritedPerm)) break
46 inheritedPerm = inheritedPerm.children
47 if (inheritedPerm !== undefined) break
48 p = p.parent
49 }
49 - if (inheritedPerm !== undefined) // small optimization: don't expand the object
50 - child[k] ??= inheritedPerm
50 + if (inheritedPerm !== undefined && child[k] === undefined) // small optimization: don't expand the object
51 + ret[k] = inheritedPerm
52 }
53 + return _.isEmpty(ret) ? undefined : ret
54 +}
55 +
56 +function inheritFromParent(parent: VfsNode, child: VfsNode) {
57 + Object.assign(child, permsFromParent(parent, child))
58 if (typeof parent.mime === 'object' && typeof child.mime === 'object')
59 _.defaults(child.mime, parent.mime)
60 else