support HTTP method DELETE

Massimo Melina committed May 31, 2024 at 22:50 UTC f2f3d24a83aac16dd6c3b5d153f0885c72c0d9e9
4 files changed +50 -22
src/frontEndApis.ts
+8 -17
@@ -9,10 +9,10 @@ import { dirTraversal, isValidFileName } from './util-files'
9 import { HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FAILED_DEPENDENCY, HTTP_FORBIDDEN,
10 HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED } from './const'
11 import { hasPermission, statusCodeForMissingPerm, urlToNode } from './vfs'
12 -import { mkdir, rename, rm, copyFile, unlink } from 'fs/promises'
12 +import { mkdir, rename, copyFile, unlink } from 'fs/promises'
13 import { basename, dirname, join } from 'path'
14 import { getUploadMeta } from './upload'
15 -import { apiAssertTypes } from './misc'
15 +import { apiAssertTypes, deleteNode } from './misc'
16 import { getCommentFor, setCommentFor } from './comments'
17 import { SendListReadable } from './SendList'
18
@@ -69,24 +69,15 @@ export const frontEndApis: ApiHandlers = {
69
70 async delete({ uri }, ctx) {
71 apiAssertTypes({ string: { uri } })
72 - ctx.logExtra(null, { target: decodeURI(uri) })
72 const node = await urlToNode(uri, ctx)
73 if (!node)
74 throw new ApiError(HTTP_NOT_FOUND)
76 - if (!node.source)
77 - throw new ApiError(HTTP_FORBIDDEN)
78 - if (!hasPermission(node, 'can_delete', ctx))
79 - throw new ApiError(HTTP_UNAUTHORIZED)
80 - try {
81 - if (await events.emitAsync('deleting', { node, ctx }).preventDefault())
82 - return null // stop
83 - await rm(node.source, { recursive: true })
84 - void setCommentFor(node.source, '')
85 - return {}
86 - }
87 - catch (e: any) {
88 - throw new ApiError(HTTP_SERVER_ERROR, e)
89 - }
75 + const res = await deleteNode(ctx, node, uri)
76 + if (typeof res === 'number')
77 + throw new ApiError(res)
78 + if (res instanceof Error)
79 + throw new ApiError(HTTP_SERVER_ERROR, res)
80 + return res && {}
81 },
82
83 async rename({ uri, dest }, ctx) {
src/misc.ts
+23 -1
@@ -12,9 +12,13 @@ export * from './debounceAsync'
12 import { Readable, Transform } from 'stream'
13 import { SocketAddress, BlockList } from 'node:net'
14 import { ApiError } from './apiMiddleware'
15 -import { HTTP_BAD_REQUEST } from './const'
15 +import { HTTP_BAD_REQUEST, HTTP_METHOD_NOT_ALLOWED } from './const'
16 import { isIpLocalHost, makeMatcher } from './cross'
17 import { isIPv6 } from 'net'
18 +import { statusCodeForMissingPerm, VfsNode } from './vfs'
19 +import events from './events'
20 +import { rm } from 'fs/promises'
21 +import { setCommentFor } from './comments'
22
23 export function pattern2filter(pattern: string){
24 const matcher = makeMatcher(pattern.includes('*') ? pattern // if you specify *, we'll respect its position
@@ -119,4 +123,22 @@ export function createStreamLimiter(limit: number) {
123 cb()
124 }
125 })
126 +}
127 +
128 +export async function deleteNode(ctx: Koa.Context, node: VfsNode, uri: string) {
129 + const { source } = node
130 + if (!source)
131 + return HTTP_METHOD_NOT_ALLOWED
132 + if (statusCodeForMissingPerm(node, 'can_delete', ctx))
133 + return ctx.status
134 + try {
135 + if (await events.emitAsync('deleting', { node, ctx }).preventDefault())
136 + return null // stop
137 + ctx.logExtra(null, { target: decodeURI(uri) })
138 + await rm(source, { recursive: true })
139 + void setCommentFor(source, '')
140 + return true
141 + } catch (e: any) {
142 + return e
143 + }
144 }
\ No newline at end of file
src/serveGuiAndSharedFiles.ts
+14 -2
@@ -3,7 +3,7 @@ import { basename, dirname } from 'path'
3 import { getNodeName, nodeIsDirectory, statusCodeForMissingPerm, urlToNode, vfs, VfsNode, walkNode } from './vfs'
4 import { sendErrorPage } from './errorPages'
5 import { ADMIN_URI, FRONTEND_URI, HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND,
6 - HTTP_UNAUTHORIZED } from './cross-const'
6 + HTTP_UNAUTHORIZED, HTTP_SERVER_ERROR, HTTP_OK } from './cross-const'
7 import { uploadWriter } from './upload'
8 import { pipeline } from 'stream/promises'
9 import formidable from 'formidable'
@@ -15,7 +15,7 @@ import { allowAdmin, favicon } from './adminApis'
15 import { serveGuiFiles } from './serveGuiFiles'
16 import mount from 'koa-mount'
17 import { baseUrl } from './listen'
18 -import { asyncGeneratorToReadable, filterMapGenerator, pathEncode } from './misc'
18 +import { asyncGeneratorToReadable, deleteNode, filterMapGenerator, pathEncode } from './misc'
19 import { basicWeb } from './basicWeb'
20
21 const serveFrontendFiles = serveGuiFiles(process.env.FRONTEND_PROXY, FRONTEND_URI)
@@ -84,6 +84,18 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
84 res(Promise.all(locks))
85 }))
86 }
87 + if (ctx.method === 'DELETE') {
88 + const res = await deleteNode(ctx, node, ctx.path)
89 + if (typeof res === 'number')
90 + return ctx.status = res
91 + if (res instanceof Error) {
92 + ctx.body = res.message || String(res)
93 + return ctx.status = HTTP_SERVER_ERROR
94 + }
95 + if (res)
96 + return ctx.status = HTTP_OK
97 + return
98 + }
99 const { get } = ctx.query
100 if (node.default && path.endsWith('/') && !get) // final/ needed on browser to make resource urls correctly with html pages
101 node = await urlToNode(node.default, ctx, node) ?? node
tests/test.ts
+5 -2
@@ -109,8 +109,9 @@ describe('basics', () => {
109
110 it('upload.need account', reqUpload( UPLOAD_DEST, 401))
111 it('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 'temp' }, 401))
112 - it('delete.no perm', reqApi('delete', { uri: '/for-admins' }, 403))
113 - it('delete.need account', reqApi('delete', { uri: '/for-admins/upload' }, 401))
112 + it('delete.no perm', reqApi('delete', { uri: '/for-admins/' }, 405))
113 + it('delete.need account', reqApi('delete', { uri: UPLOAD_ROOT }, 401))
114 + it('delete.need account.method', req(UPLOAD_ROOT, 401, { method: 'DELETE' }))
115 it('rename.no perm', reqApi('delete', { uri: '/for-admins', dest: 'any' }, 403))
116 it('of_disabled.cantLogin', () => login('of_disabled').then(() => { throw Error('logged in') }, () => 0))
117 })
@@ -140,6 +141,8 @@ describe('after-login', () => {
141 it('rename.ok', reqApi('rename', { uri: UPLOAD_DEST, dest: renameTo }, 200))
142 it('delete.miss renamed', reqApi('delete', { uri: UPLOAD_DEST }, 404))
143 it('delete.ok', reqApi('delete', { uri: dirname(UPLOAD_DEST) + '/' + renameTo }, 200))
144 + it('reupload', reqUpload(UPLOAD_DEST, 200))
145 + it('delete.method', req(UPLOAD_DEST, 200, { method: 'DELETE' }))
146 it('delete.miss deleted', reqApi('delete', { uri: UPLOAD_DEST }, 404))
147 it('upload.size', async () => {
148 const fn = 'temp/size'