Skip to content

Commit

Permalink
Added auto-backup functionality. Restore to come later.
Browse files Browse the repository at this point in the history
  • Loading branch information
rstrouse committed Sep 21, 2021
1 parent fd30fc7 commit 433b3af
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ replay/
config.json
# data files
data/
backups/
poolConfig.json
poolState.json
*.crt
Expand Down
2 changes: 2 additions & 0 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export async function initAsync() {
await state.init();
await webApp.init();
await sys.start();
await webApp.initAutoBackup();
} catch (err) { console.log(`Error Initializing nodejs-PoolController ${err.message}`); }
//return Promise.resolve()
// .then(function () { config.init(); })
Expand Down Expand Up @@ -70,6 +71,7 @@ export async function stopAsync(): Promise<void> {
try {
console.log('Shutting down open processes');
// await sys.board.virtualPumpControllers.stopAsync();
await webApp.stopAutoBackup();
await sys.stopAsync();
await state.stopAsync();
await conn.stopAsync();
Expand Down
3 changes: 3 additions & 0 deletions config/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ class Config {
c[section] = val;
this.update();
}
// RKS: 09-21-21 - We are counting on the return from this being immutable. A copy of the data
// should always be returned here.
public getSection(section?: string, opts?: any): any {
if (typeof section === 'undefined') return this._cfg;
let c: any = this._cfg;
Expand All @@ -118,6 +120,7 @@ class Config {
let baseDir = process.cwd();
this.ensurePath(baseDir + '/logs/');
this.ensurePath(baseDir + '/data/');
this.ensurePath(baseDir + '/backups/');
// this.ensurePath(baseDir + '/replay/');
//setTimeout(() => { config.update(); }, 100);
}
Expand Down
11 changes: 11 additions & 0 deletions defaultConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@
"autoOpen": false,
"lock": false
}
},
"backups": {
"automatic": false,
"interval": {
"days": 30,
"hours": 0
},
"keepCount": 5,
"njsPC": true,
"servers": []
}

},
"web": {
"servers": {
Expand Down
159 changes: 159 additions & 0 deletions web/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ import { ConfigSocket } from "./services/config/ConfigSocket";
// This class serves data and pages for
// external interfaces as well as an internal dashboard.
export class WebServer {
public autoBackup = false;
public lastBackup;
private _servers: ProtoServer[] = [];
private family = 'IPv4';
private _autoBackupTimer: NodeJS.Timeout;
constructor() { }
public async init() {
try {
Expand Down Expand Up @@ -87,6 +90,7 @@ export class WebServer {
}
}
this.initInterfaces(cfg.interfaces);

} catch (err) { logger.error(`Error initializing web server ${err.message}`) }
}
public async initInterfaces(interfaces: any) {
Expand Down Expand Up @@ -201,6 +205,155 @@ export class WebServer {
}
return config.getInterfaceByUuid(obj.uuid);
}
public async initAutoBackup() {
try {
let bu = config.getSection('controller.backups');
this.autoBackup = false;
// These will be returned in reverse order with the newest backup first.
let files = await this.readBackupFiles();
let afiles = files.filter(elem => elem.options.automatic === true);
this.lastBackup = (afiles.length > 0) ? Date.parse(afiles[0].options.backupDate) || 0 : 0;
// Set the last backup date.
this.autoBackup = utils.makeBool(bu.automatic);
logger.info(`Auto-backup initialized Last Backup: ${Timestamp.toISOLocal(new Date(this.lastBackup))}`);
// Lets wait a good 20 seconds before we auto-backup anything. Now that we are initialized let the OCP have its way with everything.
setTimeout(() => { this.checkAutoBackup(); }, 20000);
}
catch (err) { logger.error(`Error initializing auto-backup: ${err.message}`); }
}
public async stopAutoBackup() {
this.autoBackup = false;
if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
}
public async readBackupFiles(): Promise<{ file: string, options: any }[]> {
try {
let backupDir = path.join(process.cwd(), 'backups');
let files = fs.readdirSync(backupDir);
let backups = [];
for (let i = 0; i < files.length; i++) {
let file = files[i];
if (path.extname(file) === '.zip') {
let name = path.parse(file).name;
if (name.length === 19) {
// Extract the options from the file.
let opts = await this.extractBackupOptions(path.join(backupDir, file));
if (typeof opts !== 'undefined') {
backups.push(opts);
}
}
}
}
backups.sort((a, b) => { return Date.parse(b.options.backupDate) - Date.parse(a.options.backupDate) });
return backups;
}
catch (err) { logger.error(`Error reading backup file directory: ${err.message}`); }
}
protected async extractBackupOptions(file: string | Buffer): Promise<{ file: string, options: any }> {
try {
let opts = { file: Buffer.isBuffer(file) ? 'Buffer' : file, options: {} as any };
let jszip = require("jszip");
let buff = Buffer.isBuffer(file) ? file : fs.readFileSync(file);
await jszip.loadAsync(buff).then(async (zip) => {
await zip.file('options.json').async('string').then((data) => {
opts.options = JSON.parse(data);
if (typeof opts.options.backupDate === 'undefined' && typeof file === 'string') {
let name = path.parse(file).name;
if (name.length === 19) {
let date = name.substring(0, 10).replace(/-/g, '/');
let time = name.substring(11).replace(/-/g, ':');
let dt = Date.parse(`${date} ${time}`);
if (!isNaN(dt)) opts.options.backupDate = Timestamp.toISOLocal(new Date(dt));
}
}
});
});
return opts;
} catch (err) { logger.error(`Error extracting backup options from ${file}: ${err.message}`); }
}
public async pruneAutoBackups(keepCount: number) {
try {
// We only automatically prune backups that njsPC put there in the first place so only
// look at auto-backup files.
let files = await this.readBackupFiles();
let afiles = files.filter(elem => elem.options.automatic === true);
if (afiles.length > keepCount) {
// Prune off the oldest backups until we get to our keep count. When we read in the files
// these were sorted newest first.
while (afiles.length > keepCount) {
let afile = afiles.pop();
logger.info(`Pruning auto-backup file: ${afile.file}`);
try {
fs.unlinkSync(afile.file);
} catch (err) { logger.error(`Error deleting auto-backup file: ${afile.file}`); }
}
}
} catch (err) { logger.error(`Error pruning auto-backups: ${err.message}`); }
}
public async backupServer(opts: any): Promise<{ file: string, options: any }> {
let ret = { file: '', options: extend(true, {}, opts, { version: 1.0, errors: [] }) };
let jszip = require("jszip");
function pad(n) { return (n < 10 ? '0' : '') + n; }
let zip = new jszip();
let ts = new Date();
let baseDir = process.cwd();
let zipPath = path.join(baseDir, 'backups', ts.getFullYear() + '-' + pad(ts.getMonth() + 1) + '-' + pad(ts.getDate()) + '_' + pad(ts.getHours()) + '-' + pad(ts.getMinutes()) + '-' + pad(ts.getSeconds()) + '.zip');
if (opts.njsPC === true) {
zip.folder('njsPC');
zip.folder('njsPC/data');
// Create the backup file and copy it into it.
zip.file('njsPC/config.json', fs.readFileSync(path.join(baseDir, 'config.json')));
zip.file('njsPC/data/poolConfig.json', fs.readFileSync(path.join(baseDir, 'data', 'poolConfig.json')));
zip.file('njsPC/data/poolState.json', fs.readFileSync(path.join(baseDir, 'data', 'poolState.json')));
}
if (typeof opts.servers !== 'undefined' && opts.servers.length > 0) {
// Back up all our servers.
for (let i = 0; i < opts.servers.length; i++) {
if (opts.servers[i].backup === false) continue;
let server = this.findServerByGuid(opts.servers[i].uuid) as REMInterfaceServer;
if (typeof server === 'undefined') ret.options.errors.push(`Could not find server ${opts.servers[i].name} : ${opts.servers[i].uuid}`);
else if (!server.isConnected) ret.options.errors.push(`Server ${opts.servers[i].name} : ${opts.servers[i].uuid} not connected cannot back up`);
else {
// Try to get the data from the server.
zip.folder(server.name);
zip.file(`${server.name}/serverConfig.json`, JSON.stringify(server.cfg));
zip.folder(`${server.name}/data`);
let ccfg = await server.getControllerConfig();
zip.file(`${server.name}/data/controllerConfig.json`, JSON.stringify(ccfg));
}
}
}
ret.options.backupDate = Timestamp.toISOLocal(ts);
zip.file('options.json', JSON.stringify(ret.options));
await zip.generateAsync({ type: 'nodebuffer' }).then(content => {
fs.writeFileSync(zipPath, content);
this.lastBackup = ts.valueOf();
});
ret.file = zipPath;
return ret;
}
public async checkAutoBackup() {
if (typeof this._autoBackupTimer !== 'undefined' || this._autoBackupTimer) clearTimeout(this._autoBackupTimer);
this._autoBackupTimer = undefined;
let bu = config.getSection('controller.backups');
if (bu.automatic === true) {
if (typeof this.lastBackup === 'undefined' ||
(this.lastBackup < new Date().valueOf() - (bu.interval.days * 86400000) - (bu.interval.hours * 3600000))) {
bu.name = 'Automatic Backup';
await this.backupServer(bu);
}
}
else this.autoBackup = false;
if (this.autoBackup) {
await this.pruneAutoBackups(bu.keepCount);
let nextBackup = this.lastBackup + (bu.interval.days * 86400000) + (bu.interval.hours * 3600000);
setTimeout(async () => {
try {
await this.checkAutoBackup();
} catch (err) { logger.error(`Error checking auto-backup: ${err.message}`); }
}, nextBackup -= new Date().valueOf());
logger.info(`Next auto-backup ${Timestamp.toISOLocal(new Date(nextBackup))}`);
}
}
}
class ProtoServer {
constructor(name: string, type: string) { this.name = name; this.type = type; }
Expand Down Expand Up @@ -930,6 +1083,12 @@ export class REMInterfaceServer extends ProtoServer {
}, 5000);
}
}
public async getControllerConfig() {
try {
let response = await this.sendClientRequest('GET', '/config/backup/controller', undefined, 10000);
return (response.status.code === 200) ? JSON.parse(response.data) : {};
} catch (err) { logger.error(err); }
}
private async initConnection() {
try {
// find HTTP server
Expand Down
34 changes: 33 additions & 1 deletion web/services/config/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ export class ConfigRoute {
});

/***** END OF ENDPOINTS FOR MODIFYINC THE OUTDOOR CONTROL PANEL SETTINGS *****/



app.get('/config/circuits/names', (req, res) => {
Expand Down Expand Up @@ -866,5 +866,37 @@ export class ConfigRoute {
app.get('/app/config/:section', (req, res) => {
return res.status(200).send(config.getSection(req.params.section));
});
app.get('/app/config/options/backup', async (req, res, next) => {
try {
let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0, keepCount: 5, servers: [] } });
let servers = await sys.ncp.getREMServers();
for (let i = 0; i < servers.length; i++) {
let srv = servers[i];
if (typeof opts.servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.push({ name: srv.name, uuid: srv.uuid, backup: false });
}
for (let i = opts.servers.length - 1; i >= 0; i--) {
let srv = opts.servers[i];
if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1);
}
return res.status(200).send(opts);
} catch (err) { next(err); }
});
app.put('/app/config/options/backup', async (req, res, next) => {
try {
config.setSection('controller.backups', req.body);
let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0, keepCount: 5, servers: [] } });
webApp.autoBackup = utils.makeBool(opts.automatic);
await webApp.checkAutoBackup();
return res.status(200).send(opts);
} catch (err) { next(err); }

});
app.put('/app/config/createBackup', async (req, res, next) => {
try {
let ret = await webApp.backupServer(req.body);
res.download(ret.file);
}
catch (err) { next(err); }
});
}
}

0 comments on commit 433b3af

Please sign in to comment.