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/vfs.ts DELETED
@@ -1,258 +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 fs from 'fs/promises'
4
- import { basename, join } from 'path'
5
- import { isMatch } from 'micromatch'
6
- import { dirStream, dirTraversal, enforceFinal, getOrSet, isDirectory, typedKeys } from './misc'
7
- import Koa from 'koa'
8
- import _ from 'lodash'
9
- import { defineConfig, setConfig } from './config'
10
- import { FORBIDDEN, IS_WINDOWS, UNAUTHORIZED } from './const'
11
- import events from './events'
12
- import { getCurrentUsernameExpanded } from './perm'
13
- import { with_ } from './misc'
14
-
15
- const WHO_ANYONE = true
16
- const WHO_NO_ONE = false
17
- const WHO_ANY_ACCOUNT = '*'
18
- type AccountList = string[]
19
- type Who = typeof WHO_ANYONE
20
- | typeof WHO_NO_ONE
21
- | typeof WHO_ANY_ACCOUNT
22
- | AccountList
23
-
24
- interface VfsPerm {
25
- can_see: Who
26
- can_read: Who
27
- }
28
-
29
- type Masks = Record<string, VfsNode>
30
-
31
- export interface VfsNode extends Partial<VfsPerm> {
32
- name?: string
33
- source?: string
34
- children?: VfsNode[]
35
- default?: string
36
- mime?: string | Record<string,string>
37
- rename?: Record<string, string>
38
- masks?: Masks // express fields for descendants that are not in the tree
39
- // fields that are only filled at run-time
40
- isTemp?: true // this node doesn't belong to the tree and was created by necessity
41
- url?: string // what url brought to this node
42
- parents?: VfsNode[]
43
- original?: VfsNode // if this is a temp node but reflecting an existing node
44
- }
45
-
46
- export const defaultPerms: VfsPerm = {
47
- can_see: WHO_ANYONE,
48
- can_read: WHO_ANYONE,
49
- }
50
-
51
- export const MIME_AUTO = 'auto'
52
-
53
- function inheritFromParent(parent: VfsNode, child: VfsNode) {
54
- for (const k of typedKeys(defaultPerms)) {
55
- const v = parent[k]
56
- if (v !== undefined)
57
- child[k] ??= v
58
- }
59
- if (typeof parent.mime === 'object' && typeof child.mime === 'object')
60
- _.defaults(child.mime, parent.mime)
61
- else
62
- child.mime ||= parent.mime
63
- return child
64
- }
65
-
66
- export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=vfs) : Promise<VfsNode | undefined> {
67
- let initialSlashes = 0
68
- while (url[initialSlashes] === '/')
69
- initialSlashes++
70
- let nextSlash = url.indexOf('/', initialSlashes)
71
- const name = decodeURIComponent(url.slice(initialSlashes, nextSlash < 0 ? undefined : nextSlash))
72
- if (!name)
73
- return parent
74
- const rest = nextSlash < 0 ? '' : url.slice(nextSlash+1, url.endsWith('/') ? -1 : undefined)
75
- if (dirTraversal(name) || /[\/]/.test(name)) {
76
- if (ctx)
77
- ctx.status = 418
78
- return
79
- }
80
- const parents = parent.parents || [] // don't waste time cloning the array, as we won't keep intermediate nodes
81
- const ret: VfsNode = {
82
- isTemp: true,
83
- url: enforceFinal('/', parent.url || '') + name,
84
- parents,
85
- }
86
- parents.push(parent)
87
- inheritFromParent(parent, ret)
88
- inheritMasks(ret, parent, name)
89
- applyMasks(ret, parent, name)
90
- // does the tree node have a child that goes by this name?
91
- const sameName = !IS_WINDOWS ? (x:string) => x === name // easy
92
- : with_(name.toLowerCase(), lc =>
93
- (x: string) => x.toLowerCase() === lc)
94
- const child = parent.children?.find(x => sameName(getNodeName(x)))
95
- if (child) // yes
96
- return urlToNode(rest, ctx, Object.assign(ret, child, { original: child }))
97
- // not in the tree, we can see consider continuing on the disk
98
- if (!parent.source) return // but then we need the current node to be linked to the disk, otherwise, we give up
99
- let onDisk = name
100
- if (parent.rename) { // reverse the mapping
101
- for (const [from, to] of Object.entries(parent.rename))
102
- if (name === to) {
103
- onDisk = from
104
- break // found, search no more
105
- }
106
- ret.rename = renameUnderPath(parent.rename, name)
107
- }
108
- ret.source = enforceFinal('/', parent.source) + onDisk
109
- if (parent.default)
110
- inheritFromParent({ mime: { '*': MIME_AUTO } }, ret)
111
- if (rest)
112
- return urlToNode(rest, ctx, ret)
113
- if (ret.source)
114
- try { await fs.stat(ret.source) } // check existence
115
- catch { return }
116
- return ret
117
- }
118
-
119
- export let vfs: VfsNode = {}
120
- defineConfig<VfsNode>('vfs', {}).sub(data =>
121
- vfs = data)
122
-
123
- export function saveVfs() {
124
- return setConfig({ vfs: _.cloneDeep(vfs) }, true)
125
- }
126
-
127
- export function getNodeName(node: VfsNode) {
128
- return node.name
129
- || node.source && (
130
- /^[a-zA-Z]:\\?$/.test(node.source) && node.source.slice(0, 2)
131
- || basename(node.source)
132
- || node.source
133
- )
134
- || '' // should happen only for root
135
- }
136
-
137
- export async function nodeIsDirectory(node: VfsNode) {
138
- return Boolean(!node.source || await isDirectory(node.source))
139
- }
140
-
141
- export function hasPermission(node: VfsNode, perm: keyof VfsPerm, ctx: Koa.Context): boolean {
142
- return matchWho(node[perm] ?? defaultPerms[perm], ctx)
143
- && (perm !== 'can_see' || hasPermission(node, 'can_read', ctx)) // for can_see you must also can_read
144
- }
145
-
146
- export async function* walkNode(parent:VfsNode, ctx?: Koa.Context, depth:number=0, prefixPath:string=''): AsyncIterableIterator<VfsNode> {
147
- const { children, source } = parent
148
- if (children)
149
- for (let idx = 0; idx < children.length; idx++) {
150
- const child = children[idx]
151
- yield* workItem({
152
- ...child,
153
- name: prefixPath ? (prefixPath + getNodeName(child)) : child.name
154
- }, depth > 0 && await nodeIsDirectory(child).catch(() => false))
155
- }
156
- if (!source)
157
- return
158
- try {
159
- for await (const path of dirStream(source, depth)) {
160
- if (ctx?.req.aborted)
161
- return
162
- const renamed = parent.rename?.[path]
163
- yield* workItem({
164
- name: prefixPath + (renamed || path),
165
- source: join(source, path),
166
- rename: renameUnderPath(parent.rename, path),
167
- })
168
- }
169
- }
170
- catch(e) {
171
- console.debug('glob', source, e) // ENOTDIR, or lacking permissions
172
- }
173
-
174
- // item will be changed, so be sure to pass a temp node
175
- async function* workItem(item: VfsNode, recur=false) {
176
- const name = getNodeName(item)
177
- // we basename for depth>0 where we already have the rest of the path in the parent's url, and would be duplicated
178
- const virtualBasename = basename(name)
179
- const url = enforceFinal('/', parent.url || '') + virtualBasename
180
- Object.assign(item, {
181
- isTemp: true,
182
- url,
183
- parents: [ ...parent.parents||[], parent],
184
- })
185
- inheritFromParent(parent, item)
186
- applyMasks(item, parent, virtualBasename)
187
- if (ctx && !hasPermission(item, 'can_see', ctx))
188
- return
189
- yield item
190
- if (!recur) return
191
- inheritMasks(item, parent, virtualBasename)
192
- yield* walkNode(item, ctx, depth - 1, name + '/')
193
- }
194
- }
195
- function applyMasks(item: VfsNode, parent: VfsNode, virtualBasename: string) {
196
- const { masks } = parent
197
- if (!masks) return
198
- for (const k in masks)
199
- if (k.startsWith('**/') && isMatch(virtualBasename, k.slice(3))
200
- || !k.includes('/') && isMatch(virtualBasename, k))
201
- Object.assign(item, masks[k])
202
- }
203
-
204
- function inheritMasks(item: VfsNode, parent: VfsNode, virtualBasename:string) {
205
- const { masks } = parent
206
- if (!masks) return
207
- const o: Masks = {}
208
- for (const k in masks)
209
- if (k.startsWith('**/'))
210
- o[k.slice(3)] = masks[k]
211
- else if (k.startsWith(virtualBasename+'/'))
212
- o[k.slice(virtualBasename.length+1)] = masks[k]
213
- if (Object.keys(o).length)
214
- item.masks = o
215
- }
216
-
217
- function renameUnderPath(rename:undefined | Record<string,string>, path: string) {
218
- if (!rename) return rename
219
- const match = path+'/'
220
- rename = Object.fromEntries(Object.entries(rename).map(([k, v]) =>
221
- [k.startsWith(match) ? k.slice(match.length) : '', v]))
222
- delete rename['']
223
- return _.isEmpty(rename) ? undefined : rename
224
- }
225
-
226
- function matchWho(who: Who, ctx: Koa.Context) {
227
- return who === WHO_ANYONE
228
- || who === WHO_ANY_ACCOUNT && Boolean(ctx.state.account)
229
- || Array.isArray(who) && (() => // check if I or any ancestor match `who`, but cache ancestors' usernames inside context state
230
- getOrSet(ctx.state, 'usernames', () => getCurrentUsernameExpanded(ctx)).some((u:string) =>
231
- who.includes(u) ))()
232
- }
233
-
234
- export function cantReadStatusCode(node: VfsNode) {
235
- return node.can_read === false ? FORBIDDEN : UNAUTHORIZED
236
- }
237
-
238
- events.on('accountRenamed', (from, to) => {
239
- recur(vfs)
240
- saveVfs()
241
-
242
- function recur(n: VfsNode) {
243
- replace(n.can_see)
244
- replace(n.can_read)
245
-
246
- if (n.masks)
247
- Object.values(n.masks).forEach(recur)
248
- n.children?.forEach(recur)
249
- }
250
-
251
- function replace(a?: Who) {
252
- if (!Array.isArray(a)) return
253
- for (let i=0; i < a.length; i++)
254
- if (a[i] === from)
255
- a[i] = to
256
- }
257
-
258
- })
package/src/watchLoad.ts DELETED
@@ -1,75 +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 { FSWatcher, watch } from 'fs'
4
- import fs from 'fs/promises'
5
- import yaml from 'yaml'
6
- import { debounceAsync, readFileBusy } from './misc'
7
-
8
- export type WatchLoadCanceller = () => void
9
-
10
- interface Options { failedOnFirstAttempt?: ()=>void }
11
-
12
- type WriteFile = typeof fs.writeFile
13
- interface WatchLoadReturn { unwatch:WatchLoadCanceller, save:WriteFile }
14
- export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>, { failedOnFirstAttempt }:Options={}): WatchLoadReturn {
15
- let doing = false
16
- let watcher: FSWatcher | undefined
17
- const debounced = debounceAsync(load, 500, { leading: true })
18
- let retry: NodeJS.Timeout
19
- let saving: Promise<unknown> | undefined
20
- let lastStats: any
21
- init().then(ok => ok || failedOnFirstAttempt?.())
22
- return {
23
- unwatch(){
24
- watcher?.close()
25
- clearTimeout(retry)
26
- watcher = undefined
27
- },
28
- save(...args:Parameters<WriteFile>) {
29
- return Promise.resolve(saving).then(() => // wait in case another is ongoing
30
- saving = fs.writeFile(...args).finally(() => // save but also keep track of the current operation
31
- saving = undefined)) // clear
32
- }
33
- }
34
-
35
- async function init() {
36
- try {
37
- debounced().then()
38
- watcher = watch(path, ()=> {
39
- if (!saving)
40
- debounced().then()
41
- })
42
- return true // used actually just by the first invocation
43
- }
44
- catch(e) {
45
- retry = setTimeout(init, 3_000) // manual watching until watch is successful
46
- }
47
- }
48
-
49
- async function load(){
50
- if (doing) return
51
- doing = true
52
- let data: any
53
- try {
54
- try { // I've seen watch() firing 'change' without any change, so we'll check if any change is detectable before going on
55
- const stats = await fs.stat(path)
56
- if (stats.mtimeMs === lastStats?.mtimeMs) return
57
- lastStats = stats
58
-
59
- data = await readFileBusy(path)
60
- console.debug('loaded', path)
61
- }
62
- catch (e: any) {
63
- if (e.code === 'EPERM')
64
- console.error("missing permissions on file", path) // warn user, who could be clueless about this problem
65
- return // ignore read errors
66
- }
67
- if (path.endsWith('.yaml'))
68
- data = yaml.parse(data)
69
- await parser(data)
70
- }
71
- finally {
72
- doing = false
73
- }
74
- }
75
- }
package/src/zip.ts DELETED
@@ -1,69 +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 { getNodeName, hasPermission, nodeIsDirectory, urlToNode, VfsNode, walkNode } from './vfs'
4
- import Koa from 'koa'
5
- import { filterMapGenerator, pattern2filter } from './misc'
6
- import { QuickZipStream } from './QuickZipStream'
7
- import { createReadStream } from 'fs'
8
- import fs from 'fs/promises'
9
- import { defineConfig } from './config'
10
- import { dirname } from 'path'
11
- import { getRange } from './serveFile'
12
-
13
- export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
14
- ctx.status = 200
15
- ctx.mime = 'zip'
16
- const name = getNodeName(node)
17
- ctx.attachment((name || 'archive') + '.zip')
18
- const filter = pattern2filter(String(ctx.query.search||''))
19
- const { list } = ctx.query
20
- const walker = !list ? walkNode(node, ctx, Infinity)
21
- : (async function*(): AsyncIterableIterator<VfsNode> {
22
- for await (const el of String(list).split('*')) { // we are using * as separator because it cannot be used in a file name and doesn't need url encoding
23
- const subNode = await urlToNode(el, ctx, node)
24
- if (!subNode || !hasPermission(subNode,'can_read',ctx))
25
- continue
26
- if (await nodeIsDirectory(subNode)) {// a directory needs to walked
27
- yield* walkNode(subNode, ctx, Infinity, el + '/')
28
- continue
29
- }
30
- let folder = dirname(el)
31
- folder = folder === '.' ? '' : folder + '/'
32
- yield { ...subNode, name: folder + getNodeName(subNode) } // reflect relative path in archive, otherwise way may have name-clashes
33
- }
34
- })()
35
- const mappedWalker = filterMapGenerator(walker, async (el:VfsNode) => {
36
- const { source } = el
37
- const name = getNodeName(el)
38
- if (!source || ctx.req.aborted || !filter(name))
39
- return
40
- try {
41
- const st = await fs.stat(source)
42
- if (!st || !st.isFile())
43
- return
44
- return {
45
- path: name,
46
- size: st.size,
47
- ts: st.mtime || st.ctime,
48
- mode: st.mode,
49
- sourcePath: source,
50
- getData: () => createReadStream(source, { start: 0 , end: Math.max(0, st.size-1) })
51
- }
52
- }
53
- catch {}
54
- })
55
- const zip = new QuickZipStream(mappedWalker)
56
- const time = 1000 * zipSeconds.get()
57
- const size = await zip.calculateSize(time)
58
- ctx.response.length = size
59
- const range = getRange(ctx, size) // keep var size as ctx.response.length won't preserve a NaN
60
- if (ctx.status >= 400)
61
- return
62
- if (range)
63
- zip.applyRange(range.start, range.end)
64
- ctx.body = zip
65
- ctx.req.on('close', ()=> zip.destroy())
66
- ctx.state.archive = 'zip'
67
- }
68
-
69
- const zipSeconds = defineConfig('zip_calculate_size_for_seconds', 1)