markdown-note/markdown-notes

View on GitHub
src/common/settings.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

var _fs = require('fs');
var _path = require('path');
var _async = require('async');
const _ipcRenderer = require('electron').ipcRenderer;
var _i18n = require('i18n');

// Custom
var _appConfig = require(__dirname + '/../../config.js');
var _appUtil = require(_appConfig.commonsPath + 'utility.js');
var _appError = require(_appConfig.commonsPath + 'app-error.js');

var Settings = function() {
  var appSettings = null;

  var loadSettings = function() {
    try {
      var jsonStrSettings = _fs.readFileSync(_appConfig.settingsPath);
      if (jsonStrSettings && jsonStrSettings.toString().trim().length === 0) {
        // Empty settings, throw an error and let
        // the code handle it.
        var e = new Error('Empty settings!');
        e.code = 'ENOENT';
        throw e;
      }
      appSettings = JSON.parse(jsonStrSettings);
    } catch (e) {
      var err = new _appError(e, 'Error while reading the settings.');
      appSettings = getDefaultSettings();
      if (e.code === 'ENOENT') {
        // Creating the settings file
        saveAppSettings(appSettings);
      } else {
        // Invalid Settings, backup and then restore to defaults.
        handleInvalidSettings();
      }
    }
  };

  var getAppSettings = function(readFromFile) {
    if (!appSettings || readFromFile) {
      loadSettings();
    }
    return appSettings;
  };

  var saveAppSettings = function(settings, cbMain) {
    if (!settings) {
      return cbMain(new _appError(new Error('Invalid settings object passed!'),
        'Invalid settings object passed!'));
    }
    var settingsJSON = JSON.stringify(settings, null, 2);
    _fs.writeFile(_appConfig.settingsPath, settingsJSON, function(err) {
      var customErr = null;
      if (err) {
        customErr = new _appError(err, 'There was an error while storing the settings.');
      }
      appSettings = settings;
      if (cbMain) {
        return cbMain(customErr, null);
      }
    });
  };

  var updateAppSettings = function(newSettingsToApply, cbMain) {
    var oldSettings = getAppSettings(true);
    var settingsToApply = [];
    if (newSettingsToApply.dbLocation) {
      // Time to move the database.
      settingsToApply.push(function(next) {
        moveDbFiles(oldSettings.dbLocation, newSettingsToApply.dbLocation, next);
      });
    }

    _async.parallel(settingsToApply, function(err) {
      settingsToApply = null;
      if (err) {
        // TODO Do not apply any settings! Maybe show an error!
        return cbMain(err);
      }
      // Normalize and write the settings to file.
      var finalSettings = normalizeSettings(oldSettings, newSettingsToApply);
      newSettingsToApply = null;
      saveAppSettings(finalSettings, cbMain);
    });
  };

  function moveDbFiles(oldPath, newPath, cbMain) {
    var oldNotesPath = _path.normalize(oldPath + _path.sep +
      _appConfig.database.notes);
    var newNotesPath = _path.normalize(newPath + _path.sep +
      _appConfig.database.notes);

    var oldNotebookPath = _path.normalize(oldPath + _path.sep +
      _appConfig.database.notebooks);
    var newNotebookPath = _path.normalize(newPath + _path.sep +
      _appConfig.database.notebooks);

    _async.parallel([
      function(next) {
        _appUtil.mvFile(oldNotesPath, newNotesPath, next);
      },
      function(next) {
        _appUtil.mvFile(oldNotebookPath, newNotebookPath, next);
      }
    ], function(err) {
      if (err) {
        return cbMain(err);
      }
      return cbMain();
    });
  }


  /**
   * Compares the new settings with the old ones, and performs necessary updates.
   * Called when the user clicks on Save in the settings dialog. Applies whatever
   * settings that can be applied immediately and schedules the others to be
   * applied on restart.
   * @param  {object} newSettings New settings object, retrieved from the form.
   * @return {boolean}            True / False depending on success / failure.
   */
  var updateSettings = function(newSettings, cbMain) {
    var oldSettings = getAppSettings();
    var requiresRestart = false;
    var newSettingsToApply = {};
    if (newSettings.dbLocation !== oldSettings.dbLocation) {
      // Store the location of the new path and change when
      // app is about to restart.
      var newDbLocation = newSettings.dbLocation;
      newDbLocation += _path.sep;
      newDbLocation = _path.normalize(newDbLocation);
      newSettingsToApply.dbLocation = newDbLocation;
      requiresRestart = true;
    }

    if (newSettings.globalShortcut.toLowerCase() !==
      oldSettings.globalShortcut.toLowerCase()) {
      // Update the keystrokes
      var arg = {
        old: oldSettings.globalShortcut,
        new: newSettings.globalShortcut
      };
      if (!_ipcRenderer.sendSync('update-shortcut', arg)) {
        // TODO Show error message about shortcut register error.
        // Reset the value since there was an error.
        newSettings.globalShortcut = oldSettings.globalShortcut;
      }
    }

    // IMPORTANT - Delete the settings that will be applied on restart of app.
    delete newSettings.dbLocation;

    // Normalize and write the settings to file.
    var finalSettings = normalizeSettings(oldSettings, newSettings);
    saveAppSettings(finalSettings, function(err) {
      if (err) {
        return cbMain(err);
      }

      if (requiresRestart) {
        // Hook the updateAppSettings to be called on restart.
        var arg = {
          newSettings: newSettingsToApply
        };
        _ipcRenderer.send('settings-updated', arg);
      }
      newSettingsToApply = null;
      cbMain(null, requiresRestart);
    });
  };

  function normalizeSettings(oldSettings, newSettings) {
    for (var prop in newSettings) {
      if (!oldSettings.hasOwnProperty(prop)) {
        continue;
      }
      oldSettings[prop] = newSettings[prop];
    }
    return oldSettings;
  }

  /**
   * Called when the loading /parsing of the settings JSON fails, maybe
   * because of invalid JSON or permissions when reading the file.
   * Takes a backup of the existing file, and then writes the defaults to
   * the settings. If this method fails, the application will show an error,
   * and quit.
   * @return {undefined} No return type.
   */
  function handleInvalidSettings() {
    // 1. Take a backup of the existing settings file.
    _appUtil.mvFile(_appConfig.settingsPath,
      _appConfig.settingsPath + '_' + Date.now(),
      function(err) {
        if (err) {
          _ipcRenderer.sendSync('fatal-error', generateSettingsFatalError(err));
          return;
        }
        // 2. Write the defaults to settings.json
        saveAppSettings(appSettings, function(err) {
          if (err) {
            _ipcRenderer.sendSync('fatal-error', generateSettingsFatalError(err));
          }
        });
      });
  }

  /**
   * Generates a fatal APP Error. Called when handleInvalidSettings fails to
   * backup or write settings.
   * @return {AppError} An error object.
   */
  function generateSettingsFatalError(err) {
    return new _appError(err, _i18n.__('settings.settings_save_fatal') + '\n\n' +
      _i18n.__('settings.app_fatal_close') + '\n\n' + _i18n.__('settings.app_support'));
  }

  /**
   * Returns the default settings for the app, called on initial startup and
   * when the settings file is corrupted.
   * @return {Object} Default settings object
   */
  function getDefaultSettings() {
    return {
      "dbLocation": _appConfig.database.path,
      "globalShortcut": "D"
    };
  }

  return {
    saveAppSettings: saveAppSettings,
    getAppSettings: getAppSettings,
    loadSettings: loadSettings,
    updateAppSettings: updateAppSettings,
    updateSettings: updateSettings
  };
};

module.exports = new Settings();