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