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)