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 ''