main
ts 97 lines 4.56 KB
Raw
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import { getNodeName, hasPermission, nodeIsFolder, nodeIsLink, urlToNode, VfsNode, walkNode, statusCodeForMissingPerm } from './vfs'
4 import Koa from 'koa'
5 import { filterMapGenerator, isWindowsDrive, pathDecodeSegments, safeDecodeURIComponent, statWithTimeout, wantArray } from './misc'
6 import { QuickZipStream } from './QuickZipStream'
7 import { createReadStream } from 'fs'
8 import { defineConfig } from './config'
9 import { basename, dirname } from 'path'
10 import { applyRange, forceDownload, monitorAsDownload } from './serveFile'
11 import { HTTP_OK, IS_WINDOWS } from './const'
12 import { paramsToFilter } from './api.get_file_list'
13 import { getCommentFor } from './comments'
14 import { decodeUrlList } from './urlList'
15
16 // expects 'node' to have had permissions checked by caller
17 export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
18 const list = decodeUrlList(wantArray(ctx.query.list)[0])
19 if (!list && statusCodeForMissingPerm(node, 'can_archive', ctx)) return
20 ctx.status = HTTP_OK
21 ctx.mime = 'zip'
22 // ctx.query.list is undefined | string | string[]
23 const name = list?.length === 1 ? safeDecodeURIComponent(basename(list[0]!), '') : getNodeName(node)
24 try {
25 forceDownload(ctx, (isWindowsDrive(name) ? name[0] : (name || 'archive')) + '.zip')
26 }
27 catch { return ctx.status = 400 }
28 const { filterName, filterComment } = paramsToFilter(ctx.query)
29 const walker = !list ? walkNode(node, { ctx, requiredPerm: 'can_archive' })
30 : (async function*(): AsyncIterableIterator<VfsNode> {
31 for await (const uri of list) {
32 if (ctx.isAborted()) return
33 const subNode = await urlToNode(uri, ctx, node)
34 if (!subNode)
35 continue
36 if (nodeIsFolder(subNode)) { // a directory needs to be walked
37 if (hasPermission(subNode, 'can_list', ctx) && hasPermission(subNode, 'can_archive', ctx)) {
38 const archivePath = pathDecodeSegments(uri)
39 yield { ...subNode, name: archivePath } // keep empty selected folders at the same path used for their contents
40 yield* walkNode(subNode, { ctx, prefixPath: archivePath + '/', requiredPerm: 'can_archive' })
41 }
42 continue
43 }
44 let folder = dirname(decodeURIComponent(uri)) // decodeURI() won't account for %23=# . uri is safe because otherwise urlToNode above would have already returned undefined
45 folder = folder === '.' ? '' : folder + '/'
46 yield { ...subNode, name: folder + getNodeName(subNode) } // reflect relative path in archive, otherwise way may have name-clashes
47 }
48 })()
49 const mappedWalker = filterMapGenerator(walker, async (el:VfsNode) => {
50 if (nodeIsLink(el)) return
51 if (!hasPermission(el, 'can_archive', ctx)) return // the fact you see it doesn't mean you can get it
52 const { source } = el
53 let name = getNodeName(el)
54 if (!IS_WINDOWS) // posix supports \ in file names, but zip tools don't
55 name = name.replaceAll('\\', '_')
56 if (filterName && !filterName(name)
57 || filterComment && !filterComment(await getCommentFor(source) || ''))
58 return
59 try {
60 if (nodeIsFolder(el))
61 return { path: name + '/' }
62 if (!source) return
63 const st = await (el.stats || statWithTimeout(source))
64 if (!st || !st.isFile())
65 return
66 return {
67 path: name,
68 size: st.size,
69 ts: st.mtime,
70 mode: st.mode,
71 sourcePath: source,
72 getData: () => createReadStream(source, { start: 0 , end: Math.max(0, st.size-1) })
73 }
74 }
75 catch {}
76 })
77 const zip = new QuickZipStream(mappedWalker)
78 const time = 1000 * zipSeconds.get()
79 const size = await zip.calculateSize(time)
80 const range = applyRange(ctx, size) // keep var size as ctx.response.length won't preserve a NaN
81 if (ctx.status >= 400)
82 return
83 if (range)
84 zip.applyRange(range.start, range.end)
85 ctx.body = zip
86 ctx.req.on('close', ()=> zip.destroy())
87 ctx.state.archive = 'zip'
88 monitorAsDownload(ctx, size, range?.start)
89 }
90
91 const zipSeconds = defineConfig('zip_calculate_size_for_seconds', 5)
92
93 declare module "koa" {
94 interface DefaultState {
95 archive?: string
96 }
97 }