create folder
Massimo Melina committed
Feb 12, 2023 at 21:28 UTC
6d06ee0e051f540a02b74742811cbfef2567856d
5 files changed
+69
-18
frontend/src/upload.ts
+38
-14
@@ -2,12 +2,12 @@
2
3
import { createElement as h, useMemo, useState } from 'react'
4
import { Flex, FlexV } from './components'
5
-import { DialogCloser, formatBytes, hIcon, newDialog, prefix } from './misc'
5
+import { closeDialog, DialogCloser, formatBytes, hIcon, newDialog, prefix } from './misc'
6
import _ from 'lodash'
7
import { proxy, ref, subscribe, useSnapshot } from 'valtio'
8
-import { alertDialog, confirmDialog } from './dialog'
8
+import { alertDialog, confirmDialog, promptDialog } from './dialog'
9
import { reloadList } from './useFetchList'
10
-import { getNotification } from './api'
10
+import { apiCall, getNotification } from './api'
11
import { useSnapState } from './state'
12
import { Link } from 'react-router-dom'
13
@@ -63,7 +63,7 @@ export function showUpload() {
63
doneByte: 0,
64
})
65
const close = newDialog({
66
- dialogProps: { style: { minHeight: '6em', minWidth: 'min(20em, 100vw - 1em)' } },
66
+ dialogProps: { style: { minWidth: 'min(20em, 100vw - 1em)' } },
67
title: "Upload",
68
icon: () => hIcon('upload'),
69
Content,
@@ -81,16 +81,19 @@ export function showUpload() {
81
const etaStr = useMemo(() => !eta ? '' : formatTime(eta*1000, 0, 2), [eta])
82
83
return h(FlexV, { props: acceptDropFiles(x => setFiles([ ...files, ...x ])) },
84
- h(Flex, { gap: '.5em', flexWrap: 'wrap', justifyContent: 'center', position: 'sticky', top: -4, background: 'var(--bg)', boxShadow: '0 3px 3px #000' },
85
- can_upload && h('button', { onClick: () => selectFiles() }, "Add file(s)"),
86
- can_upload && h('button', { onClick: () => selectFiles(true) }, "Add folder"),
87
- files.length > 0 && h('button', {
88
- onClick() {
89
- enqueue(files)
90
- setFiles([])
91
- }
92
- }, `Send ${files.length} file(s), ${formatBytes(files.reduce((a, f) => a + f.size, 0))}`),
93
- files.length > 1 && h('button', { onClick() { setFiles([]) } }, "Clear"),
84
+ h(FlexV, { position: 'sticky', top: -4, background: 'var(--bg)' },
85
+ h(Flex, { justifyContent: 'center', flexWrap: 'wrap', },
86
+ can_upload && h('button', { onClick: () => selectFiles() }, "Pick files"),
87
+ can_upload && h('button', { onClick: () => selectFiles(true) }, "Pick folder"),
88
+ files.length > 0 && h('button', {
89
+ onClick() {
90
+ enqueue(files)
91
+ setFiles([])
92
+ }
93
+ }, `Send ${files.length} files, ${formatBytes(files.reduce((a, f) => a + f.size, 0))}`),
94
+ files.length > 1 && h('button', { onClick() { setFiles([]) } }, "Clear"),
95
+ can_upload && h('button', { onClick: createFolder }, "Create folder"),
96
+ ),
97
),
98
h(FilesList, {
99
files,
@@ -328,4 +331,25 @@ export function acceptDropFiles(cb: false | ((files:File[]) => void)) {
331
cb && cb(Array.from(ev.dataTransfer!.files))
332
},
333
}
334
+}
335
+
336
+async function createFolder() {
337
+ const name = await promptDialog("Enter folder name")
338
+ if (!name) return
339
+ const path = location.pathname
340
+ try {
341
+ await apiCall('create_folder', { path, name })
342
+ reloadList()
343
+ return alertDialog(h(() =>
344
+ h(FlexV, {},
345
+ h('div', {}, "Successfully created"),
346
+ h(Link, { to: path + name + '/', onClick() {
347
+ closeDialog()
348
+ closeDialog()
349
+ } }, "Enter the folder"),
350
+ )))
351
+ }
352
+ catch(e: any) {
353
+ await alertDialog(e.code === 409 ? "Folder with same name already exists" : e)
354
+ }
355
}
\ No newline at end of file
src/api.vfs.ts
+1
-1
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from 'path'
8
import { dirStream, isWindowsDrive, objSameKeys } from './misc'
9
import {
10
IS_WINDOWS,
11
- HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE
11
+ HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE,
12
} from './const'
13
import { isMatch } from 'micromatch'
14
import { getDrives } from './util-os'
src/frontEndApis.ts
+26
-2
@@ -1,11 +1,16 @@
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 { ApiHandlers, SendListReadable } from './apiMiddleware'
3
+import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
4
import { file_list } from './api.file_list'
5
import * as api_auth from './api.auth'
6
import { defineConfig } from './config'
7
import events from './events'
8
import Koa from 'koa'
9
+import { dirTraversal, isValidFileName } from './util-files'
10
+import { HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_NOT_FOUND } from './const'
11
+import { hasPermission, urlToNode } from './vfs'
12
+import { mkdir } from 'fs/promises'
13
+import { join } from 'path'
14
15
const customHeader = defineConfig('custom_header')
16
@@ -25,7 +30,26 @@ export const frontEndApis: ApiHandlers = {
30
list.custom({ name, data })
31
}
32
})
28
- }
33
+ },
34
+
35
+ async create_folder({ path, name }, ctx) {
36
+ if (!isValidFileName(name) || dirTraversal(name))
37
+ return new ApiError(HTTP_BAD_REQUEST, 'bad name')
38
+ const parentNode = await urlToNode(path)
39
+ if (!parentNode)
40
+ return new ApiError(HTTP_NOT_FOUND, 'parent not found')
41
+ const { source } = parentNode
42
+ if (!source || !hasPermission(parentNode, 'can_upload', ctx))
43
+ return new ApiError(HTTP_FORBIDDEN)
44
+ try {
45
+ await mkdir(join(source, name))
46
+ return {}
47
+ }
48
+ catch(e:any) {
49
+ return new ApiError(e.code === 'EEXIST' ? HTTP_CONFLICT : HTTP_BAD_REQUEST, e)
50
+ }
51
+ },
52
+
53
}
54
55
export function notifyClient(ctx: Koa.Context, name: string, data: any) {
src/util-files.ts
+4
@@ -139,3 +139,7 @@ export async function prepareFolder(path: string, dirnameIt=true) {
139
return false
140
}
141
}
142
+
143
+export function isValidFileName(name: string) {
144
+ return !/^\.\.?$|[/:*?"<>|\\]/.test(name)
145
+}
\ No newline at end of file
todo.md
-1
@@ -1,5 +1,4 @@
1
# To do
2
-- frontend: new-folder button in upload
2
- admin/fs: check if source exists when set
3
- plugins: after installing, switch to installed (and perhaps highlight new one)
4
- plugins' log, accessible in admin