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'