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 +}