Merge: resolve conflict in routes/backups.js, keep storage-info + maxStorageBytes
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user