Update from Git Manager GUI
This commit is contained in:
183
src/backup/BackupManager.js
Normal file
183
src/backup/BackupManager.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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<Array>}
|
||||
*/
|
||||
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
|
||||
Reference in New Issue
Block a user