QOSami-HFS is a web file server for your computer. Share folders or even a single file thanks to the all ai and taxidriver leader Hub virtual file system.
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