api.file_list: support see, Server Sent Events
Massimo Melina committed
Dec 22, 2021 at 14:40 UTC
d021a77ba346c60a079112b9028fc88174f6c343
3 files changed
+71
-25
package.json
+1
-1
@@ -1,6 +1,6 @@
1
{
2
"name": "hfs",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
"description": "HTTP File Server",
5
"keywords": [
6
"file server",
src/apis.ts
+41
-24
@@ -5,6 +5,8 @@ import { stat } from 'fs/promises'
5
import _ from 'lodash'
6
import { getCurrentUser, verifyLogin } from './perm'
7
import { sessions } from './sessions'
8
+import createSSE from './sse'
9
+import { basename } from 'path'
10
11
export const SESSION_COOKIE = 'hfs_$id'
12
@@ -17,7 +19,6 @@ export function apiMw(apis: ApiHandlers) : Koa.Middleware {
19
console.debug('API', ctx.method, ctx.path, params)
20
if (!(ctx.path in apis))
21
return ctx.throw(404, 'invalid api')
20
- ctx.body = {}
22
const cb = (apis as any)[ctx.path]
23
let res
24
try {
@@ -39,7 +40,7 @@ interface DirEntry { n:string, s?:number, m?:Date, c?:Date }
40
41
export const frontEndApis: ApiHandlers = {
42
42
- async file_list({ path, offset, limit, search, omit }, ctx) {
43
+ async file_list({ path, offset, limit, search, omit, sse }, ctx) {
44
let node = await vfs.urlToNode(path || '/', ctx)
45
if (!node)
46
return
@@ -50,31 +51,44 @@ export const frontEndApis: ApiHandlers = {
51
const re = new RegExp(_.escapeRegExp(search),'i')
52
const match = (s?:string) => !s || !search || re.test(s)
53
const who = await getCurrentUser(ctx) // cache value
53
- const list = []
54
const walker = walkNode(node, who, search ? Infinity : 0)
55
- for await (const sub of walker) {
56
- if (!match(sub.name))
57
- continue
58
- const entry = await nodeToDirEntry(sub)
59
- if (!entry)
60
- continue
61
- if (offset) {
62
- --offset
63
- continue
64
- }
65
- if (omit) {
66
- if (omit !== 'c')
67
- ctx.throw(400, 'omit')
68
- if (!entry.m)
69
- entry.m = entry.c
70
- delete entry.c
55
+ const sseSrv = sse ? createSSE(ctx) : null
56
+ const res = produceEntries()
57
+ return !sseSrv && { list: await res }
58
+
59
+ async function produceEntries() {
60
+ const list = []
61
+ for await (const sub of walker) {
62
+ if (sseSrv?.stopped) break
63
+ const filename = basename(sub.name||'')
64
+ if (!match(filename))
65
+ continue
66
+ const entry = await nodeToDirEntry(sub)
67
+ if (!entry)
68
+ continue
69
+ if (offset) {
70
+ --offset
71
+ continue
72
+ }
73
+ if (omit) {
74
+ if (omit !== 'c')
75
+ ctx.throw(400, 'omit')
76
+ if (!entry.m)
77
+ entry.m = entry.c
78
+ delete entry.c
79
+ }
80
+ if (sseSrv)
81
+ sseSrv.send({ entry })
82
+ else
83
+ list.push(entry)
84
+ if (limit && !--limit)
85
+ break
86
}
72
- list.push(entry)
73
- if (limit === list.length)
74
- break
87
+ sseSrv?.close()
88
+ return list
89
}
76
- return { list }
90
},
91
+
92
async login({ user, password }, ctx) {
93
if (!user)
94
return ctx.status = 400
@@ -86,6 +100,7 @@ export const frontEndApis: ApiHandlers = {
100
ctx.cookies.set(SESSION_COOKIE, sess.id)
101
return sess
102
},
103
+
104
async logout({}, ctx) {
105
const sid = ctx.cookies.get(SESSION_COOKIE)
106
if (!sid)
@@ -95,6 +110,7 @@ export const frontEndApis: ApiHandlers = {
110
ctx.status = 200
111
ctx.cookies.set(SESSION_COOKIE, null)
112
},
113
+
114
async refresh_session({}, ctx) {
115
const prevId = ctx.cookies.get(SESSION_COOKIE)
116
if (!prevId) return
@@ -108,7 +124,8 @@ export const frontEndApis: ApiHandlers = {
124
else
125
ctx.cookies.set(SESSION_COOKIE, sess.id)
126
return sess
111
- }
127
+ },
128
+
129
}
130
131
async function nodeToDirEntry(node: VfsNode): Promise<DirEntry | null> {
src/sse.ts
new
+29
@@ -0,0 +1,29 @@
1
+import Koa from 'koa'
2
+import { PassThrough } from 'stream'
3
+
4
+export default function createSSE(ctx: Koa.Context) {
5
+ const { socket } = ctx.req
6
+ socket.setTimeout(0)
7
+ socket.setNoDelay(true)
8
+ socket.setKeepAlive(true)
9
+ ctx.set({
10
+ "Content-Type": "text/event-stream",
11
+ "Cache-Control": "no-cache",
12
+ "Connection": "keep-alive",
13
+ })
14
+ ctx.status = 200
15
+ const stream = ctx.body = new PassThrough()
16
+ const ret = {
17
+ stream,
18
+ stopped: false,
19
+ send(data:any){
20
+ stream.write(`data: ${JSON.stringify(data)}\n\n`)
21
+ },
22
+ close() {
23
+ stream.end('data:\n\n')
24
+ }
25
+ }
26
+ stream.on('close', ()=>
27
+ ret.stopped = true)
28
+ return ret
29
+}