head method support ; gzipping of frontend files

Massimo Melina committed Jan 14, 2022 at 21:45 UTC aff2a14c9f186fb601cb2074e9039f77cd8b4fef
5 files changed +98 -58
src/index.ts
+7 -56
@@ -2,24 +2,17 @@ import Koa from 'koa'
2 import mount from 'koa-mount'
3 import bodyParser from 'koa-bodyparser'
4 import { apiMiddleware } from './apis'
5 -import { serveFrontend } from './serveFrontend'
6 -import { API_URI, DEV, FRONTEND_URI} from './const'
7 -import { serveFileNode } from './serveFile'
8 -import { vfs } from './vfs'
9 -import { isDirectory } from './misc'
10 -import proxy from 'koa-better-http-proxy'
11 -import compress from 'koa-compress'
5 +import { API_URI, DEV} from './const'
6 import { Server } from 'http'
7 import { subscribeConfig } from './config'
14 -import session from 'koa-session'
15 -import { zipStreamFromFolder } from './zip'
8 import { frontEndApis } from './frontEndApis'
9 import { log } from './log'
10 import { pluginsMiddleware } from './plugins'
11 import { throttler } from './throttler'
12 import { getAccount, getCurrentUsername } from './perm'
13 +import { headRequests, gzipper, sessions, frontendAndSharedFiles } from './middlewares'
14
22 -const BUILD_TIMESTAMP = "-"
15 +export const BUILD_TIMESTAMP = "-"
16
17 export const SESSION_DURATION = 30*60_000
18
@@ -28,66 +21,24 @@ console.log('build', BUILD_TIMESTAMP)
21 console.debug('cwd', process.cwd())
22 const app = new Koa()
23 app.keys = ['hfs-keys-test']
31 -app.use(session({
32 - key: 'hfs_$id',
33 - signed: true,
34 - rolling: true,
35 - maxAge: SESSION_DURATION,
36 -}, app))
24 +app.use(sessions(app))
25 app.use(async (ctx, next) => {
26 ctx.account = getAccount(getCurrentUsername(ctx))
27 await next()
28 })
29 +app.use(headRequests)
30 app.use(log())
31 app.use(pluginsMiddleware())
32 app.use(throttler())
33 +app.use(gzipper)
34
35 // serve apis
36 app.use(mount(API_URI, new Koa()
37 .use(bodyParser())
38 .use(apiMiddleware(frontEndApis))
49 - .use(compress({
50 - threshold: 2048,
51 - gzip: { flush: require('zlib').constants.Z_SYNC_FLUSH },
52 - deflate: { flush: require('zlib').constants.Z_SYNC_FLUSH },
53 - br: false // disable brotli
54 - }))
39 ))
40
57 -// serve shared files and front-end files
58 -const serveFrontendPrefixed = mount(FRONTEND_URI.slice(0,-1), serveFrontend)
59 -app.use(async (ctx, next) => {
60 - const { path } = ctx
61 - if (path.includes('..'))
62 - ctx.throw(500)
63 - if (ctx.body)
64 - return await next()
65 - if (path.startsWith(FRONTEND_URI))
66 - return await serveFrontendPrefixed(ctx,next)
67 - const decoded = decodeURI(path)
68 - const node = await vfs.urlToNode(decoded, ctx)
69 - if (!node)
70 - return await next()
71 - const { source } = node
72 - if (!source || await isDirectory(source)) {
73 - const { get } = ctx.query
74 - if (get === 'zip')
75 - return await zipStreamFromFolder(node, ctx)
76 - if (!path.endsWith('/')) // this folder was requested without the trailing /
77 - return ctx.redirect(path + '/')
78 - if (node.default) {
79 - const def = await vfs.urlToNode(decoded + node.default, ctx)
80 - if (def)
81 - return serveFileNode(def)(ctx, next)
82 - }
83 - ctx.set({ server:'HFS '+BUILD_TIMESTAMP })
84 - return await serveFrontend(ctx, next)
85 - }
86 - if (source)
87 - return source.includes('//') ? mount(path,proxy(source,{}))(ctx,next)
88 - : serveFileNode(node)(ctx,next)
89 - await next()
90 -})
41 +app.use(frontendAndSharedFiles)
42
43 app.on('error', err => {
44 if (DEV && err.code === 'ENOENT' && err.path.endsWith('sockjs-node')) return // spam out
src/middlewares.ts new
+80
@@ -0,0 +1,80 @@
1 +import compress from 'koa-compress'
2 +import Koa from 'koa'
3 +import session from 'koa-session'
4 +import { BUILD_TIMESTAMP, SESSION_DURATION } from './index'
5 +import Application from 'koa'
6 +import { FRONTEND_URI } from './const'
7 +import { vfs } from './vfs'
8 +import { isDirectory } from './misc'
9 +import { zipStreamFromFolder } from './zip'
10 +import { serveFileNode } from './serveFile'
11 +import { serveFrontend } from './serveFrontend'
12 +import mount from 'koa-mount'
13 +import proxy from 'koa-better-http-proxy'
14 +import { Readable } from 'stream'
15 +
16 +export const gzipper = compress({
17 + threshold: 2048,
18 + gzip: { flush: require('zlib').constants.Z_SYNC_FLUSH },
19 + deflate: { flush: require('zlib').constants.Z_SYNC_FLUSH },
20 + br: false, // disable brotli
21 + filter(type) {
22 + return /text|javascript|style/i.test(type)
23 + },
24 +})
25 +
26 +export const headRequests: Koa.Middleware = async (ctx, next) => {
27 + const head = ctx.method === 'HEAD'
28 + if (head)
29 + ctx.method = 'GET' // let's other middleware work so we can collect the size at the end
30 + await next()
31 + if (!head || ctx.body === undefined) return
32 + const { length, status } = ctx.response
33 + if (ctx.body)
34 + ctx.body = Readable.from('') // empty the body for this is a HEAD request. Using Readable avoids koa from trying to set length to 0
35 + ctx.status = status
36 + if (length)
37 + ctx.response.length = length
38 +}
39 +
40 +export const sessions = (app: Application) => session({
41 + key: 'hfs_$id',
42 + signed: true,
43 + rolling: true,
44 + maxAge: SESSION_DURATION,
45 +}, app)
46 +
47 +// serve shared files and front-end files
48 +const serveFrontendPrefixed = mount(FRONTEND_URI.slice(0,-1), serveFrontend)
49 +export const frontendAndSharedFiles: Koa.Middleware = async (ctx, next) => {
50 + const { path } = ctx
51 + if (path.includes('..'))
52 + ctx.throw(500)
53 + if (ctx.body)
54 + return await next()
55 + if (path.startsWith(FRONTEND_URI))
56 + return await serveFrontendPrefixed(ctx,next)
57 + const decoded = decodeURI(path)
58 + const node = await vfs.urlToNode(decoded, ctx)
59 + if (!node)
60 + return await next()
61 + const { source } = node
62 + if (!source || await isDirectory(source)) {
63 + const { get } = ctx.query
64 + if (get === 'zip')
65 + return await zipStreamFromFolder(node, ctx)
66 + if (!path.endsWith('/')) // this folder was requested without the trailing /
67 + return ctx.redirect(path + '/')
68 + if (node.default) {
69 + const def = await vfs.urlToNode(decoded + node.default, ctx)
70 + if (def)
71 + return serveFileNode(def)(ctx, next)
72 + }
73 + ctx.set({ server:'HFS '+BUILD_TIMESTAMP })
74 + return await serveFrontend(ctx, next)
75 + }
76 + if (source)
77 + return source.includes('//') ? mount(path,proxy(source,{}))(ctx,next)
78 + : serveFileNode(node)(ctx,next)
79 + await next()
80 +}
src/serveFile.ts
+1 -1
@@ -29,7 +29,7 @@ export function serveFile(source:string, mime?:string) : Koa.Middleware {
29 ctx.type = mime
30 if (ctx.method === 'OPTIONS') {
31 ctx.status = NO_CONTENT
32 - ctx.set({ Allow: 'OPTIONS, GET' })
32 + ctx.set({ Allow: 'OPTIONS, GET, HEAD' })
33 return
34 }
35 if (ctx.method !== 'GET')
tests/test.ts
+4 -1
@@ -27,7 +27,10 @@ describe('basics', () => {
27 it('website', req('/f1/page/', s => s.includes('This is a test')))
28 it('missing perm', req('/for-admins/', 404))
29 it('proxy', req('/proxy', s => s.includes('github')))
30 - it('login', req('/~/api/login', 200, {
30 + it('zip+head', req('/f1/?get=zip',
31 + (data, res) => !data && res.headers['content-length'] === '13058',
32 + { method:'HEAD' }) )
33 + it('login', req('/~/api/login', 406, { // by default we don't support clear-text login
34 data: { username, password }
35 }))
36 })
todo.md
+6
@@ -1,4 +1,10 @@
1 # To do
2 +- confirm archive
3 +- automatic theme based on system preferences
4 +- archive button as a link that can be copied
5 +- emoji as an alternative to icons?
6 +- smaller icons file?
7 +- remove seconds from time
8 - update tests to SRP login
9 - upload
10 - upload unzipping (while streaming?)