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) })