/** * BackupManager — Orchestrates backup operations */ const archiver = require('archiver') const { createReadStream, createWriteStream } = require('fs') const { mkdir } = require('fs').promises const path = require('path') const { Transform } = require('stream') class BackupManager { constructor(provider) { this.provider = provider } /** * Create a backup from a project folder * @param {string} projectPath - Path to project folder * @param {string} repoName - Repository name (used for filenames) * @returns {Promise<{filename, size, timestamp}>} */ async createBackup(projectPath, repoName) { try { // Create ZIP buffer const buffer = await this._createZip(projectPath) const timestamp = this._getTimestamp() const filename = `${repoName}-backup-${timestamp}.zip` // Upload to provider const result = await this.provider.uploadBackup(buffer, filename) // Cleanup old backups await this._cleanupOldBackups(repoName) return { filename, size: result.size || buffer.length, timestamp } } catch (err) { throw new Error(`Backup creation failed: ${err.message}`) } } /** * List all backups for a repository * @param {string} repoName - Repository name * @returns {Promise} */ async listBackups(repoName) { try { const backups = await this.provider.listBackups() return backups .filter(b => b.name.startsWith(repoName)) .sort((a, b) => new Date(b.date || b.name) - new Date(a.date || a.name)) } catch (err) { throw new Error(`Failed to list backups: ${err.message}`) } } /** * Restore a backup to a target folder * @param {string} repoName - Repository name * @param {string} filename - Backup filename * @param {string} targetPath - Target folder path */ async restoreBackup(repoName, filename, targetPath) { try { // Download backup const buffer = await this.provider.downloadBackup(filename) // Extract ZIP await this._extractZip(buffer, targetPath) return { ok: true, restored: filename } } catch (err) { throw new Error(`Restore failed: ${err.message}`) } } /** * Delete a backup * @param {string} filename - Backup filename */ async deleteBackup(filename) { try { await this.provider.deleteBackup(filename) return { ok: true } } catch (err) { throw new Error(`Delete failed: ${err.message}`) } } // ==================== PRIVATE METHODS ==================== /** * Create ZIP buffer from project folder * Excludes: .git, node_modules, dist, build, .env */ async _createZip(projectPath) { return new Promise((resolve, reject) => { const output = [] const archive = archiver('zip', { zlib: { level: 5 } }) archive.on('data', chunk => output.push(chunk)) archive.on('end', () => resolve(Buffer.concat(output))) archive.on('error', reject) // Add files with exclusions archive.glob('**/*', { cwd: projectPath, ignore: [ '.git/**', '.git', 'node_modules/**', 'node_modules', 'dist/**', 'dist', 'build/**', 'build', '.env', '.env.local', '.env.*.local', '*.log', 'data/backups/**' ], dot: true }) archive.finalize() }) } /** * Extract ZIP buffer to target folder */ async _extractZip(buffer, targetPath) { const unzipper = require('unzipper') return new Promise((resolve, reject) => { const { Readable } = require('stream') const stream = Readable.from(buffer) stream .pipe(unzipper.Extract({ path: targetPath })) .on('close', resolve) .on('error', reject) }) } /** * Get ISO timestamp (YYYYMMDD-HHMMSS format) */ _getTimestamp() { const now = new Date() const year = now.getFullYear() const month = String(now.getMonth() + 1).padStart(2, '0') const day = String(now.getDate()).padStart(2, '0') const hours = String(now.getHours()).padStart(2, '0') const minutes = String(now.getMinutes()).padStart(2, '0') const seconds = String(now.getSeconds()).padStart(2, '0') return `${year}${month}${day}-${hours}${minutes}${seconds}` } /** * Cleanup old backups (keep only last N versions) */ async _cleanupOldBackups(repoName, maxVersions = 5) { try { const backups = await this.listBackups(repoName) for (let i = maxVersions; i < backups.length; i++) { await this.provider.deleteBackup(backups[i].name) } } catch (err) { // Silently ignore cleanup errors console.warn(`Cleanup warning: ${err.message}`) } } } module.exports = BackupManager