OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
This commit is contained in:
commit
eaf05f2811
280 files changed
+123975
No files matched your search
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* Service: Backup & Recovery System
|
||||
* Melakukan backup otomatis database dan settings
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { logger } = require('../config/logger');
|
||||
const { getSetting, getCurrentDateInTimezone, getNowLocalISO } = require('../config/settingsManager');
|
||||
|
||||
const projectRoot = path.join(__dirname, '..');
|
||||
const backupDir = path.join(projectRoot, 'backups');
|
||||
const dbPath = path.join(projectRoot, 'database', 'billing.db');
|
||||
const settingsPath = path.join(projectRoot, 'settings.json');
|
||||
|
||||
// Pastikan direktori backup ada
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
logger.info('[Backup] Created backup directory');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate timestamp untuk nama file backup
|
||||
*/
|
||||
function getBackupTimestamp() {
|
||||
const now = getCurrentDateInTimezone();
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup database SQLite
|
||||
*/
|
||||
function backupDatabase() {
|
||||
try {
|
||||
const timestamp = getBackupTimestamp();
|
||||
const backupFileName = `billing_db_${timestamp}.db`;
|
||||
const backupFilePath = path.join(backupDir, backupFileName);
|
||||
|
||||
// Copy database file
|
||||
fs.copyFileSync(dbPath, backupFilePath);
|
||||
|
||||
// Compress backup (optional - bisa ditambahkan nanti)
|
||||
const stats = fs.statSync(backupFilePath);
|
||||
const sizeKB = Math.round(stats.size / 1024);
|
||||
|
||||
logger.info(`[Backup] Database backup created: ${backupFileName} (${sizeKB} KB)`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fileName: backupFileName,
|
||||
size: stats.size,
|
||||
timestamp: getNowLocalISO()
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(`[Backup] Failed to backup database: ${e.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup settings.json
|
||||
*/
|
||||
function backupSettings() {
|
||||
try {
|
||||
const timestamp = getBackupTimestamp();
|
||||
const backupFileName = `settings_${timestamp}.json`;
|
||||
const backupFilePath = path.join(backupDir, backupFileName);
|
||||
|
||||
// Copy settings file
|
||||
fs.copyFileSync(settingsPath, backupFilePath);
|
||||
|
||||
const stats = fs.statSync(backupFilePath);
|
||||
const sizeKB = Math.round(stats.size / 1024);
|
||||
|
||||
logger.info(`[Backup] Settings backup created: ${backupFileName} (${sizeKB} KB)`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fileName: backupFileName,
|
||||
size: stats.size,
|
||||
timestamp: getNowLocalISO()
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(`[Backup] Failed to backup settings: ${e.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup semua (database + settings)
|
||||
*/
|
||||
function backupAll() {
|
||||
const dbResult = backupDatabase();
|
||||
const settingsResult = backupSettings();
|
||||
|
||||
return {
|
||||
database: dbResult,
|
||||
settings: settingsResult,
|
||||
timestamp: getNowLocalISO()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore database dari backup
|
||||
*/
|
||||
function restoreDatabase(backupFileName) {
|
||||
try {
|
||||
const backupFilePath = path.join(backupDir, backupFileName);
|
||||
|
||||
// Cek apakah file backup ada
|
||||
if (!fs.existsSync(backupFilePath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Backup file not found: ${backupFileName}`
|
||||
};
|
||||
}
|
||||
|
||||
// Backup database saat ini sebelum restore
|
||||
const preRestoreBackup = backupDatabase();
|
||||
if (!preRestoreBackup.success) {
|
||||
logger.warn('[Backup] Failed to create pre-restore backup');
|
||||
}
|
||||
|
||||
// Restore database
|
||||
fs.copyFileSync(backupFilePath, dbPath);
|
||||
|
||||
const stats = fs.statSync(dbPath);
|
||||
const sizeKB = Math.round(stats.size / 1024);
|
||||
|
||||
logger.info(`[Backup] Database restored from: ${backupFileName} (${sizeKB} KB)`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fileName: backupFileName,
|
||||
size: stats.size,
|
||||
timestamp: getNowLocalISO(),
|
||||
preRestoreBackup: preRestoreBackup.fileName
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(`[Backup] Failed to restore database: ${e.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore settings dari backup
|
||||
*/
|
||||
function restoreSettings(backupFileName) {
|
||||
try {
|
||||
const backupFilePath = path.join(backupDir, backupFileName);
|
||||
|
||||
// Cek apakah file backup ada
|
||||
if (!fs.existsSync(backupFilePath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Backup file not found: ${backupFileName}`
|
||||
};
|
||||
}
|
||||
|
||||
// Backup settings saat ini sebelum restore
|
||||
const preRestoreBackup = backupSettings();
|
||||
if (!preRestoreBackup.success) {
|
||||
logger.warn('[Backup] Failed to create pre-restore backup');
|
||||
}
|
||||
|
||||
// Restore settings
|
||||
fs.copyFileSync(backupFilePath, settingsPath);
|
||||
|
||||
const stats = fs.statSync(settingsPath);
|
||||
const sizeKB = Math.round(stats.size / 1024);
|
||||
|
||||
logger.info(`[Backup] Settings restored from: ${backupFileName} (${sizeKB} KB)`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fileName: backupFileName,
|
||||
size: stats.size,
|
||||
timestamp: getNowLocalISO(),
|
||||
preRestoreBackup: preRestoreBackup.fileName
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(`[Backup] Failed to restore settings: ${e.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Daftar semua backup yang tersedia
|
||||
*/
|
||||
function listBackups() {
|
||||
try {
|
||||
const files = fs.readdirSync(backupDir);
|
||||
const backups = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(backupDir, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
// Parse filename untuk mendapatkan tanggal
|
||||
let backupDate = null;
|
||||
let backupType = null;
|
||||
|
||||
if (file.startsWith('billing_db_') && file.endsWith('.db')) {
|
||||
backupType = 'database';
|
||||
const timestamp = file.replace('billing_db_', '').replace('.db', '');
|
||||
backupDate = parseBackupTimestamp(timestamp);
|
||||
} else if (file.startsWith('settings_') && file.endsWith('.json')) {
|
||||
backupType = 'settings';
|
||||
const timestamp = file.replace('settings_', '').replace('.json', '');
|
||||
backupDate = parseBackupTimestamp(timestamp);
|
||||
}
|
||||
|
||||
backups.push({
|
||||
fileName: file,
|
||||
type: backupType,
|
||||
size: stats.size,
|
||||
sizeKB: Math.round(stats.size / 1024),
|
||||
created: stats.birthtime,
|
||||
createdDate: backupDate,
|
||||
modified: stats.mtime
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by created date (terbaru dulu)
|
||||
backups.sort((a, b) => b.created - a.created);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
backups: backups,
|
||||
total: backups.length
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(`[Backup] Failed to list backups: ${e.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: e.message,
|
||||
backups: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse timestamp dari nama file backup
|
||||
*/
|
||||
function parseBackupTimestamp(timestamp) {
|
||||
try {
|
||||
// Format: YYYYMMDD_HHMMSS
|
||||
const [datePart, timePart] = timestamp.split('_');
|
||||
const year = datePart.substring(0, 4);
|
||||
const month = datePart.substring(4, 6);
|
||||
const day = datePart.substring(6, 8);
|
||||
const hours = timePart.substring(0, 2);
|
||||
const minutes = timePart.substring(2, 4);
|
||||
const seconds = timePart.substring(4, 6);
|
||||
|
||||
return new Date(year, month - 1, day, hours, minutes, seconds);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus backup lama berdasarkan retention policy
|
||||
*/
|
||||
function cleanupOldBackups(retentionDays = 30) {
|
||||
try {
|
||||
const result = listBackups();
|
||||
if (!result.success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
||||
|
||||
let deletedCount = 0;
|
||||
const deletedFiles = [];
|
||||
|
||||
for (const backup of result.backups) {
|
||||
if (backup.created < cutoffDate) {
|
||||
const filePath = path.join(backupDir, backup.fileName);
|
||||
fs.unlinkSync(filePath);
|
||||
deletedCount++;
|
||||
deletedFiles.push(backup.fileName);
|
||||
logger.info(`[Backup] Deleted old backup: ${backup.fileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount,
|
||||
deletedFiles,
|
||||
retentionDays
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(`[Backup] Failed to cleanup old backups: ${e.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek kapasitas backup dan hapus jika perlu
|
||||
*/
|
||||
function checkBackupCapacity(maxSizeMB = 500) {
|
||||
try {
|
||||
const result = listBackups();
|
||||
if (!result.success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const totalSize = result.backups.reduce((sum, backup) => sum + backup.size, 0);
|
||||
const totalSizeMB = totalSize / (1024 * 1024);
|
||||
|
||||
if (totalSizeMB > maxSizeMB) {
|
||||
logger.warn(`[Backup] Backup size (${totalSizeMB.toFixed(2)} MB) exceeds limit (${maxSizeMB} MB)`);
|
||||
|
||||
// Hapus backup paling lama sampai kapasitas aman
|
||||
const sortedBackups = [...result.backups].sort((a, b) => a.created - b.created);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const backup of sortedBackups) {
|
||||
if (totalSizeMB <= maxSizeMB * 0.8) { // Hapus sampai 80% dari limit
|
||||
break;
|
||||
}
|
||||
|
||||
const filePath = path.join(backupDir, backup.fileName);
|
||||
fs.unlinkSync(filePath);
|
||||
totalSizeMB -= backup.size / (1024 * 1024);
|
||||
deletedCount++;
|
||||
logger.info(`[Backup] Deleted backup for capacity: ${backup.fileName}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'cleanup',
|
||||
deletedCount,
|
||||
totalSizeMB: totalSizeMB.toFixed(2),
|
||||
maxSizeMB
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'none',
|
||||
totalSizeMB: totalSizeMB.toFixed(2),
|
||||
maxSizeMB
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(`[Backup] Failed to check backup capacity: ${e.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jadwal backup otomatis
|
||||
*/
|
||||
function scheduleAutoBackup() {
|
||||
const nodeCron = require('node-cron');
|
||||
const enabled = getSetting('auto_backup_enabled', true);
|
||||
const schedule = getSetting('auto_backup_schedule', '0 2 * * *'); // Default jam 2 pagi setiap hari
|
||||
|
||||
if (!enabled) {
|
||||
logger.info('[Backup] Auto backup disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
nodeCron.schedule(schedule, () => {
|
||||
logger.info('[Backup] Starting scheduled backup...');
|
||||
const result = backupAll();
|
||||
|
||||
if (result.database.success && result.settings.success) {
|
||||
logger.info('[Backup] Scheduled backup completed successfully');
|
||||
} else {
|
||||
logger.error('[Backup] Scheduled backup failed');
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`[Backup] Auto backup scheduled: ${schedule}`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
backupDatabase,
|
||||
backupSettings,
|
||||
backupAll,
|
||||
restoreDatabase,
|
||||
restoreSettings,
|
||||
listBackups,
|
||||
cleanupOldBackups,
|
||||
checkBackupCapacity,
|
||||
scheduleAutoBackup
|
||||
};
|
||||
Reference in new issue
Block a user