@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,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DownloadMonitor - Transfer monitoring for @kadi.build/file-sharing
|
|
3
|
+
*
|
|
4
|
+
* Tracks active downloads, monitors progress, records completion statistics,
|
|
5
|
+
* and provides real-time download information.
|
|
6
|
+
*
|
|
7
|
+
* Migrated from src/downloadMonitor.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
|
|
12
|
+
export class DownloadMonitor extends EventEmitter {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.activeDownloads = new Map();
|
|
16
|
+
this.completedDownloads = [];
|
|
17
|
+
this.stats = {
|
|
18
|
+
totalBytes: 0,
|
|
19
|
+
totalDownloads: 0,
|
|
20
|
+
activeCount: 0,
|
|
21
|
+
failedCount: 0,
|
|
22
|
+
averageSpeed: 0,
|
|
23
|
+
peakConcurrent: 0
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register a new download being tracked
|
|
29
|
+
* @param {string} id - Unique download identifier
|
|
30
|
+
* @param {object} metadata - Download metadata
|
|
31
|
+
* @returns {object} The download record
|
|
32
|
+
*/
|
|
33
|
+
startDownload(id, metadata = {}) {
|
|
34
|
+
const download = {
|
|
35
|
+
id,
|
|
36
|
+
file: metadata.file || 'unknown',
|
|
37
|
+
totalSize: metadata.totalSize || metadata.size || 0,
|
|
38
|
+
clientIp: metadata.clientIp || 'unknown',
|
|
39
|
+
bytesTransferred: 0,
|
|
40
|
+
progress: 0,
|
|
41
|
+
speed: 0,
|
|
42
|
+
startTime: Date.now(),
|
|
43
|
+
status: 'active',
|
|
44
|
+
...metadata
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.activeDownloads.set(id, download);
|
|
48
|
+
this.stats.activeCount = this.activeDownloads.size;
|
|
49
|
+
|
|
50
|
+
if (this.stats.activeCount > this.stats.peakConcurrent) {
|
|
51
|
+
this.stats.peakConcurrent = this.stats.activeCount;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.emit('download:start', download);
|
|
55
|
+
return download;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Update download progress
|
|
60
|
+
* @param {string} id - Download identifier
|
|
61
|
+
* @param {number} bytesTransferred - Bytes transferred so far
|
|
62
|
+
*/
|
|
63
|
+
updateProgress(id, bytesTransferred) {
|
|
64
|
+
const download = this.activeDownloads.get(id);
|
|
65
|
+
if (!download) return;
|
|
66
|
+
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const elapsed = (now - download.startTime) / 1000; // seconds
|
|
69
|
+
|
|
70
|
+
download.bytesTransferred = bytesTransferred;
|
|
71
|
+
download.progress = download.totalSize
|
|
72
|
+
? (bytesTransferred / download.totalSize) * 100
|
|
73
|
+
: 0;
|
|
74
|
+
download.speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
|
|
75
|
+
|
|
76
|
+
this.emit('download:progress', { ...download });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mark a download as completed
|
|
81
|
+
* @param {string} id - Download identifier
|
|
82
|
+
*/
|
|
83
|
+
completeDownload(id) {
|
|
84
|
+
const download = this.activeDownloads.get(id);
|
|
85
|
+
if (!download) return;
|
|
86
|
+
|
|
87
|
+
download.status = 'completed';
|
|
88
|
+
download.endTime = Date.now();
|
|
89
|
+
download.duration = download.endTime - download.startTime;
|
|
90
|
+
download.progress = 100;
|
|
91
|
+
|
|
92
|
+
this.activeDownloads.delete(id);
|
|
93
|
+
this.completedDownloads.push(download);
|
|
94
|
+
|
|
95
|
+
this.stats.activeCount = this.activeDownloads.size;
|
|
96
|
+
this.stats.totalDownloads++;
|
|
97
|
+
this.stats.totalBytes += download.bytesTransferred;
|
|
98
|
+
this._updateAverageSpeed();
|
|
99
|
+
|
|
100
|
+
this.emit('download:complete', download);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mark a download as failed
|
|
105
|
+
* @param {string} id - Download identifier
|
|
106
|
+
* @param {Error} error - The error that caused the failure
|
|
107
|
+
*/
|
|
108
|
+
failDownload(id, error) {
|
|
109
|
+
const download = this.activeDownloads.get(id);
|
|
110
|
+
if (!download) return;
|
|
111
|
+
|
|
112
|
+
download.status = 'failed';
|
|
113
|
+
download.error = error.message || String(error);
|
|
114
|
+
download.endTime = Date.now();
|
|
115
|
+
download.duration = download.endTime - download.startTime;
|
|
116
|
+
|
|
117
|
+
this.activeDownloads.delete(id);
|
|
118
|
+
this.completedDownloads.push(download);
|
|
119
|
+
|
|
120
|
+
this.stats.activeCount = this.activeDownloads.size;
|
|
121
|
+
this.stats.failedCount++;
|
|
122
|
+
|
|
123
|
+
this.emit('download:error', { download, error });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Cancel an active download
|
|
128
|
+
* @param {string} id - Download identifier
|
|
129
|
+
*/
|
|
130
|
+
cancelDownload(id) {
|
|
131
|
+
const download = this.activeDownloads.get(id);
|
|
132
|
+
if (!download) return;
|
|
133
|
+
|
|
134
|
+
download.status = 'cancelled';
|
|
135
|
+
download.endTime = Date.now();
|
|
136
|
+
download.duration = download.endTime - download.startTime;
|
|
137
|
+
|
|
138
|
+
this.activeDownloads.delete(id);
|
|
139
|
+
this.completedDownloads.push(download);
|
|
140
|
+
this.stats.activeCount = this.activeDownloads.size;
|
|
141
|
+
|
|
142
|
+
this.emit('download:cancel', download);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get current download statistics
|
|
147
|
+
* @returns {object} Download stats
|
|
148
|
+
*/
|
|
149
|
+
getStats() {
|
|
150
|
+
return {
|
|
151
|
+
...this.stats,
|
|
152
|
+
activeDownloads: this.getActiveDownloads()
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get all active downloads
|
|
158
|
+
* @returns {Array} Active download records
|
|
159
|
+
*/
|
|
160
|
+
getActiveDownloads() {
|
|
161
|
+
return Array.from(this.activeDownloads.values());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get completed downloads
|
|
166
|
+
* @param {number} limit - Max number of records to return
|
|
167
|
+
* @returns {Array} Completed download records
|
|
168
|
+
*/
|
|
169
|
+
getCompletedDownloads(limit = 100) {
|
|
170
|
+
return this.completedDownloads.slice(-limit);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear completed download history
|
|
175
|
+
*/
|
|
176
|
+
clearHistory() {
|
|
177
|
+
this.completedDownloads = [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Update the running average speed
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_updateAverageSpeed() {
|
|
185
|
+
const completed = this.completedDownloads.filter(d => d.status === 'completed' && d.duration > 0);
|
|
186
|
+
if (completed.length === 0) {
|
|
187
|
+
this.stats.averageSpeed = 0;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const totalSpeed = completed.reduce((sum, d) => {
|
|
192
|
+
return sum + (d.bytesTransferred / (d.duration / 1000));
|
|
193
|
+
}, 0);
|
|
194
|
+
|
|
195
|
+
this.stats.averageSpeed = totalSpeed / completed.length;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default DownloadMonitor;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventNotifier - Webhook and event notification for @kadi.build/file-sharing
|
|
3
|
+
*
|
|
4
|
+
* Sends notifications to registered webhooks when events occur.
|
|
5
|
+
* Supports filtering by event type, retry logic, and multiple notification channels.
|
|
6
|
+
*
|
|
7
|
+
* Migrated from src/eventNotifier.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
|
|
12
|
+
export class EventNotifier extends EventEmitter {
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
super();
|
|
15
|
+
|
|
16
|
+
this.config = {
|
|
17
|
+
maxRetries: 3,
|
|
18
|
+
retryDelay: 1000,
|
|
19
|
+
timeout: 5000,
|
|
20
|
+
...config
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
this.webhooks = new Map();
|
|
24
|
+
this._notificationHistory = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register a webhook for event notifications
|
|
29
|
+
* @param {string} url - Webhook URL to send notifications to
|
|
30
|
+
* @param {string[]} events - Array of event names to subscribe to (empty = all events)
|
|
31
|
+
* @param {object} options - Additional options (headers, etc.)
|
|
32
|
+
*/
|
|
33
|
+
addWebhook(url, events = [], options = {}) {
|
|
34
|
+
this.webhooks.set(url, {
|
|
35
|
+
url,
|
|
36
|
+
events: events.length > 0 ? new Set(events) : null, // null = all events
|
|
37
|
+
headers: options.headers || {},
|
|
38
|
+
active: true,
|
|
39
|
+
failCount: 0,
|
|
40
|
+
lastNotified: null
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remove a registered webhook
|
|
46
|
+
* @param {string} url - Webhook URL to remove
|
|
47
|
+
*/
|
|
48
|
+
removeWebhook(url) {
|
|
49
|
+
this.webhooks.delete(url);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Send a notification for an event
|
|
54
|
+
* @param {string} event - Event name
|
|
55
|
+
* @param {object} data - Event data payload
|
|
56
|
+
*/
|
|
57
|
+
async notify(event, data = {}) {
|
|
58
|
+
const payload = {
|
|
59
|
+
event,
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
data
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const promises = [];
|
|
65
|
+
|
|
66
|
+
for (const [url, webhook] of this.webhooks) {
|
|
67
|
+
if (!webhook.active) continue;
|
|
68
|
+
|
|
69
|
+
// Check event filter
|
|
70
|
+
if (webhook.events && !webhook.events.has(event)) continue;
|
|
71
|
+
|
|
72
|
+
promises.push(this._sendNotification(url, webhook, payload));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (promises.length > 0) {
|
|
76
|
+
await Promise.allSettled(promises);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Record in history
|
|
80
|
+
this._notificationHistory.push({
|
|
81
|
+
event,
|
|
82
|
+
timestamp: payload.timestamp,
|
|
83
|
+
recipientCount: promises.length
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Keep history bounded
|
|
87
|
+
if (this._notificationHistory.length > 1000) {
|
|
88
|
+
this._notificationHistory = this._notificationHistory.slice(-500);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get list of registered webhooks
|
|
94
|
+
* @returns {Array} Webhook configurations
|
|
95
|
+
*/
|
|
96
|
+
getWebhooks() {
|
|
97
|
+
return Array.from(this.webhooks.values()).map(w => ({
|
|
98
|
+
url: w.url,
|
|
99
|
+
events: w.events ? Array.from(w.events) : ['*'],
|
|
100
|
+
active: w.active,
|
|
101
|
+
failCount: w.failCount,
|
|
102
|
+
lastNotified: w.lastNotified
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get notification history
|
|
108
|
+
* @param {number} limit - Max records to return
|
|
109
|
+
* @returns {Array}
|
|
110
|
+
*/
|
|
111
|
+
getHistory(limit = 50) {
|
|
112
|
+
return this._notificationHistory.slice(-limit);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Send notification to a single webhook with retry logic
|
|
117
|
+
* @param {string} url - Webhook URL
|
|
118
|
+
* @param {object} webhook - Webhook config
|
|
119
|
+
* @param {object} payload - Notification payload
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
async _sendNotification(url, webhook, payload) {
|
|
123
|
+
let lastError;
|
|
124
|
+
|
|
125
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
126
|
+
try {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
129
|
+
|
|
130
|
+
const response = await fetch(url, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
'User-Agent': 'KadiFileSharing/1.0',
|
|
135
|
+
...webhook.headers
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(payload),
|
|
138
|
+
signal: controller.signal
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
clearTimeout(timeoutId);
|
|
142
|
+
|
|
143
|
+
if (response.ok) {
|
|
144
|
+
webhook.failCount = 0;
|
|
145
|
+
webhook.lastNotified = new Date().toISOString();
|
|
146
|
+
this.emit('notification:sent', { url, event: payload.event });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
lastError = error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Wait before retry
|
|
156
|
+
if (attempt < this.config.maxRetries) {
|
|
157
|
+
await new Promise(resolve =>
|
|
158
|
+
setTimeout(resolve, this.config.retryDelay * (attempt + 1))
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// All retries failed
|
|
164
|
+
webhook.failCount++;
|
|
165
|
+
if (webhook.failCount >= 10) {
|
|
166
|
+
webhook.active = false;
|
|
167
|
+
this.emit('webhook:disabled', { url, reason: 'Too many failures' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.emit('notification:failed', {
|
|
171
|
+
url,
|
|
172
|
+
event: payload.event,
|
|
173
|
+
error: lastError?.message || 'Unknown error'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export default EventNotifier;
|