@samitouri / QOSami-HFS / commits / 42b62d6b

new command: transfers

Massimo Melina committed Apr 21, 2026 at 00:44 UTC 42b62d6b2d485d986e158ef98ee927e1e8ac937f
2 files changed +48 -25
src/api.monitor.ts
+26 -23
@@ -55,29 +55,6 @@ export default {
55 list.update(getConnAddress(conn), change)
56 },
57 })
58 -
59 - function serializeConnection(conn: Connection) {
60 - const { socket, started, secure } = conn
61 - return {
62 - ...getConnAddress(conn),
63 - v: (socket.remoteFamily?.endsWith('6') ? 6 : 4),
64 - got: socket.bytesRead,
65 - sent: socket.bytesWritten,
66 - country: conn.country,
67 - started,
68 - secure: (secure || undefined) as boolean|undefined, // undefined will save some space once json-ed
69 - ...fromCtx(conn.ctx),
70 - }
71 - }
72 -
73 - function fromCtx(ctx?: Koa.Context) {
74 - if (!ctx) return
75 - return {
76 - user: getCurrentUsername(ctx),
77 - agent: shortenAgent(ctx.get('user-agent')),
78 - ...inferOperation(ctx)
79 - }
80 - }
58 },
59
60 async *get_connection_stats() {
@@ -107,6 +84,32 @@ function ignore(conn: Connection) {
84 return false //conn.socket && isLocalHost(conn)
85 }
86
87 +export function serializeConnection(conn: Connection) {
88 + const { socket, started, secure } = conn
89 + return {
90 + ...getConnAddress(conn),
91 + v: (socket.remoteFamily?.endsWith('6') ? 6 : 4),
92 + // connection fields are request-scoped once transfer tracking starts; socket counters cover earlier snapshots
93 + got: conn.got || socket.bytesRead,
94 + sent: conn.sent || socket.bytesWritten,
95 + outSpeedKb: conn.outSpeedKb,
96 + inSpeedKb: conn.inSpeedKb,
97 + country: conn.country,
98 + started,
99 + secure: (secure || undefined) as boolean|undefined, // undefined will save some space once json-ed
100 + ...fromCtx(conn.ctx),
101 + }
102 +}
103 +
104 +function fromCtx(ctx?: Koa.Context) {
105 + if (!ctx) return
106 + return {
107 + user: getCurrentUsername(ctx),
108 + agent: shortenAgent(ctx.get('user-agent')),
109 + ...inferOperation(ctx)
110 + }
111 +}
112 +
113 export function inferOperation(ctx: Koa.Context) {
114 const s = ctx.state // short alias
115 return {
src/commands.ts
+22 -2
@@ -12,8 +12,8 @@ import { quitting } from './first'
12 import { getInactivePlugins, mapPlugins, startPlugin, stopPlugin } from './plugins'
13 import { purgeFileAttr } from './fileAttr'
14 import { downloadPlugin } from './github'
15 -import { Dict, formatBytes, formatSpeed, formatTimestamp, makeMatcher } from './cross'
16 -import apiMonitor, { inferOperation } from './api.monitor'
15 +import { Dict, formatBytes, formatPerc, formatSpeed, formatTimestamp, makeMatcher, with_ } from './cross'
16 +import apiMonitor, { inferOperation, serializeConnection } from './api.monitor'
17 import { getConnections } from './connections'
18 import { argv } from './argv'
19 import { getServerStatus } from './listen'
@@ -191,6 +191,23 @@ const commands = {
191 params: '',
192 cb: purgeFileAttr,
193 },
194 + transfers: {
195 + params: '',
196 + cb() {
197 + const transfers = getConnections().map(serializeConnection).filter(x => x.op === 'upload' || x.op === 'download')
198 + if (!transfers.length)
199 + return console.log("No ongoing uploads/downloads")
200 + console.table(transfers.map(x => ({
201 + type: x.op,
202 + progress: with_(x.opProgress ?? x.opOffset, v => v == null ? '' : formatPerc(v)),
203 + transferred: formatBytes(Math.max(x.sent || 0, x.got || 0)),
204 + total: x.opTotal == null ? '' : formatBytes(x.opTotal),
205 + speed: formatSpeed(Math.max(x.outSpeedKb || 0, x.inSpeedKb || 0) * 1000),
206 + user: x.user,
207 + path: x.path,
208 + })))
209 + }
210 + },
211 status: {
212 params: '',
213 async cb() {
@@ -210,3 +227,6 @@ const commands = {
227 }
228 }
229 }
230 +
231 +type ConnectionSnapshot = ReturnType<typeof serializeConnection>
232 +type TransferSnapshot = ConnectionSnapshot & { op: 'upload' | 'download' }