sheercms
Version:
Sheer Cliff CMS is a simple and powerful content management system (CMS) for Node JS.
677 lines (606 loc) • 24.6 kB
JavaScript
var logger = require('../logger');
var api = require('../api');
var mongoose = require('mongoose');
var async = require('async');
var config = require("../config");
var merge = require('object-merge');
var util = require('../utilities');
var fs = require('fs');
var path_mod = require('path');
var moment = require('moment');
var cms = require('sheercms');
var process = require('child_process');
var NodeZip = require('node-zip');
var rimraf = require('rimraf');
var S = require('string');
/*
callback: err, stats {}
*/
module.exports.getSystemStats = function(callback) {
try {
var stats = {
scale: 1024,
database: {},
collections: []
};
var cols = ["drafts", "publisheds", "users"];
var db = mongoose.connection.db;
//get the total database stats
db.executeDbCommand({dbStats: 1, scale: 1024}, function(err, result) {
if (err) {
logger.error(err);
if (callback) callback(err);
}
else {
stats.database = result.documents[0];
//get stats for each collection in array
async.each(cols, function (key, done) {
db.executeDbCommand({collstats: key, scale: 1024 }, function(err, result) {
if (err) {
logger.error(err);
done(err);
}
else {
if (result) {
var obj = result.documents[0];
obj.name = key;
stats.collections.push(obj);
}
done();
}
});
}, function (err) {
if (err) {
logger.error(err);
callback(err);
}
else {
callback(null, stats);
}
});
}
});
}
catch(e) {
logger.error(e);
if (callback) callback(e);
}
};
/*
options: {
path: the path to save the backup. If null, it uses the config setting
zip: true (default)
}
callback: (err, backupName)
*/
module.exports.backupDatabase = function(options, callback) {
try {
logger.info("Starting database backup.");
if (!config.settings || !config.settings.backups)
throw new Error("The backup settings are not in the config file.");
countBackups(function(err, count) {
logger.info("Count of existing backups: " + count);
if (config.settings.backups.max && config.settings.backups.max <= count) {
var msg = 'You have reached the maximum number of backups on the server (' + config.settings.backups.max + '). You must delete a backup before taking another one.';
logger.warn(msg);
if (callback) callback(msg, null);
return;
}
var defaults = {
path: config.settings.backups.path,
zip: true
};
//merge settings with options
var settings = merge(defaults, options);
//fix the path to be absolute
if (settings.path && settings.path.indexOf("~/") == 0) {
settings.path = path_mod.join(cms.getRoot(), settings.path.substr(2));
}
//create a subfolder in the backups folder for this particular backup
var time = util.replaceAll(moment().format(), ":", "-");
var tempName = "backup_" + time;
var subfolder = path_mod.join(settings.path, tempName);
logger.info("Ensuring subfolder for database backup: " + subfolder);
util.ensureDirectoryExists(subfolder, function(err, created) {
if (err) {
var e = new Error("Error creating the backup subfolder: " + err.message);
logger.error(e);
if (callback) callback(e);
}
else {
//start backup to this subfolder
//mongodump --out /data/backup/ --db [DB NAME HERE]
logger.info("Running mongodump command for database backup.");
var dbname = config.settings.database_name;
runCommand("mongodump", ["--out", subfolder, "--db", dbname], function(text) {
//verify that there are contents in this folder
fs.readdir(subfolder, function(err, files){
try {
if (err) {
logger.error(err);
if (callback) callback(err);
}
else if (files && files.length > 0) {
//backup was successful
logger.info("Database backup saved to: " + subfolder);
if (settings.zip === true) {
logger.info("Zipping database backup: " + tempName);
zipBackup(subfolder, tempName, files, function (err, outpath) {
try {
if (err) {
if (callback) callback(err);
}
else {
logger.info("Zip file for backup created at: " + outpath);
//now delete the temp folder
logger.info("Deleting the temp folder: " + subfolder);
rimraf(subfolder, function (err) {
if (err) {
logger.error(err);
var e = new Error("Error deleting the folder: " + subfolder);
logger.error(e);
if (callback) callback(e);
}
else {
if (callback) callback(null, tempName + ".zip");
}
});
}
}
catch (e) {
logger.error(e);
if (callback) callback(e);
}
});
}
else {
//not zipping up...process complete
logger.info("Database backup complete: " + tempName);
if (callback) callback(null, tempName);
}
}
else {
var e = new Error("The backup did not create any files at: " + subfolder);
logger.error(e);
if (callback) callback(e);
}
}
catch(e) {
logger.error(e);
if (callback) callback(e);
}
});
});
}
});
});
}
catch(e) {
logger.error(e);
if (callback) callback(e);
}
};
function zipBackup(path, name, folders, callback) {
try {
var zip = new NodeZip();
//there should only be one item in the folders array, and that's the folder created by mongodump
var folderName = folders[0];
var folderPath = path_mod.join(path, folderName);
fs.readdir(folderPath, function(err, files) {
try {
if (err) {
logger.error(err);
callback(err);
}
else {
//loop through each file and add it to the zip file
async.eachSeries(files, function (fileName, done) {
var filePath = path_mod.join(folderPath, fileName);
//read the file contents
fs.readFile(filePath, function (err, data) {
try {
if (err) {
logger.error(err);
done(err);
}
else {
var entryName = folderName + '/' + fileName;
logger.info('Adding backup zip entry: ' + entryName);
zip.file(entryName, data);
done();
}
}
catch(e) {
logger.error(e);
done(e);
}
});
},
function(err) {
if (err)
callback(err);
else {
//now save the file to the backups folder
var zipPath = path + ".zip";
var options = {
type: 'nodebuffer',
compression: 'DEFLATE',
comment: "Database backup of " + config.settings.database_name
};
var buffer = zip.generate(options);
fs.writeFile(zipPath, buffer, function(err) {
if (err) {
logger.error(err);
callback(err);
}
else {
callback(null, zipPath);
}
});
}
});
}
}
catch(e) {
logger.error(e);
callback(e);
}
});
}
catch(e) {
logger.error(e);
callback(e);
}
}
/*
returns a list of backups in the default backups folder
callback: (err, backups[])
*/
module.exports.getBackups = function(callback) {
if (callback) {
try {
var path = config.settings.backups.path;
//fix the path to be absolute
if (path && path.indexOf("~/") == 0)
path = path_mod.join(cms.getRoot(), path.substr(2));
//get the zip files within this folder
fs.readdir(path, function(err, files) {
try {
if (err) {
logger.error(err);
if (callback) callback(err);
}
else if (files && files.length > 0) {
var backups = [];
for(var i = 0; i < files.length; i++) {
var filename = files[i];
if (S(filename).endsWith(".zip"))
backups.push(filename);
}
callback(null, backups);
}
}
catch (e) {
logger.error(e);
callback(e);
}
});
}
catch (e) {
logger.error(e);
callback(e);
}
}
};
/*
callback: (err, count)
*/
function countBackups(callback) {
if (callback) {
try {
var path = config.settings.backups.path;
//fix the path to be absolute
if (path && path.indexOf("~/") == 0)
path = path_mod.join(cms.getRoot(), path.substr(2));
//get the zip files within this folder
fs.readdir(path, function(err, files) {
try {
if (err) {
logger.error(err);
if (callback) callback(err);
}
else if (files) {
var count = 0;
for(var i = 0; i < files.length; i++) {
var filename = files[i];
if (S(filename).endsWith(".zip"))
count++;
}
callback(null, count);
}
else
callback(null, 0);
}
catch (e) {
logger.error(e);
callback(e);
}
});
}
catch (e) {
logger.error(e);
callback(e);
}
}
}
/*
Deletes the backup zip file with this name
callback: (err)
*/
module.exports.deleteBackup = function(name, callback) {
try {
logger.info("Attempting to delete database backup: " + name);
var path = module.exports.getBackupPath(name);
fs.unlink(path, function (err) {
if (err) {
logger.error(err);
if (callback) callback(err);
}
else {
logger.info("Database backup deleted: " + path);
callback(null);
}
});
}
catch (e) {
logger.error(e);
if (callback) callback(e);
}
};
/*
Unzips the backup zip file and restores it to (overwrites) the existing database.
callback: (err, text) - text is the text from the mongorestore command
*/
module.exports.restoreBackup = function(name, callback) {
try {
logger.info("Attempting to restore the database backup: " + name);
var path = module.exports.getBackupPath(name);
//first get the folder name that the zip will be extracted to
var dot = path.lastIndexOf(".");
if (dot > 0) {
var folder = path.substr(0, dot);
createRestoreFolder(folder, function(err) {
try {
if (err) {
if (callback) callback(err);
}
else {
unzipBackup(path, folder, function(err, backupPath) {
try {
if (err) {
if (callback) callback(err);
}
else if (backupPath) {
//execute mongo restore with the backupPath as the source
//mongorestore --drop --db [database name] [backup path]
var dbname = config.settings.database_name;
logger.info("Running mongorestore command for database: " + dbname);
runCommand("mongorestore", ["--drop", "--db", dbname, backupPath], function(text) {
//clear the cache
api.cache.removeAll(null);
async.series([
function(done){
//reload the templates
api.templates.loadTemplates(function(err) {
done(err);
});
},
function(done){
//reload the content types
api.contentTypes.loadContentTypes(function(err) {
done(err);
});
}
],
function(err, results) {
if (callback) callback(err, text);
});
});
}
else
throw new Error("Backup path not created successfully.");
}
catch(e) {
logger.error(e);
if (callback) callback(e);
}
});
}
}
catch(e) {
logger.error(e);
if (callback) callback(e);
}
});
}
else
throw new Error("The path does not have a file extension.");
}
catch (e) {
logger.error(e);
if (callback) callback(e);
}
};
/*
callback: (err, backupPath)
*/
function unzipBackup(path, folder, callback) {
//extract the zip file to this folder
fs.readFile(path, function (err, data) {
if (err) {
logger.error(err);
callback(err);
}
else {
try {
logger.info("Unzipping the database backup: " + path);
//unzip the package
var zipOptions = {
base64: false, //binary
checkCRC32: false
};
var zip = new NodeZip(data, zipOptions);
var entries = Object.keys(zip.files);
var backupPath = null;
async.eachSeries(entries, function (entry, done) {
logger.info("Unzipping backup entry: " + entry);
var isFolder = S(entry).endsWith('/');
if (isFolder === true) {
//create the folder
var dirPath = path_mod.join(folder, entry.substr(0, entry.length - 1));
logger.info("Creating folder: " + dirPath);
util.ensureDirectoryExists(dirPath, function(err, created) {
if (backupPath === null)
backupPath = dirPath;
done(err);
});
}
else {
//write the file
var fileBuffer = zip.file(entry).asNodeBuffer();
var filename = path_mod.join(folder, entry);
logger.info("Creating file: " + filename);
fs.writeFile(filename, fileBuffer, function(err) {
if (err) {
logger.error(err);
done(err);
}
else {
done(null);
}
});
}
},
function(err) {
if (err)
logger.error("Unzip backup failed with errors.");
else
logger.info("Unzip backup completed.");
callback(err, backupPath);
});
}
catch(e) {
logger.error(e);
callback(e);
}
}
});
}
function createRestoreFolder(path, callback) {
//if this folder already exists, delete it
fs.exists(path, function(exists) {
try {
if (exists == true) {
//delete the folder
rimraf(path, function (err) {
if (err) {
logger.error(err);
var e = new Error("Error deleting the folder: " + path);
logger.error(e);
callback(e);
}
else {
//create the folder (empty)
util.ensureDirectoryExists(path, function(err, created) {
if (err) {
logger.error(err);
callback(err);
}
else
callback(null);
});
}
});
}
else {
//create the folder
//create the folder (empty)
util.ensureDirectoryExists(path, function(err, created) {
if (err) {
logger.error(err);
callback(err);
}
else
callback(null);
});
}
}
catch(e) {
logger.error(e);
callback(e);
}
});
}
module.exports.getBackupPath = function(name, throwError) {
try {
//validate that the name ends with .zip and doesn't contain '..'
if (!name || !S(name).endsWith(".zip") || name.indexOf('..') > -1 || name.indexOf('./') > -1)
throw new Error('Invalid backup name: ' + name);
var path = config.settings.backups.path;
//fix the path to be absolute
if (path && path.indexOf("~/") == 0)
path = path_mod.join(cms.getRoot(), path.substr(2));
path = path_mod.join(path, name);
logger.info("getBackupPath - returning path: " + path);
return path;
}
catch(e) {
logger.error(e);
if (throwError === true)
throw e;
else
return null;
}
};
/*
Copies the backup file from the temp path (path) to the backups folder
options: {name, path, size, username }
callback: err, newPath, filename
*/
module.exports.uploadBackup = function(options, callback) {
try {
logger.info("Uploading backup file from: " + options.path);
if (options.path && options.name) {
//get the new path
var newPath = module.exports.getBackupPath(options.name, true);
if (newPath) {
logger.info("Moving backup file to: " + newPath);
fs.rename(options.path, newPath, function (err) {
if (err) {
logger.error(err);
if (callback) callback(err);
}
else {
logger.info("Backup file moved successfully.");
if (callback) callback(null, newPath, options.name);
}
});
}
else
throw new Error("Error retrieving backup path.");
}
else
throw new Error("Invalid path or name: path=" + options.path + "; name=" + options.name);
}
catch(e) {
logger.error(e);
if (callback) callback(e);
}
};
function runCommand(cmd, args, callBack ) {
var spawn = process.spawn;
var child = spawn(cmd, args);
var resp = "";
child.stdout.on('data', function (buffer) { resp += buffer.toString() });
child.stdout.on('end', function() { callBack (resp) });
}