hfs 0.26.8 → 0.27.2

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.
Files changed (163) hide show
  1. package/README.md +15 -2
  2. package/admin/assets/index-509bb1d6.js +415 -0
  3. package/admin/assets/index-60a380a7.css +1 -0
  4. package/admin/assets/sha512-738f0943.js +8 -0
  5. package/admin/index.html +3 -1
  6. package/admin/{public/logo.svg → logo.svg} +0 -0
  7. package/frontend/assets/index-6e178dfd.css +1 -0
  8. package/frontend/assets/index-aea7654e.js +85 -0
  9. package/frontend/assets/sha512-bf915587.js +8 -0
  10. package/frontend/{public/fontello.css → fontello.css} +0 -0
  11. package/frontend/{public/fontello.woff2 → fontello.woff2} +0 -0
  12. package/frontend/index.html +4 -2
  13. package/package.json +2 -6
  14. package/plugins/vhosting/plugin.js +23 -20
  15. package/src/QuickZipStream.js +285 -0
  16. package/src/ThrottledStream.js +93 -0
  17. package/src/adminApis.js +169 -0
  18. package/src/api.accounts.js +59 -0
  19. package/src/api.auth.js +128 -0
  20. package/src/api.file_list.js +110 -0
  21. package/src/api.helpers.js +32 -0
  22. package/src/api.monitor.js +104 -0
  23. package/src/api.plugins.js +128 -0
  24. package/src/api.vfs.js +167 -0
  25. package/src/apiMiddleware.js +123 -0
  26. package/src/block.js +34 -0
  27. package/src/commands.js +125 -0
  28. package/src/config.js +168 -0
  29. package/src/connections.js +57 -0
  30. package/src/const.js +94 -0
  31. package/src/crypt.js +21 -0
  32. package/src/debounceAsync.js +49 -0
  33. package/src/events.js +9 -0
  34. package/src/frontEndApis.js +38 -0
  35. package/src/github.js +104 -0
  36. package/src/index.js +57 -0
  37. package/src/listen.js +235 -0
  38. package/src/log.js +137 -0
  39. package/src/middlewares.js +195 -0
  40. package/src/misc.js +160 -0
  41. package/src/pbkdf2.js +74 -0
  42. package/src/perm.js +183 -0
  43. package/src/plugins.js +343 -0
  44. package/src/serveFile.js +105 -0
  45. package/src/serveGuiFiles.js +113 -0
  46. package/src/sse.js +30 -0
  47. package/src/throttler.js +91 -0
  48. package/src/update.js +70 -0
  49. package/src/util-files.js +163 -0
  50. package/src/util-generators.js +31 -0
  51. package/src/util-http.js +32 -0
  52. package/src/vfs.js +232 -0
  53. package/src/watchLoad.js +73 -0
  54. package/src/zip.js +73 -0
  55. package/admin/.DS_Store +0 -0
  56. package/admin/.eslintrc +0 -8
  57. package/admin/.gitignore +0 -23
  58. package/admin/package.json +0 -67
  59. package/admin/src/AccountForm.ts +0 -92
  60. package/admin/src/AccountsPage.ts +0 -143
  61. package/admin/src/App.ts +0 -83
  62. package/admin/src/ArrayField.ts +0 -84
  63. package/admin/src/ConfigPage.ts +0 -279
  64. package/admin/src/FileField.ts +0 -52
  65. package/admin/src/FileForm.ts +0 -148
  66. package/admin/src/FilePicker.ts +0 -166
  67. package/admin/src/HomePage.ts +0 -96
  68. package/admin/src/InstalledPlugins.ts +0 -158
  69. package/admin/src/LoginRequired.ts +0 -75
  70. package/admin/src/LogoutPage.ts +0 -27
  71. package/admin/src/LogsPage.ts +0 -75
  72. package/admin/src/MainMenu.ts +0 -74
  73. package/admin/src/MenuButton.ts +0 -38
  74. package/admin/src/MonitorPage.ts +0 -200
  75. package/admin/src/OnlinePlugins.ts +0 -101
  76. package/admin/src/PermField.ts +0 -80
  77. package/admin/src/PluginsPage.ts +0 -27
  78. package/admin/src/VfsMenuBar.ts +0 -58
  79. package/admin/src/VfsPage.ts +0 -124
  80. package/admin/src/VfsTree.ts +0 -95
  81. package/admin/src/addFiles.ts +0 -59
  82. package/admin/src/api.ts +0 -246
  83. package/admin/src/dialog.ts +0 -203
  84. package/admin/src/index.css +0 -21
  85. package/admin/src/index.ts +0 -10
  86. package/admin/src/md.ts +0 -31
  87. package/admin/src/misc.ts +0 -141
  88. package/admin/src/react-app-env.d.ts +0 -1
  89. package/admin/src/reportWebVitals.ts +0 -15
  90. package/admin/src/setupTests.ts +0 -5
  91. package/admin/src/state.ts +0 -40
  92. package/admin/src/theme.ts +0 -37
  93. package/admin/tsconfig.json +0 -26
  94. package/admin/vite.config.ts +0 -32
  95. package/frontend/.DS_Store +0 -0
  96. package/frontend/.eslintrc +0 -8
  97. package/frontend/.gitignore +0 -23
  98. package/frontend/package.json +0 -51
  99. package/frontend/src/App.ts +0 -25
  100. package/frontend/src/Breadcrumbs.ts +0 -43
  101. package/frontend/src/BrowseFiles.ts +0 -141
  102. package/frontend/src/Head.ts +0 -45
  103. package/frontend/src/UserPanel.ts +0 -52
  104. package/frontend/src/api.ts +0 -78
  105. package/frontend/src/components.ts +0 -54
  106. package/frontend/src/dialog.css +0 -76
  107. package/frontend/src/dialog.ts +0 -105
  108. package/frontend/src/icons.ts +0 -46
  109. package/frontend/src/index.scss +0 -307
  110. package/frontend/src/index.ts +0 -10
  111. package/frontend/src/login.ts +0 -50
  112. package/frontend/src/menu.ts +0 -188
  113. package/frontend/src/misc.ts +0 -54
  114. package/frontend/src/options.ts +0 -52
  115. package/frontend/src/react-app-env.d.ts +0 -1
  116. package/frontend/src/reportWebVitals.ts +0 -15
  117. package/frontend/src/setupTests.ts +0 -5
  118. package/frontend/src/state.ts +0 -82
  119. package/frontend/src/useAuthorized.ts +0 -17
  120. package/frontend/src/useFetchList.ts +0 -144
  121. package/frontend/src/useTheme.ts +0 -23
  122. package/frontend/tsconfig.json +0 -26
  123. package/frontend/vite.config.ts +0 -21
  124. package/src/QuickZipStream.ts +0 -279
  125. package/src/ThrottledStream.ts +0 -98
  126. package/src/adminApis.ts +0 -161
  127. package/src/api.accounts.ts +0 -78
  128. package/src/api.auth.ts +0 -131
  129. package/src/api.file_list.ts +0 -102
  130. package/src/api.helpers.ts +0 -30
  131. package/src/api.monitor.ts +0 -106
  132. package/src/api.plugins.ts +0 -139
  133. package/src/api.vfs.ts +0 -182
  134. package/src/apiMiddleware.ts +0 -124
  135. package/src/block.ts +0 -35
  136. package/src/commands.ts +0 -122
  137. package/src/config.ts +0 -166
  138. package/src/connections.ts +0 -60
  139. package/src/const.ts +0 -57
  140. package/src/crypt.ts +0 -16
  141. package/src/debounceAsync.ts +0 -51
  142. package/src/events.ts +0 -6
  143. package/src/frontEndApis.ts +0 -17
  144. package/src/github.ts +0 -102
  145. package/src/index.ts +0 -53
  146. package/src/listen.ts +0 -220
  147. package/src/log.ts +0 -128
  148. package/src/middlewares.ts +0 -176
  149. package/src/misc.ts +0 -149
  150. package/src/pbkdf2.ts +0 -83
  151. package/src/perm.ts +0 -194
  152. package/src/plugins.ts +0 -342
  153. package/src/serveFile.ts +0 -104
  154. package/src/serveGuiFiles.ts +0 -95
  155. package/src/sse.ts +0 -29
  156. package/src/throttler.ts +0 -106
  157. package/src/update.ts +0 -67
  158. package/src/util-files.ts +0 -137
  159. package/src/util-generators.ts +0 -29
  160. package/src/util-http.ts +0 -29
  161. package/src/vfs.ts +0 -258
  162. package/src/watchLoad.ts +0 -75
  163. package/src/zip.ts +0 -69
package/src/config.ts DELETED
@@ -1,166 +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 EventEmitter from 'events'
4
- import { APP_PATH, argv, ORIGINAL_CWD } from './const'
5
- import { watchLoad } from './watchLoad'
6
- import yaml from 'yaml'
7
- import _ from 'lodash'
8
- import { debounceAsync, same, objSameKeys, onOff, wait, with_ } from './misc'
9
- import { copyFileSync, existsSync, renameSync, statSync } from 'fs'
10
- import { join, resolve } from 'path'
11
- import events from './events'
12
-
13
- const FILE = 'config.yaml'
14
-
15
- const configProps: Record<string, ConfigProps<any>> = {}
16
-
17
- let started = false // this will tell the difference for subscribeConfig()s that are called before or after config is loaded
18
- let state: Record<string, any> = {}
19
- const cfgEvents = new EventEmitter()
20
- cfgEvents.setMaxListeners(10_000)
21
- const path = with_(argv.config || process.env.HFS_CONFIG, p => {
22
- if (!p)
23
- return FILE
24
- p = resolve(ORIGINAL_CWD, p)
25
- try {
26
- if (statSync(p).isDirectory()) // try to detect if path points to a folder, in which case we add the standard filename
27
- return join(p, FILE)
28
- }
29
- catch {}
30
- return p
31
- })
32
- console.log("config", path)
33
- const legacyPosition = join(APP_PATH, FILE)
34
- if (!existsSync(path) && existsSync(legacyPosition))
35
- try {
36
- renameSync(legacyPosition, path)
37
- console.log("moved from legacy position", legacyPosition)
38
- }
39
- catch {
40
- try { // attempt copying, in case moving the source file proves to be impractical
41
- copyFileSync(legacyPosition, path)
42
- console.log("copied from legacy position", legacyPosition)
43
- }
44
- catch {}
45
- }
46
- const { save } = watchLoad(path, values => setConfig(values||{}, false), {
47
- failedOnFirstAttempt(){
48
- console.log("No config file, using defaults")
49
- setConfig({}, false)
50
- }
51
- })
52
-
53
- interface ConfigProps<T> {
54
- defaultValue?: T,
55
- }
56
- export function defineConfig<T>(k: string, defaultValue?: T) {
57
- configProps[k] = { defaultValue }
58
- type Updater = (currentValue:T) => T
59
- return {
60
- key() {
61
- return k
62
- },
63
- get(): T {
64
- return getConfig(k)
65
- },
66
- sub(cb: (v:T, was?:T)=>void) {
67
- return subscribeConfig(k, cb)
68
- },
69
- set(v: T | Updater) {
70
- if (typeof v === 'function')
71
- this.set((v as Updater)(this.get()))
72
- else
73
- setConfig1(k, v)
74
- }
75
- }
76
- }
77
-
78
- export function getConfigDefinition(k: string) {
79
- return configProps[k]
80
- }
81
-
82
- const stack: any[] = []
83
- function subscribeConfig<T>(k:string, cb: (v:T, was?:T)=>void) {
84
- if (started) // initial event already passed, we'll make the first call
85
- cb(getConfig(k))
86
- const eventName = 'new.'+k
87
- return onOff(cfgEvents, {
88
- [eventName]() {
89
- if (stack.includes(cb)) return // avoid infinite loop in case a subscriber changes the value
90
- stack.push(cb) // @ts-ignore arguments
91
- try { return cb.apply(this,arguments) }
92
- finally { stack.pop() }
93
- }
94
- })
95
- }
96
-
97
- export function getConfig(k:string) {
98
- return state[k] ?? _.cloneDeep(configProps[k]?.defaultValue) // clone to avoid changing
99
- }
100
-
101
- export function getWholeConfig({ omit, only }: { omit?:string[], only?:string[] }) {
102
- const defs = objSameKeys(configProps, x => x.defaultValue)
103
- let copy = _.defaults({}, state, defs)
104
- if (omit?.length)
105
- copy = _.omit(copy, omit)
106
- if (only)
107
- copy = _.pick(copy, only)
108
- return _.cloneDeep(copy)
109
- }
110
-
111
- // pass a value to `save` to force saving decision, or leave undefined for auto. Passing false will also reset previously loaded configs.
112
- export function setConfig(newCfg: Record<string,any>, save?: boolean) {
113
- if (!started) { // first time we consider also CLI args
114
- const argCfg = _.pickBy(objSameKeys(configProps, (x,k) => argv[k]), x => x !== undefined)
115
- if (! _.isEmpty(argCfg)) {
116
- saveConfigAsap().then() // don't set `save` argument, as it would interfere below at check `save===false`
117
- Object.assign(newCfg, argCfg)
118
- }
119
- }
120
- for (const k in newCfg)
121
- apply(k, newCfg[k])
122
- if (save) {
123
- saveConfigAsap().then()
124
- return
125
- }
126
- if (started) {
127
- if (save === false) // false is used when loading whole config, and in such case we should not leave previous values untreated. Also, we need this only after we already `started`.
128
- for (const k of Object.keys(state))
129
- if (!newCfg.hasOwnProperty(k))
130
- apply(k, newCfg[k])
131
- return
132
- }
133
- // first time we emit also for the default values
134
- for (const k of Object.keys(configProps))
135
- if (!newCfg.hasOwnProperty(k))
136
- apply(k, newCfg[k])
137
- started = true
138
- events.emit('config ready')
139
-
140
- function apply(k: string, newV: any) {
141
- return setConfig1(k, newV, save === undefined)
142
- }
143
- }
144
-
145
- function setConfig1(k: string, newV: any, saveChanges=true) {
146
- if (_.isPlainObject(newV))
147
- newV = _.pickBy(newV, x => x !== undefined)
148
- if (same(newV, configProps[k]?.defaultValue))
149
- newV = undefined
150
- if (started && same(newV, state[k])) return // no change
151
- const was = getConfig(k) // include cloned default, if necessary
152
- state[k] = newV
153
- cfgEvents.emit('new.'+k, getConfig(k), was)
154
- if (saveChanges)
155
- saveConfigAsap().then()
156
- }
157
-
158
- export const saveConfigAsap = debounceAsync(async () => {
159
- while (!started)
160
- await wait(100)
161
- let txt = yaml.stringify(state, { lineWidth:1000 })
162
- if (txt.trim() === '{}') // most users wouldn't understand
163
- txt = ''
164
- save(path, txt)
165
- .catch(err => console.error('Failed at saving config file, please ensure it is writable.', String(err)))
166
- })
@@ -1,60 +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 { Socket } from 'net'
4
- import events from './events'
5
- import Koa from 'koa'
6
- import _ from 'lodash'
7
-
8
- export class Connection {
9
- readonly started = new Date()
10
- sent = 0
11
- outSpeed?: number
12
- ctx?: Koa.Context
13
- private _cachedIp?: string
14
- [rest:symbol]: any // let other modules add extra data, but using symbols to avoid name collision
15
-
16
- constructor(readonly socket: Socket) {
17
- all.push(this)
18
- socket.on('close', () => {
19
- all.splice(all.indexOf(this), 1)
20
- events.emit('connectionClosed', this)
21
- })
22
- events.emit('connection', this)
23
- }
24
-
25
- get ip() { // prioritize ctx.ip as it supports proxies, but fallback for when ctx is not yet available
26
- return this.ctx?.ip || (this._cachedIp ??= normalizeIp(this.socket.remoteAddress||''))
27
- }
28
-
29
- get secure() {
30
- return (this.socket as any).server.cert > ''
31
- }
32
- }
33
-
34
- export function normalizeIp(ip: string) {
35
- return ip.replace(/^::ffff:/,'') // simplify ipv6-mapped addresses
36
- }
37
-
38
- const all: Connection[] = []
39
-
40
- export function newConnection(socket: Socket) {
41
- new Connection(socket)
42
- }
43
-
44
- export function getConnections(): Readonly<typeof all> {
45
- return all
46
- }
47
-
48
- export function socket2connection(socket: Socket) {
49
- return all.find(x => // socket exposed by Koa is TLSSocket which encapsulates simple Socket, and I've found no way to access it for simple comparison
50
- x.socket.remotePort === socket.remotePort // but we can still match them because IP:PORT is key
51
- && x.socket.remoteAddress === socket.remoteAddress )
52
- }
53
-
54
- export function updateConnection(conn: Connection, change: Partial<Connection>) {
55
- // if no change is detected, skip update. ctx is a special case
56
- if (!change.ctx && Object.entries(change).every(([k,v]) => _.isEqual(v, conn[k as keyof Connection]) ))
57
- return
58
- Object.assign(conn, change)
59
- events.emit('connectionUpdated', conn, change)
60
- }
package/src/const.ts DELETED
@@ -1,57 +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 minimist from 'minimist'
4
- import * as fs from 'fs'
5
- import { homedir } from 'os'
6
- import { mkdirSync } from 'fs'
7
- import { basename, dirname, join } from 'path'
8
-
9
- export const argv = minimist(process.argv.slice(2))
10
- export const DEV = process.env.DEV || argv.dev ? 'DEV' : ''
11
- export const ORIGINAL_CWD = process.cwd()
12
- export const HFS_STARTED = new Date()
13
- const PKG_PATH = join(__dirname, '..', 'package.json')
14
- 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 DAY = 86_400_000
18
- export const SESSION_DURATION = DAY
19
-
20
- export const API_VERSION = 4.1 // array.$width
21
- export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise is made equal to API_VERSION
22
-
23
- export const SPECIAL_URI = '/~/'
24
- export const FRONTEND_URI = SPECIAL_URI + 'frontend/'
25
- export const ADMIN_URI = SPECIAL_URI + 'admin/'
26
- export const API_URI = SPECIAL_URI + 'api/'
27
- export const PLUGINS_PUB_URI = SPECIAL_URI + 'plugins/'
28
-
29
- export const METHOD_NOT_ALLOWED = 405
30
- export const NO_CONTENT = 204
31
- export const FORBIDDEN = 403
32
- export const UNAUTHORIZED = 401
33
-
34
- export const IS_WINDOWS = process.platform === 'win32'
35
- const IS_BINARY = !basename(process.argv0).includes('node') // this won't be node if pkg was used
36
- export const APP_PATH = dirname(IS_BINARY ? process.argv0 : __dirname)
37
-
38
- // we want this to be the first stuff to be printed, then we print it in this module, that is executed at the beginning
39
- if (DEV) console.clear()
40
- else console.debug = ()=>{}
41
- console.log(`HFS ~ HTTP File Server - Copyright 2021-2022, Massimo Melina <a@rejetto.com>`)
42
- console.log(`License https://www.gnu.org/licenses/gpl-3.0.txt`)
43
- console.log('started', HFS_STARTED.toLocaleString(), DEV)
44
- console.log('version', VERSION||'-')
45
- console.log('build', BUILD_TIMESTAMP||'-')
46
- if (argv.cwd)
47
- process.chdir(argv.cwd)
48
- else if (!process.argv0.endsWith('.exe')) { // still considering whether to use this behavior with Windows users, who may be less accustomed to it
49
- const dir = join(homedir(), '.hfs')
50
- try { mkdirSync(dir) }
51
- catch(e: any) {
52
- if (e.code !== 'EEXIST')
53
- console.error(e)
54
- }
55
- process.chdir(dir)
56
- }
57
- console.log('cwd', process.cwd())
package/src/crypt.ts DELETED
@@ -1,16 +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
- // simple wrapper
4
- // @ts-ignore
5
- import { pbkdf2, pbkdf2Verify } from "./pbkdf2"
6
- import assert from 'assert'
7
-
8
- export async function hashPassword(s: string) {
9
- return 'p2:' + await pbkdf2(s)
10
- }
11
-
12
- export async function verifyPassword(hashed: string, given: string) {
13
- const i = hashed.indexOf(':')
14
- assert(i>0, 'bad hashed')
15
- return await pbkdf2Verify(hashed.slice(i+1), given) // for the time being we totally ignore the "method" part
16
- }
@@ -1,51 +0,0 @@
1
- // like lodash.debounce, but also avoids async invocations to overlap
2
- export default function debounceAsync<CB extends (...args: any[]) => Promise<R>, R>(
3
- callback: CB,
4
- wait: number=100,
5
- { leading=false, maxWait=Infinity }={}
6
- ) {
7
- let started = 0 // latest callback invocation
8
- let runningCallback: Promise<R> | undefined // latest callback invocation result
9
- let runningDebouncer: Promise<R | undefined> // latest wrapper invocation
10
- let waitingSince = 0 // we are delaying invocation since
11
- let whoIsWaiting: undefined | any[] // args' array object identifies the pending instance, and incidentally stores args
12
- const interceptingWrapper = (...args:any[]) => runningDebouncer = debouncer.apply(null, args)
13
- return Object.assign(interceptingWrapper, {
14
- cancel: () => {
15
- waitingSince = 0
16
- whoIsWaiting = undefined
17
- },
18
- flush: () => runningCallback ?? exec(),
19
- })
20
-
21
- async function debouncer(...args:any[]) {
22
- if (runningCallback)
23
- return await runningCallback
24
- whoIsWaiting = args
25
- waitingSince ||= Date.now()
26
- const waitingCap = maxWait - (Date.now() - (waitingSince || started))
27
- const waitFor = Math.min(waitingCap, leading ? wait - (Date.now() - started) : wait)
28
- if (waitFor > 0)
29
- await new Promise(resolve => setTimeout(resolve, waitFor))
30
- if (!whoIsWaiting) // canceled
31
- return void(waitingSince = 0)
32
- if (whoIsWaiting !== args) // another fresher call is waiting
33
- return runningDebouncer
34
- return await exec()
35
- }
36
-
37
- async function exec() {
38
- if (!whoIsWaiting) return
39
- waitingSince = 0
40
- started = Date.now()
41
- try {
42
- runningCallback = callback.apply(null, whoIsWaiting)
43
- return await runningCallback
44
- }
45
- finally {
46
- whoIsWaiting = undefined
47
- runningCallback = undefined
48
- }
49
- }
50
- }
51
-
package/src/events.ts DELETED
@@ -1,6 +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 EventEmitter from 'events'
4
-
5
- // app-wide events
6
- export default new EventEmitter()
@@ -1,17 +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 { ApiHandlers } from './apiMiddleware'
4
- import { file_list } from './api.file_list'
5
- import * as api_auth from './api.auth'
6
- import { defineConfig } from './config'
7
-
8
- const customHeader = defineConfig('custom_header')
9
-
10
- export const frontEndApis: ApiHandlers = {
11
- file_list,
12
- ...api_auth,
13
-
14
- config() {
15
- return Object.fromEntries([customHeader].map(x => [x.key(), x.get()]))
16
- }
17
- }
package/src/github.ts DELETED
@@ -1,102 +0,0 @@
1
- import events from './events'
2
- import { httpsString, httpsStream, unzip } from './misc'
3
- import { getAvailablePlugins, mapPlugins, parsePluginSource, PATH as PLUGINS_PATH, rescan } from './plugins'
4
- // @ts-ignore
5
- import unzipper from 'unzip-stream'
6
- import { ApiError } from './apiMiddleware'
7
- import _ from 'lodash'
8
-
9
- const DIST_ROOT = 'dist/'
10
-
11
- type DownloadStatus = true | undefined
12
- const downloading: Record<string, DownloadStatus> = {}
13
-
14
- function downloadProgress(id: string, status: DownloadStatus) {
15
- if (status === undefined)
16
- delete downloading[id]
17
- else
18
- downloading[id] = status
19
- events.emit('pluginDownload_'+id, status)
20
- }
21
-
22
- export async function downloadPlugin(repo: string, branch='', overwrite?: boolean) {
23
- if (downloading[repo])
24
- return new ApiError(409, "already downloading")
25
- downloadProgress(repo, true)
26
- const rec = await getRepoInfo(repo)
27
- if (!branch)
28
- branch = rec.default_branch
29
- const short = repo.split('/')[1] // second part, repo without the owner
30
- const folder2repo = getFolder2repo()
31
- const folder = overwrite ? _.findKey(folder2repo, x => x===repo)! // use existing folder
32
- : short in folder2repo ? repo.replace('/','-') // longer form only if another plugin is using short form
33
- : short
34
- const installPath = PLUGINS_PATH + '/' + folder
35
- const GITHUB_ZIP_ROOT = short + '-' + branch // GitHub puts everything within this folder
36
- const rootWithinZip = GITHUB_ZIP_ROOT + '/' + DIST_ROOT
37
- const stream = await httpsStream(`https://github.com/${repo}/archive/refs/heads/${branch}.zip`)
38
- await unzip(stream, path =>
39
- path.startsWith(rootWithinZip) && installPath + '/' + path.slice(rootWithinZip.length) )
40
- downloadProgress(repo, undefined)
41
- await rescan() // workaround: for some reason, operations are not triggering the rescan of the watched folder. Let's invoke it.
42
- return folder
43
- }
44
-
45
- export function getRepoInfo(id: string) {
46
- return apiGithub('repos/'+id)
47
- }
48
-
49
- export async function readOnlinePlugin(repoInfo: { full_name: string, default_branch: string }, branch='') {
50
- const url = `https://raw.githubusercontent.com/${repoInfo.full_name}/${branch || repoInfo.default_branch}/${DIST_ROOT}plugin.js`
51
- const res = await httpsString(url)
52
- if (!res.ok)
53
- throw res.statusCode
54
- const pl = parsePluginSource(repoInfo.full_name, res.body) // use 'repo' as 'id' client-side
55
- pl.branch = branch || undefined
56
- return pl
57
- }
58
-
59
- export function getFolder2repo() {
60
- const ret = Object.fromEntries(getAvailablePlugins().map(x => [x.id, x.repo]))
61
- Object.assign(ret, Object.fromEntries(mapPlugins(x => [x.id, x.getData().repo])))
62
- return ret
63
- }
64
-
65
- async function apiGithub(uri: string) {
66
- const res = await httpsString('https://api.github.com/'+uri, {
67
- headers: {
68
- 'User-Agent': 'HFS',
69
- Accept: 'application/vnd.github.v3+json',
70
- }
71
- })
72
- if (!res.ok)
73
- throw res.statusCode
74
- return JSON.parse(res.body)
75
- }
76
-
77
- export async function* searchPlugins(text: string) {
78
- const res = await apiGithub('search/repositories?q=topic:hfs-plugin+' + encodeURI(text))
79
- for (const it of res.items) {
80
- const repo = it.full_name
81
- let pl = await readOnlinePlugin(it)
82
- if (!pl.apiRequired) continue // mandatory field
83
- if (pl.badApi) { // we try other branches (starting with 'api')
84
- const branches: string[] = (await apiGithub('repos/' + it.full_name + '/branches'))
85
- .map((x: any) => x.name).filter((x: string) => x.startsWith('api')).sort()
86
- for (const branch of branches) {
87
- pl = await readOnlinePlugin(it, branch)
88
- if (!pl.apiRequired)
89
- pl.badApi = '-'
90
- if (!pl.badApi)
91
- break
92
- }
93
- }
94
- if (pl.badApi)
95
- continue
96
- Object.assign(pl, { // inject some extra useful fields
97
- downloading: downloading[repo],
98
- license: it.license?.spdx_id,
99
- }, _.pick(it, ['pushed_at', 'stargazers_count']))
100
- yield pl
101
- }
102
- }
package/src/index.ts DELETED
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env node
2
- // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
3
-
4
- import Koa from 'koa'
5
- import mount from 'koa-mount'
6
- import { apiMiddleware } from './apiMiddleware'
7
- import { API_URI, DEV } from './const'
8
- import { frontEndApis } from './frontEndApis'
9
- import { log } from './log'
10
- import { pluginsMiddleware } from './plugins'
11
- import { throttler } from './throttler'
12
- import { headRequests, gzipper, sessions, serveGuiAndSharedFiles, someSecurity, prepareState, paramsDecoder } from './middlewares'
13
- import './listen'
14
- import './commands'
15
- import { adminApis } from './adminApis'
16
- import { defineConfig } from './config'
17
- import { ok } from 'assert'
18
- import _ from 'lodash'
19
- import { randomId } from './misc'
20
-
21
- ok(_.intersection(Object.keys(frontEndApis), Object.keys(adminApis)).length === 0) // they share same endpoints
22
-
23
- const keys = process.env.COOKIE_SIGN_KEYS?.split(',') || [randomId(30)]
24
- export const app = new Koa({ keys })
25
- app.use(someSecurity)
26
- .use(sessions(app))
27
- .use(prepareState)
28
- .use(headRequests)
29
- .use(log())
30
- .use(throttler)
31
- .use(gzipper)
32
- .use(paramsDecoder)
33
- .use(pluginsMiddleware())
34
- .use(mount(API_URI, apiMiddleware({ ...frontEndApis, ...adminApis })))
35
- .use(serveGuiAndSharedFiles)
36
- .on('error', errorHandler)
37
-
38
- function errorHandler(err:Error & { code:string, path:string }) {
39
- const { code } = err
40
- if (DEV && code === 'ENOENT' && err.path.endsWith('sockjs-node')) return // spam out dev stuff
41
- if (code === 'ECANCELED' || code === 'ECONNRESET' || code === 'ECONNABORTED' || code === 'EPIPE') return // someone interrupted, don't care
42
- console.error('server error', err)
43
- }
44
-
45
- process.on('uncaughtException', err => {
46
- if ((err as any).syscall !== 'watch')
47
- console.error(err)
48
- })
49
-
50
- defineConfig('proxies', 0).sub(n => {
51
- app.proxy = n > 0
52
- app.maxIpsCount = n
53
- })