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