now 'delete' is only available as method and not as "api"
Massimo Melina committed
Mar 7, 2025 at 10:12 UTC
09a6eed63b3e73c8b04a3bfdae9ef37f009b09f4
6 files changed
+28
-51
frontend/src/menu.ts
+1
-1
@@ -194,7 +194,7 @@ export async function deleteFiles(uris: string[]) {
194
return false
195
const stop = working()
196
const errors = onlyTruthy(await Promise.all(uris.map(uri =>
197
- apiCall('delete', { uri }).then(() => null, err => ({ uri, err }))
197
+ apiCall('delete', {}, { restUri: uri }).then(() => null, err => ({ uri, err }))
198
)))
199
stop()
200
reloadList()
shared/api.ts
+4
-2
@@ -21,6 +21,7 @@ interface ApiCallOptions {
21
method?: string
22
skipParse?: boolean
23
skipLog?: boolean
24
+ restUri?: string
25
}
26
27
const defaultApiCallOptions: ApiCallOptions = {}
@@ -38,9 +39,10 @@ export function apiCall<T=any>(cmd: string, params?: Dict, options: ApiCallOptio
39
controller.abort(aborted = 'timeout')
40
console.debug('API TIMEOUT', cmd, params??'')
41
}, ms)
42
+ const asRest = options.restUri
43
// rebuilding the whole url makes it resistant to url-with-credentials
42
- return Object.assign(fetch(`${location.origin}${getPrefixUrl()}${API_URL}${cmd}`, {
43
- method: options.method || 'POST',
44
+ return Object.assign(fetch(`${location.origin}${getPrefixUrl()}${asRest || (API_URL + cmd)}`, {
45
+ method: asRest ? cmd : (options.method || 'POST'),
46
headers: { 'content-type': 'application/json', 'x-hfs-anti-csrf': '1' },
47
signal: controller.signal,
48
body: params && JSON.stringify(params),
src/frontEndApis.ts
+1
-14
@@ -17,7 +17,7 @@ import fs from 'fs'
17
import { mkdir, rename, copyFile, unlink } from 'fs/promises'
18
import { basename, dirname, join } from 'path'
19
import { getUploadMeta } from './upload'
20
-import { apiAssertTypes, deleteNode, popKey } from './misc'
20
+import { apiAssertTypes, popKey } from './misc'
21
import { getCommentFor, setCommentFor } from './comments'
22
import { SendListReadable } from './SendList'
23
import { ctxAdminAccess } from './adminApis'
@@ -80,19 +80,6 @@ export const frontEndApis: ApiHandlers = {
80
}
81
},
82
83
- async delete({ uri }, ctx) {
84
- apiAssertTypes({ string: { uri } })
85
- const node = await urlToNode(uri, ctx)
86
- if (!node)
87
- throw new ApiError(HTTP_NOT_FOUND)
88
- const res = await deleteNode(ctx, node, uri)
89
- if (typeof res === 'number')
90
- throw new ApiError(res)
91
- if (res instanceof Error)
92
- throw new ApiError(HTTP_SERVER_ERROR, res)
93
- return res && {}
94
- },
95
-
83
async rename({ uri, dest }, ctx) {
84
apiAssertTypes({ string: { uri, dest } })
85
ctx.logExtra(null, { target: decodeURI(uri), destination: decodeURI(dest) })
src/misc.ts
-18
@@ -141,21 +141,3 @@ export function createStreamLimiter(limit: number) {
141
}
142
})
143
}
144
-
145
-export async function deleteNode(ctx: Koa.Context, node: VfsNode, uri: string) {
146
- const { source } = node
147
- if (!source)
148
- return HTTP_METHOD_NOT_ALLOWED
149
- if (statusCodeForMissingPerm(node, 'can_delete', ctx))
150
- return ctx.status
151
- try {
152
- if ((await events.emitAsync('deleting', { node, ctx }))?.isDefaultPrevented())
153
- return null // stop
154
- ctx.logExtra(null, { target: decodeURI(uri) })
155
- await rm(source, { recursive: true })
156
- void setCommentFor(source, '') // necessary only to clean a possible descript.ion or kvstorage
157
- return true
158
- } catch (e: any) {
159
- return e
160
- }
161
-}
src/serveGuiAndSharedFiles.ts
+17
-10
@@ -5,7 +5,7 @@ import { sendErrorPage } from './errorPages'
5
import events from './events'
6
import {
7
ADMIN_URI, FRONTEND_URI, HTTP_BAD_REQUEST, HTTP_FORBIDDEN, HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND,
8
- HTTP_UNAUTHORIZED, HTTP_SERVER_ERROR, HTTP_OK, ICONS_URI
8
+ HTTP_UNAUTHORIZED, HTTP_SERVER_ERROR, HTTP_OK, ICONS_URI, HTTP_FAILED_DEPENDENCY
9
} from './cross-const'
10
import { uploadWriter } from './upload'
11
import formidable from 'formidable'
@@ -17,7 +17,9 @@ import { allowAdmin, favicon } from './adminApis'
17
import { serveGuiFiles } from './serveGuiFiles'
18
import mount from 'koa-mount'
19
import { baseUrl } from './listen'
20
-import { asyncGeneratorToReadable, deleteNode, filterMapGenerator, pathEncode, try_ } from './misc'
20
+import { asyncGeneratorToReadable, filterMapGenerator, pathEncode, try_ } from './misc'
21
+import { rm } from 'fs/promises'
22
+import { setCommentFor } from './comments'
23
import { basicWeb, detectBasicAgent } from './basicWeb'
24
import { customizedIcons, ICONS_FOLDER } from './icons'
25
import { getPluginInfo } from './plugins'
@@ -104,16 +106,21 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
106
return
107
}
108
if (ctx.method === 'DELETE') {
107
- const res = await deleteNode(ctx, node, ctx.path)
108
- if (typeof res === 'number')
109
- return ctx.status = res
110
- if (res instanceof Error) {
111
- ctx.body = res.message || String(res)
109
+ const { source } = node
110
+ if (!source)
111
+ return ctx.status = HTTP_METHOD_NOT_ALLOWED
112
+ if (statusCodeForMissingPerm(node, 'can_delete', ctx))
113
+ return
114
+ try {
115
+ if ((await events.emitAsync('deleting', { node, ctx }))?.isDefaultPrevented())
116
+ return ctx.status = HTTP_FAILED_DEPENDENCY
117
+ await rm(source, { recursive: true })
118
+ void setCommentFor(source, '') // necessary only to clean a possible descript.ion or kvstorage
119
+ return ctx.status = HTTP_OK
120
+ } catch (e: any) {
121
+ ctx.body = String(e)
122
return ctx.status = HTTP_SERVER_ERROR
123
}
114
- if (res)
115
- return ctx.status = HTTP_OK
116
- return
124
}
125
const { get } = ctx.query
126
if (node.default && path.endsWith('/') && !get) { // final/ needed on browser to make resource urls correctly with html pages
tests/test.ts
+5
-6
@@ -141,9 +141,8 @@ describe('basics', () => {
141
})
142
})
143
it('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 'temp' }, 401))
144
- it('delete.no perm', reqApi('delete', { uri: '/for-admins/' }, 405))
145
- it('delete.need account', reqApi('delete', { uri: UPLOAD_ROOT }, 401))
146
- it('delete.need account.method', req(UPLOAD_ROOT, 401, { method: 'DELETE' }))
144
+ it('delete.no perm', req('/for-admins/', 405, { method: 'delete' }))
145
+ it('delete.need account', req(UPLOAD_ROOT, 401, { method: 'delete'}))
146
it('rename.no perm', reqApi('rename', { uri: '/for-admins', dest: 'any' }, 401))
147
it('of_disabled.cantLogin', () => login('of_disabled').then(() => { throw Error('logged in') }, () => 0))
148
it('allow_net.canLogin', () => login('rejetto')) // localhost is normally resolved as ::1
@@ -230,11 +229,11 @@ describe('after-login', () => {
229
})
230
const renameTo = 'z'
231
it('rename.ok', reqApi('rename', { uri: UPLOAD_DEST, dest: renameTo }, 200))
233
- it('delete.miss renamed', reqApi('delete', { uri: UPLOAD_DEST }, 404))
234
- it('delete.ok', reqApi('delete', { uri: dirname(UPLOAD_DEST) + '/' + renameTo }, 200))
232
+ it('delete.miss renamed', req(UPLOAD_DEST, 404, { method: 'delete' }))
233
+ it('delete.ok', req(dirname(UPLOAD_DEST) + '/' + renameTo, 200, { method: 'delete' }))
234
it('reupload', reqUpload(UPLOAD_DEST, 200))
235
it('delete.method', req(UPLOAD_DEST, 200, { method: 'DELETE' }))
237
- it('delete.miss deleted', reqApi('delete', { uri: UPLOAD_DEST }, 404))
236
+ it('delete.miss deleted', req(UPLOAD_DEST, 404, { method: 'delete' }))
237
it('upload.too much', async () => {
238
const fn = 'temp/tooMuch'
239
const wrongSize = BIG_CONTENT.length / 2