UNPKG

sheercms

Version:

Sheer Cliff CMS is a simple and powerful content management system (CMS) for Node JS.

677 lines (606 loc) 24.6 kB
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) }); }