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

View File

@@ -0,0 +1,59 @@
/**
* BackupProvider — Abstract base class for backup providers
* All providers must implement these methods
*/
class BackupProvider {
/**
* Authenticate with the backup service
* @param {Object} credentials - Provider-specific credentials
*/
async authenticate(credentials) {
throw new Error('authenticate() not implemented in ' + this.constructor.name)
}
/**
* Upload a backup file
* @param {Buffer} buffer - File content
* @param {string} filename - Filename (e.g., 'repo-backup-2025-03-24.zip')
* @returns {Promise<{size: number}>}
*/
async uploadBackup(buffer, filename) {
throw new Error('uploadBackup() not implemented in ' + this.constructor.name)
}
/**
* List all backups for a repository
* @returns {Promise<Array>} Array of {name, size, date}
*/
async listBackups() {
throw new Error('listBackups() not implemented in ' + this.constructor.name)
}
/**
* Download a specific backup
* @param {string} filename - Filename to download
* @returns {Promise<Buffer>}
*/
async downloadBackup(filename) {
throw new Error('downloadBackup() not implemented in ' + this.constructor.name)
}
/**
* Delete a backup file
* @param {string} filename - Filename to delete
*/
async deleteBackup(filename) {
throw new Error('deleteBackup() not implemented in ' + this.constructor.name)
}
/**
* Test connection to backup service
* @returns {Promise<{ok: boolean, error?: string}>}
*/
async testConnection() {
throw new Error('testConnection() not implemented in ' + this.constructor.name)
}
}
module.exports = BackupProvider

View File

@@ -0,0 +1,83 @@
/**
* LocalProvider — Backup provider for local folders
*/
const path = require('path')
const fs = require('fs').promises
const BackupProvider = require('./BackupProvider')
class LocalProvider extends BackupProvider {
constructor() {
super()
this.basePath = null
}
async authenticate(credentials) {
const basePath = String(credentials && credentials.basePath ? credentials.basePath : '').trim()
if (!basePath) {
throw new Error('Lokaler Backup-Ordner fehlt')
}
this.basePath = path.resolve(basePath)
await fs.mkdir(this.basePath, { recursive: true })
}
async testConnection() {
if (!this.basePath) return { ok: false, error: 'Not authenticated' }
try {
const stat = await fs.stat(this.basePath)
if (!stat.isDirectory()) {
return { ok: false, error: 'Pfad ist kein Ordner' }
}
return { ok: true }
} catch (err) {
return { ok: false, error: err.message }
}
}
async uploadBackup(buffer, filename) {
if (!this.basePath) throw new Error('Not authenticated')
const target = path.join(this.basePath, filename)
await fs.writeFile(target, buffer)
return { size: buffer.length }
}
async listBackups() {
if (!this.basePath) throw new Error('Not authenticated')
const entries = await fs.readdir(this.basePath, { withFileTypes: true })
const files = entries.filter(e => e.isFile() && e.name.endsWith('.zip'))
const backups = []
for (const f of files) {
const full = path.join(this.basePath, f.name)
const stat = await fs.stat(full)
backups.push({
name: f.name,
size: stat.size,
date: stat.mtime.toISOString()
})
}
backups.sort((a, b) => new Date(b.date) - new Date(a.date))
return backups
}
async downloadBackup(filename) {
if (!this.basePath) throw new Error('Not authenticated')
const file = path.join(this.basePath, filename)
return fs.readFile(file)
}
async deleteBackup(filename) {
if (!this.basePath) throw new Error('Not authenticated')
const file = path.join(this.basePath, filename)
await fs.unlink(file)
}
}
module.exports = LocalProvider