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