copy_files API (not used by UI, but you can install copy-files plugin) #680
Massimo Melina committed
Sep 21, 2024 at 11:56 UTC
13a068ab46e421cb6cf9a606e5f2d62b6a55cac0
4 files changed
+32
-17
dev-plugins.md
+3
-2
@@ -120,7 +120,8 @@ used must be strictly JSON (thus, no single quotes, only double quotes for strin
120
- `configDialog: DialogOptions` object to override dialog options. Please refer to sources for details.
121
- `onFrontendConfig: (config: object) => void | object` manipulate config values exposed to front-end.
122
- `customHtml: object | () => object` return custom-html sections programmatically.
123
-- `customRest: { [name]: (parameters: object) => any }` declare backend functions to be called by frontend with `HFS.customRestCall`
123
+- `customRest: { [name]: (parameters: object) => any }` declare backend functions to be called by frontend with `HFS.customRestCall`
124
+- `customApi: { [name]: (parameters) => any }` declare functions to be called by other plugins (only backend, not frontend) using `api.customApiCall` (documented below)
125
126
### FieldDescriptor
127
@@ -644,7 +645,7 @@ If you want to override a text regardless of the language, use the special langu
645
646
## API version history
647
647
-- 9.1 (v0.54.0)
648
+- 9.2 (v0.54.0)
649
- frontend event: showPlay
650
- api.addBlock
651
- api.misc
src/apiMiddleware.ts
+2
-1
@@ -14,7 +14,8 @@ export class ApiError extends Error {
14
}
15
}
16
type ApiHandlerResult = Record<string,any> | ApiError | Readable | AsyncGenerator<any> | null
17
-export type ApiHandler = (params:any, ctx:Koa.Context) => Promisable<ApiHandlerResult>
17
+// allow defining extra parameters that can be used when an api to invoke another (like copy_files)
18
+export type ApiHandler = (params:any, ctx:Koa.Context, ...ignore: unknown[]) => Promisable<ApiHandlerResult>
19
export type ApiHandlers = Record<string, ApiHandler>
20
21
const logApi = defineConfig(CFG.log_api, true)
src/const.ts
+1
-1
@@ -7,7 +7,7 @@ import { mkdirSync } from 'fs'
7
import { basename, dirname, join } from 'path'
8
export * from './cross-const'
9
10
-export const API_VERSION = 9.1
10
+export const API_VERSION = 9.2
11
export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise it is made equal to API_VERSION
12
export const HFS_REPO = 'rejetto/hfs'
13
src/frontEndApis.ts
+26
-13
@@ -8,7 +8,8 @@ import Koa from 'koa'
8
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'
11
+import { hasPermission, statusCodeForMissingPerm, urlToNode, VfsNode } from './vfs'
12
+import fs from 'fs'
13
import { mkdir, rename, copyFile, unlink } from 'fs/promises'
14
import { basename, dirname, join } from 'path'
15
import { getUploadMeta } from './upload'
@@ -117,29 +118,41 @@ export const frontEndApis: ApiHandlers = {
118
}
119
},
120
120
- async move_files({ uri_from, uri_to }, ctx) {
121
+ async move_files({ uri_from, uri_to }, ctx, override) {
122
apiAssertTypes({ array: { uri_from }, string: { uri_to } })
123
ctx.logExtra(null, { target: uri_from.map(decodeURI), destination: decodeURI(uri_to) })
124
const destNode = await urlToNode(uri_to, ctx)
124
- const code = !destNode ? HTTP_NOT_FOUND : statusCodeForMissingPerm(destNode, 'can_upload', ctx)
125
- if (code) return new ApiError(code)
125
+ const err = !destNode ? HTTP_NOT_FOUND : statusCodeForMissingPerm(destNode, 'can_upload', ctx)
126
+ if (err)
127
+ return new ApiError(err)
128
return {
127
- errors: await Promise.all(uri_from.map(async (src: any) => {
128
- if (typeof src !== 'string') return HTTP_BAD_REQUEST
129
- const srcNode = await urlToNode(src, ctx)
130
- if (!srcNode) return HTTP_NOT_FOUND
131
- const s = srcNode.source!
132
- const d = join(destNode!.source!, basename(srcNode.source!))
129
+ errors: await Promise.all(uri_from.map(async (from1: any) => {
130
+ if (typeof from1 !== 'string') return HTTP_BAD_REQUEST
131
+ const srcNode = await urlToNode(from1, ctx)
132
+ const src = srcNode?.source
133
+ if (!src) return HTTP_NOT_FOUND
134
+ const dest = join(destNode!.source!, basename(src))
135
+ if (_.isFunction(override))
136
+ return override?.(srcNode, dest)
137
return statusCodeForMissingPerm(srcNode, 'can_delete', ctx)
134
- || rename(s, d).catch(async e => {
138
+ || rename(src, dest).catch(async e => {
139
if (e.code !== 'EXDEV') throw e // exdev = different drive
136
- await copyFile(s, d)
137
- await unlink(s)
140
+ await copyFile(src, dest)
141
+ await unlink(src)
142
}).catch(e => e.code || String(e))
143
}))
144
}
145
},
146
147
+ async copy_files(params, ctx) {
148
+ return frontEndApis.move_files!(params, ctx, // same parameters
149
+ (srcNode: VfsNode, dest: string) => // but override behavior
150
+ statusCodeForMissingPerm(srcNode, 'can_read', ctx)
151
+ || copyFile(srcNode.source!, dest, fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE)
152
+ .catch(e => e.code || String(e))
153
+ )
154
+ },
155
+
156
async comment({ uri, comment }, ctx) {
157
apiAssertTypes({ string: { uri, comment } })
158
ctx.logExtra(null, { target: decodeURI(uri) })