main
ts 82 lines 3.02 KB
Raw
1 import Koa from 'koa'
2 import Busboy from 'busboy'
3 import { once } from 'events'
4 import { hasPermission, urlToNode, VfsNode } from './vfs'
5 import { dirname } from 'path'
6 import { uploadWriter } from './upload'
7 import { HTTP_BAD_REQUEST } from './cross-const'
8 import { onFirstEvent } from './first'
9 import { try_ } from './cross'
10
11 export async function handleMultipartUpload(ctx: Koa.Context, node: VfsNode) {
12 if (ctx.request.type !== 'multipart/form-data')
13 return ctx.status = HTTP_BAD_REQUEST
14 ctx.state.uploads = []
15 const locks: Promise<string>[] = []
16 const fileJobs: Promise<any>[] = []
17 const errors: string[] = []
18 const bb = try_(() => Busboy({ headers: ctx.req.headers, preservePath: true }), e => {
19 ctx.body = String(e) // busboy validates multipart headers at construction time, so malformed requests must stop here as 400
20 ctx.status = HTTP_BAD_REQUEST
21 })
22 if (!bb) return
23 bb.on('field', (name: string) => {
24 if (name === 'upload')
25 errors.push('empty filename')
26 })
27 bb.on('file', (_field, file, info) => {
28 const fn = info.filename || ''
29 if (!fn) {
30 errors.push('empty filename')
31 fileJobs.push(drainStream(file))
32 return
33 }
34 ctx.state.uploadPath = decodeURI(ctx.path) + fn
35 ctx.state.uploads!.push(fn)
36 file.pause()
37 fileJobs.push(handleFile(file, fn))
38 })
39 bb.on('error', (err: Error) => {
40 console.warn("Couldn't parse POST requests:", String(err))
41 ctx.status = HTTP_BAD_REQUEST
42 })
43 ctx.req.pipe(bb)
44 await once(bb, 'finish')
45 await Promise.all(fileJobs)
46 if (!ctx.state.uploads?.length) {
47 if (!errors.length)
48 errors.push('no files')
49 ctx.status = HTTP_BAD_REQUEST
50 }
51 const uris = await Promise.all(locks)
52 ctx.body = errors.length ? { uris, errors } : { uris }
53 return
54
55 async function handleFile(file: NodeJS.ReadableStream, fn: string) {
56 try { // it is still possible to allow upload in a folder and block in a subfolder, so check for it
57 const ret = !await subfolderBlocksUpload(fn) && uploadWriter(node, ctx.path, fn, ctx)
58 if (!ret)
59 return drainStream(file)
60 locks.push(ret.lockMiddleware)
61 file.pipe(ret)
62 file.resume()
63 }
64 catch (e) {
65 console.warn("Couldn't handle uploaded file:", String(e))
66 file.resume()
67 }
68 }
69
70 function drainStream(stream: NodeJS.ReadableStream) {
71 stream.resume()
72 return new Promise(res => onFirstEvent(stream, ['end','close','error'], res))
73 }
74
75 async function subfolderBlocksUpload(fn: string) {
76 const prefix = dirname(fn.replaceAll('\\', '/'))
77 if (prefix === '.') // no subdir
78 return false
79 const subfolderNode = await urlToNode(prefix + '/', ctx, node, true) // final slash = explicitly a folder even if it doesn't exist on disk
80 return subfolderNode && !hasPermission(subfolderNode, 'can_upload', ctx)
81 }
82 }