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.
- package/README.md +15 -2
- package/admin/assets/index-509bb1d6.js +415 -0
- package/admin/assets/index-60a380a7.css +1 -0
- package/admin/assets/sha512-738f0943.js +8 -0
- package/admin/index.html +3 -1
- package/admin/{public/logo.svg → logo.svg} +0 -0
- package/frontend/assets/index-6e178dfd.css +1 -0
- package/frontend/assets/index-aea7654e.js +85 -0
- package/frontend/assets/sha512-bf915587.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 +4 -2
- package/package.json +2 -6
- package/plugins/vhosting/plugin.js +23 -20
- 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 +110 -0
- package/src/api.helpers.js +32 -0
- package/src/api.monitor.js +104 -0
- package/src/api.plugins.js +128 -0
- package/src/api.vfs.js +167 -0
- package/src/apiMiddleware.js +123 -0
- package/src/block.js +34 -0
- package/src/commands.js +125 -0
- package/src/config.js +168 -0
- package/src/connections.js +57 -0
- package/src/const.js +94 -0
- package/src/crypt.js +21 -0
- package/src/debounceAsync.js +49 -0
- package/src/events.js +9 -0
- package/src/frontEndApis.js +38 -0
- package/src/github.js +104 -0
- package/src/index.js +57 -0
- package/src/listen.js +235 -0
- package/src/log.js +137 -0
- package/src/middlewares.js +195 -0
- package/src/misc.js +160 -0
- package/src/pbkdf2.js +74 -0
- package/src/perm.js +183 -0
- package/src/plugins.js +343 -0
- package/src/serveFile.js +105 -0
- package/src/serveGuiFiles.js +113 -0
- package/src/sse.js +30 -0
- package/src/throttler.js +91 -0
- package/src/update.js +70 -0
- package/src/util-files.js +163 -0
- package/src/util-generators.js +31 -0
- package/src/util-http.js +32 -0
- package/src/vfs.js +232 -0
- package/src/watchLoad.js +73 -0
- package/src/zip.js +73 -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/frontend/src/state.ts
DELETED
|
@@ -1,82 +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 _ from 'lodash'
|
|
4
|
-
import { proxy, useSnapshot } from 'valtio'
|
|
5
|
-
import { subscribeKey } from 'valtio/utils'
|
|
6
|
-
import { apiCall } from './api'
|
|
7
|
-
import { DirList } from './BrowseFiles'
|
|
8
|
-
|
|
9
|
-
export const state = proxy<{
|
|
10
|
-
stopSearch?: ()=>void,
|
|
11
|
-
stoppedSearch?: boolean,
|
|
12
|
-
iconsClass: string,
|
|
13
|
-
username: string,
|
|
14
|
-
list: DirList,
|
|
15
|
-
filteredList?: DirList,
|
|
16
|
-
loading: boolean,
|
|
17
|
-
error?: string,
|
|
18
|
-
listReloader: number,
|
|
19
|
-
patternFilter: string,
|
|
20
|
-
showFilter: boolean,
|
|
21
|
-
selected: Record<string,true>, // optimization: by using an object instead of an array, components are not rendered when the array changes, but only when their specific property change
|
|
22
|
-
remoteSearch: string,
|
|
23
|
-
sortBy: string,
|
|
24
|
-
invertOrder: boolean,
|
|
25
|
-
foldersFirst: boolean,
|
|
26
|
-
theme: string,
|
|
27
|
-
adminUrl?: string,
|
|
28
|
-
serverConfig?: any,
|
|
29
|
-
loginRequired?: boolean, // force user to login before proceeding
|
|
30
|
-
messageOnly?: string, // no gui, just show this message
|
|
31
|
-
}>({
|
|
32
|
-
iconsClass: '',
|
|
33
|
-
username: '',
|
|
34
|
-
list: [],
|
|
35
|
-
filteredList: undefined,
|
|
36
|
-
loading: false,
|
|
37
|
-
listReloader: 0,
|
|
38
|
-
patternFilter: '',
|
|
39
|
-
showFilter: false,
|
|
40
|
-
selected: {},
|
|
41
|
-
remoteSearch: '',
|
|
42
|
-
sortBy: 'name',
|
|
43
|
-
invertOrder: false,
|
|
44
|
-
foldersFirst: true,
|
|
45
|
-
theme: '',
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
export function useSnapState() {
|
|
49
|
-
return useSnapshot(state)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const SETTINGS_KEY = 'hfs_settings'
|
|
53
|
-
const SETTINGS_TO_STORE: (keyof typeof state)[] = ['sortBy', 'invertOrder', 'foldersFirst', 'theme']
|
|
54
|
-
|
|
55
|
-
loadSettings()
|
|
56
|
-
for (const k of SETTINGS_TO_STORE)
|
|
57
|
-
subscribeKey(state, k, storeSettings)
|
|
58
|
-
|
|
59
|
-
// load server config
|
|
60
|
-
setTimeout(() =>
|
|
61
|
-
apiCall('config', {}, { noModal: true }).then(res =>
|
|
62
|
-
state.serverConfig = res) )
|
|
63
|
-
|
|
64
|
-
function loadSettings() {
|
|
65
|
-
const json = localStorage.getItem(SETTINGS_KEY)
|
|
66
|
-
if (!json) return
|
|
67
|
-
let read
|
|
68
|
-
try { read = JSON.parse(json) }
|
|
69
|
-
catch {
|
|
70
|
-
console.error('invalid settings stored', json)
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
for (const k of SETTINGS_TO_STORE) {
|
|
74
|
-
const v = read[k]
|
|
75
|
-
if (v !== undefined) // @ts-ignore
|
|
76
|
-
state[k] = v
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function storeSettings() {
|
|
81
|
-
localStorage.setItem(SETTINGS_KEY, JSON.stringify(_.pick(state, SETTINGS_TO_STORE)))
|
|
82
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { state, useSnapState } from './state'
|
|
2
|
-
import { useNavigate } from 'react-router-dom'
|
|
3
|
-
import { useEffect } from 'react'
|
|
4
|
-
import { loginDialog } from './menu'
|
|
5
|
-
|
|
6
|
-
export default function useAuthorized() {
|
|
7
|
-
const { loginRequired } = useSnapState()
|
|
8
|
-
const navigate = useNavigate()
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
(async () => {
|
|
11
|
-
while (state.loginRequired)
|
|
12
|
-
await loginDialog(navigate).then()
|
|
13
|
-
})()
|
|
14
|
-
}, [loginRequired, navigate])
|
|
15
|
-
return loginRequired ? null : true
|
|
16
|
-
}
|
|
17
|
-
|
|
@@ -1,144 +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 { state, useSnapState } from './state'
|
|
4
|
-
import { useEffect, useRef } from 'react'
|
|
5
|
-
import { apiEvents } from './api'
|
|
6
|
-
import { DirEntry, DirList, usePath } from './BrowseFiles'
|
|
7
|
-
import _ from 'lodash'
|
|
8
|
-
import { subscribeKey } from 'valtio/utils'
|
|
9
|
-
|
|
10
|
-
export default function useFetchList() {
|
|
11
|
-
const snap = useSnapState()
|
|
12
|
-
const desiredPath = usePath()
|
|
13
|
-
const search = snap.remoteSearch || undefined
|
|
14
|
-
const lastPath = useRef('')
|
|
15
|
-
|
|
16
|
-
useEffect(()=>{
|
|
17
|
-
const previous = lastPath.current
|
|
18
|
-
lastPath.current = desiredPath
|
|
19
|
-
if (previous !== desiredPath) {
|
|
20
|
-
state.showFilter = false
|
|
21
|
-
state.stopSearch?.()
|
|
22
|
-
}
|
|
23
|
-
state.stoppedSearch = false
|
|
24
|
-
if (previous !== desiredPath && search) {
|
|
25
|
-
state.remoteSearch = ''
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const API = 'file_list'
|
|
30
|
-
const baseParams = { path:desiredPath, search, sse:true, omit:'c' }
|
|
31
|
-
state.list = []
|
|
32
|
-
state.filteredList = undefined
|
|
33
|
-
state.selected = {}
|
|
34
|
-
state.loading = true
|
|
35
|
-
state.error = undefined
|
|
36
|
-
// buffering entries is necessary against burst of events that will hang the browser
|
|
37
|
-
const buffer: DirList = []
|
|
38
|
-
const flush = () => {
|
|
39
|
-
const chunk = buffer.splice(0, Infinity)
|
|
40
|
-
if (chunk.length)
|
|
41
|
-
state.list = sort([...state.list, ...chunk.map(precalculate)])
|
|
42
|
-
}
|
|
43
|
-
const timer = setInterval(flush, 1000)
|
|
44
|
-
const src = apiEvents(API, baseParams, (type, data) => {
|
|
45
|
-
switch (type) {
|
|
46
|
-
case 'error':
|
|
47
|
-
state.stopSearch?.()
|
|
48
|
-
state.error = JSON.stringify(data)
|
|
49
|
-
return
|
|
50
|
-
case 'closed':
|
|
51
|
-
flush()
|
|
52
|
-
state.stopSearch?.()
|
|
53
|
-
state.loading = false
|
|
54
|
-
return
|
|
55
|
-
case 'msg':
|
|
56
|
-
data.forEach((data: any) => {
|
|
57
|
-
if (data.add)
|
|
58
|
-
return buffer.push(data.add)
|
|
59
|
-
const { error } = data
|
|
60
|
-
if (error === 405) { // "method not allowed" happens when we try to directly access an unauthorized file, and we get a login prompt, and then file_list the file (because we didn't know it was file or folder)
|
|
61
|
-
state.messageOnly = "Your download should now start"
|
|
62
|
-
window.location.reload() // reload will start the download, because now we got authenticated
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
if (error) {
|
|
66
|
-
state.stopSearch?.()
|
|
67
|
-
state.error = (ERRORS as any)[error] || String(error)
|
|
68
|
-
state.loginRequired = error === 401
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
if (src?.readyState === src?.CLOSED)
|
|
73
|
-
return state.stopSearch?.()
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
state.stopSearch = ()=>{
|
|
77
|
-
state.stopSearch = undefined
|
|
78
|
-
buffer.length = 0
|
|
79
|
-
state.loading = false
|
|
80
|
-
clearInterval(timer)
|
|
81
|
-
src.close()
|
|
82
|
-
}
|
|
83
|
-
}, [desiredPath, search, snap.username, snap.listReloader])
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const ERRORS = {
|
|
87
|
-
404: "Not found"
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function reloadList() {
|
|
91
|
-
state.listReloader = Date.now()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const { compare:localCompare } = new Intl.Collator(navigator.language)
|
|
95
|
-
|
|
96
|
-
function sort(list: DirList) {
|
|
97
|
-
const { sortBy, foldersFirst } = state
|
|
98
|
-
// optimization: precalculate string comparisons
|
|
99
|
-
const bySize = sortBy === 'size'
|
|
100
|
-
const byExt = sortBy === 'extension'
|
|
101
|
-
const byTime = sortBy === 'time'
|
|
102
|
-
const invert = state.invertOrder ? -1 : 1
|
|
103
|
-
return list.sort((a,b) =>
|
|
104
|
-
foldersFirst && -compare(a.isFolder, b.isFolder)
|
|
105
|
-
|| invert*(bySize ? compare(a.s||0, b.s||0)
|
|
106
|
-
: byExt ? localCompare(a.ext, b.ext)
|
|
107
|
-
: byTime ? compare(a.t, b.t)
|
|
108
|
-
: 0
|
|
109
|
-
)
|
|
110
|
-
|| invert*localCompare(a.n, b.n) // fallback to name/path
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function precalculate(rec:DirEntry) {
|
|
115
|
-
const i = rec.n.lastIndexOf('.') + 1
|
|
116
|
-
rec.ext = i ? rec.n.substring(i) : ''
|
|
117
|
-
rec.isFolder = rec.n.endsWith('/')
|
|
118
|
-
const t = rec.m || rec.c
|
|
119
|
-
if (t)
|
|
120
|
-
rec.t = new Date(t)
|
|
121
|
-
return rec
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// generic comparison
|
|
125
|
-
function compare(a:any, b:any) {
|
|
126
|
-
return a < b ? -1 : a > b ? 1 : 0
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// update list on sorting criteria
|
|
130
|
-
const sortAgain = _.debounce(()=> state.list = sort(state.list), 100)
|
|
131
|
-
subscribeKey(state, 'sortBy', sortAgain)
|
|
132
|
-
subscribeKey(state, 'invertOrder', sortAgain)
|
|
133
|
-
subscribeKey(state, 'foldersFirst', sortAgain)
|
|
134
|
-
|
|
135
|
-
subscribeKey(state, 'patternFilter', v => {
|
|
136
|
-
if (!v)
|
|
137
|
-
return state.filteredList = undefined
|
|
138
|
-
const filter = new RegExp(_.escapeRegExp(v),'i')
|
|
139
|
-
const newList = []
|
|
140
|
-
for (const entry of state.list)
|
|
141
|
-
if (filter.test(entry.n))
|
|
142
|
-
newList.push(entry)
|
|
143
|
-
state.filteredList = newList
|
|
144
|
-
})
|
package/frontend/src/useTheme.ts
DELETED
|
@@ -1,23 +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 { useSnapState } from './state'
|
|
4
|
-
import { useEffect } from 'react'
|
|
5
|
-
import { useDarkMode } from 'usehooks-ts'
|
|
6
|
-
|
|
7
|
-
export default function useTheme() {
|
|
8
|
-
const { theme } = useSnapState()
|
|
9
|
-
const { isDarkMode } = useDarkMode()
|
|
10
|
-
useEffect(()=>{
|
|
11
|
-
const e = document.body
|
|
12
|
-
if (!e) return
|
|
13
|
-
const name = theme || (isDarkMode ? 'dark' : 'light')
|
|
14
|
-
const pre = 'theme-'
|
|
15
|
-
const ct = pre + name
|
|
16
|
-
const list = e.classList
|
|
17
|
-
for (const c of Array.from(list))
|
|
18
|
-
if (c.startsWith(pre) && c !== ct)
|
|
19
|
-
list.remove(c)
|
|
20
|
-
if (name)
|
|
21
|
-
list.add(ct)
|
|
22
|
-
}, [theme, isDarkMode])
|
|
23
|
-
}
|
package/frontend/tsconfig.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "es2017",
|
|
4
|
-
"lib": [
|
|
5
|
-
"dom",
|
|
6
|
-
"dom.iterable",
|
|
7
|
-
"esnext"
|
|
8
|
-
],
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"esModuleInterop": true,
|
|
12
|
-
"allowSyntheticDefaultImports": true,
|
|
13
|
-
"strict": true,
|
|
14
|
-
"forceConsistentCasingInFileNames": true,
|
|
15
|
-
"noFallthroughCasesInSwitch": true,
|
|
16
|
-
"module": "esnext",
|
|
17
|
-
"moduleResolution": "node",
|
|
18
|
-
"resolveJsonModule": true,
|
|
19
|
-
"isolatedModules": true,
|
|
20
|
-
"noEmit": true,
|
|
21
|
-
"jsx": "react-jsx"
|
|
22
|
-
},
|
|
23
|
-
"include": [
|
|
24
|
-
"src"
|
|
25
|
-
]
|
|
26
|
-
}
|
package/frontend/vite.config.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite'
|
|
2
|
-
|
|
3
|
-
// https://vitejs.dev/config/
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
build: {
|
|
6
|
-
outDir: '../dist/frontend',
|
|
7
|
-
emptyOutDir: true,
|
|
8
|
-
target: "es2015",
|
|
9
|
-
},
|
|
10
|
-
server: {
|
|
11
|
-
port: 3005,
|
|
12
|
-
proxy: {
|
|
13
|
-
'/~/': {
|
|
14
|
-
target: 'http://localhost',
|
|
15
|
-
proxyTimeout: 2000,
|
|
16
|
-
changeOrigin: true,
|
|
17
|
-
ws: true,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
})
|
package/src/QuickZipStream.ts
DELETED
|
@@ -1,279 +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
|
-
// @ts-ignore
|
|
5
|
-
import { unsigned } from 'buffer-crc32'
|
|
6
|
-
import assert from 'assert'
|
|
7
|
-
|
|
8
|
-
const ZIP64_SIZE_LIMIT = 0xffffffff
|
|
9
|
-
const ZIP64_NUMBER_LIMIT = 0xffff
|
|
10
|
-
|
|
11
|
-
let crc32function: (input: string | Buffer, initialState?: number | undefined | null) => number
|
|
12
|
-
import('@node-rs/crc32').then(lib => crc32function = lib.crc32, () => {
|
|
13
|
-
console.log('using generic lib for crc32')
|
|
14
|
-
crc32function = unsigned
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const FLAGS = 0x0808 // bit3 = no crc in local header + bit11 = utf8
|
|
18
|
-
|
|
19
|
-
interface ZipSource {
|
|
20
|
-
path: string
|
|
21
|
-
sourcePath?: string
|
|
22
|
-
getData: () => Readable // deferred stream, so that we don't keep many open files because of calculateSize()
|
|
23
|
-
size: number
|
|
24
|
-
ts: Date
|
|
25
|
-
mode?: number
|
|
26
|
-
}
|
|
27
|
-
export class QuickZipStream extends Readable {
|
|
28
|
-
private workingFile: Readable | undefined
|
|
29
|
-
private finished = false
|
|
30
|
-
private readonly entries: ({ size:number, crc:number, ts:Date, pathAsBuffer:Buffer, offset:number, version:number, extAttr: number })[] = []
|
|
31
|
-
private dataWritten = 0
|
|
32
|
-
private consumedCalculating: ZipSource[] = []
|
|
33
|
-
private skip: number = 0
|
|
34
|
-
private limit?: number
|
|
35
|
-
|
|
36
|
-
constructor(private readonly walker: AsyncIterableIterator<ZipSource>) {
|
|
37
|
-
super({})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
earlyClose() {
|
|
41
|
-
this.finished = true
|
|
42
|
-
this.push(null)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
applyRange(start: number, end: number) {
|
|
46
|
-
if (end < start)
|
|
47
|
-
return this.earlyClose()
|
|
48
|
-
this.skip = start
|
|
49
|
-
this.limit = end - start + 1
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
controlledPush(chunk: number[] | Buffer) {
|
|
53
|
-
if (Array.isArray(chunk))
|
|
54
|
-
chunk = buffer(chunk)
|
|
55
|
-
this.dataWritten += chunk.length
|
|
56
|
-
if (this.skip) {
|
|
57
|
-
if (this.skip >= chunk.length) {
|
|
58
|
-
this.skip -= chunk.length
|
|
59
|
-
return true
|
|
60
|
-
}
|
|
61
|
-
chunk = chunk.subarray(this.skip)
|
|
62
|
-
this.skip = 0
|
|
63
|
-
}
|
|
64
|
-
const lastBit = this.limit! < chunk.length
|
|
65
|
-
if (lastBit)
|
|
66
|
-
chunk = chunk.subarray(0, this.limit)
|
|
67
|
-
|
|
68
|
-
const ret = this.push(chunk)
|
|
69
|
-
if (lastBit)
|
|
70
|
-
this.earlyClose()
|
|
71
|
-
return ret
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async calculateSize(howLong:number = 1000) {
|
|
75
|
-
const endBy = Date.now() + howLong
|
|
76
|
-
while (1) {
|
|
77
|
-
if (Date.now() >= endBy)
|
|
78
|
-
return NaN
|
|
79
|
-
const { value } = await this.walker.next()
|
|
80
|
-
if (!value) break
|
|
81
|
-
this.consumedCalculating.push(value) // we keep same shape of the generator, so
|
|
82
|
-
}
|
|
83
|
-
// if we reach here, then we were able to consume all entries of the walker (in time)
|
|
84
|
-
let offset = 0
|
|
85
|
-
let centralDirSize = 0
|
|
86
|
-
for (const file of this.consumedCalculating) {
|
|
87
|
-
const pathSize = Buffer.from(file.path, 'utf8').length
|
|
88
|
-
const extraLength = (file.size > ZIP64_SIZE_LIMIT ? 2 : 0) + (offset > ZIP64_SIZE_LIMIT ? 1 : 0)
|
|
89
|
-
const extraDataSize = extraLength && (2+2 + extraLength*8)
|
|
90
|
-
offset += 4+2+2+2+ 4+4+4+4+ 2+2+ pathSize + file.size
|
|
91
|
-
centralDirSize += 4+2+2+2+2+ 4+4+4+4+ 2+2+2+2+2+ 4+4 + pathSize + extraDataSize
|
|
92
|
-
}
|
|
93
|
-
const n = this.consumedCalculating.length
|
|
94
|
-
const centralDirOffset = offset
|
|
95
|
-
if (n >= ZIP64_NUMBER_LIMIT
|
|
96
|
-
|| centralDirOffset >= ZIP64_SIZE_LIMIT
|
|
97
|
-
|| centralDirSize >= ZIP64_SIZE_LIMIT)
|
|
98
|
-
centralDirSize += 4+8+2+2+4+4+8+8+8+8+4+4+8+4
|
|
99
|
-
centralDirSize += 4+4+2+2+4+4+2
|
|
100
|
-
return offset + centralDirSize
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async _read() {
|
|
104
|
-
if (this.finished || this.destroyed) return
|
|
105
|
-
if (this.workingFile)
|
|
106
|
-
return this.workingFile.resume()
|
|
107
|
-
const file = this.consumedCalculating.shift() || (await this.walker.next()).value as ZipSource
|
|
108
|
-
if (!file)
|
|
109
|
-
return this.closeArchive()
|
|
110
|
-
let { path, sourcePath, getData, size, ts, mode } = file
|
|
111
|
-
const pathAsBuffer = Buffer.from(path, 'utf8')
|
|
112
|
-
const offset = this.dataWritten
|
|
113
|
-
const version = 20
|
|
114
|
-
this.controlledPush([
|
|
115
|
-
4, 0x04034b50,
|
|
116
|
-
2, version,
|
|
117
|
-
2, FLAGS,
|
|
118
|
-
2, 0, // compression = store
|
|
119
|
-
...ts2buf(ts),
|
|
120
|
-
4, 0, // crc
|
|
121
|
-
4, 0, // size
|
|
122
|
-
4, 0, // size
|
|
123
|
-
2, pathAsBuffer.length,
|
|
124
|
-
2, 0, // extra length
|
|
125
|
-
])
|
|
126
|
-
this.controlledPush(pathAsBuffer)
|
|
127
|
-
if (this.finished) return
|
|
128
|
-
|
|
129
|
-
const cache = sourcePath ? crcCache[sourcePath] : undefined
|
|
130
|
-
const cacheHit = Number(cache?.ts) === Number(ts)
|
|
131
|
-
let crc = cacheHit ? cache!.crc : crc32function('')
|
|
132
|
-
const extAttr = !mode ? 0 : (mode | 0x8000) * 0x10000 // it's like <<16 but doesn't overflow so easily
|
|
133
|
-
const entry = { size, crc, pathAsBuffer, ts, offset, version, extAttr }
|
|
134
|
-
if (this.skip >= size && cacheHit) {
|
|
135
|
-
this.skip -= size
|
|
136
|
-
this.dataWritten += size
|
|
137
|
-
this.entries.push(entry)
|
|
138
|
-
setTimeout(() => this.push('')) // this "signal" works only after _read() is done
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
const data = getData()
|
|
142
|
-
data.on('error', (err) => console.error(err))
|
|
143
|
-
data.on('end', ()=>{
|
|
144
|
-
this.workingFile = undefined
|
|
145
|
-
entry.crc = crc
|
|
146
|
-
if (sourcePath)
|
|
147
|
-
crcCache[sourcePath] = { ts, crc }
|
|
148
|
-
this.entries.push(entry)
|
|
149
|
-
this.push('') // continue piping
|
|
150
|
-
})
|
|
151
|
-
this.workingFile = data
|
|
152
|
-
data.on('data', chunk => {
|
|
153
|
-
if (this.destroyed)
|
|
154
|
-
return data.destroy()
|
|
155
|
-
if (!this.controlledPush(chunk)) // destination buffer full
|
|
156
|
-
data.pause() // slow down
|
|
157
|
-
if (!cacheHit)
|
|
158
|
-
crc = crc32function(chunk, crc)
|
|
159
|
-
if (this.finished)
|
|
160
|
-
return data.destroy()
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
closeArchive() {
|
|
165
|
-
this.finished = true
|
|
166
|
-
let centralDirOffset = this.dataWritten
|
|
167
|
-
for (let { size, ts, crc, offset, pathAsBuffer, version, extAttr } of this.entries) {
|
|
168
|
-
const extra = []
|
|
169
|
-
if (size > ZIP64_SIZE_LIMIT) {
|
|
170
|
-
extra.push(size, size)
|
|
171
|
-
size = ZIP64_SIZE_LIMIT
|
|
172
|
-
}
|
|
173
|
-
if (offset > ZIP64_SIZE_LIMIT) {
|
|
174
|
-
extra.push(offset)
|
|
175
|
-
offset = ZIP64_SIZE_LIMIT
|
|
176
|
-
}
|
|
177
|
-
const extraData = buffer(!extra.length ? []
|
|
178
|
-
: [ 2,1, 2,8*extra.length, ...extra.map(x=> [8,x]).flat() ])
|
|
179
|
-
if (extraData.length && version < 45)
|
|
180
|
-
version = 45
|
|
181
|
-
this.controlledPush([
|
|
182
|
-
4, 0x02014b50, // central dir signature
|
|
183
|
-
2, version,
|
|
184
|
-
2, version,
|
|
185
|
-
2, FLAGS,
|
|
186
|
-
2, 0, // compression method = store
|
|
187
|
-
...ts2buf(ts),
|
|
188
|
-
4, crc,
|
|
189
|
-
4, size, // compressed
|
|
190
|
-
4, size,
|
|
191
|
-
2, pathAsBuffer.length,
|
|
192
|
-
2, extraData.length,
|
|
193
|
-
2, 0, //comment length
|
|
194
|
-
2, 0, // disk
|
|
195
|
-
2, 0, // attr
|
|
196
|
-
4, extAttr,
|
|
197
|
-
4, offset,
|
|
198
|
-
])
|
|
199
|
-
this.controlledPush(pathAsBuffer)
|
|
200
|
-
this.controlledPush(extraData)
|
|
201
|
-
}
|
|
202
|
-
const after = this.dataWritten
|
|
203
|
-
let centralDirSize = after - centralDirOffset
|
|
204
|
-
let n = this.entries.length
|
|
205
|
-
if (n >= ZIP64_NUMBER_LIMIT
|
|
206
|
-
|| centralDirOffset >= ZIP64_SIZE_LIMIT
|
|
207
|
-
|| centralDirSize >= ZIP64_SIZE_LIMIT) {
|
|
208
|
-
this.controlledPush([
|
|
209
|
-
4, 0x06064b50, // end of central dir zip64
|
|
210
|
-
8, 44,
|
|
211
|
-
2, 45,
|
|
212
|
-
2, 45,
|
|
213
|
-
4, 0,
|
|
214
|
-
4, 0,
|
|
215
|
-
8, n,
|
|
216
|
-
8, n,
|
|
217
|
-
8, centralDirSize,
|
|
218
|
-
8, centralDirOffset,
|
|
219
|
-
])
|
|
220
|
-
this.controlledPush([
|
|
221
|
-
4, 0x07064b50,
|
|
222
|
-
4, 0,
|
|
223
|
-
8, after,
|
|
224
|
-
4, 1,
|
|
225
|
-
])
|
|
226
|
-
centralDirOffset = ZIP64_SIZE_LIMIT
|
|
227
|
-
centralDirSize = ZIP64_SIZE_LIMIT
|
|
228
|
-
n = ZIP64_NUMBER_LIMIT
|
|
229
|
-
}
|
|
230
|
-
this.controlledPush([
|
|
231
|
-
4, 0x06054b50, // end of central directory signature
|
|
232
|
-
4, 0, // disk-related stuff
|
|
233
|
-
2, n,
|
|
234
|
-
2, n,
|
|
235
|
-
4, centralDirSize,
|
|
236
|
-
4, centralDirOffset,
|
|
237
|
-
2, 0, // comment length
|
|
238
|
-
])
|
|
239
|
-
this.push(null) // EOF
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function buffer(pairs: number[]) {
|
|
244
|
-
assert(pairs.length % 2 === 0)
|
|
245
|
-
let total = 0
|
|
246
|
-
for (let i=0; i < pairs.length; i+=2)
|
|
247
|
-
total += pairs[i]
|
|
248
|
-
const ret = Buffer.alloc(total, 0)
|
|
249
|
-
let offset = 0
|
|
250
|
-
let i = 0
|
|
251
|
-
while (i < pairs.length) {
|
|
252
|
-
const size = pairs[i++]
|
|
253
|
-
const data = pairs[i++]
|
|
254
|
-
if (size === 1)
|
|
255
|
-
ret.writeUInt8(data, offset)
|
|
256
|
-
else if (size === 2)
|
|
257
|
-
ret.writeUInt16LE(data, offset)
|
|
258
|
-
else if (size === 4)
|
|
259
|
-
ret.writeUInt32LE(data, offset)
|
|
260
|
-
else if (size === 8)
|
|
261
|
-
ret.writeBigUInt64LE(BigInt(data), offset)
|
|
262
|
-
else
|
|
263
|
-
throw 'unsupported'
|
|
264
|
-
offset += size
|
|
265
|
-
}
|
|
266
|
-
return ret
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function ts2buf(ts:Date) {
|
|
270
|
-
const date = ((ts.getFullYear() - 1980) & 0x7F) << 9 | (ts.getMonth() + 1) << 5 | ts.getDate()
|
|
271
|
-
const time = ts.getHours() << 11 | ts.getMinutes() << 5 | (ts.getSeconds() / 2) & 0x0F
|
|
272
|
-
return [
|
|
273
|
-
2, time,
|
|
274
|
-
2, date,
|
|
275
|
-
]
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
interface CrcCacheEntry { ts: Date, crc: number }
|
|
279
|
-
const crcCache: Record<string, CrcCacheEntry> = {}
|
package/src/ThrottledStream.ts
DELETED
|
@@ -1,98 +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 { Transform, TransformCallback } from 'stream'
|
|
4
|
-
import { TokenBucket } from 'limiter'
|
|
5
|
-
|
|
6
|
-
// throttled stream
|
|
7
|
-
export class ThrottledStream extends Transform {
|
|
8
|
-
|
|
9
|
-
private sent: number = 0
|
|
10
|
-
private lastSpeed: number = 0
|
|
11
|
-
private lastSpeedTime = Date.now()
|
|
12
|
-
private totalSent: number = 0 // total sent over connection, since connection can be re-used for multiple requests
|
|
13
|
-
|
|
14
|
-
constructor(private group: ThrottleGroup, copyStats?: ThrottledStream) {
|
|
15
|
-
super()
|
|
16
|
-
if (!copyStats) return
|
|
17
|
-
this.sent = copyStats.sent
|
|
18
|
-
this.totalSent = copyStats.totalSent
|
|
19
|
-
this.lastSpeedTime = copyStats.lastSpeedTime
|
|
20
|
-
this.lastSpeed = copyStats.lastSpeed
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async _transform(chunk: any, encoding: BufferEncoding, done: TransformCallback) {
|
|
24
|
-
let pos = 0
|
|
25
|
-
while (1) {
|
|
26
|
-
let n = this.group.suggestChunkSize()
|
|
27
|
-
const slice = chunk.slice(pos, pos + n)
|
|
28
|
-
n = slice.length
|
|
29
|
-
if (!n) // we're done here
|
|
30
|
-
return done()
|
|
31
|
-
try {
|
|
32
|
-
await this.group.consume(n)
|
|
33
|
-
this.push(slice)
|
|
34
|
-
this.sent += n
|
|
35
|
-
this.totalSent += n
|
|
36
|
-
pos += n
|
|
37
|
-
this.emit('sent', n)
|
|
38
|
-
} catch (e) {
|
|
39
|
-
done(e as Error)
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// @return kBs
|
|
46
|
-
getSpeed(): number {
|
|
47
|
-
const now = Date.now()
|
|
48
|
-
const past = now - this.lastSpeedTime
|
|
49
|
-
if (past >= 1000) { // recalculate?
|
|
50
|
-
this.lastSpeedTime = now
|
|
51
|
-
this.lastSpeed = this.sent / past
|
|
52
|
-
this.sent = 0
|
|
53
|
-
}
|
|
54
|
-
return this.lastSpeed
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
getBytesSent() {
|
|
58
|
-
return this.totalSent
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export class ThrottleGroup {
|
|
63
|
-
|
|
64
|
-
private bucket: TokenBucket
|
|
65
|
-
|
|
66
|
-
constructor(kBs: number, private parent?: ThrottleGroup) {
|
|
67
|
-
this.bucket = this.updateLimit(kBs) // assignment is redundant and yet the best way I've found to shut up typescript
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// @return kBs
|
|
71
|
-
getLimit() {
|
|
72
|
-
return this.bucket.bucketSize / 1000
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
updateLimit(kBs: number) {
|
|
76
|
-
if (kBs < 0)
|
|
77
|
-
throw new Error('invalid bytesPerSecond')
|
|
78
|
-
kBs *= 1000
|
|
79
|
-
return this.bucket = new TokenBucket({
|
|
80
|
-
bucketSize: kBs,
|
|
81
|
-
tokensPerInterval: kBs,
|
|
82
|
-
interval: 'second',
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
suggestChunkSize() {
|
|
87
|
-
let b: TokenBucket | undefined = this.bucket
|
|
88
|
-
b.parentBucket = this.parent?.bucket
|
|
89
|
-
let min = b.bucketSize
|
|
90
|
-
while (b = b.parentBucket)
|
|
91
|
-
min = Math.min(min, b.bucketSize)
|
|
92
|
-
return min / 10
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
consume(n: number) {
|
|
96
|
-
return this.bucket.removeTokens(n)
|
|
97
|
-
}
|
|
98
|
-
}
|