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?)