Skip to content

Commit

Permalink
inheritable "default" rejetto#377
Browse files Browse the repository at this point in the history
  • Loading branch information
rejetto committed Oct 18, 2023
1 parent 49841ad commit 7aaa79c
Show file tree
Hide file tree
Showing 11 changed files with 40 additions and 25 deletions.
19 changes: 15 additions & 4 deletions admin/src/FileForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
setValues(Object.assign(objSameKeys(defaultPerms, () => null), rest))
}, [file]) //eslint-disable-line

const inheritedDefault = useMemo(() => {
let p = file.parent
while (p) {
if (p.default != null)
return p.default
p = p.parent
}
}, [file])
const { source } = file
const isDir = file.type === 'folder'
const hasSource = source !== undefined // we need a boolean
Expand All @@ -50,6 +58,7 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
const showTimestamps = lg || hasSource
const showSize = lg || (hasSource && !realFolder)
const showAccept = file.accept! > '' || isDir && (file.can_upload ?? file.inherited?.can_upload)
const showWebsite = isDir
const barColors = useDialogBarColors()

const { data, element } = useApiEx<{ list: Account[] }>('get_accounts')
Expand Down Expand Up @@ -107,11 +116,13 @@ export default function FileForm({ file, anyMask, addToBar, statusApi }: FileFor
showSize && { k: 'size', comp: DisplayField, lg: 4, toField: formatBytes },
showTimestamps && { k: 'ctime', comp: DisplayField, md: 6, lg: showSize && 4, label: "Created", toField: formatTimestamp },
showTimestamps && { k: 'mtime', comp: DisplayField, md: 6, lg: showSize && 4, label: "Modified", toField: formatTimestamp },
showAccept && { k: 'accept', label: "Accept on upload", placeholder: "anything", xl: file.website ? 4 : 12,
showAccept && { k: 'accept', label: "Accept on upload", placeholder: "anything", xl: showWebsite ? 4 : 12,
helperText: h(Link, { href: ACCEPT_LINK, target: '_blank' }, "Example: .zip") },
file.website && { k: 'default', comp: BoolField, label:"Serve index.html", xl: true,
toField: Boolean, fromField: (v:boolean) => v ? 'index.html' : null,
helperText: md("This folder may be a website because contains `index.html`. Enabling this will show the website instead of the list of files.")
showWebsite && { k: 'default', comp: BoolField, xl: true,
label: "Serve as website if index.html is found" + (inheritedDefault && values.default == null ? ' (inherited)' : ''),
value: values.default ?? inheritedDefault,
toField: Boolean, fromField: (v:boolean) => v && !inheritedDefault ? 'index.html' : v ? null : false,
helperText: md("...instead of showing list of files")
},
isDir && { k: 'masks', multiline: true,
toField: yaml.stringify, fromField: v => v ? yaml.parse(v) : undefined,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/fileMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (FileMe
...addToMenu.map(x => {
if (x === 'open') {
if (entry.cantOpen) return
const open = { id: 'open', icon: 'play', label: t('file_open', "Open"), href: uri, target: isFolder ? undefined : '_blank' }
const open = { id: 'open', icon: 'play', label: t('file_open', "Open"), href: uri, target: isFolder || entry.web ? undefined : '_blank' }
return !isFolder ? open : h(Link, { to: uri, onClick: () => close() }, hIcon(open.icon), open.label)
}
if (x === 'delete')
Expand Down Expand Up @@ -90,7 +90,7 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (FileMe
href: '#',
..._.omit(e, ['label', 'icon', 'onClick']),
async onClick(event: MouseEvent) {
if (!e.href) // even with #, the
if (!e.href)
event.preventDefault()
if (false !== await e.onClick?.(event))
close()
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import _ from 'lodash'
import { proxy, useSnapshot } from 'valtio'
import { subscribeKey } from 'valtio/utils'
import { getHFS, hIcon } from './misc'
import { Dict, getHFS, hIcon } from './misc'

export const state = proxy<{
stopSearch?: ()=>void,
Expand Down Expand Up @@ -91,6 +91,7 @@ export class DirEntry {
public readonly c?: string
public readonly p?: string
public readonly icon?: string
public readonly web?: true
public comment?: string
// we memoize these value for speed
public readonly name: string
Expand All @@ -100,10 +101,12 @@ export class DirEntry {
public readonly t?:Date
public readonly cantOpen: boolean

constructor(n: string, rest?: object) {
constructor(n: string, rest?: any) {
Object.assign(this, rest) // we actually allow any custom property to be memorized
this.n = n // must do it after rest to avoid overwriting
this.uri = (n[0] === '/' ? '' : location.pathname) + pathEncode(this.n)
if (rest?.web) // this is actually a folder pointing to a default file, and it requires a final slash for correct handling
this.uri += '/'
this.isFolder = this.n.endsWith('/')
if (!this.isFolder) {
const i = this.n.lastIndexOf('.') + 1
Expand Down
1 change: 1 addition & 0 deletions frontend/src/useFetchList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default function useFetchList() {
case 'msg':
state.loginRequired = false
for (const entry of data) {
if (!Array.isArray(entry)) continue // unexpected
const [op, par] = entry
const error = op === 'error' && par
if (error === 405) { // "method not allowed" happens when we try to directly access an unauthorized file, and we get a login prompt, and then get_file_list the file (because we didn't know it was file or folder)
Expand Down
2 changes: 1 addition & 1 deletion shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export async function getNotification(channel: string, cb: (name: string, data:a
if (type === 'connected')
return resolve(ret)
if (type !== 'msg') return
for (const { name, data } of entries)
for (const [name, data] of entries)
if (name)
cb(name, data)
})
Expand Down
19 changes: 9 additions & 10 deletions src/api.file_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import { ApiError, ApiHandler, SendListReadable } from './apiMiddleware'
import { stat } from 'fs/promises'
import { mapPlugins } from './plugins'
import { asyncGeneratorToArray, dirTraversal, pattern2filter, WHO_NO_ONE } from './misc'
import _ from 'lodash'
import { HTTP_FOOL, HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND } from './const'
import Koa from 'koa'
import { descriptIon, DESCRIPT_ION, getCommentFor } from './comments'
import { basename } from 'path'

export interface DirEntry { n:string, s?:number, m?:Date, c?:Date, p?: string, comment?: string }
export interface DirEntry { n:string, s?:number, m?:Date, c?:Date, p?: string, comment?: string, web?: boolean }

export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c }, ctx) => {
const node = await urlToNode(uri || '/', ctx)
Expand All @@ -23,11 +22,7 @@ export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c
return fail()
if (dirTraversal(search))
return fail(HTTP_FOOL)
if (node.default)
return (list?.custom ?? _.identity)({ // sse will wrap the object in a 'custom' message, otherwise we plainly return the object
redirect: uri // tell the browser to access the folder (instead of using this api), so it will get the default file
})
if (!await nodeIsDirectory(node))
if (await hasDefaultFile(node) || !await nodeIsDirectory(node))
return fail(HTTP_METHOD_NOT_ALLOWED)
offset = Number(offset)
limit = Number(limit)
Expand Down Expand Up @@ -92,13 +87,17 @@ export const get_file_list: ApiHandler = async ({ uri, offset, limit, search, c
}
}

async function hasDefaultFile(node: VfsNode) {
return node.default && await urlToNode(node.default, ctx, node)
}

async function nodeToDirEntry(ctx: Koa.Context, node: VfsNode): Promise<DirEntry | null> {
let { source, default:def } = node
let { source } = node
const name = getNodeName(node)
if (!source)
return name ? { n: name + '/' } : null
if (def)
return { n: name }
if (node.isFolder && await hasDefaultFile(node))
return { n: name, web: true }
try {
const st = await stat(source)
const folder = st.isDirectory()
Expand Down
4 changes: 2 additions & 2 deletions src/apiMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ export class SendListReadable<T> extends Readable {
ready() { // useful to indicate the end of an initial phase, but we leave open for updates
this._push(['ready'])
}
custom(data: any) {
this._push(data)
custom(name: string, data: any) {
this._push([name, data])
}
props(props: object) {
this._push(['props', props])
Expand Down
2 changes: 1 addition & 1 deletion src/frontEndApis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const frontEndApis: ApiHandlers = {
list.ready() // on chrome109 EventSource doesn't emit 'open' until something is sent
return list.events(ctx, {
[NOTIFICATION_PREFIX + channel](name, data) {
list.custom({ name, data })
list.custom(name, data)
}
})
},
Expand Down
2 changes: 1 addition & 1 deletion src/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
return serveFile(ctx, favicon.get())
let node = await urlToNode(path, ctx)
if (node?.default && (path.endsWith('/') || !node.default.match(/\.html?$/i))) // final/ needed on browser to make resource urls correctly
node = await urlToNode(node.default, ctx, node)
node = await urlToNode(node.default, ctx, node) ?? node
if (!node)
return sendErrorPage(ctx, HTTP_NOT_FOUND)
if (ctx.method === 'POST') { // curl -F upload=@file url/
Expand Down
3 changes: 2 additions & 1 deletion src/vfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface VfsNode extends VfsPerms {
name?: string
source?: string
children?: VfsNode[]
default?: string
default?: string | false // we could have used empty string to override inherited default, but false is clearer, even reading the yaml, and works well with pickProps(), where empty strings are removed
mime?: string | Record<string,string>
rename?: Record<string, string>
masks?: Masks // express fields for descendants that are not in the tree
Expand Down Expand Up @@ -60,6 +60,7 @@ function inheritFromParent(parent: VfsNode, child: VfsNode) {
else
child.mime ??= parent.mime
child.accept ??= parent.accept
child.default ??= parent.default
return child
}

Expand Down
2 changes: 1 addition & 1 deletion tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('basics', () => {
//before(async () => appStarted)
it('frontend', req('/', /<body>/, { headers: { accept: '*/*' } })) // workaround: 'accept' is necessary when running server-for-test-dev, still don't know why
it('force slash', req('/f1', 302))
it('list', reqList('/f1/', { inList:['f2/', 'page'] }))
it('list', reqList('/f1/', { inList:['f2/', 'page/'] }))
it('search', reqList('f1', { inList:['f2/'], outList:['page'] }, { search:'2' }))
it('search root', reqList('/', { inList:['cantListPage/'], outList:['cantListPage/page/'] }, { search:'page' }))
it('download', req('/f1/f2/alfa.txt', { re:/abcd/, mime:'text/plain' }))
Expand Down

0 comments on commit 7aaa79c

Please sign in to comment.