ziplayer 0.2.7-dev.1 → 0.2.7-dev.3
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 +17 -6
- package/dist/persistence/PersistenceManager.d.ts +50 -16
- package/dist/persistence/PersistenceManager.d.ts.map +1 -1
- package/dist/persistence/PersistenceManager.js +484 -60
- package/dist/persistence/PersistenceManager.js.map +1 -1
- package/dist/structures/Player.d.ts +2 -0
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +5 -0
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +3 -1
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +49 -31
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +26 -5
- package/dist/types/persistence.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/persistence/PersistenceManager.ts +570 -65
- package/src/structures/Player.ts +8 -0
- package/src/structures/PlayerManager.ts +66 -32
- package/src/types/index.ts +9 -0
- package/src/types/persistence.ts +37 -17
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PersistenceManager =
|
|
36
|
+
exports.PersistenceManager = void 0;
|
|
37
37
|
const events_1 = require("events");
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
@@ -41,11 +41,13 @@ const zlib = __importStar(require("zlib"));
|
|
|
41
41
|
const util_1 = require("util");
|
|
42
42
|
const gzip = (0, util_1.promisify)(zlib.gzip);
|
|
43
43
|
const gunzip = (0, util_1.promisify)(zlib.gunzip);
|
|
44
|
-
// File provider implementation
|
|
44
|
+
// File provider implementation with enhanced backup management
|
|
45
45
|
class FileProvider {
|
|
46
|
-
constructor(basePath,
|
|
46
|
+
constructor(basePath, options = {}) {
|
|
47
47
|
this.basePath = basePath;
|
|
48
|
-
this.maxBackups = maxBackups;
|
|
48
|
+
this.maxBackups = options.maxBackups ?? 5;
|
|
49
|
+
this.maxTotalBackups = options.maxTotalBackups ?? 50;
|
|
50
|
+
this.backupRetentionDays = options.backupRetentionDays ?? 7;
|
|
49
51
|
if (!fs.existsSync(basePath)) {
|
|
50
52
|
fs.mkdirSync(basePath, { recursive: true });
|
|
51
53
|
}
|
|
@@ -56,17 +58,142 @@ class FileProvider {
|
|
|
56
58
|
getBackupPath(key, timestamp) {
|
|
57
59
|
return path.join(this.basePath, `${key}_backup_${timestamp}.json`);
|
|
58
60
|
}
|
|
61
|
+
getAllBackups() {
|
|
62
|
+
const files = fs.readdirSync(this.basePath);
|
|
63
|
+
const backups = [];
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const match = file.match(/^(.+)_backup_(\d+)\.json(\.gz)?$/);
|
|
66
|
+
if (match) {
|
|
67
|
+
const [, key, timestampStr] = match;
|
|
68
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
69
|
+
const filePath = path.join(this.basePath, file);
|
|
70
|
+
const stats = fs.statSync(filePath);
|
|
71
|
+
const isCompressed = file.endsWith(".gz");
|
|
72
|
+
backups.push({
|
|
73
|
+
key,
|
|
74
|
+
path: filePath,
|
|
75
|
+
timestamp,
|
|
76
|
+
size: stats.size,
|
|
77
|
+
isCompressed,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return backups.sort((a, b) => b.timestamp - a.timestamp);
|
|
82
|
+
}
|
|
83
|
+
getBackupsByKey(key) {
|
|
84
|
+
return this.getAllBackups().filter((b) => b.key === key);
|
|
85
|
+
}
|
|
59
86
|
cleanOldBackups(key) {
|
|
60
|
-
const backups =
|
|
61
|
-
|
|
62
|
-
.filter((f) => f.startsWith(key) && f.includes("backup"))
|
|
63
|
-
.sort()
|
|
64
|
-
.reverse();
|
|
65
|
-
// Keep only maxBackups most recent
|
|
87
|
+
const backups = this.getBackupsByKey(key);
|
|
88
|
+
// Delete old backups exceeding maxBackups per player
|
|
66
89
|
for (let i = this.maxBackups; i < backups.length; i++) {
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
try {
|
|
91
|
+
fs.unlinkSync(backups[i].path);
|
|
92
|
+
console.log(`[Persistence] Deleted old backup: ${path.basename(backups[i].path)}`);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error(`[Persistence] Failed to delete backup: ${backups[i].path}`, err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
cleanOldBackupsByAge() {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const retentionMs = this.backupRetentionDays * 24 * 60 * 60 * 1000;
|
|
102
|
+
const backups = this.getAllBackups();
|
|
103
|
+
let deletedCount = 0;
|
|
104
|
+
for (const backup of backups) {
|
|
105
|
+
if (now - backup.timestamp > retentionMs) {
|
|
106
|
+
try {
|
|
107
|
+
fs.unlinkSync(backup.path);
|
|
108
|
+
deletedCount++;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.error(`[Persistence] Failed to delete old backup: ${backup.path}`, err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (deletedCount > 0) {
|
|
116
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups older than ${this.backupRetentionDays} days`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
cleanTotalBackupsLimit() {
|
|
120
|
+
let backups = this.getAllBackups();
|
|
121
|
+
if (backups.length <= this.maxTotalBackups)
|
|
122
|
+
return;
|
|
123
|
+
// Delete oldest backups
|
|
124
|
+
const toDelete = backups.slice(this.maxTotalBackups);
|
|
125
|
+
let deletedCount = 0;
|
|
126
|
+
for (const backup of toDelete) {
|
|
127
|
+
try {
|
|
128
|
+
fs.unlinkSync(backup.path);
|
|
129
|
+
deletedCount++;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (deletedCount > 0) {
|
|
136
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups (exceeded limit ${this.maxTotalBackups})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// NEW: Clean all backups for a specific player
|
|
140
|
+
async cleanAllBackupsForPlayer(key) {
|
|
141
|
+
const backups = this.getBackupsByKey(key);
|
|
142
|
+
let deletedCount = 0;
|
|
143
|
+
for (const backup of backups) {
|
|
144
|
+
try {
|
|
145
|
+
fs.unlinkSync(backup.path);
|
|
146
|
+
deletedCount++;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (deletedCount > 0) {
|
|
153
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups for player: ${key}`);
|
|
154
|
+
}
|
|
155
|
+
return deletedCount;
|
|
156
|
+
}
|
|
157
|
+
// NEW: Clean all backups
|
|
158
|
+
async cleanAllBackups() {
|
|
159
|
+
const backups = this.getAllBackups();
|
|
160
|
+
let deletedCount = 0;
|
|
161
|
+
for (const backup of backups) {
|
|
162
|
+
try {
|
|
163
|
+
fs.unlinkSync(backup.path);
|
|
164
|
+
deletedCount++;
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (deletedCount > 0) {
|
|
171
|
+
console.log(`[Persistence] Deleted all ${deletedCount} backups`);
|
|
172
|
+
}
|
|
173
|
+
return deletedCount;
|
|
174
|
+
}
|
|
175
|
+
// NEW: Get backup statistics
|
|
176
|
+
getBackupStats() {
|
|
177
|
+
const backups = this.getAllBackups();
|
|
178
|
+
let totalSize = 0;
|
|
179
|
+
let oldestBackup = null;
|
|
180
|
+
let newestBackup = null;
|
|
181
|
+
const backupsByPlayer = new Map();
|
|
182
|
+
for (const backup of backups) {
|
|
183
|
+
totalSize += backup.size;
|
|
184
|
+
if (oldestBackup === null || backup.timestamp < oldestBackup)
|
|
185
|
+
oldestBackup = backup.timestamp;
|
|
186
|
+
if (newestBackup === null || backup.timestamp > newestBackup)
|
|
187
|
+
newestBackup = backup.timestamp;
|
|
188
|
+
backupsByPlayer.set(backup.key, (backupsByPlayer.get(backup.key) || 0) + 1);
|
|
69
189
|
}
|
|
190
|
+
return {
|
|
191
|
+
totalBackups: backups.length,
|
|
192
|
+
totalSize,
|
|
193
|
+
oldestBackup,
|
|
194
|
+
newestBackup,
|
|
195
|
+
backupsByPlayer,
|
|
196
|
+
};
|
|
70
197
|
}
|
|
71
198
|
async save(key, data, compress = false) {
|
|
72
199
|
const filePath = this.getFilePath(key);
|
|
@@ -82,6 +209,8 @@ class FileProvider {
|
|
|
82
209
|
const backupPath = this.getBackupPath(key, Date.now());
|
|
83
210
|
fs.copyFileSync(filePath, backupPath);
|
|
84
211
|
this.cleanOldBackups(key);
|
|
212
|
+
this.cleanTotalBackupsLimit();
|
|
213
|
+
this.cleanOldBackupsByAge();
|
|
85
214
|
}
|
|
86
215
|
fs.writeFileSync(filePath, content);
|
|
87
216
|
}
|
|
@@ -107,10 +236,19 @@ class FileProvider {
|
|
|
107
236
|
fs.unlinkSync(filePath);
|
|
108
237
|
if (fs.existsSync(gzPath))
|
|
109
238
|
fs.unlinkSync(gzPath);
|
|
239
|
+
// Also delete backups for this player
|
|
240
|
+
await this.cleanAllBackupsForPlayer(key);
|
|
110
241
|
}
|
|
111
242
|
async list() {
|
|
112
243
|
const files = fs.readdirSync(this.basePath);
|
|
113
|
-
return files
|
|
244
|
+
return files
|
|
245
|
+
.filter((f) => {
|
|
246
|
+
// Exclude backup files
|
|
247
|
+
if (f.includes("_backup_"))
|
|
248
|
+
return false;
|
|
249
|
+
return f.endsWith(".json") || f.endsWith(".json.gz");
|
|
250
|
+
})
|
|
251
|
+
.map((f) => f.replace(/\.json(\.gz)?$/, ""));
|
|
114
252
|
}
|
|
115
253
|
async restoreBackup(key, backupTimestamp) {
|
|
116
254
|
let backupFile = null;
|
|
@@ -122,13 +260,9 @@ class FileProvider {
|
|
|
122
260
|
}
|
|
123
261
|
else {
|
|
124
262
|
// Get latest backup
|
|
125
|
-
const backups =
|
|
126
|
-
.readdirSync(this.basePath)
|
|
127
|
-
.filter((f) => f.startsWith(key) && f.includes("backup"))
|
|
128
|
-
.sort()
|
|
129
|
-
.reverse();
|
|
263
|
+
const backups = this.getBackupsByKey(key);
|
|
130
264
|
if (backups.length > 0) {
|
|
131
|
-
backupFile =
|
|
265
|
+
backupFile = backups[0].path;
|
|
132
266
|
}
|
|
133
267
|
}
|
|
134
268
|
if (backupFile && fs.existsSync(backupFile)) {
|
|
@@ -140,8 +274,7 @@ class FileProvider {
|
|
|
140
274
|
return false;
|
|
141
275
|
}
|
|
142
276
|
}
|
|
143
|
-
|
|
144
|
-
// Custom provider for database integration
|
|
277
|
+
// Custom provider for database integration (giữ nguyên)
|
|
145
278
|
class CustomProvider {
|
|
146
279
|
constructor(saveFn, loadFn, deleteFn, listFn) {
|
|
147
280
|
this.saveFn = saveFn;
|
|
@@ -172,18 +305,27 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
172
305
|
super();
|
|
173
306
|
this.saveInterval = null;
|
|
174
307
|
this.isSaving = false;
|
|
308
|
+
this.isRestoring = false;
|
|
309
|
+
this.destroyedPlayers = new Map();
|
|
310
|
+
this.restoredPlayers = new Set();
|
|
311
|
+
this.backupCleanupDone = false;
|
|
175
312
|
this.manager = manager;
|
|
176
|
-
//
|
|
313
|
+
// Default options
|
|
177
314
|
this.options = {
|
|
178
315
|
enabled: true,
|
|
179
316
|
provider: "file",
|
|
180
317
|
saveInterval: 60000,
|
|
181
318
|
autoLoad: true,
|
|
319
|
+
autoRestoreOnRestart: true,
|
|
320
|
+
restoreDelay: 5000,
|
|
182
321
|
maxBackups: 5,
|
|
322
|
+
maxTotalBackups: 10,
|
|
323
|
+
autoCleanupBackupsOnStart: true,
|
|
324
|
+
backupRetentionDays: 2,
|
|
183
325
|
compress: false,
|
|
184
326
|
filePath: "./players_data",
|
|
185
327
|
};
|
|
186
|
-
// Merge
|
|
328
|
+
// Merge manually
|
|
187
329
|
if (options.enabled !== undefined)
|
|
188
330
|
this.options.enabled = options.enabled;
|
|
189
331
|
if (options.provider !== undefined)
|
|
@@ -192,8 +334,18 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
192
334
|
this.options.saveInterval = options.saveInterval;
|
|
193
335
|
if (options.autoLoad !== undefined)
|
|
194
336
|
this.options.autoLoad = options.autoLoad;
|
|
337
|
+
if (options.autoRestoreOnRestart !== undefined)
|
|
338
|
+
this.options.autoRestoreOnRestart = options.autoRestoreOnRestart;
|
|
339
|
+
if (options.restoreDelay !== undefined)
|
|
340
|
+
this.options.restoreDelay = options.restoreDelay;
|
|
195
341
|
if (options.maxBackups !== undefined)
|
|
196
342
|
this.options.maxBackups = options.maxBackups;
|
|
343
|
+
if (options.maxTotalBackups !== undefined)
|
|
344
|
+
this.options.maxTotalBackups = options.maxTotalBackups;
|
|
345
|
+
if (options.autoCleanupBackupsOnStart !== undefined)
|
|
346
|
+
this.options.autoCleanupBackupsOnStart = options.autoCleanupBackupsOnStart;
|
|
347
|
+
if (options.backupRetentionDays !== undefined)
|
|
348
|
+
this.options.backupRetentionDays = options.backupRetentionDays;
|
|
197
349
|
if (options.compress !== undefined)
|
|
198
350
|
this.options.compress = options.compress;
|
|
199
351
|
if (options.filePath !== undefined)
|
|
@@ -210,7 +362,17 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
210
362
|
this.options.delete = options.delete;
|
|
211
363
|
if (options.list !== undefined)
|
|
212
364
|
this.options.list = options.list;
|
|
365
|
+
if (options.autoConnect !== undefined)
|
|
366
|
+
this.options.autoConnect = options.autoConnect;
|
|
213
367
|
this.provider = this.createProvider();
|
|
368
|
+
// Hook into player destroy events
|
|
369
|
+
this.setupDestroyTracking();
|
|
370
|
+
// Clean up old backups on start
|
|
371
|
+
if (this.options.enabled && this.options.autoCleanupBackupsOnStart) {
|
|
372
|
+
this.cleanupBackupsOnStart().catch((err) => {
|
|
373
|
+
this.debug("Backup cleanup on start error:", err);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
214
376
|
if (this.options.enabled) {
|
|
215
377
|
this.startAutoSave();
|
|
216
378
|
if (this.options.autoLoad) {
|
|
@@ -220,28 +382,32 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
220
382
|
}
|
|
221
383
|
}
|
|
222
384
|
}
|
|
385
|
+
setupDestroyTracking() {
|
|
386
|
+
this.manager.on("playerDestroy", (player) => {
|
|
387
|
+
this.markAsDestroyed(player.guildId);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
223
390
|
createProvider() {
|
|
224
391
|
switch (this.options.provider) {
|
|
225
392
|
case "file":
|
|
226
|
-
return new FileProvider(this.options.filePath,
|
|
393
|
+
return new FileProvider(this.options.filePath, {
|
|
394
|
+
maxBackups: this.options.maxBackups,
|
|
395
|
+
maxTotalBackups: this.options.maxTotalBackups,
|
|
396
|
+
backupRetentionDays: this.options.backupRetentionDays,
|
|
397
|
+
});
|
|
227
398
|
case "redis":
|
|
228
|
-
// Implement Redis provider if needed
|
|
229
399
|
throw new Error("Redis provider not implemented yet");
|
|
230
400
|
case "database":
|
|
231
401
|
if (!this.options.save || !this.options.load) {
|
|
232
402
|
throw new Error("Database provider requires save/load functions");
|
|
233
403
|
}
|
|
234
|
-
// Fix: Pass the save and load functions with correct signatures
|
|
235
404
|
return new CustomProvider(async (key, data) => {
|
|
236
405
|
if (this.options.save) {
|
|
237
|
-
// Call with single object argument if that's expected
|
|
238
406
|
const saveFn = this.options.save;
|
|
239
407
|
if (saveFn.length === 1) {
|
|
240
|
-
// Save function expects { key, data }
|
|
241
408
|
await saveFn({ key, data });
|
|
242
409
|
}
|
|
243
410
|
else {
|
|
244
|
-
// Save function expects (key, data)
|
|
245
411
|
await saveFn(key, data);
|
|
246
412
|
}
|
|
247
413
|
}
|
|
@@ -249,19 +415,21 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
249
415
|
if (this.options.load) {
|
|
250
416
|
const loadFn = this.options.load;
|
|
251
417
|
if (loadFn.length === 0) {
|
|
252
|
-
// Load function expects no args, returns all data
|
|
253
418
|
const allData = await loadFn();
|
|
254
419
|
return allData?.get?.(key) || allData?.[key] || null;
|
|
255
420
|
}
|
|
256
421
|
else {
|
|
257
|
-
// Load function expects key
|
|
258
422
|
return await loadFn(key);
|
|
259
423
|
}
|
|
260
424
|
}
|
|
261
425
|
return null;
|
|
262
426
|
}, this.options.delete, this.options.list);
|
|
263
427
|
default:
|
|
264
|
-
return new FileProvider(this.options.filePath,
|
|
428
|
+
return new FileProvider(this.options.filePath, {
|
|
429
|
+
maxBackups: this.options.maxBackups,
|
|
430
|
+
maxTotalBackups: this.options.maxTotalBackups,
|
|
431
|
+
backupRetentionDays: this.options.backupRetentionDays,
|
|
432
|
+
});
|
|
265
433
|
}
|
|
266
434
|
}
|
|
267
435
|
debug(message, ...params) {
|
|
@@ -281,8 +449,137 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
281
449
|
}, this.options.saveInterval);
|
|
282
450
|
this.debug(`Auto-save started (interval: ${this.options.saveInterval}ms)`);
|
|
283
451
|
}
|
|
452
|
+
// NEW: Cleanup backups on startup
|
|
453
|
+
async cleanupBackupsOnStart() {
|
|
454
|
+
if (this.backupCleanupDone)
|
|
455
|
+
return;
|
|
456
|
+
this.debug("Starting backup cleanup on startup...");
|
|
457
|
+
// Only works for file provider
|
|
458
|
+
if (this.provider instanceof FileProvider) {
|
|
459
|
+
try {
|
|
460
|
+
// Clean old backups by age
|
|
461
|
+
// This is already handled in FileProvider, but we can log stats
|
|
462
|
+
const stats = this.provider.getBackupStats();
|
|
463
|
+
this.debug(`Backup stats before cleanup:`, {
|
|
464
|
+
totalBackups: stats.totalBackups,
|
|
465
|
+
totalSizeMB: (stats.totalSize / 1024 / 1024).toFixed(2),
|
|
466
|
+
oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup).toISOString() : null,
|
|
467
|
+
newestBackup: stats.newestBackup ? new Date(stats.newestBackup).toISOString() : null,
|
|
468
|
+
backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
|
|
469
|
+
});
|
|
470
|
+
// Emit stats event
|
|
471
|
+
this.emit("backupStats", stats);
|
|
472
|
+
// The cleanup is already happening in FileProvider.save()
|
|
473
|
+
// But we can do a one-time deep cleanup on start
|
|
474
|
+
if (this.options.backupRetentionDays && this.options.backupRetentionDays > 0) {
|
|
475
|
+
// Force a cleanup pass
|
|
476
|
+
const deletedCount = await this.cleanOldBackupsByAge();
|
|
477
|
+
if (deletedCount > 0) {
|
|
478
|
+
this.debug(`Cleaned up ${deletedCount} old backups on startup`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Enforce total backup limit
|
|
482
|
+
const totalLimitDeleted = await this.enforceTotalBackupLimit();
|
|
483
|
+
if (totalLimitDeleted > 0) {
|
|
484
|
+
this.debug(`Enforced backup limit: deleted ${totalLimitDeleted} backups`);
|
|
485
|
+
}
|
|
486
|
+
this.backupCleanupDone = true;
|
|
487
|
+
this.emit("backupCleanupDone");
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
this.debug("Backup cleanup error:", error);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// NEW: Clean old backups by age
|
|
498
|
+
async cleanOldBackupsByAge() {
|
|
499
|
+
if (!(this.provider instanceof FileProvider))
|
|
500
|
+
return 0;
|
|
501
|
+
const retentionMs = (this.options.backupRetentionDays ?? 2) * 24 * 60 * 60 * 1000;
|
|
502
|
+
const now = Date.now();
|
|
503
|
+
const backups = this.provider.getAllBackups();
|
|
504
|
+
let deletedCount = 0;
|
|
505
|
+
for (const backup of backups) {
|
|
506
|
+
if (now - backup.timestamp > retentionMs) {
|
|
507
|
+
try {
|
|
508
|
+
fs.unlinkSync(backup.path);
|
|
509
|
+
deletedCount++;
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
this.debug(`Failed to delete old backup: ${backup.path}`, err);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (deletedCount > 0) {
|
|
517
|
+
this.debug(`Deleted ${deletedCount} backups older than ${this.options.backupRetentionDays} days`);
|
|
518
|
+
}
|
|
519
|
+
return deletedCount;
|
|
520
|
+
}
|
|
521
|
+
// NEW: Enforce total backup limit
|
|
522
|
+
async enforceTotalBackupLimit() {
|
|
523
|
+
if (!(this.provider instanceof FileProvider))
|
|
524
|
+
return 0;
|
|
525
|
+
const backups = this.provider.getAllBackups();
|
|
526
|
+
const maxTotal = this.options.maxTotalBackups ?? 50;
|
|
527
|
+
if (backups.length <= maxTotal)
|
|
528
|
+
return 0;
|
|
529
|
+
const toDelete = backups.slice(maxTotal);
|
|
530
|
+
let deletedCount = 0;
|
|
531
|
+
for (const backup of toDelete) {
|
|
532
|
+
try {
|
|
533
|
+
fs.unlinkSync(backup.path);
|
|
534
|
+
deletedCount++;
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
this.debug(`Failed to delete backup: ${backup.path}`, err);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (deletedCount > 0) {
|
|
541
|
+
this.debug(`Deleted ${deletedCount} backups (exceeded limit ${maxTotal})`);
|
|
542
|
+
}
|
|
543
|
+
return deletedCount;
|
|
544
|
+
}
|
|
545
|
+
markAsDestroyed(guildId) {
|
|
546
|
+
this.destroyedPlayers.set(guildId, {
|
|
547
|
+
guildId,
|
|
548
|
+
destroyedAt: Date.now(),
|
|
549
|
+
reason: "player_destroy",
|
|
550
|
+
});
|
|
551
|
+
this.debug(`Marked player as destroyed: ${guildId}`);
|
|
552
|
+
this.saveDestroyedStatus().catch((err) => {
|
|
553
|
+
this.debug("Failed to save destroyed status:", err);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
isDestroyed(guildId) {
|
|
557
|
+
return this.destroyedPlayers.has(guildId);
|
|
558
|
+
}
|
|
559
|
+
async saveDestroyedStatus() {
|
|
560
|
+
const destroyedData = Array.from(this.destroyedPlayers.values());
|
|
561
|
+
await this.provider.save("__destroyed_players__", destroyedData);
|
|
562
|
+
}
|
|
563
|
+
async loadDestroyedStatus() {
|
|
564
|
+
try {
|
|
565
|
+
const data = await this.provider.load("__destroyed_players__");
|
|
566
|
+
if (data && Array.isArray(data)) {
|
|
567
|
+
this.destroyedPlayers.clear();
|
|
568
|
+
for (const record of data) {
|
|
569
|
+
this.destroyedPlayers.set(record.guildId, record);
|
|
570
|
+
}
|
|
571
|
+
this.debug(`Loaded ${this.destroyedPlayers.size} destroyed player records`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
this.debug("Failed to load destroyed status:", error);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async clearDestroyedStatus(guildId) {
|
|
579
|
+
this.destroyedPlayers.delete(guildId);
|
|
580
|
+
await this.saveDestroyedStatus();
|
|
581
|
+
}
|
|
284
582
|
serializeTrack(track) {
|
|
285
|
-
// Create base object with required fields only (avoid duplication)
|
|
286
583
|
const serialized = {
|
|
287
584
|
id: track.id,
|
|
288
585
|
title: track.title,
|
|
@@ -293,13 +590,11 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
293
590
|
requestedBy: track.requestedBy,
|
|
294
591
|
isLive: track.isLive || false,
|
|
295
592
|
};
|
|
296
|
-
// Add optional fields if they exist on the track
|
|
297
593
|
const trackAny = track;
|
|
298
594
|
if (trackAny.author)
|
|
299
595
|
serialized.author = trackAny.author;
|
|
300
596
|
if (trackAny.artwork)
|
|
301
597
|
serialized.artwork = trackAny.artwork;
|
|
302
|
-
// Add any extra metadata (excluding fields we already set)
|
|
303
598
|
const excludedFields = new Set([
|
|
304
599
|
"id",
|
|
305
600
|
"title",
|
|
@@ -330,7 +625,6 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
330
625
|
};
|
|
331
626
|
}
|
|
332
627
|
serializePlayer(player) {
|
|
333
|
-
// Get filters safely - access through public method
|
|
334
628
|
let filters = [];
|
|
335
629
|
try {
|
|
336
630
|
const filterString = player.filter?.getFilterString();
|
|
@@ -338,9 +632,7 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
338
632
|
filters = filterString.split(",").filter(Boolean);
|
|
339
633
|
}
|
|
340
634
|
}
|
|
341
|
-
catch (e) {
|
|
342
|
-
// Filter may not be accessible
|
|
343
|
-
}
|
|
635
|
+
catch (e) { }
|
|
344
636
|
return {
|
|
345
637
|
guildId: player.guildId,
|
|
346
638
|
queue: this.serializeQueue(player),
|
|
@@ -351,10 +643,11 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
351
643
|
filters: filters.length > 0 ? filters : undefined,
|
|
352
644
|
lastUpdate: Date.now(),
|
|
353
645
|
version: "1.0.0",
|
|
646
|
+
wasDestroyed: false,
|
|
647
|
+
channelConnection: player.channelConnection,
|
|
354
648
|
};
|
|
355
649
|
}
|
|
356
650
|
deserializeTrack(data) {
|
|
357
|
-
// Create base track object
|
|
358
651
|
const track = {
|
|
359
652
|
id: data.id,
|
|
360
653
|
title: data.title,
|
|
@@ -365,12 +658,10 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
365
658
|
requestedBy: data.requestedBy,
|
|
366
659
|
isLive: data.isLive || false,
|
|
367
660
|
};
|
|
368
|
-
// Add optional fields if they exist
|
|
369
661
|
if (data.author)
|
|
370
662
|
track.author = data.author;
|
|
371
663
|
if (data.artwork)
|
|
372
664
|
track.artwork = data.artwork;
|
|
373
|
-
// Add any extra metadata from serialized data
|
|
374
665
|
for (const key of Object.keys(data)) {
|
|
375
666
|
if (!["id", "title", "url", "source", "duration", "thumbnail", "requestedBy", "isLive", "author", "artwork"].includes(key)) {
|
|
376
667
|
track[key] = data[key];
|
|
@@ -384,9 +675,13 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
384
675
|
async savePlayer(player) {
|
|
385
676
|
if (!this.options.enabled)
|
|
386
677
|
return false;
|
|
678
|
+
if (this.isDestroyed(player.guildId)) {
|
|
679
|
+
this.debug(`Skipping save for destroyed player: ${player.guildId}`);
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
387
682
|
try {
|
|
388
683
|
const data = this.serializePlayer(player);
|
|
389
|
-
await this.provider.save(player.guildId, data);
|
|
684
|
+
await this.provider.save(player.guildId, data, this.options.compress);
|
|
390
685
|
this.debug(`Saved player: ${player.guildId}`);
|
|
391
686
|
this.emit("playerSaved", player.guildId);
|
|
392
687
|
return true;
|
|
@@ -408,7 +703,6 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
408
703
|
try {
|
|
409
704
|
const players = this.manager.getAll();
|
|
410
705
|
this.debug(`Saving ${players.length} players...`);
|
|
411
|
-
// Save in parallel with limit
|
|
412
706
|
const batchSize = 5;
|
|
413
707
|
for (let i = 0; i < players.length; i += batchSize) {
|
|
414
708
|
const batch = players.slice(i, i + batchSize);
|
|
@@ -427,34 +721,51 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
427
721
|
/**
|
|
428
722
|
* Load a single player
|
|
429
723
|
*/
|
|
430
|
-
async loadPlayer(guildId, restorePosition = true) {
|
|
724
|
+
async loadPlayer(guildId, restorePosition = true, skipIfDestroyed = true) {
|
|
431
725
|
if (!this.options.enabled)
|
|
432
726
|
return false;
|
|
727
|
+
if (skipIfDestroyed && this.isDestroyed(guildId)) {
|
|
728
|
+
this.debug(`Skipping load for destroyed player: ${guildId}`);
|
|
729
|
+
this.emit("playerSkipped", guildId, "destroyed");
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
if (this.restoredPlayers.has(guildId)) {
|
|
733
|
+
this.debug(`Skipping already restored player: ${guildId}`);
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
433
736
|
try {
|
|
434
737
|
const data = await this.provider.load(guildId);
|
|
435
738
|
if (!data)
|
|
436
739
|
return false;
|
|
437
|
-
|
|
740
|
+
if (data.wasDestroyed === true) {
|
|
741
|
+
this.debug(`Skipping load for player marked as destroyed: ${guildId}`);
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
438
744
|
let player = this.manager.get(guildId);
|
|
439
745
|
if (!player) {
|
|
440
746
|
player = await this.manager.create(guildId, data.options);
|
|
441
747
|
}
|
|
442
|
-
//
|
|
748
|
+
// try connecting to the voice channel if we have one saved and autoConnect is enabled
|
|
749
|
+
if (data.options?.autoConnect && player.connection === null) {
|
|
750
|
+
try {
|
|
751
|
+
const voicechannel = await data.channelConnection;
|
|
752
|
+
await player.connect(voicechannel);
|
|
753
|
+
}
|
|
754
|
+
catch (err) {
|
|
755
|
+
this.debug(`Failed to auto-connect player ${guildId} on load:`, err);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
443
758
|
const queue = data.queue;
|
|
444
|
-
// Clear current queue
|
|
445
759
|
player.queue.clear();
|
|
446
760
|
player.queue.loop(queue.loopMode);
|
|
447
761
|
player.queue.autoPlay(queue.autoPlay);
|
|
448
|
-
// Restore tracks
|
|
449
762
|
if (queue.tracks.length > 0) {
|
|
450
763
|
const tracks = queue.tracks.map((t) => this.deserializeTrack(t));
|
|
451
764
|
player.queue.addMultiple(tracks);
|
|
452
765
|
}
|
|
453
|
-
// Restore current track if exists
|
|
454
766
|
if (queue.current && player.connection) {
|
|
455
767
|
const currentTrack = this.deserializeTrack(queue.current);
|
|
456
768
|
player.queue.willNextTrack(currentTrack);
|
|
457
|
-
// Restore playback position if requested
|
|
458
769
|
if (restorePosition && queue.position && queue.position > 0) {
|
|
459
770
|
await player.refreshPlayerResource(true, queue.position);
|
|
460
771
|
}
|
|
@@ -462,9 +773,7 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
462
773
|
await player.play(currentTrack);
|
|
463
774
|
}
|
|
464
775
|
}
|
|
465
|
-
// Restore volume
|
|
466
776
|
player.setVolume(data.volume);
|
|
467
|
-
// Restore filters - safely access through public method
|
|
468
777
|
if (data.filters && data.filters.length > 0) {
|
|
469
778
|
try {
|
|
470
779
|
const filterManager = player.filter;
|
|
@@ -476,6 +785,8 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
476
785
|
this.debug(`Failed to restore filters for ${guildId}:`, e);
|
|
477
786
|
}
|
|
478
787
|
}
|
|
788
|
+
this.restoredPlayers.add(guildId);
|
|
789
|
+
await this.clearDestroyedStatus(guildId);
|
|
479
790
|
this.debug(`Loaded player: ${guildId}`);
|
|
480
791
|
this.emit("playerLoaded", guildId, data);
|
|
481
792
|
return true;
|
|
@@ -487,20 +798,28 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
487
798
|
}
|
|
488
799
|
}
|
|
489
800
|
/**
|
|
490
|
-
* Load all saved players
|
|
801
|
+
* Load all saved players with auto-restore logic
|
|
491
802
|
*/
|
|
492
803
|
async loadAll(restorePosition = true) {
|
|
493
804
|
if (!this.options.enabled)
|
|
494
805
|
return new Map();
|
|
806
|
+
await this.loadDestroyedStatus();
|
|
495
807
|
const results = new Map();
|
|
496
808
|
try {
|
|
497
809
|
const keys = await this.provider.list();
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
810
|
+
const playerKeys = keys.filter((k) => k !== "__destroyed_players__");
|
|
811
|
+
this.debug(`Found ${playerKeys.length} saved players`);
|
|
812
|
+
if (this.options.autoRestoreOnRestart) {
|
|
813
|
+
this.debug(`Auto-restore enabled, restoring players after ${this.options.restoreDelay}ms delay...`);
|
|
814
|
+
if (this.options.restoreDelay && this.options.restoreDelay > 0) {
|
|
815
|
+
await new Promise((resolve) => setTimeout(resolve, this.options.restoreDelay));
|
|
816
|
+
}
|
|
817
|
+
for (const guildId of playerKeys) {
|
|
818
|
+
const success = await this.loadPlayer(guildId, restorePosition, true);
|
|
819
|
+
results.set(guildId, success);
|
|
820
|
+
}
|
|
821
|
+
this.emit("loadedAll", results);
|
|
502
822
|
}
|
|
503
|
-
this.emit("loadedAll", results);
|
|
504
823
|
}
|
|
505
824
|
catch (error) {
|
|
506
825
|
this.debug("Failed to load players:", error);
|
|
@@ -508,6 +827,49 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
508
827
|
}
|
|
509
828
|
return results;
|
|
510
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Mark a player as destroyed
|
|
832
|
+
*/
|
|
833
|
+
async markPlayerDestroyed(guildId, reason) {
|
|
834
|
+
this.destroyedPlayers.set(guildId, {
|
|
835
|
+
guildId,
|
|
836
|
+
destroyedAt: Date.now(),
|
|
837
|
+
reason: reason || "manual_destroy",
|
|
838
|
+
});
|
|
839
|
+
try {
|
|
840
|
+
const data = await this.provider.load(guildId);
|
|
841
|
+
if (data) {
|
|
842
|
+
data.wasDestroyed = true;
|
|
843
|
+
data.destroyedAt = Date.now();
|
|
844
|
+
await this.provider.save(guildId, data, this.options.compress);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
this.debug(`Failed to mark player data as destroyed: ${guildId}`, error);
|
|
849
|
+
}
|
|
850
|
+
await this.saveDestroyedStatus();
|
|
851
|
+
this.debug(`Marked player as destroyed (won't restore on restart): ${guildId}`);
|
|
852
|
+
this.emit("playerMarkedDestroyed", guildId);
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Clear destroyed status for a player
|
|
856
|
+
*/
|
|
857
|
+
async clearDestroyed(guildId) {
|
|
858
|
+
await this.clearDestroyedStatus(guildId);
|
|
859
|
+
try {
|
|
860
|
+
const data = await this.provider.load(guildId);
|
|
861
|
+
if (data) {
|
|
862
|
+
data.wasDestroyed = false;
|
|
863
|
+
delete data.destroyedAt;
|
|
864
|
+
await this.provider.save(guildId, data, this.options.compress);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
this.debug(`Failed to clear destroyed flag in data: ${guildId}`, error);
|
|
869
|
+
}
|
|
870
|
+
this.debug(`Cleared destroyed status for: ${guildId}`);
|
|
871
|
+
this.emit("playerDestroyedCleared", guildId);
|
|
872
|
+
}
|
|
511
873
|
/**
|
|
512
874
|
* Delete a player's saved data
|
|
513
875
|
*/
|
|
@@ -516,6 +878,8 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
516
878
|
return false;
|
|
517
879
|
try {
|
|
518
880
|
await this.provider.delete(guildId);
|
|
881
|
+
await this.clearDestroyedStatus(guildId);
|
|
882
|
+
this.restoredPlayers.delete(guildId);
|
|
519
883
|
this.debug(`Deleted saved data for: ${guildId}`);
|
|
520
884
|
this.emit("playerDeleted", guildId);
|
|
521
885
|
return true;
|
|
@@ -533,7 +897,54 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
533
897
|
this.debug("Restore from backup only supported for file provider");
|
|
534
898
|
return false;
|
|
535
899
|
}
|
|
536
|
-
|
|
900
|
+
const success = await this.provider.restoreBackup(guildId, timestamp);
|
|
901
|
+
if (success) {
|
|
902
|
+
await this.clearDestroyed(guildId);
|
|
903
|
+
}
|
|
904
|
+
return success;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Clean all backups for a specific player
|
|
908
|
+
*/
|
|
909
|
+
async cleanBackupsForPlayer(guildId) {
|
|
910
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
911
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
const deleted = await this.provider.cleanAllBackupsForPlayer(guildId);
|
|
915
|
+
this.debug(`Cleaned ${deleted} backups for player: ${guildId}`);
|
|
916
|
+
this.emit("backupsCleaned", guildId, deleted);
|
|
917
|
+
return deleted;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Clean all backups
|
|
921
|
+
*/
|
|
922
|
+
async cleanAllBackups() {
|
|
923
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
924
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
925
|
+
return 0;
|
|
926
|
+
}
|
|
927
|
+
const deleted = await this.provider.cleanAllBackups();
|
|
928
|
+
this.debug(`Cleaned all ${deleted} backups`);
|
|
929
|
+
this.emit("allBackupsCleaned", deleted);
|
|
930
|
+
return deleted;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Get backup statistics
|
|
934
|
+
*/
|
|
935
|
+
getBackupStats() {
|
|
936
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
937
|
+
this.debug("Backup stats only supported for file provider");
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
const stats = this.provider.getBackupStats();
|
|
941
|
+
return {
|
|
942
|
+
totalBackups: stats.totalBackups,
|
|
943
|
+
totalSizeMB: stats.totalSize / 1024 / 1024,
|
|
944
|
+
oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup) : null,
|
|
945
|
+
newestBackup: stats.newestBackup ? new Date(stats.newestBackup) : null,
|
|
946
|
+
backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
|
|
947
|
+
};
|
|
537
948
|
}
|
|
538
949
|
/**
|
|
539
950
|
* Stop auto-save and clean up
|
|
@@ -544,8 +955,21 @@ class PersistenceManager extends events_1.EventEmitter {
|
|
|
544
955
|
this.saveInterval = null;
|
|
545
956
|
}
|
|
546
957
|
await this.saveAll();
|
|
958
|
+
await this.saveDestroyedStatus();
|
|
547
959
|
this.debug("Persistence manager shut down");
|
|
548
960
|
}
|
|
961
|
+
/**
|
|
962
|
+
* Get list of destroyed players
|
|
963
|
+
*/
|
|
964
|
+
getDestroyedPlayers() {
|
|
965
|
+
return Array.from(this.destroyedPlayers.values());
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Check if auto-restore is enabled
|
|
969
|
+
*/
|
|
970
|
+
isAutoRestoreEnabled() {
|
|
971
|
+
return this.options.autoRestoreOnRestart === true;
|
|
972
|
+
}
|
|
549
973
|
}
|
|
550
974
|
exports.PersistenceManager = PersistenceManager;
|
|
551
975
|
//# sourceMappingURL=PersistenceManager.js.map
|