hfs 0.26.8 → 0.26.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/assets/index.bb5198ec.js +281 -0
- package/admin/assets/index.dcc78777.css +1 -0
- package/admin/assets/sha512.9dfe82e1.js +8 -0
- package/admin/index.html +3 -1
- package/admin/{public/logo.svg → logo.svg} +0 -0
- package/frontend/assets/index.27a78796.js +85 -0
- package/frontend/assets/index.93366732.css +1 -0
- package/frontend/assets/sha512.6af42937.js +8 -0
- package/frontend/{public/fontello.css → fontello.css} +0 -0
- package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
- package/frontend/index.html +3 -1
- package/package.json +1 -1
- package/src/QuickZipStream.js +285 -0
- package/src/ThrottledStream.js +93 -0
- package/src/adminApis.js +169 -0
- package/src/api.accounts.js +59 -0
- package/src/api.auth.js +128 -0
- package/src/api.file_list.js +103 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +102 -0
- package/src/api.plugins.js +127 -0
- package/src/api.vfs.js +164 -0
- package/src/apiMiddleware.js +120 -0
- package/src/block.js +33 -0
- package/src/commands.js +124 -0
- package/src/config.js +168 -0
- package/src/connections.js +57 -0
- package/src/const.js +83 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +48 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +102 -0
- package/src/index.js +56 -0
- package/src/listen.js +235 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +175 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +181 -0
- package/src/plugins.js +343 -0
- package/src/serveFile.js +105 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +29 -0
- package/src/throttler.js +91 -0
- package/src/update.js +69 -0
- package/src/util-files.js +148 -0
- package/src/util-generators.js +30 -0
- package/src/util-http.js +30 -0
- package/src/vfs.js +230 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +72 -0
- package/admin/.DS_Store +0 -0
- package/admin/.eslintrc +0 -8
- package/admin/.gitignore +0 -23
- package/admin/package.json +0 -67
- package/admin/src/AccountForm.ts +0 -92
- package/admin/src/AccountsPage.ts +0 -143
- package/admin/src/App.ts +0 -83
- package/admin/src/ArrayField.ts +0 -84
- package/admin/src/ConfigPage.ts +0 -279
- package/admin/src/FileField.ts +0 -52
- package/admin/src/FileForm.ts +0 -148
- package/admin/src/FilePicker.ts +0 -166
- package/admin/src/HomePage.ts +0 -96
- package/admin/src/InstalledPlugins.ts +0 -158
- package/admin/src/LoginRequired.ts +0 -75
- package/admin/src/LogoutPage.ts +0 -27
- package/admin/src/LogsPage.ts +0 -75
- package/admin/src/MainMenu.ts +0 -74
- package/admin/src/MenuButton.ts +0 -38
- package/admin/src/MonitorPage.ts +0 -200
- package/admin/src/OnlinePlugins.ts +0 -101
- package/admin/src/PermField.ts +0 -80
- package/admin/src/PluginsPage.ts +0 -27
- package/admin/src/VfsMenuBar.ts +0 -58
- package/admin/src/VfsPage.ts +0 -124
- package/admin/src/VfsTree.ts +0 -95
- package/admin/src/addFiles.ts +0 -59
- package/admin/src/api.ts +0 -246
- package/admin/src/dialog.ts +0 -203
- package/admin/src/index.css +0 -21
- package/admin/src/index.ts +0 -10
- package/admin/src/md.ts +0 -31
- package/admin/src/misc.ts +0 -141
- package/admin/src/react-app-env.d.ts +0 -1
- package/admin/src/reportWebVitals.ts +0 -15
- package/admin/src/setupTests.ts +0 -5
- package/admin/src/state.ts +0 -40
- package/admin/src/theme.ts +0 -37
- package/admin/tsconfig.json +0 -26
- package/admin/vite.config.ts +0 -32
- package/frontend/.DS_Store +0 -0
- package/frontend/.eslintrc +0 -8
- package/frontend/.gitignore +0 -23
- package/frontend/package.json +0 -51
- package/frontend/src/App.ts +0 -25
- package/frontend/src/Breadcrumbs.ts +0 -43
- package/frontend/src/BrowseFiles.ts +0 -141
- package/frontend/src/Head.ts +0 -45
- package/frontend/src/UserPanel.ts +0 -52
- package/frontend/src/api.ts +0 -78
- package/frontend/src/components.ts +0 -54
- package/frontend/src/dialog.css +0 -76
- package/frontend/src/dialog.ts +0 -105
- package/frontend/src/icons.ts +0 -46
- package/frontend/src/index.scss +0 -307
- package/frontend/src/index.ts +0 -10
- package/frontend/src/login.ts +0 -50
- package/frontend/src/menu.ts +0 -188
- package/frontend/src/misc.ts +0 -54
- package/frontend/src/options.ts +0 -52
- package/frontend/src/react-app-env.d.ts +0 -1
- package/frontend/src/reportWebVitals.ts +0 -15
- package/frontend/src/setupTests.ts +0 -5
- package/frontend/src/state.ts +0 -82
- package/frontend/src/useAuthorized.ts +0 -17
- package/frontend/src/useFetchList.ts +0 -144
- package/frontend/src/useTheme.ts +0 -23
- package/frontend/tsconfig.json +0 -26
- package/frontend/vite.config.ts +0 -21
- package/src/QuickZipStream.ts +0 -279
- package/src/ThrottledStream.ts +0 -98
- package/src/adminApis.ts +0 -161
- package/src/api.accounts.ts +0 -78
- package/src/api.auth.ts +0 -131
- package/src/api.file_list.ts +0 -102
- package/src/api.helpers.ts +0 -30
- package/src/api.monitor.ts +0 -106
- package/src/api.plugins.ts +0 -139
- package/src/api.vfs.ts +0 -182
- package/src/apiMiddleware.ts +0 -124
- package/src/block.ts +0 -35
- package/src/commands.ts +0 -122
- package/src/config.ts +0 -166
- package/src/connections.ts +0 -60
- package/src/const.ts +0 -57
- package/src/crypt.ts +0 -16
- package/src/debounceAsync.ts +0 -51
- package/src/events.ts +0 -6
- package/src/frontEndApis.ts +0 -17
- package/src/github.ts +0 -102
- package/src/index.ts +0 -53
- package/src/listen.ts +0 -220
- package/src/log.ts +0 -128
- package/src/middlewares.ts +0 -176
- package/src/misc.ts +0 -149
- package/src/pbkdf2.ts +0 -83
- package/src/perm.ts +0 -194
- package/src/plugins.ts +0 -342
- package/src/serveFile.ts +0 -104
- package/src/serveGuiFiles.ts +0 -95
- package/src/sse.ts +0 -29
- package/src/throttler.ts +0 -106
- package/src/update.ts +0 -67
- package/src/util-files.ts +0 -137
- package/src/util-generators.ts +0 -29
- package/src/util-http.ts +0 -29
- package/src/vfs.ts +0 -258
- package/src/watchLoad.ts +0 -75
- package/src/zip.ts +0 -69
package/src/serveFile.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
-
|
|
3
|
-
import Koa from 'koa'
|
|
4
|
-
import { createReadStream, stat } from 'fs'
|
|
5
|
-
import { FORBIDDEN, METHOD_NOT_ALLOWED, NO_CONTENT } from './const'
|
|
6
|
-
import { getNodeName, MIME_AUTO, VfsNode } from './vfs'
|
|
7
|
-
import mimetypes from 'mime-types'
|
|
8
|
-
import { defineConfig } from './config'
|
|
9
|
-
import { isMatch } from 'micromatch'
|
|
10
|
-
import _ from 'lodash'
|
|
11
|
-
import path from 'path'
|
|
12
|
-
import { promisify } from 'util'
|
|
13
|
-
|
|
14
|
-
const allowedReferer = defineConfig('allowed_referer', '')
|
|
15
|
-
|
|
16
|
-
export function serveFileNode(node: VfsNode) : Koa.Middleware {
|
|
17
|
-
const { source, mime } = node
|
|
18
|
-
const name = getNodeName(node)
|
|
19
|
-
const mimeString = typeof mime === 'string' ? mime
|
|
20
|
-
: _.find(mime, (val,mask) => isMatch(name, mask))
|
|
21
|
-
return (ctx, next) => {
|
|
22
|
-
const allowed = allowedReferer.get()
|
|
23
|
-
if (allowed) {
|
|
24
|
-
const ref = /\/\/([^:/]+)/.exec(ctx.get('referer'))?.[1] // extract host from url
|
|
25
|
-
if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
|
|
26
|
-
&& !isMatch(ref, allowed))
|
|
27
|
-
return ctx.status = FORBIDDEN
|
|
28
|
-
|
|
29
|
-
function host() {
|
|
30
|
-
const s = ctx.get('host')
|
|
31
|
-
return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s?.split(':')[0]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
ctx.vfsNode = node // useful to tell service files from files shared by the user
|
|
36
|
-
return serveFile(source||'', mimeString)(ctx, next)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const mimeCfg = defineConfig<Record<string,string>>('mime', { '*': 'auto' })
|
|
41
|
-
|
|
42
|
-
export function serveFile(source:string, mime?:string, content?: string | Buffer) : Koa.Middleware {
|
|
43
|
-
return async (ctx) => {
|
|
44
|
-
if (!source)
|
|
45
|
-
return
|
|
46
|
-
const fn = path.basename(source)
|
|
47
|
-
mime = mime ?? _.find(mimeCfg.get(), (v,k) => k>'' && isMatch(fn, k)) // isMatch throws on an empty string
|
|
48
|
-
if (mime === MIME_AUTO)
|
|
49
|
-
mime = mimetypes.lookup(source) || ''
|
|
50
|
-
if (mime)
|
|
51
|
-
ctx.type = mime
|
|
52
|
-
if (ctx.method === 'OPTIONS') {
|
|
53
|
-
ctx.status = NO_CONTENT
|
|
54
|
-
ctx.set({ Allow: 'OPTIONS, GET, HEAD' })
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
if (ctx.method !== 'GET')
|
|
58
|
-
return ctx.status = METHOD_NOT_ALLOWED
|
|
59
|
-
try {
|
|
60
|
-
const stats = await promisify(stat)(source) // using fs's function instead of fs/promises, because only the former is supported by pkg
|
|
61
|
-
ctx.set('Last-Modified', stats.mtime.toUTCString())
|
|
62
|
-
ctx.fileSource = source
|
|
63
|
-
ctx.status = 200
|
|
64
|
-
if (ctx.fresh)
|
|
65
|
-
return ctx.status = 304
|
|
66
|
-
if (content !== undefined)
|
|
67
|
-
return ctx.body = content
|
|
68
|
-
const range = getRange(ctx, stats.size)
|
|
69
|
-
ctx.body = createReadStream(source, range)
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return ctx.status = 404
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function getRange(ctx: Koa.Context, totalSize: number) {
|
|
78
|
-
ctx.set('Accept-Ranges', 'bytes')
|
|
79
|
-
const { range } = ctx.request.header
|
|
80
|
-
if (!range) {
|
|
81
|
-
ctx.response.length = totalSize
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
const ranges = range.split('=')[1]
|
|
85
|
-
if (ranges.includes(','))
|
|
86
|
-
return ctx.throw(400, 'multi-range not supported')
|
|
87
|
-
let bytes = ranges?.split('-')
|
|
88
|
-
if (!bytes?.length)
|
|
89
|
-
return ctx.throw(400, 'bad range')
|
|
90
|
-
const max = totalSize - 1
|
|
91
|
-
const start = bytes[0] ? Number(bytes[0]) : Math.max(0, totalSize-Number(bytes[1])) // a negative start is relative to the end
|
|
92
|
-
const end = bytes[0] ? Number(bytes[1] || max) : max
|
|
93
|
-
// we don't support last-bytes without knowing max
|
|
94
|
-
if (isNaN(end) && isNaN(max) || end > max || start > max) {
|
|
95
|
-
ctx.status = 416
|
|
96
|
-
ctx.set('Content-Range', `bytes ${totalSize}`)
|
|
97
|
-
ctx.body = 'Requested Range Not Satisfiable'
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
ctx.status = 206
|
|
101
|
-
ctx.set('Content-Range', `bytes ${start}-${isNaN(end) ? '' : end}/${isNaN(totalSize) ? '*' : totalSize}`)
|
|
102
|
-
ctx.response.length = end - start + 1
|
|
103
|
-
return { start, end }
|
|
104
|
-
}
|
package/src/serveGuiFiles.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
-
|
|
3
|
-
import Koa from 'koa'
|
|
4
|
-
import fs from 'fs/promises'
|
|
5
|
-
import { METHOD_NOT_ALLOWED, NO_CONTENT, PLUGINS_PUB_URI } from './const'
|
|
6
|
-
import { serveFile } from './serveFile'
|
|
7
|
-
import { mapPlugins } from './plugins'
|
|
8
|
-
import { refresh_session } from './api.auth'
|
|
9
|
-
import { ApiError } from './apiMiddleware'
|
|
10
|
-
import { join, extname } from 'path'
|
|
11
|
-
import { getOrSet } from './misc'
|
|
12
|
-
|
|
13
|
-
// in case of dev env we have our static files within the 'dist' folder'
|
|
14
|
-
const DEV_STATIC = process.env.DEV ? 'dist/' : ''
|
|
15
|
-
|
|
16
|
-
function serveStatic(uri: string): Koa.Middleware {
|
|
17
|
-
const folder = uri.slice(2,-1) // we know folder is very similar to uri
|
|
18
|
-
const cache: Record<string, Promise<string>> = {}
|
|
19
|
-
return async (ctx, next) => {
|
|
20
|
-
if(ctx.method === 'OPTIONS') {
|
|
21
|
-
ctx.status = NO_CONTENT
|
|
22
|
-
ctx.set({ Allow: 'OPTIONS, GET' })
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
if (ctx.method !== 'GET')
|
|
26
|
-
return ctx.status = METHOD_NOT_ALLOWED
|
|
27
|
-
const serveApp = shouldServeApp(ctx)
|
|
28
|
-
const fullPath = join(__dirname, '..', DEV_STATIC, folder, serveApp ? '/index.html': ctx.path)
|
|
29
|
-
const content = await getOrSet(cache, ctx.path, async () => {
|
|
30
|
-
const data = await fs.readFile(fullPath).catch(() => null)
|
|
31
|
-
return serveApp || !data ? data
|
|
32
|
-
: adjustBundlerLinks(ctx.path, uri, data)
|
|
33
|
-
})
|
|
34
|
-
if (content === null)
|
|
35
|
-
return ctx.status = 404
|
|
36
|
-
if (!serveApp)
|
|
37
|
-
return serveFile(fullPath, 'auto', content)(ctx, next)
|
|
38
|
-
// we don't cache the index as it's small and may prevent plugins change to apply
|
|
39
|
-
ctx.body = await treatIndex(ctx, String(content), uri)
|
|
40
|
-
ctx.type = 'html'
|
|
41
|
-
ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate')
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function shouldServeApp(ctx: Koa.Context) {
|
|
46
|
-
return ctx.state.serveApp ||= ctx.path.endsWith('/')
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function adjustBundlerLinks(path: string, uri: string, data: string | Buffer) {
|
|
50
|
-
const ext = extname(path)
|
|
51
|
-
return ext && !ext.match(/\.(css|html|js|ts|scss)/) ? data
|
|
52
|
-
: String(data).replace(/((?:import | from )['"])\//g, `$1${uri}`)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function treatIndex(ctx: Koa.Context, body: string, filesUri: string) {
|
|
56
|
-
const session = await refresh_session({}, ctx)
|
|
57
|
-
ctx.set('etag', '')
|
|
58
|
-
return body
|
|
59
|
-
.replace(/((?:src|href) *= *['"])\/?(?![a-z]+:\/\/)/g, '$1' + filesUri)
|
|
60
|
-
.replace('_HFS_SESSION_', session instanceof ApiError ? 'null' : JSON.stringify(session))
|
|
61
|
-
// replacing this text allow us to avoid injecting in frontends that don't support plugins. Don't use a <--comment--> or it will be removed by webpack
|
|
62
|
-
.replace('_HFS_PLUGINS_', pluginsInjection)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function serveProxied(port: string | undefined, uri: string) { // used for development only
|
|
66
|
-
if (!port)
|
|
67
|
-
return
|
|
68
|
-
console.debug('proxied on port', port)
|
|
69
|
-
let proxy: Koa.Middleware
|
|
70
|
-
import('koa-better-http-proxy').then(lib => // dynamic import to avoid having this in final distribution
|
|
71
|
-
proxy = lib.default('127.0.0.1:'+port, {
|
|
72
|
-
proxyReqPathResolver: (ctx) =>
|
|
73
|
-
shouldServeApp(ctx) ? '/' : ctx.path,
|
|
74
|
-
userResDecorator(res, data, ctx) {
|
|
75
|
-
return shouldServeApp(ctx) ? treatIndex(ctx, String(data), uri)
|
|
76
|
-
: adjustBundlerLinks(ctx.path, uri, data)
|
|
77
|
-
}
|
|
78
|
-
}) )
|
|
79
|
-
return function() { //@ts-ignore
|
|
80
|
-
return proxy.apply(this,arguments)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function pluginsInjection() {
|
|
85
|
-
const css = mapPlugins((plug,k) =>
|
|
86
|
-
plug.frontend_css?.map(f => PLUGINS_PUB_URI + k + '/' + f)).flat().filter(Boolean)
|
|
87
|
-
const js = mapPlugins((plug,k) =>
|
|
88
|
-
plug.frontend_js?.map(f => PLUGINS_PUB_URI + k + '/' + f)).flat().filter(Boolean)
|
|
89
|
-
return css.map(uri => `\n<link rel='stylesheet' type='text/css' href='${uri}'/>`).join('')
|
|
90
|
-
+ js.map(uri => `\n<script defer src='${uri}'></script>`).join('')
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function serveGuiFiles(proxyPort:string | undefined, uri:string) {
|
|
94
|
-
return serveProxied(proxyPort, uri) || serveStatic(uri)
|
|
95
|
-
}
|
package/src/sse.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
-
|
|
3
|
-
import Koa from 'koa'
|
|
4
|
-
import { Transform } from 'stream'
|
|
5
|
-
|
|
6
|
-
export default function createSSE(ctx: Koa.Context) {
|
|
7
|
-
const { socket } = ctx.req
|
|
8
|
-
socket.setTimeout(0)
|
|
9
|
-
socket.setNoDelay(true)
|
|
10
|
-
socket.setKeepAlive(true)
|
|
11
|
-
ctx.set({
|
|
12
|
-
'Content-Type': 'text/event-stream',
|
|
13
|
-
'Cache-Control': 'no-cache',
|
|
14
|
-
'Connection': 'keep-alive',
|
|
15
|
-
'X-Accel-Buffering': 'no', // avoid buffering when reverse-proxied through nginx
|
|
16
|
-
})
|
|
17
|
-
ctx.status = 200
|
|
18
|
-
return ctx.body = new Transform({
|
|
19
|
-
objectMode: true,
|
|
20
|
-
transform(chunk, encoding, cb) {
|
|
21
|
-
this.push(`data: ${JSON.stringify(chunk)}\n\n`)
|
|
22
|
-
cb()
|
|
23
|
-
},
|
|
24
|
-
flush(cb) {
|
|
25
|
-
this.push('data:\n\n')
|
|
26
|
-
cb()
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
}
|
package/src/throttler.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
|
|
2
|
-
|
|
3
|
-
import { Readable } from 'stream'
|
|
4
|
-
import Koa from 'koa'
|
|
5
|
-
import { ThrottledStream, ThrottleGroup } from './ThrottledStream'
|
|
6
|
-
import { defineConfig } from './config'
|
|
7
|
-
import { getOrSet, isLocalHost } from './misc'
|
|
8
|
-
import { Connection, updateConnection } from './connections'
|
|
9
|
-
import _ from 'lodash'
|
|
10
|
-
import events from './events'
|
|
11
|
-
|
|
12
|
-
const mainThrottleGroup = new ThrottleGroup(Infinity)
|
|
13
|
-
|
|
14
|
-
defineConfig('max_kbps', Infinity).sub(v =>
|
|
15
|
-
mainThrottleGroup.updateLimit(v))
|
|
16
|
-
|
|
17
|
-
const ip2group: Record<string, {
|
|
18
|
-
count: number
|
|
19
|
-
group: ThrottleGroup
|
|
20
|
-
destroy: () => void
|
|
21
|
-
}> = {}
|
|
22
|
-
|
|
23
|
-
const SymThrStr = Symbol('stream')
|
|
24
|
-
const SymTimeout = Symbol('timeout')
|
|
25
|
-
|
|
26
|
-
const maxKbpsPerIp = defineConfig('max_kbps_per_ip', Infinity)
|
|
27
|
-
|
|
28
|
-
export const throttler: Koa.Middleware = async (ctx, next) => {
|
|
29
|
-
await next()
|
|
30
|
-
const { body } = ctx
|
|
31
|
-
if (!body || !(body instanceof Readable))
|
|
32
|
-
return
|
|
33
|
-
// we wrap the stream also for unlimited connections to get speed and other features
|
|
34
|
-
const ipGroup = getOrSet(ip2group, ctx.ip, ()=> {
|
|
35
|
-
const doLimit = ctx.state.account?.ignore_limits || isLocalHost(ctx) ? undefined : true
|
|
36
|
-
const group = new ThrottleGroup(Infinity, doLimit && mainThrottleGroup)
|
|
37
|
-
|
|
38
|
-
const unsub = doLimit && maxKbpsPerIp.sub(v =>
|
|
39
|
-
group.updateLimit(v))
|
|
40
|
-
return { group, count:0, destroy: unsub }
|
|
41
|
-
})
|
|
42
|
-
const conn = ctx.state.connection as Connection | undefined
|
|
43
|
-
if (!conn) throw 'assert throttler connection'
|
|
44
|
-
|
|
45
|
-
const ts = conn[SymThrStr] = new ThrottledStream(ipGroup.group, conn[SymThrStr])
|
|
46
|
-
let closed = false
|
|
47
|
-
|
|
48
|
-
const DELAY = 1000
|
|
49
|
-
const update = _.debounce(() => {
|
|
50
|
-
const ts = conn[SymThrStr] as ThrottledStream
|
|
51
|
-
const outSpeed = roundKb(ts.getSpeed())
|
|
52
|
-
updateConnection(conn, { outSpeed, sent: ts.getBytesSent() })
|
|
53
|
-
/* in case this stream stands still for a while (before the end), we'll have neither 'sent' or 'close' events,
|
|
54
|
-
* so who will take care to updateConnection? This artificial next-call will ensure just that */
|
|
55
|
-
clearTimeout(conn[SymTimeout])
|
|
56
|
-
if (outSpeed || !closed)
|
|
57
|
-
conn[SymTimeout] = setTimeout(update, DELAY)
|
|
58
|
-
}, DELAY, { maxWait:DELAY })
|
|
59
|
-
ts.on('sent', (n: number) => {
|
|
60
|
-
totalSent += n
|
|
61
|
-
update()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
++ipGroup.count
|
|
65
|
-
ts.on('close', ()=> {
|
|
66
|
-
update.flush()
|
|
67
|
-
closed = true
|
|
68
|
-
if (--ipGroup.count) return // any left?
|
|
69
|
-
ipGroup.destroy?.()
|
|
70
|
-
delete ip2group[ctx.ip]
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const bak = ctx.response.length // preserve
|
|
74
|
-
ctx.body = ctx.body.pipe(ts)
|
|
75
|
-
|
|
76
|
-
if (bak)
|
|
77
|
-
ctx.response.length = bak
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function roundKb(n: number) {
|
|
81
|
-
return _.round(n, 1) || _.round(n, 3) // further precision if necessary
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export let totalSent = 0
|
|
85
|
-
export let totalGot = 0
|
|
86
|
-
export let totalOutSpeed = 0
|
|
87
|
-
export let totalInSpeed = 0
|
|
88
|
-
|
|
89
|
-
let lastSent = totalSent
|
|
90
|
-
let lastGot = totalGot
|
|
91
|
-
let last = Date.now()
|
|
92
|
-
setInterval(() => {
|
|
93
|
-
const now = Date.now()
|
|
94
|
-
const past = (now - last) / 1000 // seconds
|
|
95
|
-
last = now
|
|
96
|
-
const deltaSentKb = (totalSent - lastSent) / 1000
|
|
97
|
-
lastSent = totalSent
|
|
98
|
-
const deltaGotKb = (totalGot - lastGot) / 1000
|
|
99
|
-
lastGot = totalGot
|
|
100
|
-
totalOutSpeed = roundKb(deltaSentKb / past)
|
|
101
|
-
totalInSpeed = roundKb(deltaGotKb / past)
|
|
102
|
-
}, 1000)
|
|
103
|
-
|
|
104
|
-
events.on('connection', (c: Connection) =>
|
|
105
|
-
c.socket.on('data', data =>
|
|
106
|
-
totalGot += data.length ))
|
package/src/update.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { getRepoInfo } from './github'
|
|
2
|
-
import { argv, IS_WINDOWS, VERSION } from './const'
|
|
3
|
-
import { basename, dirname, join } from 'path'
|
|
4
|
-
import { spawn } from 'child_process'
|
|
5
|
-
import { httpsStream, onProcessExit, unzip } from './misc'
|
|
6
|
-
import { renameSync, unlinkSync } from 'fs'
|
|
7
|
-
import { pluginsWatcher } from './plugins'
|
|
8
|
-
import { chmod, stat } from 'fs/promises'
|
|
9
|
-
|
|
10
|
-
const HFS_REPO = 'rejetto/hfs'
|
|
11
|
-
|
|
12
|
-
export async function getUpdate() {
|
|
13
|
-
const [latest] = await getRepoInfo(HFS_REPO + '/releases?per_page=1')
|
|
14
|
-
if (latest.name === VERSION)
|
|
15
|
-
throw "you already have the latest version: " + VERSION
|
|
16
|
-
return latest
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function update() {
|
|
20
|
-
if (process.argv0.endsWith('node'))
|
|
21
|
-
throw "only binary versions are supported for now"
|
|
22
|
-
const update = await getUpdate()
|
|
23
|
-
const assetSearch = ({ win32: 'windows', darwin: 'mac', linux: 'linux' } as any)[process.platform]
|
|
24
|
-
if (!assetSearch)
|
|
25
|
-
throw "this feature doesn't support your platform: " + process.platform
|
|
26
|
-
const asset = update.assets.find((x: any) => x.name.endsWith('.zip') && x.name.includes(assetSearch))
|
|
27
|
-
if (!asset)
|
|
28
|
-
throw "asset not found"
|
|
29
|
-
const url = asset.browser_download_url
|
|
30
|
-
console.log("downloading", url)
|
|
31
|
-
const bin = process.argv[0]
|
|
32
|
-
const binPath = dirname(bin)
|
|
33
|
-
const binFile = basename(bin)
|
|
34
|
-
const newBinFile = 'new-' + binFile
|
|
35
|
-
pluginsWatcher.pause()
|
|
36
|
-
try {
|
|
37
|
-
await unzip(await httpsStream(url), path =>
|
|
38
|
-
join(binPath, path === binFile ? newBinFile : path))
|
|
39
|
-
const newBin = join(binPath, newBinFile)
|
|
40
|
-
if (!IS_WINDOWS) {
|
|
41
|
-
const { mode } = await stat(bin)
|
|
42
|
-
await chmod(newBin, mode).catch(console.error)
|
|
43
|
-
}
|
|
44
|
-
onProcessExit(() => {
|
|
45
|
-
const oldBinFile = 'old-' + binFile
|
|
46
|
-
console.log("old version is kept as", oldBinFile)
|
|
47
|
-
const oldBin = join(binPath, oldBinFile)
|
|
48
|
-
try { unlinkSync(oldBin) }
|
|
49
|
-
catch {}
|
|
50
|
-
renameSync(bin, oldBin)
|
|
51
|
-
console.log("launching new version in background", newBinFile)
|
|
52
|
-
spawn(newBin, ['--updating', binFile], { detached: true, shell: true })
|
|
53
|
-
.on('error', console.error)
|
|
54
|
-
})
|
|
55
|
-
console.log('quitting')
|
|
56
|
-
process.exit()
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
pluginsWatcher.unpause()
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (argv.updating) { // we were launched with a temporary name, restore original name to avoid breaking references
|
|
64
|
-
const bin = process.argv[0]
|
|
65
|
-
renameSync(bin, join(dirname(bin), argv.updating))
|
|
66
|
-
console.log("renamed binary file to", argv.updating)
|
|
67
|
-
}
|
package/src/util-files.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises'
|
|
2
|
-
import { wait } from './misc'
|
|
3
|
-
import { createWriteStream, mkdirSync, watch } from 'fs'
|
|
4
|
-
import { basename, dirname } from 'path'
|
|
5
|
-
import glob from 'fast-glob'
|
|
6
|
-
import { IS_WINDOWS } from './const'
|
|
7
|
-
import { execFile } from 'child_process'
|
|
8
|
-
import { once, Readable } from 'stream'
|
|
9
|
-
// @ts-ignore
|
|
10
|
-
import unzipper from 'unzip-stream'
|
|
11
|
-
|
|
12
|
-
export async function isDirectory(path: string) {
|
|
13
|
-
try { return (await fs.stat(path)).isDirectory() }
|
|
14
|
-
catch { return false }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function isFile(path: string) {
|
|
18
|
-
try { return (await fs.stat(path)).isFile() }
|
|
19
|
-
catch { return false }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function readFileBusy(path: string): Promise<string> {
|
|
23
|
-
return fs.readFile(path, 'utf8').catch(e => {
|
|
24
|
-
if ((e as any)?.code !== 'EBUSY')
|
|
25
|
-
throw e
|
|
26
|
-
console.debug('busy')
|
|
27
|
-
return wait(100).then(()=> readFileBusy(path))
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function watchDir(dir: string, cb: ()=>void) {
|
|
32
|
-
let watcher: ReturnType<typeof watch>
|
|
33
|
-
let paused = false
|
|
34
|
-
try {
|
|
35
|
-
watcher = watch(dir, controlledCb)
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// failing watching the content of the dir, we try to monitor its parent, but filtering events only for our target dir
|
|
39
|
-
const base = basename(dir)
|
|
40
|
-
try {
|
|
41
|
-
watcher = watch(dirname(dir), (event,name) => {
|
|
42
|
-
if (name !== base) return
|
|
43
|
-
try {
|
|
44
|
-
watcher.close() // if we succeed, we give up the parent watching
|
|
45
|
-
watcher = watch(dir, controlledCb) // attempt at passing to a more specific watching
|
|
46
|
-
}
|
|
47
|
-
catch {}
|
|
48
|
-
controlledCb()
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
console.debug(String(e))
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
working() { return Boolean(watcher) },
|
|
57
|
-
stop() { watcher?.close() },
|
|
58
|
-
pause() { paused = true },
|
|
59
|
-
unpause() { paused = false },
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function controlledCb() {
|
|
63
|
-
if (!paused)
|
|
64
|
-
cb()
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function dirTraversal(s?: string) {
|
|
69
|
-
return s && /(^|[/\\])\.\.($|[/\\])/.test(s)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function isWindowsDrive(s?: string) {
|
|
73
|
-
return s && /^[a-zA-Z]:$/.test(s)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// apply this to paths that may contain \ as separator (not supported by fast-glob) and other special chars to be escaped (parenthesis)
|
|
77
|
-
export function adjustStaticPathForGlob(path: string) {
|
|
78
|
-
return glob.escapePath(path.replace(/\\/g, '/'))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function* dirStream(path: string, deep?: number) {
|
|
82
|
-
if (!await isDirectory(path))
|
|
83
|
-
throw Error('ENOTDIR')
|
|
84
|
-
const dirStream = glob.stream(deep ? '**/*' : '*', {
|
|
85
|
-
cwd: path,
|
|
86
|
-
dot: true,
|
|
87
|
-
deep,
|
|
88
|
-
onlyFiles: false,
|
|
89
|
-
suppressErrors: true,
|
|
90
|
-
objectMode: true,
|
|
91
|
-
})
|
|
92
|
-
const skip = await getItemsToSkip(path)
|
|
93
|
-
for await (const entry of dirStream) {
|
|
94
|
-
let { path, dirent } = entry as any
|
|
95
|
-
if (!dirent.isDirectory() && !dirent.isFile()) continue
|
|
96
|
-
path = String(path)
|
|
97
|
-
if (!skip?.includes(path))
|
|
98
|
-
yield path
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function getItemsToSkip(path: string) {
|
|
102
|
-
if (!IS_WINDOWS) return
|
|
103
|
-
const out = await run('dir', ['/ah', '/b', path.replace(/\//g, '\\')])
|
|
104
|
-
.catch(()=>'') // error in case of no matching file
|
|
105
|
-
return out.split('\r\n').slice(0,-1)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function run(cmd: string, args: string[] = []): Promise<string> {
|
|
110
|
-
return new Promise((resolve, reject) =>
|
|
111
|
-
execFile('cmd', ['/c', cmd, ...args], (err, stdout) => {
|
|
112
|
-
if (err)
|
|
113
|
-
reject(err)
|
|
114
|
-
else
|
|
115
|
-
resolve(stdout)
|
|
116
|
-
}))
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function unzip(stream: Readable, cb: (path: string) => false | string) {
|
|
120
|
-
let pending: Promise<any> = Promise.resolve()
|
|
121
|
-
return new Promise(resolve =>
|
|
122
|
-
stream.pipe(unzipper.Parse())
|
|
123
|
-
.on('end', () => pending.then(resolve))
|
|
124
|
-
.on('entry', async (entry: any) => {
|
|
125
|
-
const { path, type } = entry
|
|
126
|
-
const dest = cb(path)
|
|
127
|
-
if (!dest || type !== 'File')
|
|
128
|
-
return entry.autodrain()
|
|
129
|
-
await pending // don't overlap writings
|
|
130
|
-
console.debug('unzip', dest)
|
|
131
|
-
mkdirSync(dirname(dest), { recursive: true }) // easy way be sure to have the folder ready before proceeding
|
|
132
|
-
const thisFile = entry.pipe(createWriteStream(dest))
|
|
133
|
-
pending = once(thisFile, 'finish')
|
|
134
|
-
})
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
|
package/src/util-generators.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// callback can return undefined to skip element
|
|
2
|
-
import { Readable } from 'stream'
|
|
3
|
-
|
|
4
|
-
export async function* filterMapGenerator<IN,OUT>(generator: AsyncIterableIterator<IN>, filterMap: (el: IN) => Promise<OUT>) {
|
|
5
|
-
for await (const x of generator) {
|
|
6
|
-
const res:OUT = await filterMap(x)
|
|
7
|
-
if (res !== undefined)
|
|
8
|
-
yield res as Exclude<OUT,undefined>
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function asyncGeneratorToArray<T>(generator: AsyncIterable<T>): Promise<T[]> {
|
|
13
|
-
const ret: T[] = []
|
|
14
|
-
for await(const x of generator)
|
|
15
|
-
ret.push(x)
|
|
16
|
-
return ret
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function asyncGeneratorToReadable<T>(generator: AsyncIterable<T>) {
|
|
20
|
-
const iterator = generator[Symbol.asyncIterator]()
|
|
21
|
-
return new Readable({
|
|
22
|
-
objectMode: true,
|
|
23
|
-
read() {
|
|
24
|
-
iterator.next().then(it =>
|
|
25
|
-
this.push(it.done ? null : it.value))
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
package/src/util-http.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { RequestOptions } from 'https'
|
|
2
|
-
import { IncomingMessage } from 'node:http'
|
|
3
|
-
import https from 'node:https'
|
|
4
|
-
|
|
5
|
-
export function httpsString(url: string, options:RequestOptions={}): Promise<IncomingMessage & { ok: boolean, body: string }> {
|
|
6
|
-
return httpsStream(url, options).then(res =>
|
|
7
|
-
new Promise(resolve => {
|
|
8
|
-
let buf = ''
|
|
9
|
-
res.on('data', chunk => buf += chunk.toString())
|
|
10
|
-
res.on('end', () => resolve(Object.assign(res, {
|
|
11
|
-
ok: (res.statusCode || 400) < 400,
|
|
12
|
-
body: buf
|
|
13
|
-
})))
|
|
14
|
-
})
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function httpsStream(url: string, options:RequestOptions={}): Promise<IncomingMessage> {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
https.request(url, options, res => {
|
|
21
|
-
if (!res.statusCode || res.statusCode >= 400)
|
|
22
|
-
throw res
|
|
23
|
-
if (res.statusCode === 302 && res.headers.location)
|
|
24
|
-
return resolve(httpsStream(res.headers.location, options))
|
|
25
|
-
resolve(res)
|
|
26
|
-
}).on('error', reject).end()
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|