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
|
@@ -3,7 +3,15 @@ import * as fs from "fs";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as zlib from "zlib";
|
|
5
5
|
import { promisify } from "util";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
SerializedPlayer,
|
|
8
|
+
SerializedQueue,
|
|
9
|
+
SerializedTrack,
|
|
10
|
+
PersistenceOptions,
|
|
11
|
+
PersistenceProvider,
|
|
12
|
+
DestroyedRecord,
|
|
13
|
+
BackupInfo,
|
|
14
|
+
} from "../types";
|
|
7
15
|
import type { Player } from "../structures/Player";
|
|
8
16
|
import type { PlayerManager } from "../structures/PlayerManager";
|
|
9
17
|
import type { Track } from "../types";
|
|
@@ -11,14 +19,26 @@ import type { Track } from "../types";
|
|
|
11
19
|
const gzip = promisify(zlib.gzip);
|
|
12
20
|
const gunzip = promisify(zlib.gunzip);
|
|
13
21
|
|
|
14
|
-
// File provider implementation
|
|
15
|
-
|
|
22
|
+
// File provider implementation with enhanced backup management
|
|
23
|
+
class FileProvider implements PersistenceProvider {
|
|
16
24
|
private basePath: string;
|
|
17
25
|
private maxBackups: number;
|
|
26
|
+
private maxTotalBackups: number;
|
|
27
|
+
private backupRetentionDays: number;
|
|
18
28
|
|
|
19
|
-
constructor(
|
|
29
|
+
constructor(
|
|
30
|
+
basePath: string,
|
|
31
|
+
options: {
|
|
32
|
+
maxBackups?: number;
|
|
33
|
+
maxTotalBackups?: number;
|
|
34
|
+
backupRetentionDays?: number;
|
|
35
|
+
} = {},
|
|
36
|
+
) {
|
|
20
37
|
this.basePath = basePath;
|
|
21
|
-
this.maxBackups = maxBackups;
|
|
38
|
+
this.maxBackups = options.maxBackups ?? 5;
|
|
39
|
+
this.maxTotalBackups = options.maxTotalBackups ?? 50;
|
|
40
|
+
this.backupRetentionDays = options.backupRetentionDays ?? 7;
|
|
41
|
+
|
|
22
42
|
if (!fs.existsSync(basePath)) {
|
|
23
43
|
fs.mkdirSync(basePath, { recursive: true });
|
|
24
44
|
}
|
|
@@ -32,18 +52,165 @@ export class FileProvider implements PersistenceProvider {
|
|
|
32
52
|
return path.join(this.basePath, `${key}_backup_${timestamp}.json`);
|
|
33
53
|
}
|
|
34
54
|
|
|
55
|
+
private getAllBackups(): BackupInfo[] {
|
|
56
|
+
const files = fs.readdirSync(this.basePath);
|
|
57
|
+
const backups: BackupInfo[] = [];
|
|
58
|
+
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const match = file.match(/^(.+)_backup_(\d+)\.json(\.gz)?$/);
|
|
61
|
+
if (match) {
|
|
62
|
+
const [, key, timestampStr] = match;
|
|
63
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
64
|
+
const filePath = path.join(this.basePath, file);
|
|
65
|
+
const stats = fs.statSync(filePath);
|
|
66
|
+
const isCompressed = file.endsWith(".gz");
|
|
67
|
+
|
|
68
|
+
backups.push({
|
|
69
|
+
key,
|
|
70
|
+
path: filePath,
|
|
71
|
+
timestamp,
|
|
72
|
+
size: stats.size,
|
|
73
|
+
isCompressed,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return backups.sort((a, b) => b.timestamp - a.timestamp);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private getBackupsByKey(key: string): BackupInfo[] {
|
|
82
|
+
return this.getAllBackups().filter((b) => b.key === key);
|
|
83
|
+
}
|
|
84
|
+
|
|
35
85
|
private cleanOldBackups(key: string): void {
|
|
36
|
-
const backups =
|
|
37
|
-
.readdirSync(this.basePath)
|
|
38
|
-
.filter((f) => f.startsWith(key) && f.includes("backup"))
|
|
39
|
-
.sort()
|
|
40
|
-
.reverse();
|
|
86
|
+
const backups = this.getBackupsByKey(key);
|
|
41
87
|
|
|
42
|
-
//
|
|
88
|
+
// Delete old backups exceeding maxBackups per player
|
|
43
89
|
for (let i = this.maxBackups; i < backups.length; i++) {
|
|
44
|
-
|
|
45
|
-
|
|
90
|
+
try {
|
|
91
|
+
fs.unlinkSync(backups[i].path);
|
|
92
|
+
console.log(`[Persistence] Deleted old backup: ${path.basename(backups[i].path)}`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`[Persistence] Failed to delete backup: ${backups[i].path}`, err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private cleanOldBackupsByAge(): void {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const retentionMs = this.backupRetentionDays * 24 * 60 * 60 * 1000;
|
|
102
|
+
const backups = this.getAllBackups();
|
|
103
|
+
|
|
104
|
+
let deletedCount = 0;
|
|
105
|
+
for (const backup of backups) {
|
|
106
|
+
if (now - backup.timestamp > retentionMs) {
|
|
107
|
+
try {
|
|
108
|
+
fs.unlinkSync(backup.path);
|
|
109
|
+
deletedCount++;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`[Persistence] Failed to delete old backup: ${backup.path}`, err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (deletedCount > 0) {
|
|
117
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups older than ${this.backupRetentionDays} days`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private cleanTotalBackupsLimit(): void {
|
|
122
|
+
let backups = this.getAllBackups();
|
|
123
|
+
|
|
124
|
+
if (backups.length <= this.maxTotalBackups) return;
|
|
125
|
+
|
|
126
|
+
// Delete oldest backups
|
|
127
|
+
const toDelete = backups.slice(this.maxTotalBackups);
|
|
128
|
+
let deletedCount = 0;
|
|
129
|
+
|
|
130
|
+
for (const backup of toDelete) {
|
|
131
|
+
try {
|
|
132
|
+
fs.unlinkSync(backup.path);
|
|
133
|
+
deletedCount++;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (deletedCount > 0) {
|
|
140
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups (exceeded limit ${this.maxTotalBackups})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// NEW: Clean all backups for a specific player
|
|
145
|
+
async cleanAllBackupsForPlayer(key: string): Promise<number> {
|
|
146
|
+
const backups = this.getBackupsByKey(key);
|
|
147
|
+
let deletedCount = 0;
|
|
148
|
+
|
|
149
|
+
for (const backup of backups) {
|
|
150
|
+
try {
|
|
151
|
+
fs.unlinkSync(backup.path);
|
|
152
|
+
deletedCount++;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (deletedCount > 0) {
|
|
159
|
+
console.log(`[Persistence] Deleted ${deletedCount} backups for player: ${key}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return deletedCount;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// NEW: Clean all backups
|
|
166
|
+
async cleanAllBackups(): Promise<number> {
|
|
167
|
+
const backups = this.getAllBackups();
|
|
168
|
+
let deletedCount = 0;
|
|
169
|
+
|
|
170
|
+
for (const backup of backups) {
|
|
171
|
+
try {
|
|
172
|
+
fs.unlinkSync(backup.path);
|
|
173
|
+
deletedCount++;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (deletedCount > 0) {
|
|
180
|
+
console.log(`[Persistence] Deleted all ${deletedCount} backups`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return deletedCount;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// NEW: Get backup statistics
|
|
187
|
+
getBackupStats(): {
|
|
188
|
+
totalBackups: number;
|
|
189
|
+
totalSize: number;
|
|
190
|
+
oldestBackup: number | null;
|
|
191
|
+
newestBackup: number | null;
|
|
192
|
+
backupsByPlayer: Map<string, number>;
|
|
193
|
+
} {
|
|
194
|
+
const backups = this.getAllBackups();
|
|
195
|
+
let totalSize = 0;
|
|
196
|
+
let oldestBackup: number | null = null;
|
|
197
|
+
let newestBackup: number | null = null;
|
|
198
|
+
const backupsByPlayer = new Map<string, number>();
|
|
199
|
+
|
|
200
|
+
for (const backup of backups) {
|
|
201
|
+
totalSize += backup.size;
|
|
202
|
+
if (oldestBackup === null || backup.timestamp < oldestBackup) oldestBackup = backup.timestamp;
|
|
203
|
+
if (newestBackup === null || backup.timestamp > newestBackup) newestBackup = backup.timestamp;
|
|
204
|
+
backupsByPlayer.set(backup.key, (backupsByPlayer.get(backup.key) || 0) + 1);
|
|
46
205
|
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
totalBackups: backups.length,
|
|
209
|
+
totalSize,
|
|
210
|
+
oldestBackup,
|
|
211
|
+
newestBackup,
|
|
212
|
+
backupsByPlayer,
|
|
213
|
+
};
|
|
47
214
|
}
|
|
48
215
|
|
|
49
216
|
async save(key: string, data: any, compress: boolean = false): Promise<void> {
|
|
@@ -62,6 +229,8 @@ export class FileProvider implements PersistenceProvider {
|
|
|
62
229
|
const backupPath = this.getBackupPath(key, Date.now());
|
|
63
230
|
fs.copyFileSync(filePath, backupPath);
|
|
64
231
|
this.cleanOldBackups(key);
|
|
232
|
+
this.cleanTotalBackupsLimit();
|
|
233
|
+
this.cleanOldBackupsByAge();
|
|
65
234
|
}
|
|
66
235
|
|
|
67
236
|
fs.writeFileSync(filePath, content);
|
|
@@ -92,11 +261,20 @@ export class FileProvider implements PersistenceProvider {
|
|
|
92
261
|
|
|
93
262
|
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
94
263
|
if (fs.existsSync(gzPath)) fs.unlinkSync(gzPath);
|
|
264
|
+
|
|
265
|
+
// Also delete backups for this player
|
|
266
|
+
await this.cleanAllBackupsForPlayer(key);
|
|
95
267
|
}
|
|
96
268
|
|
|
97
269
|
async list(): Promise<string[]> {
|
|
98
270
|
const files = fs.readdirSync(this.basePath);
|
|
99
|
-
return files
|
|
271
|
+
return files
|
|
272
|
+
.filter((f) => {
|
|
273
|
+
// Exclude backup files
|
|
274
|
+
if (f.includes("_backup_")) return false;
|
|
275
|
+
return f.endsWith(".json") || f.endsWith(".json.gz");
|
|
276
|
+
})
|
|
277
|
+
.map((f) => f.replace(/\.json(\.gz)?$/, ""));
|
|
100
278
|
}
|
|
101
279
|
|
|
102
280
|
async restoreBackup(key: string, backupTimestamp?: number): Promise<boolean> {
|
|
@@ -109,13 +287,9 @@ export class FileProvider implements PersistenceProvider {
|
|
|
109
287
|
}
|
|
110
288
|
} else {
|
|
111
289
|
// Get latest backup
|
|
112
|
-
const backups =
|
|
113
|
-
.readdirSync(this.basePath)
|
|
114
|
-
.filter((f) => f.startsWith(key) && f.includes("backup"))
|
|
115
|
-
.sort()
|
|
116
|
-
.reverse();
|
|
290
|
+
const backups = this.getBackupsByKey(key);
|
|
117
291
|
if (backups.length > 0) {
|
|
118
|
-
backupFile =
|
|
292
|
+
backupFile = backups[0].path;
|
|
119
293
|
}
|
|
120
294
|
}
|
|
121
295
|
|
|
@@ -130,7 +304,7 @@ export class FileProvider implements PersistenceProvider {
|
|
|
130
304
|
}
|
|
131
305
|
}
|
|
132
306
|
|
|
133
|
-
// Custom provider for database integration
|
|
307
|
+
// Custom provider for database integration (giữ nguyên)
|
|
134
308
|
class CustomProvider implements PersistenceProvider {
|
|
135
309
|
constructor(
|
|
136
310
|
private saveFn: (key: string, data: any) => Promise<void>,
|
|
@@ -164,30 +338,46 @@ class CustomProvider implements PersistenceProvider {
|
|
|
164
338
|
export class PersistenceManager extends EventEmitter {
|
|
165
339
|
private manager: PlayerManager;
|
|
166
340
|
private options: PersistenceOptions;
|
|
167
|
-
private provider:
|
|
341
|
+
private provider: FileProvider | CustomProvider;
|
|
168
342
|
private saveInterval: NodeJS.Timeout | null = null;
|
|
169
343
|
private isSaving: boolean = false;
|
|
344
|
+
private isRestoring: boolean = false;
|
|
345
|
+
private destroyedPlayers: Map<string, DestroyedRecord> = new Map();
|
|
346
|
+
private restoredPlayers: Set<string> = new Set();
|
|
347
|
+
private backupCleanupDone: boolean = false;
|
|
170
348
|
|
|
171
349
|
constructor(manager: PlayerManager, options: PersistenceOptions) {
|
|
172
350
|
super();
|
|
173
351
|
this.manager = manager;
|
|
174
|
-
|
|
352
|
+
|
|
353
|
+
// Default options
|
|
175
354
|
this.options = {
|
|
176
355
|
enabled: true,
|
|
177
356
|
provider: "file",
|
|
178
357
|
saveInterval: 60000,
|
|
179
358
|
autoLoad: true,
|
|
359
|
+
autoRestoreOnRestart: true,
|
|
360
|
+
restoreDelay: 5000,
|
|
180
361
|
maxBackups: 5,
|
|
362
|
+
maxTotalBackups: 10,
|
|
363
|
+
autoCleanupBackupsOnStart: true,
|
|
364
|
+
backupRetentionDays: 2,
|
|
181
365
|
compress: false,
|
|
182
366
|
filePath: "./players_data",
|
|
183
367
|
};
|
|
184
368
|
|
|
185
|
-
// Merge
|
|
369
|
+
// Merge manually
|
|
186
370
|
if (options.enabled !== undefined) this.options.enabled = options.enabled;
|
|
187
371
|
if (options.provider !== undefined) this.options.provider = options.provider;
|
|
188
372
|
if (options.saveInterval !== undefined) this.options.saveInterval = options.saveInterval;
|
|
189
373
|
if (options.autoLoad !== undefined) this.options.autoLoad = options.autoLoad;
|
|
374
|
+
if (options.autoRestoreOnRestart !== undefined) this.options.autoRestoreOnRestart = options.autoRestoreOnRestart;
|
|
375
|
+
if (options.restoreDelay !== undefined) this.options.restoreDelay = options.restoreDelay;
|
|
190
376
|
if (options.maxBackups !== undefined) this.options.maxBackups = options.maxBackups;
|
|
377
|
+
if (options.maxTotalBackups !== undefined) this.options.maxTotalBackups = options.maxTotalBackups;
|
|
378
|
+
if (options.autoCleanupBackupsOnStart !== undefined)
|
|
379
|
+
this.options.autoCleanupBackupsOnStart = options.autoCleanupBackupsOnStart;
|
|
380
|
+
if (options.backupRetentionDays !== undefined) this.options.backupRetentionDays = options.backupRetentionDays;
|
|
191
381
|
if (options.compress !== undefined) this.options.compress = options.compress;
|
|
192
382
|
if (options.filePath !== undefined) this.options.filePath = options.filePath;
|
|
193
383
|
if (options.redisUrl !== undefined) this.options.redisUrl = options.redisUrl;
|
|
@@ -196,9 +386,20 @@ export class PersistenceManager extends EventEmitter {
|
|
|
196
386
|
if (options.load !== undefined) this.options.load = options.load;
|
|
197
387
|
if (options.delete !== undefined) this.options.delete = options.delete;
|
|
198
388
|
if (options.list !== undefined) this.options.list = options.list;
|
|
389
|
+
if (options.autoConnect !== undefined) this.options.autoConnect = options.autoConnect;
|
|
199
390
|
|
|
200
391
|
this.provider = this.createProvider();
|
|
201
392
|
|
|
393
|
+
// Hook into player destroy events
|
|
394
|
+
this.setupDestroyTracking();
|
|
395
|
+
|
|
396
|
+
// Clean up old backups on start
|
|
397
|
+
if (this.options.enabled && this.options.autoCleanupBackupsOnStart) {
|
|
398
|
+
this.cleanupBackupsOnStart().catch((err) => {
|
|
399
|
+
this.debug("Backup cleanup on start error:", err);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
202
403
|
if (this.options.enabled) {
|
|
203
404
|
this.startAutoSave();
|
|
204
405
|
|
|
@@ -210,28 +411,33 @@ export class PersistenceManager extends EventEmitter {
|
|
|
210
411
|
}
|
|
211
412
|
}
|
|
212
413
|
|
|
213
|
-
private
|
|
414
|
+
private setupDestroyTracking(): void {
|
|
415
|
+
this.manager.on("playerDestroy", (player: Player) => {
|
|
416
|
+
this.markAsDestroyed(player.guildId);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private createProvider(): FileProvider | CustomProvider {
|
|
214
421
|
switch (this.options.provider) {
|
|
215
422
|
case "file":
|
|
216
|
-
return new FileProvider(this.options.filePath!,
|
|
423
|
+
return new FileProvider(this.options.filePath!, {
|
|
424
|
+
maxBackups: this.options.maxBackups,
|
|
425
|
+
maxTotalBackups: this.options.maxTotalBackups,
|
|
426
|
+
backupRetentionDays: this.options.backupRetentionDays,
|
|
427
|
+
});
|
|
217
428
|
case "redis":
|
|
218
|
-
// Implement Redis provider if needed
|
|
219
429
|
throw new Error("Redis provider not implemented yet");
|
|
220
430
|
case "database":
|
|
221
431
|
if (!this.options.save || !this.options.load) {
|
|
222
432
|
throw new Error("Database provider requires save/load functions");
|
|
223
433
|
}
|
|
224
|
-
// Fix: Pass the save and load functions with correct signatures
|
|
225
434
|
return new CustomProvider(
|
|
226
435
|
async (key: string, data: any) => {
|
|
227
436
|
if (this.options.save) {
|
|
228
|
-
// Call with single object argument if that's expected
|
|
229
437
|
const saveFn = this.options.save as any;
|
|
230
438
|
if (saveFn.length === 1) {
|
|
231
|
-
// Save function expects { key, data }
|
|
232
439
|
await saveFn({ key, data });
|
|
233
440
|
} else {
|
|
234
|
-
// Save function expects (key, data)
|
|
235
441
|
await saveFn(key, data);
|
|
236
442
|
}
|
|
237
443
|
}
|
|
@@ -240,11 +446,9 @@ export class PersistenceManager extends EventEmitter {
|
|
|
240
446
|
if (this.options.load) {
|
|
241
447
|
const loadFn = this.options.load as any;
|
|
242
448
|
if (loadFn.length === 0) {
|
|
243
|
-
// Load function expects no args, returns all data
|
|
244
449
|
const allData = await loadFn();
|
|
245
450
|
return allData?.get?.(key) || allData?.[key] || null;
|
|
246
451
|
} else {
|
|
247
|
-
// Load function expects key
|
|
248
452
|
return await loadFn(key);
|
|
249
453
|
}
|
|
250
454
|
}
|
|
@@ -254,7 +458,11 @@ export class PersistenceManager extends EventEmitter {
|
|
|
254
458
|
this.options.list,
|
|
255
459
|
);
|
|
256
460
|
default:
|
|
257
|
-
return new FileProvider(this.options.filePath!,
|
|
461
|
+
return new FileProvider(this.options.filePath!, {
|
|
462
|
+
maxBackups: this.options.maxBackups,
|
|
463
|
+
maxTotalBackups: this.options.maxTotalBackups,
|
|
464
|
+
backupRetentionDays: this.options.backupRetentionDays,
|
|
465
|
+
});
|
|
258
466
|
}
|
|
259
467
|
}
|
|
260
468
|
|
|
@@ -279,8 +487,154 @@ export class PersistenceManager extends EventEmitter {
|
|
|
279
487
|
this.debug(`Auto-save started (interval: ${this.options.saveInterval}ms)`);
|
|
280
488
|
}
|
|
281
489
|
|
|
490
|
+
// NEW: Cleanup backups on startup
|
|
491
|
+
private async cleanupBackupsOnStart(): Promise<void> {
|
|
492
|
+
if (this.backupCleanupDone) return;
|
|
493
|
+
|
|
494
|
+
this.debug("Starting backup cleanup on startup...");
|
|
495
|
+
|
|
496
|
+
// Only works for file provider
|
|
497
|
+
if (this.provider instanceof FileProvider) {
|
|
498
|
+
try {
|
|
499
|
+
// Clean old backups by age
|
|
500
|
+
// This is already handled in FileProvider, but we can log stats
|
|
501
|
+
const stats = this.provider.getBackupStats();
|
|
502
|
+
|
|
503
|
+
this.debug(`Backup stats before cleanup:`, {
|
|
504
|
+
totalBackups: stats.totalBackups,
|
|
505
|
+
totalSizeMB: (stats.totalSize / 1024 / 1024).toFixed(2),
|
|
506
|
+
oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup).toISOString() : null,
|
|
507
|
+
newestBackup: stats.newestBackup ? new Date(stats.newestBackup).toISOString() : null,
|
|
508
|
+
backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Emit stats event
|
|
512
|
+
this.emit("backupStats", stats);
|
|
513
|
+
|
|
514
|
+
// The cleanup is already happening in FileProvider.save()
|
|
515
|
+
// But we can do a one-time deep cleanup on start
|
|
516
|
+
if (this.options.backupRetentionDays && this.options.backupRetentionDays > 0) {
|
|
517
|
+
// Force a cleanup pass
|
|
518
|
+
const deletedCount = await this.cleanOldBackupsByAge();
|
|
519
|
+
if (deletedCount > 0) {
|
|
520
|
+
this.debug(`Cleaned up ${deletedCount} old backups on startup`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Enforce total backup limit
|
|
525
|
+
const totalLimitDeleted = await this.enforceTotalBackupLimit();
|
|
526
|
+
if (totalLimitDeleted > 0) {
|
|
527
|
+
this.debug(`Enforced backup limit: deleted ${totalLimitDeleted} backups`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.backupCleanupDone = true;
|
|
531
|
+
this.emit("backupCleanupDone");
|
|
532
|
+
} catch (error) {
|
|
533
|
+
this.debug("Backup cleanup error:", error);
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// NEW: Clean old backups by age
|
|
541
|
+
private async cleanOldBackupsByAge(): Promise<number> {
|
|
542
|
+
if (!(this.provider instanceof FileProvider)) return 0;
|
|
543
|
+
|
|
544
|
+
const retentionMs = (this.options.backupRetentionDays ?? 2) * 24 * 60 * 60 * 1000;
|
|
545
|
+
const now = Date.now();
|
|
546
|
+
const backups = (this.provider as any).getAllBackups();
|
|
547
|
+
let deletedCount = 0;
|
|
548
|
+
|
|
549
|
+
for (const backup of backups) {
|
|
550
|
+
if (now - backup.timestamp > retentionMs) {
|
|
551
|
+
try {
|
|
552
|
+
fs.unlinkSync(backup.path);
|
|
553
|
+
deletedCount++;
|
|
554
|
+
} catch (err) {
|
|
555
|
+
this.debug(`Failed to delete old backup: ${backup.path}`, err);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (deletedCount > 0) {
|
|
561
|
+
this.debug(`Deleted ${deletedCount} backups older than ${this.options.backupRetentionDays} days`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return deletedCount;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// NEW: Enforce total backup limit
|
|
568
|
+
private async enforceTotalBackupLimit(): Promise<number> {
|
|
569
|
+
if (!(this.provider instanceof FileProvider)) return 0;
|
|
570
|
+
|
|
571
|
+
const backups = (this.provider as any).getAllBackups();
|
|
572
|
+
const maxTotal = this.options.maxTotalBackups ?? 50;
|
|
573
|
+
|
|
574
|
+
if (backups.length <= maxTotal) return 0;
|
|
575
|
+
|
|
576
|
+
const toDelete = backups.slice(maxTotal);
|
|
577
|
+
let deletedCount = 0;
|
|
578
|
+
|
|
579
|
+
for (const backup of toDelete) {
|
|
580
|
+
try {
|
|
581
|
+
fs.unlinkSync(backup.path);
|
|
582
|
+
deletedCount++;
|
|
583
|
+
} catch (err) {
|
|
584
|
+
this.debug(`Failed to delete backup: ${backup.path}`, err);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (deletedCount > 0) {
|
|
589
|
+
this.debug(`Deleted ${deletedCount} backups (exceeded limit ${maxTotal})`);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return deletedCount;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private markAsDestroyed(guildId: string): void {
|
|
596
|
+
this.destroyedPlayers.set(guildId, {
|
|
597
|
+
guildId,
|
|
598
|
+
destroyedAt: Date.now(),
|
|
599
|
+
reason: "player_destroy",
|
|
600
|
+
});
|
|
601
|
+
this.debug(`Marked player as destroyed: ${guildId}`);
|
|
602
|
+
|
|
603
|
+
this.saveDestroyedStatus().catch((err) => {
|
|
604
|
+
this.debug("Failed to save destroyed status:", err);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private isDestroyed(guildId: string): boolean {
|
|
609
|
+
return this.destroyedPlayers.has(guildId);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private async saveDestroyedStatus(): Promise<void> {
|
|
613
|
+
const destroyedData = Array.from(this.destroyedPlayers.values());
|
|
614
|
+
await this.provider.save("__destroyed_players__", destroyedData);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private async loadDestroyedStatus(): Promise<void> {
|
|
618
|
+
try {
|
|
619
|
+
const data = await this.provider.load("__destroyed_players__");
|
|
620
|
+
if (data && Array.isArray(data)) {
|
|
621
|
+
this.destroyedPlayers.clear();
|
|
622
|
+
for (const record of data) {
|
|
623
|
+
this.destroyedPlayers.set(record.guildId, record);
|
|
624
|
+
}
|
|
625
|
+
this.debug(`Loaded ${this.destroyedPlayers.size} destroyed player records`);
|
|
626
|
+
}
|
|
627
|
+
} catch (error) {
|
|
628
|
+
this.debug("Failed to load destroyed status:", error);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private async clearDestroyedStatus(guildId: string): Promise<void> {
|
|
633
|
+
this.destroyedPlayers.delete(guildId);
|
|
634
|
+
await this.saveDestroyedStatus();
|
|
635
|
+
}
|
|
636
|
+
|
|
282
637
|
private serializeTrack(track: Track): SerializedTrack {
|
|
283
|
-
// Create base object with required fields only (avoid duplication)
|
|
284
638
|
const serialized: SerializedTrack = {
|
|
285
639
|
id: track.id,
|
|
286
640
|
title: track.title,
|
|
@@ -292,12 +646,10 @@ export class PersistenceManager extends EventEmitter {
|
|
|
292
646
|
isLive: track.isLive || false,
|
|
293
647
|
};
|
|
294
648
|
|
|
295
|
-
// Add optional fields if they exist on the track
|
|
296
649
|
const trackAny = track as any;
|
|
297
650
|
if (trackAny.author) serialized.author = trackAny.author;
|
|
298
651
|
if (trackAny.artwork) serialized.artwork = trackAny.artwork;
|
|
299
652
|
|
|
300
|
-
// Add any extra metadata (excluding fields we already set)
|
|
301
653
|
const excludedFields = new Set([
|
|
302
654
|
"id",
|
|
303
655
|
"title",
|
|
@@ -331,17 +683,13 @@ export class PersistenceManager extends EventEmitter {
|
|
|
331
683
|
}
|
|
332
684
|
|
|
333
685
|
private serializePlayer(player: Player): SerializedPlayer {
|
|
334
|
-
// Get filters safely - access through public method
|
|
335
686
|
let filters: string[] = [];
|
|
336
687
|
try {
|
|
337
688
|
const filterString = (player as any).filter?.getFilterString();
|
|
338
689
|
if (filterString) {
|
|
339
690
|
filters = filterString.split(",").filter(Boolean);
|
|
340
691
|
}
|
|
341
|
-
} catch (e) {
|
|
342
|
-
// Filter may not be accessible
|
|
343
|
-
}
|
|
344
|
-
|
|
692
|
+
} catch (e) {}
|
|
345
693
|
return {
|
|
346
694
|
guildId: player.guildId,
|
|
347
695
|
queue: this.serializeQueue(player),
|
|
@@ -352,11 +700,12 @@ export class PersistenceManager extends EventEmitter {
|
|
|
352
700
|
filters: filters.length > 0 ? filters : undefined,
|
|
353
701
|
lastUpdate: Date.now(),
|
|
354
702
|
version: "1.0.0",
|
|
703
|
+
wasDestroyed: false,
|
|
704
|
+
channelConnection: player.channelConnection,
|
|
355
705
|
};
|
|
356
706
|
}
|
|
357
707
|
|
|
358
708
|
private deserializeTrack(data: SerializedTrack): Track {
|
|
359
|
-
// Create base track object
|
|
360
709
|
const track: any = {
|
|
361
710
|
id: data.id,
|
|
362
711
|
title: data.title,
|
|
@@ -368,11 +717,9 @@ export class PersistenceManager extends EventEmitter {
|
|
|
368
717
|
isLive: data.isLive || false,
|
|
369
718
|
};
|
|
370
719
|
|
|
371
|
-
// Add optional fields if they exist
|
|
372
720
|
if (data.author) track.author = data.author;
|
|
373
721
|
if (data.artwork) track.artwork = data.artwork;
|
|
374
722
|
|
|
375
|
-
// Add any extra metadata from serialized data
|
|
376
723
|
for (const key of Object.keys(data)) {
|
|
377
724
|
if (
|
|
378
725
|
!["id", "title", "url", "source", "duration", "thumbnail", "requestedBy", "isLive", "author", "artwork"].includes(key)
|
|
@@ -390,9 +737,14 @@ export class PersistenceManager extends EventEmitter {
|
|
|
390
737
|
async savePlayer(player: Player): Promise<boolean> {
|
|
391
738
|
if (!this.options.enabled) return false;
|
|
392
739
|
|
|
740
|
+
if (this.isDestroyed(player.guildId)) {
|
|
741
|
+
this.debug(`Skipping save for destroyed player: ${player.guildId}`);
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
|
|
393
745
|
try {
|
|
394
746
|
const data = this.serializePlayer(player);
|
|
395
|
-
await this.provider.save(player.guildId, data);
|
|
747
|
+
await this.provider.save(player.guildId, data, this.options.compress);
|
|
396
748
|
this.debug(`Saved player: ${player.guildId}`);
|
|
397
749
|
this.emit("playerSaved", player.guildId);
|
|
398
750
|
return true;
|
|
@@ -416,7 +768,6 @@ export class PersistenceManager extends EventEmitter {
|
|
|
416
768
|
const players = this.manager.getAll();
|
|
417
769
|
this.debug(`Saving ${players.length} players...`);
|
|
418
770
|
|
|
419
|
-
// Save in parallel with limit
|
|
420
771
|
const batchSize = 5;
|
|
421
772
|
for (let i = 0; i < players.length; i += batchSize) {
|
|
422
773
|
const batch = players.slice(i, i + batchSize);
|
|
@@ -437,39 +788,58 @@ export class PersistenceManager extends EventEmitter {
|
|
|
437
788
|
/**
|
|
438
789
|
* Load a single player
|
|
439
790
|
*/
|
|
440
|
-
async loadPlayer(guildId: string, restorePosition: boolean = true): Promise<boolean> {
|
|
791
|
+
async loadPlayer(guildId: string, restorePosition: boolean = true, skipIfDestroyed: boolean = true): Promise<boolean> {
|
|
441
792
|
if (!this.options.enabled) return false;
|
|
442
793
|
|
|
794
|
+
if (skipIfDestroyed && this.isDestroyed(guildId)) {
|
|
795
|
+
this.debug(`Skipping load for destroyed player: ${guildId}`);
|
|
796
|
+
this.emit("playerSkipped", guildId, "destroyed");
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (this.restoredPlayers.has(guildId)) {
|
|
801
|
+
this.debug(`Skipping already restored player: ${guildId}`);
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
|
|
443
805
|
try {
|
|
444
806
|
const data = await this.provider.load(guildId);
|
|
445
807
|
if (!data) return false;
|
|
446
808
|
|
|
447
|
-
|
|
809
|
+
if (data.wasDestroyed === true) {
|
|
810
|
+
this.debug(`Skipping load for player marked as destroyed: ${guildId}`);
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
|
|
448
814
|
let player = this.manager.get(guildId);
|
|
449
815
|
if (!player) {
|
|
450
816
|
player = await this.manager.create(guildId, data.options);
|
|
451
817
|
}
|
|
818
|
+
// try connecting to the voice channel if we have one saved and autoConnect is enabled
|
|
819
|
+
if (data.options?.autoConnect && player.connection === null) {
|
|
820
|
+
try {
|
|
821
|
+
const voicechannel = await data.channelConnection;
|
|
822
|
+
await player.connect(voicechannel);
|
|
823
|
+
} catch (err) {
|
|
824
|
+
this.debug(`Failed to auto-connect player ${guildId} on load:`, err);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
452
827
|
|
|
453
|
-
// Restore queue
|
|
454
828
|
const queue = data.queue as SerializedQueue;
|
|
455
829
|
|
|
456
|
-
// Clear current queue
|
|
457
830
|
player.queue.clear();
|
|
458
831
|
player.queue.loop(queue.loopMode);
|
|
459
832
|
player.queue.autoPlay(queue.autoPlay);
|
|
460
833
|
|
|
461
|
-
// Restore tracks
|
|
462
834
|
if (queue.tracks.length > 0) {
|
|
463
|
-
const tracks = queue.tracks.map((t
|
|
835
|
+
const tracks = queue.tracks.map((t) => this.deserializeTrack(t));
|
|
464
836
|
player.queue.addMultiple(tracks);
|
|
465
837
|
}
|
|
466
838
|
|
|
467
|
-
// Restore current track if exists
|
|
468
839
|
if (queue.current && player.connection) {
|
|
469
840
|
const currentTrack = this.deserializeTrack(queue.current);
|
|
470
841
|
player.queue.willNextTrack(currentTrack);
|
|
471
842
|
|
|
472
|
-
// Restore playback position if requested
|
|
473
843
|
if (restorePosition && queue.position && queue.position > 0) {
|
|
474
844
|
await player.refreshPlayerResource(true, queue.position);
|
|
475
845
|
} else {
|
|
@@ -477,10 +847,8 @@ export class PersistenceManager extends EventEmitter {
|
|
|
477
847
|
}
|
|
478
848
|
}
|
|
479
849
|
|
|
480
|
-
// Restore volume
|
|
481
850
|
player.setVolume(data.volume);
|
|
482
851
|
|
|
483
|
-
// Restore filters - safely access through public method
|
|
484
852
|
if (data.filters && data.filters.length > 0) {
|
|
485
853
|
try {
|
|
486
854
|
const filterManager = (player as any).filter;
|
|
@@ -492,6 +860,9 @@ export class PersistenceManager extends EventEmitter {
|
|
|
492
860
|
}
|
|
493
861
|
}
|
|
494
862
|
|
|
863
|
+
this.restoredPlayers.add(guildId);
|
|
864
|
+
await this.clearDestroyedStatus(guildId);
|
|
865
|
+
|
|
495
866
|
this.debug(`Loaded player: ${guildId}`);
|
|
496
867
|
this.emit("playerLoaded", guildId, data);
|
|
497
868
|
return true;
|
|
@@ -503,23 +874,33 @@ export class PersistenceManager extends EventEmitter {
|
|
|
503
874
|
}
|
|
504
875
|
|
|
505
876
|
/**
|
|
506
|
-
* Load all saved players
|
|
877
|
+
* Load all saved players with auto-restore logic
|
|
507
878
|
*/
|
|
508
879
|
async loadAll(restorePosition: boolean = true): Promise<Map<string, boolean>> {
|
|
509
880
|
if (!this.options.enabled) return new Map();
|
|
510
881
|
|
|
882
|
+
await this.loadDestroyedStatus();
|
|
883
|
+
|
|
511
884
|
const results = new Map<string, boolean>();
|
|
512
885
|
|
|
513
886
|
try {
|
|
514
887
|
const keys = await this.provider.list();
|
|
515
|
-
|
|
888
|
+
const playerKeys = keys.filter((k) => k !== "__destroyed_players__");
|
|
889
|
+
this.debug(`Found ${playerKeys.length} saved players`);
|
|
516
890
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
results.set(guildId, success);
|
|
520
|
-
}
|
|
891
|
+
if (this.options.autoRestoreOnRestart) {
|
|
892
|
+
this.debug(`Auto-restore enabled, restoring players after ${this.options.restoreDelay}ms delay...`);
|
|
521
893
|
|
|
522
|
-
|
|
894
|
+
if (this.options.restoreDelay && this.options.restoreDelay > 0) {
|
|
895
|
+
await new Promise((resolve) => setTimeout(resolve, this.options.restoreDelay));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
for (const guildId of playerKeys) {
|
|
899
|
+
const success = await this.loadPlayer(guildId, restorePosition, true);
|
|
900
|
+
results.set(guildId, success);
|
|
901
|
+
}
|
|
902
|
+
this.emit("loadedAll", results);
|
|
903
|
+
}
|
|
523
904
|
} catch (error) {
|
|
524
905
|
this.debug("Failed to load players:", error);
|
|
525
906
|
this.emit("error", error);
|
|
@@ -528,6 +909,53 @@ export class PersistenceManager extends EventEmitter {
|
|
|
528
909
|
return results;
|
|
529
910
|
}
|
|
530
911
|
|
|
912
|
+
/**
|
|
913
|
+
* Mark a player as destroyed
|
|
914
|
+
*/
|
|
915
|
+
async markPlayerDestroyed(guildId: string, reason?: string): Promise<void> {
|
|
916
|
+
this.destroyedPlayers.set(guildId, {
|
|
917
|
+
guildId,
|
|
918
|
+
destroyedAt: Date.now(),
|
|
919
|
+
reason: reason || "manual_destroy",
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
const data = await this.provider.load(guildId);
|
|
924
|
+
if (data) {
|
|
925
|
+
data.wasDestroyed = true;
|
|
926
|
+
data.destroyedAt = Date.now();
|
|
927
|
+
await this.provider.save(guildId, data, this.options.compress);
|
|
928
|
+
}
|
|
929
|
+
} catch (error) {
|
|
930
|
+
this.debug(`Failed to mark player data as destroyed: ${guildId}`, error);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
await this.saveDestroyedStatus();
|
|
934
|
+
this.debug(`Marked player as destroyed (won't restore on restart): ${guildId}`);
|
|
935
|
+
this.emit("playerMarkedDestroyed", guildId);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Clear destroyed status for a player
|
|
940
|
+
*/
|
|
941
|
+
async clearDestroyed(guildId: string): Promise<void> {
|
|
942
|
+
await this.clearDestroyedStatus(guildId);
|
|
943
|
+
|
|
944
|
+
try {
|
|
945
|
+
const data = await this.provider.load(guildId);
|
|
946
|
+
if (data) {
|
|
947
|
+
data.wasDestroyed = false;
|
|
948
|
+
delete data.destroyedAt;
|
|
949
|
+
await this.provider.save(guildId, data, this.options.compress);
|
|
950
|
+
}
|
|
951
|
+
} catch (error) {
|
|
952
|
+
this.debug(`Failed to clear destroyed flag in data: ${guildId}`, error);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
this.debug(`Cleared destroyed status for: ${guildId}`);
|
|
956
|
+
this.emit("playerDestroyedCleared", guildId);
|
|
957
|
+
}
|
|
958
|
+
|
|
531
959
|
/**
|
|
532
960
|
* Delete a player's saved data
|
|
533
961
|
*/
|
|
@@ -536,6 +964,8 @@ export class PersistenceManager extends EventEmitter {
|
|
|
536
964
|
|
|
537
965
|
try {
|
|
538
966
|
await this.provider.delete(guildId);
|
|
967
|
+
await this.clearDestroyedStatus(guildId);
|
|
968
|
+
this.restoredPlayers.delete(guildId);
|
|
539
969
|
this.debug(`Deleted saved data for: ${guildId}`);
|
|
540
970
|
this.emit("playerDeleted", guildId);
|
|
541
971
|
return true;
|
|
@@ -554,7 +984,67 @@ export class PersistenceManager extends EventEmitter {
|
|
|
554
984
|
return false;
|
|
555
985
|
}
|
|
556
986
|
|
|
557
|
-
|
|
987
|
+
const success = await (this.provider as FileProvider).restoreBackup(guildId, timestamp);
|
|
988
|
+
if (success) {
|
|
989
|
+
await this.clearDestroyed(guildId);
|
|
990
|
+
}
|
|
991
|
+
return success;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Clean all backups for a specific player
|
|
996
|
+
*/
|
|
997
|
+
async cleanBackupsForPlayer(guildId: string): Promise<number> {
|
|
998
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
999
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
1000
|
+
return 0;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const deleted = await (this.provider as FileProvider).cleanAllBackupsForPlayer(guildId);
|
|
1004
|
+
this.debug(`Cleaned ${deleted} backups for player: ${guildId}`);
|
|
1005
|
+
this.emit("backupsCleaned", guildId, deleted);
|
|
1006
|
+
return deleted;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Clean all backups
|
|
1011
|
+
*/
|
|
1012
|
+
async cleanAllBackups(): Promise<number> {
|
|
1013
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
1014
|
+
this.debug("Backup cleanup only supported for file provider");
|
|
1015
|
+
return 0;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const deleted = await (this.provider as FileProvider).cleanAllBackups();
|
|
1019
|
+
this.debug(`Cleaned all ${deleted} backups`);
|
|
1020
|
+
this.emit("allBackupsCleaned", deleted);
|
|
1021
|
+
return deleted;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Get backup statistics
|
|
1026
|
+
*/
|
|
1027
|
+
getBackupStats(): {
|
|
1028
|
+
totalBackups: number;
|
|
1029
|
+
totalSizeMB: number;
|
|
1030
|
+
oldestBackup: Date | null;
|
|
1031
|
+
newestBackup: Date | null;
|
|
1032
|
+
backupsByPlayer: Record<string, number>;
|
|
1033
|
+
} | null {
|
|
1034
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
1035
|
+
this.debug("Backup stats only supported for file provider");
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const stats = (this.provider as FileProvider).getBackupStats();
|
|
1040
|
+
|
|
1041
|
+
return {
|
|
1042
|
+
totalBackups: stats.totalBackups,
|
|
1043
|
+
totalSizeMB: stats.totalSize / 1024 / 1024,
|
|
1044
|
+
oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup) : null,
|
|
1045
|
+
newestBackup: stats.newestBackup ? new Date(stats.newestBackup) : null,
|
|
1046
|
+
backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
|
|
1047
|
+
};
|
|
558
1048
|
}
|
|
559
1049
|
|
|
560
1050
|
/**
|
|
@@ -567,6 +1057,21 @@ export class PersistenceManager extends EventEmitter {
|
|
|
567
1057
|
}
|
|
568
1058
|
|
|
569
1059
|
await this.saveAll();
|
|
1060
|
+
await this.saveDestroyedStatus();
|
|
570
1061
|
this.debug("Persistence manager shut down");
|
|
571
1062
|
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Get list of destroyed players
|
|
1066
|
+
*/
|
|
1067
|
+
getDestroyedPlayers(): DestroyedRecord[] {
|
|
1068
|
+
return Array.from(this.destroyedPlayers.values());
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Check if auto-restore is enabled
|
|
1073
|
+
*/
|
|
1074
|
+
isAutoRestoreEnabled(): boolean {
|
|
1075
|
+
return this.options.autoRestoreOnRestart === true;
|
|
1076
|
+
}
|
|
572
1077
|
}
|