hfs 0.26.7 → 0.26.8

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 (162) hide show
  1. package/admin/.DS_Store +0 -0
  2. package/admin/.eslintrc +8 -0
  3. package/admin/.gitignore +23 -0
  4. package/admin/index.html +1 -3
  5. package/admin/package.json +67 -0
  6. package/admin/{logo.svg → public/logo.svg} +0 -0
  7. package/admin/src/AccountForm.ts +92 -0
  8. package/admin/src/AccountsPage.ts +143 -0
  9. package/admin/src/App.ts +83 -0
  10. package/admin/src/ArrayField.ts +84 -0
  11. package/admin/src/ConfigPage.ts +279 -0
  12. package/admin/src/FileField.ts +52 -0
  13. package/admin/src/FileForm.ts +148 -0
  14. package/admin/src/FilePicker.ts +166 -0
  15. package/admin/src/HomePage.ts +96 -0
  16. package/admin/src/InstalledPlugins.ts +158 -0
  17. package/admin/src/LoginRequired.ts +75 -0
  18. package/admin/src/LogoutPage.ts +27 -0
  19. package/admin/src/LogsPage.ts +75 -0
  20. package/admin/src/MainMenu.ts +74 -0
  21. package/admin/src/MenuButton.ts +38 -0
  22. package/admin/src/MonitorPage.ts +200 -0
  23. package/admin/src/OnlinePlugins.ts +101 -0
  24. package/admin/src/PermField.ts +80 -0
  25. package/admin/src/PluginsPage.ts +27 -0
  26. package/admin/src/VfsMenuBar.ts +58 -0
  27. package/admin/src/VfsPage.ts +124 -0
  28. package/admin/src/VfsTree.ts +95 -0
  29. package/admin/src/addFiles.ts +59 -0
  30. package/admin/src/api.ts +246 -0
  31. package/admin/src/dialog.ts +203 -0
  32. package/admin/src/index.css +21 -0
  33. package/admin/src/index.ts +10 -0
  34. package/admin/src/md.ts +31 -0
  35. package/admin/src/misc.ts +141 -0
  36. package/admin/src/react-app-env.d.ts +1 -0
  37. package/admin/src/reportWebVitals.ts +15 -0
  38. package/admin/src/setupTests.ts +5 -0
  39. package/admin/src/state.ts +40 -0
  40. package/admin/src/theme.ts +37 -0
  41. package/admin/tsconfig.json +26 -0
  42. package/admin/vite.config.ts +32 -0
  43. package/frontend/.DS_Store +0 -0
  44. package/frontend/.eslintrc +8 -0
  45. package/frontend/.gitignore +23 -0
  46. package/frontend/index.html +1 -3
  47. package/frontend/package.json +51 -0
  48. package/frontend/{fontello.css → public/fontello.css} +0 -0
  49. package/frontend/{fontello.woff2 → public/fontello.woff2} +0 -0
  50. package/frontend/src/App.ts +25 -0
  51. package/frontend/src/Breadcrumbs.ts +43 -0
  52. package/frontend/src/BrowseFiles.ts +141 -0
  53. package/frontend/src/Head.ts +45 -0
  54. package/frontend/src/UserPanel.ts +52 -0
  55. package/frontend/src/api.ts +78 -0
  56. package/frontend/src/components.ts +54 -0
  57. package/frontend/src/dialog.css +76 -0
  58. package/frontend/src/dialog.ts +105 -0
  59. package/frontend/src/icons.ts +46 -0
  60. package/frontend/src/index.scss +307 -0
  61. package/frontend/src/index.ts +10 -0
  62. package/frontend/src/login.ts +50 -0
  63. package/frontend/src/menu.ts +188 -0
  64. package/frontend/src/misc.ts +54 -0
  65. package/frontend/src/options.ts +52 -0
  66. package/frontend/src/react-app-env.d.ts +1 -0
  67. package/frontend/src/reportWebVitals.ts +15 -0
  68. package/frontend/src/setupTests.ts +5 -0
  69. package/frontend/src/state.ts +82 -0
  70. package/frontend/src/useAuthorized.ts +17 -0
  71. package/frontend/src/useFetchList.ts +144 -0
  72. package/frontend/src/useTheme.ts +23 -0
  73. package/frontend/tsconfig.json +26 -0
  74. package/frontend/vite.config.ts +21 -0
  75. package/package.json +2 -1
  76. package/plugins/vhosting/plugin.js +1 -1
  77. package/src/QuickZipStream.ts +279 -0
  78. package/src/ThrottledStream.ts +98 -0
  79. package/src/adminApis.ts +161 -0
  80. package/src/api.accounts.ts +78 -0
  81. package/src/api.auth.ts +131 -0
  82. package/src/api.file_list.ts +102 -0
  83. package/src/api.helpers.ts +30 -0
  84. package/src/api.monitor.ts +106 -0
  85. package/src/api.plugins.ts +139 -0
  86. package/src/api.vfs.ts +182 -0
  87. package/src/apiMiddleware.ts +124 -0
  88. package/src/block.ts +35 -0
  89. package/src/commands.ts +122 -0
  90. package/src/config.ts +166 -0
  91. package/src/connections.ts +60 -0
  92. package/src/const.ts +57 -0
  93. package/src/crypt.ts +16 -0
  94. package/src/debounceAsync.ts +51 -0
  95. package/src/events.ts +6 -0
  96. package/src/frontEndApis.ts +17 -0
  97. package/src/github.ts +102 -0
  98. package/src/index.ts +53 -0
  99. package/src/listen.ts +220 -0
  100. package/src/log.ts +128 -0
  101. package/src/middlewares.ts +176 -0
  102. package/src/misc.ts +149 -0
  103. package/src/pbkdf2.ts +83 -0
  104. package/src/perm.ts +194 -0
  105. package/src/plugins.ts +342 -0
  106. package/src/serveFile.ts +104 -0
  107. package/src/serveGuiFiles.ts +95 -0
  108. package/src/sse.ts +29 -0
  109. package/src/throttler.ts +106 -0
  110. package/src/update.ts +67 -0
  111. package/src/util-files.ts +137 -0
  112. package/src/util-generators.ts +29 -0
  113. package/src/util-http.ts +29 -0
  114. package/src/vfs.ts +258 -0
  115. package/src/watchLoad.ts +75 -0
  116. package/src/zip.ts +69 -0
  117. package/admin/assets/index.0f549e00.js +0 -281
  118. package/admin/assets/index.dcc78777.css +0 -1
  119. package/admin/assets/sha512.ea1121b3.js +0 -8
  120. package/frontend/assets/index.1151988f.js +0 -85
  121. package/frontend/assets/index.93366732.css +0 -1
  122. package/frontend/assets/sha512.bb881250.js +0 -8
  123. package/src/QuickZipStream.js +0 -285
  124. package/src/ThrottledStream.js +0 -93
  125. package/src/adminApis.js +0 -169
  126. package/src/api.accounts.js +0 -59
  127. package/src/api.auth.js +0 -130
  128. package/src/api.file_list.js +0 -103
  129. package/src/api.helpers.js +0 -32
  130. package/src/api.monitor.js +0 -102
  131. package/src/api.plugins.js +0 -127
  132. package/src/api.vfs.js +0 -164
  133. package/src/apiMiddleware.js +0 -136
  134. package/src/block.js +0 -33
  135. package/src/commands.js +0 -124
  136. package/src/config.js +0 -168
  137. package/src/connections.js +0 -57
  138. package/src/const.js +0 -83
  139. package/src/crypt.js +0 -21
  140. package/src/debounceAsync.js +0 -48
  141. package/src/events.js +0 -9
  142. package/src/frontEndApis.js +0 -38
  143. package/src/github.js +0 -102
  144. package/src/index.js +0 -55
  145. package/src/listen.js +0 -235
  146. package/src/log.js +0 -137
  147. package/src/middlewares.js +0 -154
  148. package/src/misc.js +0 -160
  149. package/src/pbkdf2.js +0 -74
  150. package/src/perm.js +0 -176
  151. package/src/plugins.js +0 -343
  152. package/src/serveFile.js +0 -104
  153. package/src/serveGuiFiles.js +0 -113
  154. package/src/sse.js +0 -29
  155. package/src/throttler.js +0 -91
  156. package/src/update.js +0 -69
  157. package/src/util-files.js +0 -148
  158. package/src/util-generators.js +0 -30
  159. package/src/util-http.js +0 -30
  160. package/src/vfs.js +0 -227
  161. package/src/watchLoad.js +0 -73
  162. package/src/zip.js +0 -69
@@ -0,0 +1,82 @@
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
+ }
@@ -0,0 +1,17 @@
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
+
@@ -0,0 +1,144 @@
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
+ })
@@ -0,0 +1,23 @@
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
+ }
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,21 @@
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.26.7",
3
+ "version": "0.26.8",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -24,6 +24,7 @@
24
24
  "build-shared": "npm run build --workspace=shared",
25
25
  "build-form": "npm run build --workspace=mui-grid-form",
26
26
  "server-for-test": "node dist/src --cwd . --config tests",
27
+ "server-for-test-dev": "cross-env DEV=1 nodemon --ignore tests/ --watch src -e ts,tsx --exec ts-node src -- --cwd . --config tests",
27
28
  "test": "mocha -r ts-node/register 'tests/**/*.ts'",
28
29
  "pub": "cd dist && npm publish",
29
30
  "dist": "npm run build-all && npm run dist-bin",
@@ -21,7 +21,7 @@ exports.init = api => ({
21
21
  middleware(ctx) {
22
22
  let toModify = ctx
23
23
  if (ctx.path.startsWith(api.const.SPECIAL_URI)) { // special uris should be excluded...
24
- toModify = ctx.request.query
24
+ toModify = ctx.params
25
25
  if (toModify.path === undefined) // ...unless they carry a path in the query. In that case we'll work that.
26
26
  return
27
27
  }
@@ -0,0 +1,279 @@
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> = {}