@samitouri / QOSami-HFS / commits / 8c8ed2dc

better code

Massimo Melina committed Sep 15, 2023 at 21:17 UTC 8c8ed2dcaf6d2ad496235dd9e876a7b1daf3776f
12 files changed +51 -42
admin/src/FileForm.ts
+1 -5
@@ -15,7 +15,7 @@ import {
15 } from '@hfs/mui-grid-form'
16 import { apiCall, useApiEx } from './api'
17 import {
18 - basename, Btn, formatBytes, IconBtn, isEqualLax, LinkBtn, modifiedSx, newDialog, objSameKeys,
18 + basename, Btn, formatBytes, formatTimestamp, IconBtn, isEqualLax, LinkBtn, modifiedSx, newDialog, objSameKeys,
19 onlyTruthy, prefix, wikiLink
20 } from './misc'
21 import { reloadVfs, VfsNode, VfsPerms, Who } from './VfsPage'
@@ -152,10 +152,6 @@ function perm2word(perm: string) {
152 return word === 'read' ? 'download' : word
153 }
154
155 -function formatTimestamp(x: string) {
156 - return x ? new Date(x).toLocaleString() : '-'
157 -}
158 -
155 interface WhoFieldProps extends FieldProps<Who | undefined> {
156 accounts: Account[],
157 otherPerms: any[],
admin/src/OptionsPage.ts
+1 -1
@@ -156,7 +156,7 @@ export default function OptionsPage() {
156 { k: 'k', label: "File mask", $width: 1, $column: {
157 renderCell: ({ value, id }: any) => h('code', {},
158 value,
159 - value === '*' && id < Object.keys(values.mime).length - 1
159 + value === '*' && id < _.size(values.mime) - 1
160 && iconTooltip(Warning, md("Mime with `*` should be the last, because first matching row applies"), {
161 color: 'warning.main', ml: 1
162 }))
shared/api.ts
+2 -2
@@ -10,7 +10,7 @@ const timeoutByApi: Dict = {
10 loginSrp1: 90, // support antibrute
11 update: 600, // download can be lengthy
12 get_nat: 10,
13 - get_status: 20 // can be lengthy on slow machines because of the find-process-on-busy-port feature
13 + get_status: 20, // can be lengthy on slow machines because of the find-process-on-busy-port feature
14 }
15
16 interface ApiCallOptions {
@@ -84,7 +84,7 @@ export function useApi<T=any>(cmd: string | Falsy, params?: object) {
84 let req: undefined | ReturnType<typeof apiCall>
85 const wholePromise = wait(0) // postpone a bit, so that if it is aborted immediately, it is never really fired (happens mostly in dev mode)
86 .then(() => aborted ? undefined : req = apiCall<T>(cmd, params))
87 - .then(res => aborted || setData(res), err => aborted || setError(err))
87 + .then(res => aborted || setData(res), err => aborted || setError(err) || setData(undefined))
88 .finally(() => loadingRef.current = reloadingRef.current = undefined)
89 loadingRef.current = Object.assign(wholePromise, {
90 abort() {
src/api.net.ts
+5 -11
@@ -10,19 +10,13 @@ import { getIps, getServerStatus } from './listen'
10 import { getProjectInfo } from './github'
11 import { httpString } from './util-http'
12 import { exec } from 'child_process'
13 -import { debounceAsync, MINUTE, repeat } from './misc'
13 +import { debounceAsync, HOUR, MINUTE, repeat } from './misc'
14
15 -const client = new Client({ timeout: 5_000 })
16 -const original = client.getGateway
15 +const client = new Client({ timeout: 4_000 })
16 +const originalMethod = client.getGateway
17 // other client methods call getGateway too, so this will ensure they reuse this same result
18 -client.getGateway = function getGatewayCaching() {
19 - const promise = original.apply(client)
20 - client.getGateway = () => promise // multiple callings = same job
21 - promise.then(() => console.debug('caching gateway'), // store in cache only if successful.
22 - ()=> client.getGateway = getGatewayCaching) // failed, try again
23 - return promise
24 -}
25 -client.getGateway()
18 +client.getGateway = debounceAsync(() => originalMethod.apply(client), 0, { retain: HOUR, retainFailure: 30_000 })
19 +client.getGateway().catch(() => {})
20
21 export let externalIp = Promise.resolve('') // poll external ip
22 repeat(10 * MINUTE, () => {
src/config.ts
+2 -2
@@ -1,11 +1,11 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import EventEmitter from 'events'
4 -import { argv, DAY, ORIGINAL_CWD, VERSION } from './const'
4 +import { argv, ORIGINAL_CWD, VERSION } from './const'
5 import { watchLoad } from './watchLoad'
6 import yaml from 'yaml'
7 import _ from 'lodash'
8 -import { debounceAsync, newObj, onOff, wait, with_ } from './misc'
8 +import { DAY, debounceAsync, newObj, onOff, wait, with_ } from './misc'
9 import { statSync } from 'fs'
10 import { join, resolve } from 'path'
11 import events from './events'
src/const.ts
-1
@@ -15,7 +15,6 @@ export const BUILD_TIMESTAMP = fs.statSync(PKG_PATH).mtime.toISOString()
15 const pkg = JSON.parse(fs.readFileSync(PKG_PATH,'utf8'))
16 export const VERSION = pkg.version
17 export const RUNNING_BETA = VERSION.includes('-')
18 -export const DAY = 86_400_000
18
19 export const API_VERSION = 8.3
20 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
src/cross.ts
+12 -2
@@ -5,6 +5,8 @@ import _ from 'lodash'
5 export const REPO_URL = 'https://github.com/rejetto/hfs/'
6 export const WIKI_URL = REPO_URL + 'wiki/'
7 export const MINUTE = 60_000
8 +export const HOUR = 60 * MINUTE
9 +export const DAY = 24 * HOUR
10
11 export type Dict<T=any> = Record<string, T>
12 export type Falsy = false | null | undefined | '' | 0
@@ -198,7 +200,7 @@ export function hasProp<T extends object>(obj: T, key: PropertyKey): key is keyo
200 return key in obj;
201 }
202
201 -export function throw_(err: any) {
203 +export function throw_(err: any): never {
204 throw err
205 }
206
@@ -220,4 +222,12 @@ export async function asyncGeneratorToArray<T>(generator: AsyncIterable<T>): Pro
222 export function repeat(every: number, cb: () => unknown) {
223 Promise.allSettled([cb()]).then(() =>
224 setTimeout(() => repeat(every, cb), every) )
223 -}
\ No newline at end of file
225 +}
226 +
227 +export function formatTimestamp(x: string) {
228 + return x ? new Date(x).toLocaleString() : '-'
229 +}
230 +
231 +export function isPrimitive(x: unknown): x is boolean | string | number | undefined | null {
232 + return !x || Object(x) !== x
233 +}
src/debounceAsync.ts
+15 -6
@@ -4,16 +4,19 @@
4 export default function debounceAsync<Cancelable extends boolean = false, A extends unknown[] = unknown[], R = unknown>(
5 callback: (...args: A) => Promise<R>,
6 wait: number=100,
7 - options: { leading?: boolean, maxWait?:number, cancelable?: Cancelable }={}
7 + options: { leading?: boolean, maxWait?:number, retain?: number, retainFailure?: number, cancelable?: Cancelable }={}
8 ) {
9 type MaybeUndefined<T> = Cancelable extends true ? undefined | T : T
10 type MaybeR = MaybeUndefined<R>
11 - const { leading=false, maxWait=Infinity, cancelable=false } = options
11 + const { leading=false, maxWait=Infinity, cancelable=false, retain=0, retainFailure } = options
12 let started = 0 // latest callback invocation
13 let runningCallback: Promise<R> | undefined // latest callback invocation result
14 - let runningDebouncer: Promise<MaybeR> // latest wrapper invocation
14 + let runningDebouncer: Promise<MaybeR | R> // latest wrapper invocation
15 let waitingSince = 0 // we are delaying invocation since
16 let whoIsWaiting: undefined | A // args object identifies the pending instance, and incidentally stores args
17 + let last: typeof runningCallback
18 + let lastFailed = false
19 + let lastSince = 0
20 const interceptingWrapper = (...args: A) => runningDebouncer = debouncer(...args)
21 return Object.assign(interceptingWrapper, {
22 flush: () => runningCallback ?? exec(),
@@ -28,10 +31,13 @@ export default function debounceAsync<Cancelable extends boolean = false, A exte
31 async function debouncer(...args: A) {
32 if (runningCallback)
33 return runningCallback as MaybeR
34 + const now = Date.now()
35 + if (last && now - lastSince < (lastFailed ? retainFailure ?? retain : retain))
36 + return await last
37 whoIsWaiting = args
32 - waitingSince ||= Date.now()
33 - const waitingCap = maxWait - (Date.now() - (waitingSince || started))
34 - const waitFor = Math.min(waitingCap, leading ? wait - (Date.now() - started) : wait)
38 + waitingSince ||= now
39 + const waitingCap = maxWait - (now - (waitingSince || started))
40 + const waitFor = Math.min(waitingCap, leading ? wait - (now - started) : wait)
41 if (waitFor > 0)
42 await new Promise(resolve => setTimeout(resolve, waitFor))
43 if (!whoIsWaiting) // canceled
@@ -50,6 +56,9 @@ export default function debounceAsync<Cancelable extends boolean = false, A exte
56 return await runningCallback as MaybeUndefined<R> // await necessary to go-finally at the right time and even on exceptions
57 }
58 finally {
59 + last = runningCallback
60 + last!.then(() => lastFailed = false, () => lastFailed = true)
61 + lastSince = Date.now()
62 whoIsWaiting = undefined
63 runningCallback = undefined
64 }
src/github.ts
+2 -2
@@ -1,7 +1,7 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import events from './events'
4 -import { httpString, httpStream, unzip, AsapStream } from './misc'
4 +import { DAY, httpString, httpStream, unzip, AsapStream } from './misc'
5 import {
6 DISABLING_POSTFIX, findPluginByRepo,
7 getAvailablePlugins,
@@ -12,7 +12,7 @@ import {
12 } from './plugins'
13 import { ApiError } from './apiMiddleware'
14 import _ from 'lodash'
15 -import { DAY, HFS_REPO, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE, HTTP_SERVER_ERROR } from './const'
15 +import { HFS_REPO, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE, HTTP_SERVER_ERROR } from './const'
16 import { rename, rm } from 'fs/promises'
17 import { join } from 'path'
18 import { readFileSync } from 'fs'
src/listen.ts
+8 -5
@@ -201,13 +201,15 @@ export async function getServerStatus() {
201 https: await serverStatus(httpsSrv, httpsPortCfg.get()),
202 }
203
204 - async function serverStatus(h: typeof httpSrv, configuredPort: number) {
205 - const busy = await h?.busy
204 + async function serverStatus(srv: typeof httpSrv, configuredPort: number) {
205 + const busy = await srv?.busy
206 await wait(0) // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
207 return {
208 - ..._.pick(h, ['listening', 'error']),
208 + ..._.pick(srv, ['listening', 'error']),
209 busy,
210 - port: (h?.address() as any)?.port as number || configuredPort,
210 + port: (srv?.address() as any)?.port as number || configuredPort,
211 + configuredPort,
212 + srv,
213 }
214 }}
215
@@ -219,7 +221,8 @@ export async function getIps() {
221 && v4first(onlyTruthy(nets.map(net => !net.internal && net.address)))[0] // for each interface we consider only 1 address
222 )).flat()
223 const e = await externalIp
222 - if (e) ips.unshift(e)
224 + if (e && !ips.includes(e))
225 + ips.unshift(e)
226 return v4first(ips)
227 .filter((x,i,a) => a.length > 1 || !x.startsWith('169.254')) // 169.254 = dhcp failure on the interface, but keep it if it's our only one
228
src/log.ts
+1 -2
@@ -6,11 +6,10 @@ import { defineConfig } from './config'
6 import { createWriteStream, renameSync } from 'fs'
7 import * as util from 'util'
8 import { stat } from 'fs/promises'
9 -import { DAY } from './const'
9 import _ from 'lodash'
10 import { createFileWithPath, prepareFolder } from './util-files'
11 import { getCurrentUsername } from './perm'
13 -import { makeNetMatcher, tryJson } from './misc'
12 +import { DAY, makeNetMatcher, tryJson } from './misc'
13 import events from './events'
14
15 class Logger {
src/middlewares.ts
+2 -3
@@ -3,14 +3,13 @@
3 import compress from 'koa-compress'
4 import Koa from 'koa'
5 import {
6 - ADMIN_URI, API_URI,
7 - BUILD_TIMESTAMP,
8 - DEV, DAY,
6 + ADMIN_URI, API_URI, BUILD_TIMESTAMP, DEV,
7 HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_FOOL, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST,
8 } from './const'
9 import { FRONTEND_URI } from './const'
10 import { statusCodeForMissingPerm, nodeIsDirectory, urlToNode, vfs, walkNode, VfsNode, getNodeName } from './vfs'
11 import {
12 + DAY,
13 asyncGeneratorToReadable,
14 dirTraversal,
15 filterMapGenerator,