better code: moved request data from connection object to ctx.state
Massimo Melina committed
Jan 5, 2024 at 16:25 UTC
05f05abe1a7658052323c0b64afa35f522bb995b
9 files changed
+53
-59
src/api.get_file_list.ts
+2
-2
@@ -10,7 +10,7 @@ import { HTTP_FOOL, HTTP_METHOD_NOT_ALLOWED, HTTP_NOT_FOUND } from './const'
10
import Koa from 'koa'
11
import { descriptIon, DESCRIPT_ION, getCommentFor, areCommentsEnabled } from './comments'
12
import { basename } from 'path'
13
-import { getConnection, updateConnection } from './connections'
13
+import { updateConnectionForCtx } from './connections'
14
import { ctxAdminAccess } from './adminApis'
15
import { dontOverwriteUploading } from './upload'
16
@@ -41,7 +41,7 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, search
41
const can_overwrite = can_upload && (can_delete || !dontOverwriteUploading.get())
42
const props = { can_archive, can_upload, can_delete, can_overwrite, accept: node.accept, can_comment }
43
ctx.state.browsing = uri.replace(/\/{2,}/g, '/')
44
- updateConnection(getConnection(ctx)!, { ctx })
44
+ updateConnectionForCtx(ctx)
45
if (!list)
46
return { ...props, list: await asyncGeneratorToArray(produceEntries()) }
47
setTimeout(async () => {
src/api.monitor.ts
+13
-11
@@ -2,13 +2,15 @@
2
3
import _ from 'lodash'
4
import { Connection, getConnections } from './connections'
5
-import { pendingPromise, shortenAgent, typedEntries, wait } from './misc'
5
+import { HTTP_NOT_MODIFIED, pendingPromise, shortenAgent, typedEntries, wait } from './misc'
6
import { ApiHandlers, SendListReadable } from './apiMiddleware'
7
import Koa from 'koa'
8
import { totalGot, totalInSpeed, totalOutSpeed, totalSent } from './throttler'
9
import { getCurrentUsername } from './auth'
10
11
-const apis: ApiHandlers = {
11
+const sent = Symbol('sent')
12
+
13
+export default {
14
15
async disconnect({ ip, port, wait }) {
16
const match = _.matches({ ip, port })
@@ -25,7 +27,6 @@ const apis: ApiHandlers = {
27
},
28
29
get_connections({}, ctx) {
28
- const sent = Symbol('sent')
30
const list = new SendListReadable({
31
addAtStart: getConnections().map(c =>
32
!ignore(c) && (c[sent] = serializeConnection(c))).filter(Boolean),
@@ -52,8 +53,6 @@ const apis: ApiHandlers = {
53
Object.assign(change, fromCtx(change.ctx))
54
change.ctx = undefined
55
}
55
- if (change.opProgress)
56
- change.opProgress = _.round(change.opProgress, 3)
56
// avoid sending non-changes
57
const last = conn[sent]
58
for (const [k, v] of typedEntries(change))
@@ -72,7 +71,7 @@ const apis: ApiHandlers = {
71
v: (socket.remoteFamily?.endsWith('6') ? 6 : 4),
72
got: socket.bytesRead,
73
sent: socket.bytesWritten,
75
- ..._.pick(conn, ['op', 'opTotal', 'opOffset', 'opProgress', 'country']),
74
+ country: conn.country,
75
started,
76
secure: (secure || undefined) as boolean|undefined, // undefined will save some space once json-ed
77
...fromCtx(conn.ctx),
@@ -86,10 +85,15 @@ const apis: ApiHandlers = {
85
user: getCurrentUsername(ctx),
86
agent: shortenAgent(ctx.get('user-agent')),
87
archive: s.archive,
89
- upload: s.uploadProgress,
88
...s.browsing ? { op: 'browsing', path: decodeURIComponent(s.browsing) }
89
: s.uploadPath ? { op: 'upload',path: decodeURIComponent(s.uploadPath) }
92
- : { path: decodeURIComponent(ctx.path) }
90
+ : {
91
+ op: s.op === 'download' && ctx.status === HTTP_NOT_MODIFIED ? 'cache' : s.op,
92
+ path: decodeURIComponent(ctx.path)
93
+ },
94
+ opProgress: _.round(s.opProgress, 3),
95
+ opTotal: s.opTotal,
96
+ opOffset: s.opOffset,
97
}
98
}
99
},
@@ -106,9 +110,7 @@ const apis: ApiHandlers = {
110
await wait(1000)
111
}
112
},
109
-}
110
-
111
-export default apis
113
+} satisfies ApiHandlers
114
115
function ignore(conn: Connection) {
116
return false //conn.socket && isLocalHost(conn)
src/connections.ts
+13
-9
@@ -3,7 +3,6 @@
3
import { Socket } from 'net'
4
import events from './events'
5
import { Context } from 'koa'
6
-import _ from 'lodash'
6
7
export class Connection {
8
readonly started = new Date()
@@ -11,10 +10,6 @@ export class Connection {
10
got = 0
11
outSpeed?: number
12
inSpeed?: number
14
- op?: 'download' | 'upload' | 'browsing' | 'cache'
15
- opTotal?: number
16
- opProgress?: number
17
- opOffset?: number
13
ctx?: Context
14
country?: string
15
private _cachedIp?: string
@@ -59,12 +54,21 @@ export function socket2connection(socket: Socket) {
54
}
55
56
export function getConnection(ctx: Context) {
62
- return _.find(all, { ctx })
57
+ return ctx.state.connection
58
}
59
65
-export function updateConnection(conn: Connection, change: Partial<Connection>) {
66
- if (change.opOffset !== undefined)
67
- change.opProgress = change.opOffset || 0
60
+export function updateConnectionForCtx(ctx: Context ) {
61
+ const conn = getConnection(ctx)
62
+ if (conn)
63
+ updateConnection(conn, { ctx })
64
+}
65
+
66
+export function updateConnection(conn: Connection, change: Partial<Connection>, changeState?: true | Partial<Context['state']>) {
67
+ const { ctx } = conn
68
+ if (changeState && ctx) {
69
+ Object.assign(ctx.state, changeState)
70
+ Object.assign(change, { ctx })
71
+ }
72
Object.assign(conn, change)
73
events.emit('connectionUpdated', conn, change)
74
}
src/log.ts
+1
-1
@@ -109,7 +109,7 @@ export const logMw: Koa.Middleware = async (ctx, next) => {
109
const length = ctx.state.length ?? ctx.length
110
const uri = ctx.originalUrl
111
ctx.logExtra(ctx.state.includesLastByte && ctx.vfsNode && ctx.res.finished && { dl: 1 }
112
- || ctx.state.uploadSize !== undefined && { size: ctx.state.uploadSize, ul: ctx.state.uploads })
112
+ || ctx.state.op === 'upload' && { size: ctx.state.opTotal, ul: ctx.state.uploads })
113
if (conn?.country)
114
ctx.logExtra({ country: conn.country })
115
if (logUA.get())
src/middlewares.ts
+3
-5
@@ -16,7 +16,7 @@ import mount from 'koa-mount'
16
import { Readable, Writable } from 'stream'
17
import { applyBlock } from './block'
18
import { Account, accountCanLogin, getAccount } from './perm'
19
-import { socket2connection, updateConnection, normalizeIp, disconnect, Connection } from './connections'
19
+import { socket2connection, normalizeIp, disconnect, Connection, updateConnectionForCtx } from './connections'
20
import basicAuth from 'basic-auth'
21
import { invalidSessions, srpCheck } from './auth'
22
import { basename, dirname } from 'path'
@@ -224,14 +224,12 @@ export const prepareState: Koa.Middleware = async (ctx, next) => {
224
ctx.session.maxAge = sessionDuration.compiled()
225
}
226
// calculate these once and for all
227
- const conn = ctx.state.connection = socket2connection(ctx.socket)!
227
+ ctx.state.connection = socket2connection(ctx.socket)!
228
const a = ctx.state.account = await urlLogin() || await getHttpAccount() || getAccount(ctx.session?.username, false)
229
if (a && !accountCanLogin(a))
230
ctx.state.account = undefined
231
ctx.state.revProxyPath = ctx.get('x-forwarded-prefix')
232
- ctx.state.browsing = undefined
233
- if (conn)
234
- updateConnection(conn, { ctx, op: undefined, opOffset: undefined, opProgress: undefined, opTotal: undefined }) // reset
232
+ updateConnectionForCtx(ctx)
233
await next()
234
235
async function urlLogin() {
src/serveFile.ts
+8
-10
@@ -11,7 +11,7 @@ import { CFG, Dict, makeMatcher, matches } from './misc'
11
import _ from 'lodash'
12
import { basename } from 'path'
13
import { promisify } from 'util'
14
-import { updateConnection } from './connections'
14
+import { getConnection, updateConnection } from './connections'
15
import { getCurrentUsername } from './auth'
16
import { sendErrorPage } from './errorPages'
17
import { Readable } from 'stream'
@@ -78,14 +78,12 @@ export async function serveFile(ctx: Koa.Context, source:string, mime?:string, c
78
ctx.fileStats = // legacy pre-0.51
79
ctx.state.fileStats = stats
80
ctx.status = HTTP_OK
81
- if (ctx.fresh) {
82
- updateConnection(ctx.state.connection, { ctx, op: 'cache' })
81
+ if (ctx.fresh)
82
return ctx.status = HTTP_NOT_MODIFIED
84
- }
83
if (content !== undefined)
84
return ctx.body = content
85
const { size } = stats
88
- const range = getRange(ctx, size)
86
+ const range = applyRange(ctx, size)
87
ctx.body = createReadStream(source, range)
88
if (ctx.vfsNode)
89
monitorAsDownload(ctx, size, range?.start)
@@ -98,18 +96,18 @@ export async function serveFile(ctx: Koa.Context, source:string, mime?:string, c
96
export function monitorAsDownload(ctx: Koa.Context, size?: number, offset?: number) {
97
if (!(ctx.body instanceof Readable))
98
throw 'incompatible body'
101
- const { connection } = ctx.state
99
+ const conn = getConnection(ctx)
100
ctx.body.on('end', () =>
103
- updateConnection(connection, { opProgress: 1 }) )
104
- updateConnection(connection, {
105
- ctx, // this will cause 'path' to be sent as well
101
+ updateConnection(conn, {}, { opProgress: 1 }) )
102
+ updateConnection(conn, {}, {
103
op: 'download',
104
+ opProgress: 0,
105
opTotal: size,
106
opOffset: size && offset && (offset / size),
107
})
108
}
109
112
-export function getRange(ctx: Koa.Context, totalSize: number) {
110
+export function applyRange(ctx: Koa.Context, totalSize=ctx.response.length) {
111
ctx.set('Accept-Ranges', 'bytes')
112
const { range } = ctx.request.header
113
if (!range) {
src/throttler.ts
+5
-7
@@ -5,7 +5,7 @@ import Koa from 'koa'
5
import { ThrottledStream, ThrottleGroup } from './ThrottledStream'
6
import { defineConfig } from './config'
7
import { getOrSet, isLocalHost } from './misc'
8
-import { Connection, updateConnection } from './connections'
8
+import { Connection, getConnection, updateConnection } from './connections'
9
import _ from 'lodash'
10
import events from './events'
11
@@ -39,7 +39,7 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
39
group.updateLimit(v))
40
return { group, count:0, destroy: unsub }
41
})
42
- const conn = ctx.state.connection
42
+ const conn = getConnection(ctx)
43
if (!conn) throw 'assert throttler connection'
44
45
const ts = conn[SymThrStr] = new ThrottledStream(ipGroup.group, conn[SymThrStr])
@@ -50,11 +50,9 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
50
const update = _.debounce(() => {
51
const ts = conn[SymThrStr] as ThrottledStream
52
const outSpeed = roundSpeed(ts.getSpeed())
53
- updateConnection(conn, {
54
- outSpeed,
55
- sent: conn.socket.bytesWritten,
56
- opProgress: conn.opTotal && ((conn.opOffset || 0) + (ts.getBytesSent() - offset) / conn.opTotal),
57
- })
53
+ const { state } = ctx
54
+ updateConnection(conn, { outSpeed, sent: conn.socket.bytesWritten },
55
+ { opProgress: state.opTotal && ((state.opOffset || 0) + (ts.getBytesSent() - offset) / state.opTotal) })
56
/* in case this stream stands still for a while (before the end), we'll have neither 'sent' or 'close' events,
57
* so who will take care to updateConnection? This artificial next-call will ensure just that */
58
clearTimeout(conn[SymTimeout])
src/upload.ts
+6
-12
@@ -12,7 +12,7 @@ import { Callback, dirTraversal, escapeHTML, loadFileAttr, storeFileAttr, try_ }
12
import { notifyClient } from './frontEndApis'
13
import { defineConfig } from './config'
14
import { getFreeDiskSync } from './util-os'
15
-import { socket2connection, updateConnection } from './connections'
15
+import { socket2connection, updateConnection, updateConnectionForCtx } from './connections'
16
import { roundSpeed } from './throttler'
17
import { getCurrentUsername } from './auth'
18
import { setCommentFor } from './comments'
@@ -129,18 +129,18 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
129
function trackProgress() {
130
let lastGot = 0
131
let lastGotTime = 0
132
- const conn = socket2connection(ctx.socket)
133
- if (!conn) return ()=>{}
132
const opTotal = reqSize + resume
135
- ctx.state.uploadSize = opTotal
136
- updateConnection(conn, { ctx, op: 'upload', opTotal, opOffset: resume / opTotal })
133
+ Object.assign(ctx.state, { op: 'upload', opTotal, opOffset: resume / opTotal, opProgress: 0 })
134
+ const conn = socket2connection(ctx.socket)
135
+ if (!conn) return
136
+ updateConnectionForCtx(ctx)
137
const h = setInterval(() => {
138
const now = Date.now()
139
const got = ret.bytesWritten
140
const inSpeed = roundSpeed((got - lastGot) / (now - lastGotTime))
141
lastGot = got
142
lastGotTime = now
143
- updateConnection(conn, { inSpeed, got, opProgress: (resume + got) / opTotal })
143
+ updateConnection(conn, { inSpeed, got }, { opProgress: (resume + got) / opTotal })
144
}, 1000)
145
ret.once('close', () => clearInterval(h) )
146
}
@@ -163,10 +163,4 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
163
ctx.status = status
164
notifyClient(ctx, 'upload.status', { [path]: ctx.status }) // allow browsers to detect failure while still sending body
165
}
166
-}
167
-
168
-declare module "koa" {
169
- interface DefaultState {
170
- uploadSize?: number
171
- }
166
}
\ No newline at end of file
src/zip.ts
+2
-2
@@ -8,7 +8,7 @@ import { createReadStream } from 'fs'
8
import fs from 'fs/promises'
9
import { defineConfig } from './config'
10
import { basename, dirname } from 'path'
11
-import { getRange, monitorAsDownload } from './serveFile'
11
+import { applyRange, monitorAsDownload } from './serveFile'
12
import { HTTP_OK } from './const'
13
14
// expects 'node' to have had permissions checked by caller
@@ -68,7 +68,7 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
68
const time = 1000 * zipSeconds.get()
69
const size = await zip.calculateSize(time)
70
ctx.response.length = size
71
- const range = getRange(ctx, size) // keep var size as ctx.response.length won't preserve a NaN
71
+ const range = applyRange(ctx, size) // keep var size as ctx.response.length won't preserve a NaN
72
if (ctx.status >= 400)
73
return
74
if (range)