better code: renamed identifiers
Massimo Melina committed
Nov 24, 2025 at 21:16 UTC
a85a9c03c0ca994ea6f0f072d2a671f1cb842205
14 files changed
+59
-51
admin/src/LogsPage.ts
+1
-1
@@ -211,7 +211,7 @@ export function LogFile({ file, footerSide, hidden, limit, filter, ...rest }: Lo
211
])),
212
initialState: isIps ? { sorting: { sortModel: [{ field: 'ts', sort: 'desc' }] } } : undefined,
213
...rest,
214
- footerSide: width => h(Box, {}, // 4 icons don't fit the tabs row on mobile
214
+ footerSide: width => h(Box, {}, // 4 icons don't fit the tab row on mobile
215
pauseButton,
216
showApiButton,
217
!connecting && skipped > 0 && h(Btn, {
frontend/src/upload.ts
+2
-2
@@ -226,8 +226,8 @@ export function UploadStatus({ snapshot, ...props }: { snapshot?: INTERNAL_Snaps
226
label: t('copy_links', "Copy links"),
227
asText: true,
228
successFeedback: true,
229
- onClick() {
230
- copyTextToClipboard(done.map(x => location.origin + x.response.uri).join('\n'))
229
+ async onClick() {
230
+ await copyTextToClipboard(done.map(x => location.origin + x.response.uri).join('\n'))
231
operationSuccessful()
232
}
233
}),
shared/index.ts
+15
-9
@@ -181,17 +181,23 @@ export function createDurationFormatter({ locale=undefined, unitDisplay='narrow'
181
}
182
}
183
184
-export function copyTextToClipboard(text: string) {
185
- //navigator.clipboard.writeText(text) // this method works only in https and localhost
186
- const d = document
187
- const ta = d.createElement("textarea")
188
- ta.textContent = text
189
- d.body.appendChild(ta)
190
- ta.select()
191
- d.execCommand("copy")
192
- d.body.removeChild(ta)
184
+export async function copyTextToClipboard(text: string) {
185
+ try {
186
+ await navigator.clipboard.writeText(text) // this method works only in https and localhost
187
+ }
188
+ catch {
189
+ console.debug('fallback clipboard method')
190
+ const d = document
191
+ const ta = d.createElement("textarea")
192
+ ta.textContent = text
193
+ d.body.appendChild(ta)
194
+ ta.select()
195
+ d.execCommand("copy")
196
+ d.body.removeChild(ta)
197
+ }
198
}
199
200
+
201
export function downloadFileWithContent(name: string, content: Blob | string) {
202
const blob = content instanceof Blob ? content : new Blob([content], {type: 'text/plain'})
203
const a = document.createElement('a')
src/comments.ts
+3
-3
@@ -1,7 +1,7 @@
1
import { defineConfig } from './config'
2
import { dirname, basename, join } from 'path'
3
import { CFG } from './cross'
4
-import { parseFileContent, parseFileCache, safeWriteStream } from './util-files'
4
+import { parseFile, parseFileCache, createSafeWriteStream } from './util-files'
5
import { loadFileAttr, singleWorkerFromBatchWorker, storeFileAttr } from './misc'
6
import _ from 'lodash'
7
import iconv from 'iconv-lite'
@@ -56,7 +56,7 @@ const setCommentDescriptIon = singleWorkerFromBatchWorker(async (jobs: [path: st
56
if (!comments.size)
57
return unlink(path)
58
// encode comments in descript.ion format
59
- const ws = await safeWriteStream(path)
59
+ const ws = await createSafeWriteStream(path)
60
comments.forEach((comment, filename) => {
61
const multiline = comment.includes('\n')
62
const line = (filename.includes(' ') ? `"${filename}"` : filename)
@@ -77,7 +77,7 @@ export function areCommentsEnabled() {
77
const MULTILINE_SUFFIX = Buffer.from([4, 0xC2])
78
function readDescriptIon(path: string) {
79
// decoding could also be done with native TextDecoder.decode, but we need iconv for the encoding anyway
80
- return parseFileContent(join(path, DESCRIPT_ION), raw => {
80
+ return parseFile(join(path, DESCRIPT_ION), raw => {
81
// for simplicity we "remove" the sequence MULTILINE_SUFFIX before iconv.decode messes it up
82
for (let i=0; i<raw.length; i++)
83
if (raw[i] === MULTILINE_SUFFIX[0] && raw[i+1] === MULTILINE_SUFFIX[1] && [undefined,13,10].includes(raw[i+2]))
src/first.ts
+7
-5
@@ -2,21 +2,23 @@
2
import { EventEmitter } from 'events'
3
4
type ProcessExitHandler = (signal:string) => any
5
-const cbs = new Set<ProcessExitHandler>()
5
+const cbsOnExit = new Set<ProcessExitHandler>()
6
export function onProcessExit(cb: ProcessExitHandler) {
7
- cbs.add(cb)
8
- return () => cbs.delete(cb)
7
+ cbsOnExit.add(cb)
8
+ return () => cbsOnExit.delete(cb)
9
}
10
11
export let quitting = false
12
onProcessExit(() => quitting = true)
13
14
+// 'exit' event is handled as the last resort, but it's not compatible with async callbacks
15
onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal =>
15
- Promise.allSettled(Array.from(cbs).map(cb => cb(signal))).then(() => {
16
- console.log('quitting')
16
+ Promise.allSettled(Array.from(cbsOnExit).map(cb => cb(signal))).then(() => {
17
+ console.log('quitting', signal||'')
18
process.exit(0)
19
}))
20
21
+// keep calling cb in a sync fashion – returning a promise instead would break the code for argv.updating (update.ts)
22
export function onFirstEvent(emitter:EventEmitter, events: string[], cb: (...args:any[])=> void) {
23
let already = false
24
for (const e of events)
src/frontEndApis.ts
+2
-2
@@ -5,7 +5,7 @@ import { get_file_list } from './api.get_file_list'
5
import * as api_auth from './api.auth'
6
import events from './events'
7
import Koa from 'koa'
8
-import { dirTraversal, isValidFileName } from './util-files'
8
+import { hasDirTraversal, isValidFileName } from './util-files'
9
import {
10
HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FAILED_DEPENDENCY, HTTP_FORBIDDEN, HTTP_METHOD_NOT_ALLOWED,
11
HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED
@@ -86,7 +86,7 @@ export const frontEndApis: ApiHandlers = {
86
const node = await urlToNode(uri, ctx)
87
if (!node)
88
throw new ApiError(HTTP_NOT_FOUND)
89
- if (isRoot(node) || dest.includes('/') || dirTraversal(dest))
89
+ if (isRoot(node) || dest.includes('/') || hasDirTraversal(dest))
90
throw new ApiError(HTTP_FORBIDDEN)
91
if (!hasPermission(node, 'can_delete', ctx))
92
throw new ApiError(HTTP_UNAUTHORIZED)
src/log.ts
+2
-2
@@ -7,7 +7,7 @@ import { defineConfig } from './config'
7
import { createWriteStream, renameSync, statSync } from 'fs'
8
import * as util from 'util'
9
import _ from 'lodash'
10
-import { createFileWithPath, prepareFolder, statWithTimeout } from './util-files'
10
+import { createFileWithPath, ensureParentFolder, statWithTimeout } from './util-files'
11
import { getCurrentUsername } from './auth'
12
import { DAY, makeNetMatcher, tryJson, Dict, Falsy, CFG, strinsert, repeat, formatTimestamp, HTTP_NOT_FOUND } from './misc'
13
import { extname } from 'path'
@@ -36,7 +36,7 @@ class Logger {
36
this.last = stats.mtime
37
}
38
catch {
39
- if (await prepareFolder(path) === false)
39
+ if (await ensureParentFolder(path) === false)
40
console.log("cannot create folder for", path)
41
}
42
this.reopen()
src/middlewares.ts
+2
-2
@@ -3,7 +3,7 @@
3
import compress from 'koa-compress'
4
import Koa from 'koa'
5
import { API_URI, DEV, HTTP_FOOL } from './const'
6
-import { ALLOW_SESSION_IP_CHANGE, DAY, dirTraversal, isLocalHost, netMatches, splitAt, stream2string, tryJson } from './misc'
6
+import { ALLOW_SESSION_IP_CHANGE, DAY, hasDirTraversal, isLocalHost, netMatches, splitAt, stream2string, tryJson } from './misc'
7
import { Readable } from 'stream'
8
import { applyBlock } from './block'
9
import { Account, accountCanLogin, getAccount, getFromAccount } from './perm'
@@ -60,7 +60,7 @@ export const someSecurity: Koa.Middleware = (ctx, next) => {
60
}
61
62
try {
63
- if (dirTraversal(decodeURI(ctx.path)))
63
+ if (hasDirTraversal(decodeURI(ctx.path)))
64
return ctx.status = HTTP_FOOL
65
if (!ctx.state.skipFilters && applyBlock(ctx.socket, ctx.ip))
66
return
src/plugins.ts
+1
-1
@@ -414,7 +414,7 @@ export async function rescan() {
414
console.debug('scanning plugins')
415
const patterns = [PATH + '/*']
416
if (APP_PATH !== process.cwd())
417
- patterns.unshift(adjustStaticPathForGlob(APP_PATH) + '/' + patterns[0]) // first search bundled plugins, because otherwise they won't be loaded because of the folders with same name in .hfs/plugins (used for storage)
417
+ patterns.unshift(escapeGlobPath(APP_PATH) + '/' + patterns[0]) // first search bundled plugins, because otherwise they won't be loaded because of the folders with same name in .hfs/plugins (used for storage)
418
const met = []
419
for (const { path, dirent } of await glob(patterns, { onlyFiles: false, suppressErrors: true, objectMode: true })) {
420
if (!dirent.isDirectory() || path.endsWith(DISABLING_SUFFIX)) continue
src/serveGuiAndSharedFiles.ts
+2
-2
@@ -18,7 +18,7 @@ import { preventAdminAccess, favicon } from './adminApis'
18
import { serveGuiFiles } from './serveGuiFiles'
19
import mount from 'koa-mount'
20
import { baseUrl } from './listen'
21
-import { asyncGeneratorToReadable, filterMapGenerator, parseFile, pathEncode, try_ } from './misc'
21
+import { asyncGeneratorToReadable, filterMapGenerator, loadFileCached, pathEncode, try_ } from './misc'
22
import XXH from 'xxhashjs'
23
import fs from 'fs'
24
import { rm } from 'fs/promises'
@@ -68,7 +68,7 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
68
ctx.state.uploadPath = decPath
69
if (getUploadTempHash)
70
return !folder.source ? sendErrorPage(ctx, HTTP_NOT_FOUND)
71
- : ctx.body = await parseFile(getUploadTempFor(join(folder.source, rest)), calcHash) // negligible memory leak
71
+ : ctx.body = await loadFileCached(getUploadTempFor(join(folder.source, rest)), calcHash) // negligible memory leak
72
const dest = uploadWriter(folder, folderUri, rest, ctx)
73
if (dest) {
74
ctx.req.pipe(dest).on('error', err => {
src/serveGuiFiles.ts
+2
-2
@@ -11,7 +11,7 @@ import { refresh_session } from './api.auth'
11
import { ApiError } from './apiMiddleware'
12
import { join, extname } from 'path'
13
import {
14
- CFG, debounceAsync, formatBytes, FRONTEND_OPTIONS, isPrimitive, newObj, objSameKeys, onlyTruthy, parseFileContent,
14
+ CFG, debounceAsync, formatBytes, FRONTEND_OPTIONS, isPrimitive, newObj, objSameKeys, onlyTruthy, parseFile,
15
enforceStarting, statWithTimeout
16
} from './misc'
17
import { favicon, title } from './adminApis'
@@ -43,7 +43,7 @@ function serveStatic(uri: string): Koa.Middleware {
43
return ctx.status = HTTP_METHOD_NOT_ALLOWED
44
const serveApp = shouldServeApp(ctx)
45
const fullPath = join(__dirname, '..', folder, serveApp ? '/index.html': ctx.path)
46
- const content = await parseFileContent(fullPath,
46
+ const content = await parseFile(fullPath,
47
raw => serveApp || !raw.length ? raw : adjustBundlerLinks(ctx, uri, raw) )
48
.catch(() => null)
49
if (content === null)
src/upload.ts
+2
-2
@@ -7,7 +7,7 @@ import {
7
import { basename, dirname, extname, join } from 'path'
8
import fs from 'fs'
9
import {
10
- dirTraversal, loadFileAttr, pendingPromise, storeFileAttr, try_, createStreamLimiter, pathEncode,
10
+ hasDirTraversal, loadFileAttr, pendingPromise, storeFileAttr, try_, createStreamLimiter, pathEncode,
11
enforceFinal, Timeout,
12
} from './misc'
13
import { defineConfig } from './config'
@@ -60,7 +60,7 @@ const diskSpaceCache = expiringCache<ReturnType<typeof getDiskSpaceSync>>(3_000)
60
const uploadingFiles = new Map<string, { ctx: Koa.Context, size: number, got: number }>()
61
// stay sync because we use this function with formidable()
62
export function uploadWriter(base: VfsNode, baseUri: string, path: string, ctx: Koa.Context) {
63
- if (dirTraversal(path))
63
+ if (hasDirTraversal(path))
64
return fail(HTTP_FOOL)
65
if (statusCodeForMissingPerm(base, 'can_upload', ctx))
66
return fail()
src/util-files.ts
+16
-16
@@ -34,12 +34,12 @@ export async function isDirectory(path: string) {
34
catch {}
35
}
36
37
-export async function readFileBusy(path: string): Promise<string> {
37
+export async function readFileWithBusyRetry(path: string): Promise<string> {
38
return readFile(path, 'utf8').catch(e => {
39
if ((e as any)?.code !== 'EBUSY')
40
throw e
41
console.debug('busy')
42
- return wait(100).then(()=> readFileBusy(path))
42
+ return wait(100).then(()=> readFileWithBusyRetry(path))
43
})
44
}
45
@@ -82,35 +82,35 @@ export function watchDir(dir: string, cb: ()=>void, atStart=false) {
82
}
83
}
84
85
-export function dirTraversal(s?: string) {
85
+export function hasDirTraversal(s?: string) {
86
return s && /(^|[/\\])\.\.($|[/\\])/.test(s)
87
}
88
89
// apply this to paths that may contain \ as separator (not supported by fast-glob) and other special chars to be escaped (parenthesis)
90
-export function adjustStaticPathForGlob(path: string) {
90
+export function escapeGlobPath(path: string) {
91
return glob.escapePath(path.replace(/\\/g, '/'))
92
}
93
94
export async function unzip(stream: Readable, cb: (path: string) => Promisable<false | string>) {
95
- let pending: Promise<any> = Promise.resolve()
95
+ let chain: Promise<any> = Promise.resolve()
96
return new Promise((resolve, reject) =>
97
stream.pipe(unzipper.Parse())
98
- .on('end', () => pending.then(resolve))
98
+ .on('end', () => chain.then(resolve))
99
.on('error', reject)
100
.on('entry', (entry: any) =>
101
- pending = pending.then(async () => { // don't overlap writings
101
+ chain = chain.then(async () => { // don't overlap writings
102
const { path, type } = entry
103
const dest = await try_(() => cb(path), e => console.warn(String(e)))
104
if (!dest || type !== 'File')
105
return entry.autodrain()
106
console.debug('unzip', dest)
107
- const thisFile = entry.pipe(await safeWriteStream(dest))
107
+ const thisFile = entry.pipe(await createSafeWriteStream(dest))
108
await once(thisFile, 'finish')
109
}) )
110
)
111
}
112
113
-export async function prepareFolder(path: string, dirnameIt=true) {
113
+export async function ensureParentFolder(path: string, dirnameIt=true) {
114
if (dirnameIt)
115
path = dirname(path)
116
if (isWindowsDrive(path)) return
@@ -133,8 +133,8 @@ export function createFileWithPath(path: string, options?: Parameters<typeof cre
133
return createWriteStream(path, options)
134
}
135
136
-export async function safeWriteStream(path: string, options?: Parameters<typeof createWriteStream>[1]) {
137
- await prepareFolder(path)
136
+export async function createSafeWriteStream(path: string, options?: Parameters<typeof createWriteStream>[1]) {
137
+ await ensureParentFolder(path)
138
return new Promise<ReturnType<typeof createWriteStream>>((resolve, reject) => {
139
const first = createWriteStream(path, options)
140
.on('open', () => resolve(first))
@@ -156,7 +156,7 @@ export async function safeWriteStream(path: string, options?: Parameters<typeof
156
}
157
158
export function isValidFileName(name: string) {
159
- return !(IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !dirTraversal(name)
159
+ return !(IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !hasDirTraversal(name)
160
}
161
162
export function exists(path: string) {
@@ -165,16 +165,16 @@ export function exists(path: string) {
165
166
// parse a file, caching unless timestamp has changed
167
export const parseFileCache = new Map<string, { ts: Date, parsed: unknown }>()
168
-export async function parseFile<T>(path: string, parse: (path: string) => T) {
168
+export async function loadFileCached<T>(path: string, loader: (path: string) => T) {
169
const { mtime: ts } = await statWithTimeout(path)
170
const cached = parseFileCache.get(path)
171
if (cached && Number(ts) === Number(cached.ts))
172
return cached.parsed as T
173
- const parsed = parse(path)
173
+ const parsed = loader(path)
174
parseFileCache.set(path, { ts, parsed })
175
return parsed
176
}
177
178
-export async function parseFileContent<T>(path: string, parse: (raw: Buffer) => T) {
179
- return parseFile(path, () => readFile(path).then(parse))
178
+export async function parseFile<T>(path: string, parse: (raw: Buffer) => T) {
179
+ return loadFileCached(path, () => readFile(path).then(parse))
180
}
\ No newline at end of file
src/watchLoad.ts
+2
-2
@@ -2,7 +2,7 @@
2
3
import { FSWatcher, watch } from 'fs'
4
import fs from 'fs/promises'
5
-import { readFileBusy } from './util-files'
5
+import { readFileWithBusyRetry } from './util-files'
6
import { debounceAsync } from './debounceAsync'
7
import { BetterEventEmitter } from './events'
8
@@ -56,7 +56,7 @@ export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>, {
56
if (doing) return
57
doing = true
58
try {
59
- const text = await readFileBusy(path).catch(e => { // ignore read errors
59
+ const text = await readFileWithBusyRetry(path).catch(e => { // ignore read errors
60
if (e.code === 'EPERM')
61
console.error("missing permissions on file", path) // warn user, who could be clueless about this problem
62
return ''