@kadi.build/file-sharing 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +477 -0
- package/package.json +60 -0
- package/src/DownloadMonitor.js +199 -0
- package/src/EventNotifier.js +178 -0
- package/src/FileSharingServer.js +598 -0
- package/src/HttpServerProvider.js +538 -0
- package/src/MonitoringDashboard.js +181 -0
- package/src/S3Server.js +984 -0
- package/src/ShutdownManager.js +135 -0
- package/src/index.js +69 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileSharingServer - Main orchestrating class for @kadi.build/file-sharing
|
|
3
|
+
*
|
|
4
|
+
* Integrates HttpServerProvider, S3Server, TunnelManager, DownloadMonitor,
|
|
5
|
+
* ShutdownManager, MonitoringDashboard, and EventNotifier into a unified
|
|
6
|
+
* file sharing solution.
|
|
7
|
+
*
|
|
8
|
+
* KĀDI is the default tunnel service.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { createFileManager } from '@kadi.build/file-manager';
|
|
15
|
+
import { TunnelManager } from '@kadi.build/tunnel-services';
|
|
16
|
+
import { HttpServerProvider } from './HttpServerProvider.js';
|
|
17
|
+
import { S3Server } from './S3Server.js';
|
|
18
|
+
import { DownloadMonitor } from './DownloadMonitor.js';
|
|
19
|
+
import { ShutdownManager } from './ShutdownManager.js';
|
|
20
|
+
import { MonitoringDashboard } from './MonitoringDashboard.js';
|
|
21
|
+
import { EventNotifier } from './EventNotifier.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a .env file's content and inject into process.env.
|
|
25
|
+
* Existing env vars take priority (are NOT overwritten).
|
|
26
|
+
* @param {string} content raw file content
|
|
27
|
+
*/
|
|
28
|
+
function _parseEnvFile(content) {
|
|
29
|
+
for (const line of content.split('\n')) {
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
32
|
+
const eqIdx = trimmed.indexOf('=');
|
|
33
|
+
if (eqIdx === -1) continue;
|
|
34
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
35
|
+
let val = trimmed.substring(eqIdx + 1).trim();
|
|
36
|
+
// Strip surrounding quotes
|
|
37
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
38
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
39
|
+
val = val.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
// Real env vars take priority over .env file values
|
|
42
|
+
if (process.env[key] === undefined) {
|
|
43
|
+
process.env[key] = val;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Walk up from cwd looking for a .env file (supports monorepo layouts
|
|
50
|
+
* where .env lives at the workspace root, not inside packages/*).
|
|
51
|
+
* Stops at the filesystem root.
|
|
52
|
+
*/
|
|
53
|
+
function _loadEnvFile() {
|
|
54
|
+
let dir = process.cwd();
|
|
55
|
+
const root = path.parse(dir).root;
|
|
56
|
+
|
|
57
|
+
while (true) {
|
|
58
|
+
try {
|
|
59
|
+
const envPath = path.join(dir, '.env');
|
|
60
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
61
|
+
_parseEnvFile(content);
|
|
62
|
+
return; // found and loaded — done
|
|
63
|
+
} catch {
|
|
64
|
+
// not found in this directory — keep climbing
|
|
65
|
+
}
|
|
66
|
+
const parent = path.dirname(dir);
|
|
67
|
+
if (parent === dir || dir === root) break; // reached fs root
|
|
68
|
+
dir = parent;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Load secrets from environment variables and optional .env file.
|
|
74
|
+
* Supports KADI_ prefixed vars and common tunnel/auth env vars.
|
|
75
|
+
*
|
|
76
|
+
* Env vars (all optional — constructor config takes priority):
|
|
77
|
+
* KADI_TUNNEL_TOKEN — KĀDI tunnel auth token
|
|
78
|
+
* KADI_TUNNEL_SERVER — KĀDI broker address (default: broker.kadi.build)
|
|
79
|
+
* KADI_TUNNEL_DOMAIN — KĀDI tunnel domain (default: tunnel.kadi.build)
|
|
80
|
+
* KADI_TUNNEL_PORT — KĀDI frpc port (default: 7000)
|
|
81
|
+
* KADI_TUNNEL_SSH_PORT— KĀDI SSH gateway port (default: 2200)
|
|
82
|
+
* KADI_TUNNEL_MODE — ssh | frpc | auto (default: auto)
|
|
83
|
+
* KADI_AGENT_ID — Agent identifier for proxy naming
|
|
84
|
+
* NGROK_AUTHTOKEN — Ngrok auth token (also: NGROK_AUTH_TOKEN)
|
|
85
|
+
* KADI_S3_ACCESS_KEY — S3 access key (default: minioadmin)
|
|
86
|
+
* KADI_S3_SECRET_KEY — S3 secret key (default: minioadmin)
|
|
87
|
+
* KADI_AUTH_USERNAME — HTTP Basic auth username
|
|
88
|
+
* KADI_AUTH_PASSWORD — HTTP Basic auth password
|
|
89
|
+
* KADI_AUTH_API_KEY — API key for Bearer/X-API-Key auth
|
|
90
|
+
*
|
|
91
|
+
* @returns {object} Secrets object (values are strings or undefined)
|
|
92
|
+
*/
|
|
93
|
+
function loadSecrets() {
|
|
94
|
+
// Find and load .env file — walk up parent directories (monorepo support).
|
|
95
|
+
// e.g. packages/file-sharing/ → packages/ → workspace root (where .env lives)
|
|
96
|
+
_loadEnvFile();
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
kadiToken: process.env.KADI_TUNNEL_TOKEN,
|
|
101
|
+
kadiServer: process.env.KADI_TUNNEL_SERVER,
|
|
102
|
+
kadiDomain: process.env.KADI_TUNNEL_DOMAIN,
|
|
103
|
+
kadiPort: process.env.KADI_TUNNEL_PORT ? Number(process.env.KADI_TUNNEL_PORT) : undefined,
|
|
104
|
+
kadiSshPort: process.env.KADI_TUNNEL_SSH_PORT ? Number(process.env.KADI_TUNNEL_SSH_PORT) : undefined,
|
|
105
|
+
kadiMode: process.env.KADI_TUNNEL_MODE,
|
|
106
|
+
kadiAgentId: process.env.KADI_AGENT_ID,
|
|
107
|
+
ngrokAuthToken: process.env.NGROK_AUTHTOKEN || process.env.NGROK_AUTH_TOKEN,
|
|
108
|
+
s3AccessKey: process.env.KADI_S3_ACCESS_KEY,
|
|
109
|
+
s3SecretKey: process.env.KADI_S3_SECRET_KEY,
|
|
110
|
+
authUsername: process.env.KADI_AUTH_USERNAME,
|
|
111
|
+
authPassword: process.env.KADI_AUTH_PASSWORD,
|
|
112
|
+
authApiKey: process.env.KADI_AUTH_API_KEY
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Strip `undefined` values from an object so they don't override downstream
|
|
118
|
+
* defaults when spread into a constructor config.
|
|
119
|
+
* @param {object} obj
|
|
120
|
+
* @returns {object}
|
|
121
|
+
*/
|
|
122
|
+
function _filterDefined(obj) {
|
|
123
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class FileSharingServer extends EventEmitter {
|
|
127
|
+
constructor(config = {}) {
|
|
128
|
+
super();
|
|
129
|
+
|
|
130
|
+
this.config = {
|
|
131
|
+
staticDir: process.cwd(),
|
|
132
|
+
port: 3000,
|
|
133
|
+
host: '0.0.0.0',
|
|
134
|
+
enableS3: false,
|
|
135
|
+
s3Port: 9000,
|
|
136
|
+
s3Config: {},
|
|
137
|
+
enableDirectoryListing: true,
|
|
138
|
+
cors: true,
|
|
139
|
+
auth: null,
|
|
140
|
+
tunnel: {
|
|
141
|
+
enabled: false,
|
|
142
|
+
service: 'kadi', // KĀDI is the default tunnel service
|
|
143
|
+
options: {},
|
|
144
|
+
autoReconnect: true,
|
|
145
|
+
reconnectDelay: 5000
|
|
146
|
+
},
|
|
147
|
+
monitoring: {
|
|
148
|
+
enabled: true,
|
|
149
|
+
dashboard: false,
|
|
150
|
+
webhooks: []
|
|
151
|
+
},
|
|
152
|
+
shutdown: {
|
|
153
|
+
gracefulTimeout: 30000,
|
|
154
|
+
finishActiveDownloads: true,
|
|
155
|
+
forceKillTimeout: 60000
|
|
156
|
+
},
|
|
157
|
+
...config
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Ensure tunnel defaults are preserved when user provides partial tunnel config
|
|
161
|
+
if (config.tunnel) {
|
|
162
|
+
this.config.tunnel = {
|
|
163
|
+
enabled: false,
|
|
164
|
+
service: 'kadi',
|
|
165
|
+
options: {},
|
|
166
|
+
autoReconnect: true,
|
|
167
|
+
reconnectDelay: 5000,
|
|
168
|
+
...config.tunnel
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ----------------------------------------------------------------
|
|
173
|
+
// Load secrets from env vars / .env (constructor config takes priority)
|
|
174
|
+
// ----------------------------------------------------------------
|
|
175
|
+
const secrets = loadSecrets();
|
|
176
|
+
|
|
177
|
+
// Build auth config: explicit config > env vars > null
|
|
178
|
+
if (!this.config.auth) {
|
|
179
|
+
if (secrets.authApiKey) {
|
|
180
|
+
this.config.auth = { apiKey: secrets.authApiKey };
|
|
181
|
+
} else if (secrets.authUsername && secrets.authPassword) {
|
|
182
|
+
this.config.auth = { username: secrets.authUsername, password: secrets.authPassword };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Initialize components
|
|
187
|
+
this.fileManager = createFileManager();
|
|
188
|
+
|
|
189
|
+
this.httpServer = new HttpServerProvider({
|
|
190
|
+
port: this.config.port,
|
|
191
|
+
host: this.config.host,
|
|
192
|
+
staticDir: this.config.staticDir,
|
|
193
|
+
enableDirectoryListing: this.config.enableDirectoryListing,
|
|
194
|
+
cors: this.config.cors,
|
|
195
|
+
auth: this.config.auth,
|
|
196
|
+
...(this.config.httpConfig || {})
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this.s3Server = null;
|
|
200
|
+
if (this.config.enableS3) {
|
|
201
|
+
this.s3Server = new S3Server({
|
|
202
|
+
port: this.config.s3Port,
|
|
203
|
+
host: this.config.host,
|
|
204
|
+
rootDir: this.config.staticDir,
|
|
205
|
+
...(secrets.s3AccessKey ? { accessKeyId: secrets.s3AccessKey } : {}),
|
|
206
|
+
...(secrets.s3SecretKey ? { secretAccessKey: secrets.s3SecretKey } : {}),
|
|
207
|
+
...this.config.s3Config
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Build TunnelManager config from env vars + explicit tunnel config.
|
|
212
|
+
// Use || to let explicit config override env, and filter out undefined
|
|
213
|
+
// values so TunnelManager's own defaults are preserved.
|
|
214
|
+
const tunnelCfg = this.config.tunnel || {};
|
|
215
|
+
const tunnelManagerConfig = _filterDefined({
|
|
216
|
+
primaryService: tunnelCfg.service || 'kadi',
|
|
217
|
+
autoFallback: tunnelCfg.autoFallback,
|
|
218
|
+
// KĀDI auth (env vars as fallback)
|
|
219
|
+
kadiToken: tunnelCfg.kadiToken || secrets.kadiToken,
|
|
220
|
+
kadiServer: tunnelCfg.kadiServer || secrets.kadiServer,
|
|
221
|
+
kadiDomain: tunnelCfg.kadiDomain || secrets.kadiDomain,
|
|
222
|
+
kadiPort: tunnelCfg.kadiPort || secrets.kadiPort,
|
|
223
|
+
kadiSshPort: tunnelCfg.kadiSshPort || secrets.kadiSshPort,
|
|
224
|
+
kadiMode: tunnelCfg.kadiMode || secrets.kadiMode,
|
|
225
|
+
kadiAgentId: tunnelCfg.kadiAgentId || secrets.kadiAgentId,
|
|
226
|
+
// Ngrok auth (env vars as fallback)
|
|
227
|
+
ngrokAuthToken: tunnelCfg.ngrokAuthToken || secrets.ngrokAuthToken,
|
|
228
|
+
// Pass through any extra service-specific options
|
|
229
|
+
...(tunnelCfg.managerOptions || {})
|
|
230
|
+
});
|
|
231
|
+
this.tunnelManager = new TunnelManager(tunnelManagerConfig);
|
|
232
|
+
this.downloadMonitor = new DownloadMonitor();
|
|
233
|
+
this.shutdownManager = new ShutdownManager(this.config.shutdown);
|
|
234
|
+
this.monitoringDashboard = new MonitoringDashboard();
|
|
235
|
+
this.eventNotifier = new EventNotifier();
|
|
236
|
+
|
|
237
|
+
// State
|
|
238
|
+
this.isRunning = false;
|
|
239
|
+
this.tunnel = null;
|
|
240
|
+
this._startTime = null;
|
|
241
|
+
|
|
242
|
+
// Wire up events
|
|
243
|
+
this._setupEventForwarding();
|
|
244
|
+
this._setupShutdownHandlers();
|
|
245
|
+
this._setupWebhooks();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Start the file sharing server
|
|
250
|
+
* @returns {Promise<object>} Server information
|
|
251
|
+
*/
|
|
252
|
+
async start() {
|
|
253
|
+
if (this.isRunning) {
|
|
254
|
+
return this.getInfo();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this._startTime = Date.now();
|
|
258
|
+
|
|
259
|
+
// Start HTTP server
|
|
260
|
+
const httpResult = await this.httpServer.start();
|
|
261
|
+
this.emit('http:started', httpResult);
|
|
262
|
+
|
|
263
|
+
// Start S3 server if enabled
|
|
264
|
+
if (this.s3Server) {
|
|
265
|
+
const s3Result = await this.s3Server.start();
|
|
266
|
+
this.emit('s3:started', s3Result);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Create tunnel if enabled
|
|
270
|
+
if (this.config.tunnel.enabled) {
|
|
271
|
+
try {
|
|
272
|
+
await this.enableTunnel(this.config.tunnel);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
this.emit('tunnel:error', error);
|
|
275
|
+
// Don't fail server start if tunnel fails
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Start monitoring dashboard if configured
|
|
280
|
+
if (this.config.monitoring.dashboard) {
|
|
281
|
+
await this.monitoringDashboard.start();
|
|
282
|
+
this.monitoringDashboard.setServerInfo(this.getInfo());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
this.isRunning = true;
|
|
286
|
+
const info = this.getInfo();
|
|
287
|
+
this.emit('started', info);
|
|
288
|
+
|
|
289
|
+
return info;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Stop the file sharing server
|
|
294
|
+
* Gracefully shuts down all components.
|
|
295
|
+
*/
|
|
296
|
+
async stop() {
|
|
297
|
+
if (!this.isRunning) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.emit('stopping');
|
|
302
|
+
|
|
303
|
+
// Stop monitoring dashboard
|
|
304
|
+
if (this.monitoringDashboard.isRunning) {
|
|
305
|
+
await this.monitoringDashboard.stop();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Disable tunnel
|
|
309
|
+
if (this.tunnel) {
|
|
310
|
+
try {
|
|
311
|
+
await this.disableTunnel();
|
|
312
|
+
} catch {
|
|
313
|
+
// Best effort tunnel cleanup
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Stop servers
|
|
318
|
+
await this.httpServer.stop();
|
|
319
|
+
|
|
320
|
+
if (this.s3Server) {
|
|
321
|
+
await this.s3Server.stop();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.isRunning = false;
|
|
325
|
+
this._startTime = null;
|
|
326
|
+
this.emit('stopped');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Enable tunnel to expose server publicly
|
|
331
|
+
* @param {object} options - Tunnel options
|
|
332
|
+
* @returns {Promise<object>} Tunnel information
|
|
333
|
+
*/
|
|
334
|
+
async enableTunnel(options = {}) {
|
|
335
|
+
if (this.tunnel) {
|
|
336
|
+
await this.disableTunnel();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const service = options.service || 'kadi'; // KĀDI is the default
|
|
340
|
+
const port = this.config.port;
|
|
341
|
+
|
|
342
|
+
// TunnelManager.createTunnel(port, options) — port is first arg (number),
|
|
343
|
+
// options.service tells it which provider to use.
|
|
344
|
+
const tunnelOptions = {
|
|
345
|
+
service,
|
|
346
|
+
...(options.options || {})
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
this.tunnel = await this.tunnelManager.createTunnel(port, tunnelOptions);
|
|
350
|
+
this.emit('tunnel:created', this.tunnel);
|
|
351
|
+
|
|
352
|
+
return this.tunnel;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Disable active tunnel
|
|
357
|
+
*/
|
|
358
|
+
async disableTunnel() {
|
|
359
|
+
if (!this.tunnel) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await this.tunnelManager.closeTunnel(this.tunnel.id);
|
|
364
|
+
this.tunnel = null;
|
|
365
|
+
this.emit('tunnel:closed');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Enable S3-compatible API dynamically
|
|
370
|
+
* @param {object} options - S3 server options
|
|
371
|
+
* @returns {Promise<object>} S3 server information
|
|
372
|
+
*/
|
|
373
|
+
async enableS3(options = {}) {
|
|
374
|
+
if (this.s3Server) {
|
|
375
|
+
await this.disableS3();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const secrets = loadSecrets();
|
|
379
|
+
this.s3Server = new S3Server({
|
|
380
|
+
port: this.config.s3Port,
|
|
381
|
+
host: this.config.host,
|
|
382
|
+
rootDir: this.config.staticDir,
|
|
383
|
+
...(secrets.s3AccessKey ? { accessKeyId: secrets.s3AccessKey } : {}),
|
|
384
|
+
...(secrets.s3SecretKey ? { secretAccessKey: secrets.s3SecretKey } : {}),
|
|
385
|
+
...this.config.s3Config,
|
|
386
|
+
...options
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Forward S3 events
|
|
390
|
+
this.s3Server.on('object:get', (data) => this.emit('s3:get', data));
|
|
391
|
+
this.s3Server.on('object:put', (data) => this.emit('s3:put', data));
|
|
392
|
+
this.s3Server.on('object:delete', (data) => this.emit('s3:delete', data));
|
|
393
|
+
this.s3Server.on('error', (err) => this.emit('error', err));
|
|
394
|
+
|
|
395
|
+
const result = await this.s3Server.start();
|
|
396
|
+
this.emit('s3:started', result);
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Disable S3 API
|
|
402
|
+
*/
|
|
403
|
+
async disableS3() {
|
|
404
|
+
if (!this.s3Server) return;
|
|
405
|
+
|
|
406
|
+
await this.s3Server.stop();
|
|
407
|
+
this.s3Server = null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get current server information and statistics
|
|
412
|
+
* @returns {object} Server information
|
|
413
|
+
*/
|
|
414
|
+
getInfo() {
|
|
415
|
+
return {
|
|
416
|
+
isRunning: this.isRunning,
|
|
417
|
+
localUrl: `http://${this.config.host}:${this.config.port}`,
|
|
418
|
+
publicUrl: this.tunnel?.publicUrl || null,
|
|
419
|
+
s3Endpoint: this.s3Server?.isRunning
|
|
420
|
+
? `http://${this.config.host}:${this.config.s3Port}`
|
|
421
|
+
: null,
|
|
422
|
+
staticDir: this.config.staticDir,
|
|
423
|
+
stats: this.downloadMonitor.getStats(),
|
|
424
|
+
uptime: this._startTime ? Date.now() - this._startTime : 0,
|
|
425
|
+
tunnelStatus: this.tunnel || null
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get download statistics
|
|
431
|
+
* @returns {object}
|
|
432
|
+
*/
|
|
433
|
+
getStats() {
|
|
434
|
+
return this.downloadMonitor.getStats();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* List files in the served directory
|
|
439
|
+
* @param {string} subPath - Optional subdirectory path
|
|
440
|
+
* @returns {Promise<Array>}
|
|
441
|
+
*/
|
|
442
|
+
async listFiles(subPath = '') {
|
|
443
|
+
const { default: path } = await import('path');
|
|
444
|
+
const { default: fsPromises } = await import('fs/promises');
|
|
445
|
+
|
|
446
|
+
const dirPath = path.join(this.config.staticDir, subPath);
|
|
447
|
+
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
|
448
|
+
|
|
449
|
+
return Promise.all(entries.map(async (entry) => {
|
|
450
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
451
|
+
const stats = await fsPromises.stat(fullPath);
|
|
452
|
+
return {
|
|
453
|
+
name: entry.name,
|
|
454
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
455
|
+
size: stats.size,
|
|
456
|
+
modified: stats.mtime.toISOString()
|
|
457
|
+
};
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Add a webhook for event notifications
|
|
463
|
+
* @param {string} url - Webhook URL
|
|
464
|
+
* @param {string[]} events - Events to subscribe to
|
|
465
|
+
*/
|
|
466
|
+
addWebhook(url, events = []) {
|
|
467
|
+
this.eventNotifier.addWebhook(url, events);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Remove a webhook
|
|
472
|
+
* @param {string} url - Webhook URL to remove
|
|
473
|
+
*/
|
|
474
|
+
removeWebhook(url) {
|
|
475
|
+
this.eventNotifier.removeWebhook(url);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** @type {number} */
|
|
479
|
+
get port() {
|
|
480
|
+
return this.config.port;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** @type {string|null} */
|
|
484
|
+
get tunnelUrl() {
|
|
485
|
+
return this.tunnel?.publicUrl || null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** @type {string|null} */
|
|
489
|
+
get s3Endpoint() {
|
|
490
|
+
return this.s3Server?.isRunning
|
|
491
|
+
? `http://${this.config.host}:${this.config.s3Port}`
|
|
492
|
+
: null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** @type {string} */
|
|
496
|
+
get staticDir() {
|
|
497
|
+
return this.config.staticDir;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Wire up event forwarding between components
|
|
502
|
+
* @private
|
|
503
|
+
*/
|
|
504
|
+
_setupEventForwarding() {
|
|
505
|
+
// Forward HTTP server events
|
|
506
|
+
this.httpServer.on('download:start', (data) => {
|
|
507
|
+
this.downloadMonitor.startDownload(
|
|
508
|
+
`http-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
509
|
+
data
|
|
510
|
+
);
|
|
511
|
+
this.emit('download:start', data);
|
|
512
|
+
});
|
|
513
|
+
this.httpServer.on('download:complete', (data) => {
|
|
514
|
+
this.emit('download:complete', data);
|
|
515
|
+
});
|
|
516
|
+
this.httpServer.on('download:error', (data) => {
|
|
517
|
+
this.emit('download:error', data);
|
|
518
|
+
});
|
|
519
|
+
this.httpServer.on('upload:complete', (data) => {
|
|
520
|
+
this.emit('upload:complete', data);
|
|
521
|
+
});
|
|
522
|
+
this.httpServer.on('error', (err) => {
|
|
523
|
+
this.emit('http:error', err);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Forward S3 events
|
|
527
|
+
if (this.s3Server) {
|
|
528
|
+
this.s3Server.on('object:get', (data) => this.emit('s3:get', data));
|
|
529
|
+
this.s3Server.on('object:put', (data) => this.emit('s3:put', data));
|
|
530
|
+
this.s3Server.on('object:delete', (data) => this.emit('s3:delete', data));
|
|
531
|
+
this.s3Server.on('error', (err) => this.emit('error', err));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Forward download monitor events to dashboard
|
|
535
|
+
this.downloadMonitor.on('download:start', () => {
|
|
536
|
+
this._updateDashboard();
|
|
537
|
+
});
|
|
538
|
+
this.downloadMonitor.on('download:complete', () => {
|
|
539
|
+
this._updateDashboard();
|
|
540
|
+
});
|
|
541
|
+
this.downloadMonitor.on('download:progress', () => {
|
|
542
|
+
this._updateDashboard();
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Register shutdown handlers
|
|
548
|
+
* @private
|
|
549
|
+
*/
|
|
550
|
+
_setupShutdownHandlers() {
|
|
551
|
+
this.shutdownManager.register(async () => {
|
|
552
|
+
await this.stop();
|
|
553
|
+
}, 1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Setup initial webhooks from config
|
|
558
|
+
* @private
|
|
559
|
+
*/
|
|
560
|
+
_setupWebhooks() {
|
|
561
|
+
if (this.config.monitoring?.webhooks) {
|
|
562
|
+
for (const webhook of this.config.monitoring.webhooks) {
|
|
563
|
+
this.eventNotifier.addWebhook(
|
|
564
|
+
webhook.url,
|
|
565
|
+
webhook.events || [],
|
|
566
|
+
webhook.options || {}
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Auto-notify on key events
|
|
572
|
+
const eventsToNotify = [
|
|
573
|
+
'started', 'stopped', 'download:start', 'download:complete',
|
|
574
|
+
'upload:complete', 's3:put', 's3:delete', 'tunnel:created', 'tunnel:closed'
|
|
575
|
+
];
|
|
576
|
+
|
|
577
|
+
for (const event of eventsToNotify) {
|
|
578
|
+
this.on(event, (data) => {
|
|
579
|
+
this.eventNotifier.notify(event, data).catch(() => {
|
|
580
|
+
// Silently handle notification failures
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Update the monitoring dashboard with latest stats
|
|
588
|
+
* @private
|
|
589
|
+
*/
|
|
590
|
+
_updateDashboard() {
|
|
591
|
+
if (this.monitoringDashboard.isRunning) {
|
|
592
|
+
this.monitoringDashboard.updateStats(this.downloadMonitor.getStats());
|
|
593
|
+
this.monitoringDashboard.setServerInfo(this.getInfo());
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export default FileSharingServer;
|