Update from Git Manager GUI

This commit is contained in:
2026-03-24 21:38:24 +01:00
parent a1f7c66f67
commit de581d878f
3 changed files with 325 additions and 0 deletions

183
src/backup/BackupManager.js Normal file
View 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