Merge: resolve conflict in routes/backups.js, keep storage-info + maxStorageBytes

This commit is contained in:
Krystie
2026-05-28 15:00:41 -07:00
parent ea9bdf9598
commit ad9400490d
6 changed files with 387 additions and 14 deletions
+74 -1
View File
@@ -140,8 +140,16 @@ class BackupManager extends EventEmitter {
// Calculate checksum
const checksum = this.calculateChecksum(finalData);
// Save to destinations
// Check and enforce maxStorageBytes limit BEFORE saving
const destinations = backup.destinations || [{ type: 'local' }];
for (const dest of destinations) {
if (backup.maxStorageBytes && backup.maxStorageBytes > 0) {
const destPath = dest.path || DEFAULT_BACKUP_DIR;
await this.enforceStorageLimit(backup.maxStorageBytes, destPath, backupId, finalData.length);
}
}
// Save to destinations
const savedLocations = [];
for (const dest of destinations) {
@@ -1053,6 +1061,71 @@ class BackupManager extends EventEmitter {
this.saveHistory();
}
/**
* Enforce storage limit by pruning oldest backups if needed.
* Works with any filesystem (ext4, Btrfs, XFS, ZFS, APFS, NTFS).
* @param {number} maxBytes - Maximum storage limit in bytes
* @param {string} backupDir - Backup directory path
* @param {string} pendingBackupId - ID of backup about to be written
* @param {number} pendingSize - Estimated size of pending backup in bytes
*/
async enforceStorageLimit(maxBytes, backupDir, pendingBackupId, pendingSize) {
console.log(`[BackupManager] Checking storage limit: max=${maxBytes} bytes`);
// Find all backup files for this destination
const files = [];
if (fs.existsSync(backupDir)) {
const entries = fs.readdirSync(backupDir);
for (const entry of entries) {
if (entry.endsWith('.backup')) {
const filePath = path.join(backupDir, entry);
try {
const stats = fs.statSync(filePath);
files.push({ path: filePath, size: stats.size, mtime: stats.mtime });
} catch (e) {
// Skip files we can't stat
}
}
}
}
// Sort by modification time (oldest first)
files.sort((a, b) => new Date(a.mtime) - new Date(b.mtime));
let totalSize = files.reduce((sum, f) => sum + f.size, 0) + pendingSize;
// If even ONE backup exceeds the limit, fail immediately
const largestFile = files.reduce((max, f) => f.size > max ? f.size : max, 0);
if (largestFile > maxBytes) {
throw new Error(
`Backup too large (${largestFile} bytes) for configured storage limit (${maxBytes} bytes). ` +
`Increase the storage limit or remove large backups manually.`
);
}
// Prune oldest backups until we fit within limit
while (files.length > 0 && totalSize > maxBytes) {
const oldest = files.shift();
console.log(`[BackupManager] Pruning oldest backup to free space: ${oldest.path} (${oldest.size} bytes)`);
try {
fs.unlinkSync(oldest.path);
// Also remove from history if present
const historyEntry = this.history.find(h =>
h.locations && h.locations.some(l => l.path === oldest.path)
);
if (historyEntry) {
this.history = this.history.filter(h => h.id !== historyEntry.id);
}
totalSize -= oldest.size;
} catch (error) {
console.error(`[BackupManager] Error pruning backup ${oldest.path}:`, error.message);
}
}
this.saveHistory();
console.log(`[BackupManager] Storage after pruning: ${totalSize}/${maxBytes} bytes`);
}
/**
* Add entry to backup history
*/