<commit_before>var formidable = Npm.require('formidable');
var http = Npm.require('http');
var sys = Npm.require('sys');
var connect = Npm.require('connect');
var path = Npm.require('path');
var fs = Npm.require('fs');

var _existsSync = fs.existsSync || path.existsSync;
var imageMagick = Npm.require('imagemagick');

var  options = {
  /** @type String*/
  tmpDir: null,
  /** @type String*/
  uploadDir: null,
  uploadUrl: '/upload/',
  maxPostSize: 11000000000, // 11 GB
  minFileSize: 1,
  maxFileSize: 10000000000, // 10 GB
  acceptFileTypes: /.+/i,
  // Files not matched by this regular expression force a download dialog,
  // to prevent executing any scripts in the context of the service domain:
  inlineFileTypes: /\.(gif|jpe?g|png)$/i,
  imageTypes: /\.(gif|jpe?g|png)$/i,
  imageVersions: {
    'thumbnail': {
      width: 200,
      height: 200
    }
  },
  getDirectory: function(file, formData) { return "" },
  getFileName: function(file, formData) { return file; },
  finished: function() {},
  accessControl: {
    allowOrigin: '*',
    allowMethods: 'OPTIONS, HEAD, GET, POST, PUT, DELETE',
    allowHeaders: 'Content-Type, Content-Range, Content-Disposition'
  }
  /* Uncomment and edit this section to provide the service via HTTPS:
   ssl: {
   key: fs.readFileSync('/Applications/XAMPP/etc/ssl.key/server.key'),
   cert: fs.readFileSync('/Applications/XAMPP/etc/ssl.crt/server.crt')
   },
   */
};

UploadServer = {
  init: function(opts) {
    if (opts.tmpDir == null) {
      throw new Meteor.Error('Temporary directory needs to be assigned!');
    } else {
      options.tmpDir = opts.tmpDir;
    }

    if (opts.uploadDir == null) {
      throw new Meteor.Error('Upload directory needs to be assigned!');
    } else {
      options.uploadDir = opts.uploadDir;
    }

    if (opts.maxPostSize != null) options.maxPostSize = opts.maxPostSize;
    if (opts.minFileSize != null) options.minFileSize = opts.maxPostSize;
    if (opts.maxFileSize != null) options.maxFileSize = opts.maxFileSize;
    if (opts.acceptFileTypes != null) options.acceptFileTypes = opts.acceptFileTypes;
    if (opts.imageTypes != null) options.imageTypes = opts.imageTypes;
    if (opts.getDirectory != null) options.getDirectory = opts.getDirectory;
    if (opts.getFileName != null) options.getFileName = opts.getFileName;
    if (opts.finished != null) options.finished = opts.finished;

    if (opts.uploadUrl) options.uploadUrl = opts.uploadUrl;

    if (opts.imageVersions != null) options.imageVersions = opts.imageVersions
    else options.imageVersions = [];
  },
  serve: function (req, res) {
    if (options.tmpDir == null || options.uploadDir == null) {
      throw new Meteor.Error('Upload component not initialised!');
    }

    res.setHeader(
      'Access-Control-Allow-Origin',
      options.accessControl.allowOrigin
    );
    res.setHeader(
      'Access-Control-Allow-Methods',
      options.accessControl.allowMethods
    );
    res.setHeader(
      'Access-Control-Allow-Headers',
      options.accessControl.allowHeaders
    );
    var handleResult = function (result, redirect) {
        if (redirect) {
          res.writeHead(302, {
            'Location': redirect.replace(
              /%s/,
              encodeURIComponent(JSON.stringify(result))
            )
          });
          res.end();
        } else {
          res.writeHead(200, {
            'Content-Type': req.headers.accept
              .indexOf('application/json') !== -1 ?
              'application/json' : 'text/plain'
          });
          res.end(JSON.stringify(result));
        }
      },
      setNoCacheHeaders = function () {
        res.setHeader('Pragma', 'no-cache');
        res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
        res.setHeader('Content-Disposition', 'inline; filename="files.json"');
      },
      handler = new UploadHandler(req, res, handleResult);

    switch (req.method) {
      case 'OPTIONS':
        res.end();
        break;
      case 'HEAD':
      case 'GET':
        setNoCacheHeaders();
        // TODO: Make safe url
        connect.static(options.uploadDir)(req, res);
        break;
      case 'POST':
        setNoCacheHeaders();
        handler.post();
        break;
      case 'DELETE':
        //handler.destroy();
        break;
      default:
        res.statusCode = 405;
        res.end();
    }
  }
}

var utf8encode = function (str) {
    return unescape(encodeURIComponent(str));
  };

var nameCountRegexp = /(?:(?: \(([\d]+)\))?(\.[^.]+))?$/;

var nameCountFunc = function (s, index, ext) {
    return ' (' + ((parseInt(index, 10) || 0) + 1) + ')' + (ext || '');
  };

var FileInfo = function (file) {
    this.name = file.name;
    this.size = file.size;
    this.type = file.type;
    this.deleteType = 'DELETE';
  };

var UploadHandler = function (req, res, callback) {
    this.req = req;
    this.res = res;
    this.callback = callback;
};

FileInfo.prototype.validate = function () {
  if (options.minFileSize && options.minFileSize > this.size) {
    this.error = 'File is too small';
  } else if (options.maxFileSize && options.maxFileSize < this.size) {
    this.error = 'File is too big';
  } else if (!options.acceptFileTypes.test(this.name)) {
    this.error = 'Filetype not allowed';
  }
  return !this.error;
};

FileInfo.prototype.safeName = function () {
  // Prevent directory traversal and creating hidden system files:
  this.name = path.basename(this.name).replace(/^\.+/, '');
  // Prevent overwriting existing files:
  while (_existsSync(options.uploadDir + '/' + this.name)) {
    this.name = this.name.replace(nameCountRegexp, nameCountFunc);
  }
};

FileInfo.prototype.initUrls = function (req, form) {
  if (!this.error) {
    var that = this,
      subDirectory = options.getDirectory(this.name, form.formFields),
      baseUrl = (options.ssl ? 'https:' : 'http:') +
        '//' + req.headers.host + options.uploadUrl;
    this.url = this.deleteUrl = baseUrl + (subDirectory ? (subDirectory + '/') : '') + encodeURIComponent(this.name);
    Object.keys(options.imageVersions).forEach(function (version) {
      if (_existsSync(
          options.uploadDir + '/' + version + '/' + that.name
        )) {
        that[version + 'Url'] = baseUrl + version + '/' +
        encodeURIComponent(that.name);
      }
    });
  }
};

UploadHandler.prototype.get = function () {
  var handler = this,
    files = [];
  fs.readdir(options.uploadDir, function (err, list) {
    list.forEach(function (name) {
      var stats = fs.statSync(options.uploadDir + '/' + name),
        fileInfo;
      if (stats.isFile() && name[0] !== '.') {
        fileInfo = new FileInfo({
          name: name,
          size: stats.size
        });
        fileInfo.initUrls(handler.req);
        files.push(fileInfo);
      }
    });
    handler.callback({files: files});
  });
};

UploadHandler.prototype.post = function () {
  var handler = this,
    form = new formidable.IncomingForm(),
    tmpFiles = [],
    files = [],
    map = {},
    counter = 1,
    redirect,
    finish = function () {
      counter -= 1;
      if (!counter) {
        files.forEach(function (fileInfo) {
          fileInfo.initUrls(handler.req, form);
        });
        handler.callback({files: files}, redirect);
      }
    };
  form.uploadDir = options.tmpDir;
  form.on('fileBegin', function (name, file) {
    tmpFiles.push(file.path);
    var fileInfo = new FileInfo(file, handler.req, true);
    fileInfo.safeName();
    map[path.basename(file.path)] = fileInfo;
    files.push(fileInfo);
  }).on('field', function (name, value) {
    if (name === 'redirect') {
      redirect = value;
    }
    // remember all the form fields
    if (this.formFields == null) {
      this.formFields = {};
    }
    this.formFields[name] = value;
  }).on('file', function (name, file) {
    var fileInfo = map[path.basename(file.path)];
    fileInfo.size = file.size;
    if (!fileInfo.validate()) {
      fs.unlink(file.path);
      return;
    }

    var folder = options.getDirectory(fileInfo.name, this.formFields);
    // check if directory exists, if not, create all the directories
    var subFolders = folder.split('/');
    var currentFolder = options.uploadDir;
    for (var i = 0; i < subFolders.length; i++) {
      currentFolder += '/' + subFolders[i];

      if (!fs.existsSync(currentFolder)) {
        fs.mkdirSync(currentFolder);
      }
    }

    // possibly rename file if needed;
    var newFileName = options.getFileName(fileInfo.name, this.formFields);

    fs.renameSync(file.path, currentFolder + "/" + newFileName);

    if (options.imageTypes.test(fileInfo.name)) {
      Object.keys(options.imageVersions).forEach(function (version) {
        counter += 1;
        var opts = options.imageVersions[version];

        // check if version directory exists
        if (!fs.existsSync(currentFolder + '/' + version)) {
          fs.mkdirSync(currentFolder + '/' + version);
        }

        imageMagick.resize({
          width: opts.width,
          height: opts.height,
          srcPath: currentFolder + '/' + newFileName,
          dstPath: currentFolder + '/' + version + '/' + newFileName
        }, finish);
      });
    }

    // call the feedback
    options.finished(newFileName, folder, this.formFields);
  }).on('aborted', function () {
    tmpFiles.forEach(function (file) {
      fs.unlink(file);
    });
  }).on('error', function (e) {
    console.log(e);
  }).on('progress', function (bytesReceived, bytesExpected) {
    if (bytesReceived > options.maxPostSize) {
      handler.req.connection.destroy();
    }
  }).on('end', finish).parse(handler.req);
};

UploadHandler.prototype.destroy = function () {
  var handler = this,
    fileName;
  if (handler.req.url.slice(0, options.uploadUrl.length) === options.uploadUrl) {
    fileName = path.basename(decodeURIComponent(handler.req.url));
    if (fileName[0] !== '.') {
      fs.unlink(options.uploadDir + '/' + fileName, function (ex) {
        Object.keys(options.imageVersions).forEach(function (version) {
          fs.unlink(options.uploadDir + '/' + version + '/' + fileName);
        });
        handler.callback({success: !ex});
      });
      return;
    }
  }
  handler.callback({success: false});
};

// declare routes

RoutePolicy.declare(options.uploadUrl, 'network');
WebApp.connectHandlers.use(options.uploadUrl, UploadServer.serve);

<commit_msg>Upload now returns a "path" describing the server path.<commit_after>var formidable = Npm.require('formidable');
var http = Npm.require('http');
var sys = Npm.require('sys');
var connect = Npm.require('connect');
var path = Npm.require('path');
var fs = Npm.require('fs');

var _existsSync = fs.existsSync || path.existsSync;
var imageMagick = Npm.require('imagemagick');

var  options = {
  /** @type String*/
  tmpDir: null,
  /** @type String*/
  uploadDir: null,
  uploadUrl: '/upload/',
  maxPostSize: 11000000000, // 11 GB
  minFileSize: 1,
  maxFileSize: 10000000000, // 10 GB
  acceptFileTypes: /.+/i,
  // Files not matched by this regular expression force a download dialog,
  // to prevent executing any scripts in the context of the service domain:
  inlineFileTypes: /\.(gif|jpe?g|png)$/i,
  imageTypes: /\.(gif|jpe?g|png)$/i,
  imageVersions: {
    'thumbnail': {
      width: 200,
      height: 200
    }
  },
  getDirectory: function(file, formData) { return "" },
  getFileName: function(file, formData) { return file; },
  finished: function() {},
  accessControl: {
    allowOrigin: '*',
    allowMethods: 'OPTIONS, HEAD, GET, POST, PUT, DELETE',
    allowHeaders: 'Content-Type, Content-Range, Content-Disposition'
  }
  /* Uncomment and edit this section to provide the service via HTTPS:
   ssl: {
   key: fs.readFileSync('/Applications/XAMPP/etc/ssl.key/server.key'),
   cert: fs.readFileSync('/Applications/XAMPP/etc/ssl.crt/server.crt')
   },
   */
};

UploadServer = {
  init: function(opts) {
    if (opts.tmpDir == null) {
      throw new Meteor.Error('Temporary directory needs to be assigned!');
    } else {
      options.tmpDir = opts.tmpDir;
    }

    if (opts.uploadDir == null) {
      throw new Meteor.Error('Upload directory needs to be assigned!');
    } else {
      options.uploadDir = opts.uploadDir;
    }

    if (opts.maxPostSize != null) options.maxPostSize = opts.maxPostSize;
    if (opts.minFileSize != null) options.minFileSize = opts.maxPostSize;
    if (opts.maxFileSize != null) options.maxFileSize = opts.maxFileSize;
    if (opts.acceptFileTypes != null) options.acceptFileTypes = opts.acceptFileTypes;
    if (opts.imageTypes != null) options.imageTypes = opts.imageTypes;
    if (opts.getDirectory != null) options.getDirectory = opts.getDirectory;
    if (opts.getFileName != null) options.getFileName = opts.getFileName;
    if (opts.finished != null) options.finished = opts.finished;

    if (opts.uploadUrl) options.uploadUrl = opts.uploadUrl;

    if (opts.imageVersions != null) options.imageVersions = opts.imageVersions
    else options.imageVersions = [];
  },
  serve: function (req, res) {
    if (options.tmpDir == null || options.uploadDir == null) {
      throw new Meteor.Error('Upload component not initialised!');
    }

    res.setHeader(
      'Access-Control-Allow-Origin',
      options.accessControl.allowOrigin
    );
    res.setHeader(
      'Access-Control-Allow-Methods',
      options.accessControl.allowMethods
    );
    res.setHeader(
      'Access-Control-Allow-Headers',
      options.accessControl.allowHeaders
    );
    var handleResult = function (result, redirect) {
        if (redirect) {
          res.writeHead(302, {
            'Location': redirect.replace(
              /%s/,
              encodeURIComponent(JSON.stringify(result))
            )
          });
          res.end();
        } else {
          res.writeHead(200, {
            'Content-Type': req.headers.accept
              .indexOf('application/json') !== -1 ?
              'application/json' : 'text/plain'
          });
          res.end(JSON.stringify(result));
        }
      },
      setNoCacheHeaders = function () {
        res.setHeader('Pragma', 'no-cache');
        res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
        res.setHeader('Content-Disposition', 'inline; filename="files.json"');
      },
      handler = new UploadHandler(req, res, handleResult);

    switch (req.method) {
      case 'OPTIONS':
        res.end();
        break;
      case 'HEAD':
      case 'GET':
        setNoCacheHeaders();
        // TODO: Make safe url
        connect.static(options.uploadDir)(req, res);
        break;
      case 'POST':
        setNoCacheHeaders();
        handler.post();
        break;
      //case 'DELETE':
      //  handler.destroy();
      //  break;
      default:
        res.statusCode = 405;
        res.end();
    }
  }
}

var utf8encode = function (str) {
    return unescape(encodeURIComponent(str));
  };

var nameCountRegexp = /(?:(?: \(([\d]+)\))?(\.[^.]+))?$/;

var nameCountFunc = function (s, index, ext) {
    return ' (' + ((parseInt(index, 10) || 0) + 1) + ')' + (ext || '');
  };

var FileInfo = function (file) {
    this.name = file.name;
    this.size = file.size;
    this.type = file.type;
  };

var UploadHandler = function (req, res, callback) {
    this.req = req;
    this.res = res;
    this.callback = callback;
};

FileInfo.prototype.validate = function () {
  if (options.minFileSize && options.minFileSize > this.size) {
    this.error = 'File is too small';
  } else if (options.maxFileSize && options.maxFileSize < this.size) {
    this.error = 'File is too big';
  } else if (!options.acceptFileTypes.test(this.name)) {
    this.error = 'Filetype not allowed';
  }
  return !this.error;
};

FileInfo.prototype.safeName = function () {
  // Prevent directory traversal and creating hidden system files:
  this.name = path.basename(this.name).replace(/^\.+/, '');
  // Prevent overwriting existing files:
  while (_existsSync(options.uploadDir + '/' + this.name)) {
    this.name = this.name.replace(nameCountRegexp, nameCountFunc);
  }
};

FileInfo.prototype.initUrls = function (req, form) {
  if (!this.error) {
    var that = this,
      subDirectory = options.getDirectory(this.name, form.formFields),
      baseUrl = (options.ssl ? 'https:' : 'http:') +
        '//' + req.headers.host + options.uploadUrl;
    this.url = baseUrl + (subDirectory ? (subDirectory + '/') : '') + encodeURIComponent(this.name);
    Object.keys(options.imageVersions).forEach(function (version) {
      if (_existsSync(
          options.uploadDir + '/' + version + '/' + that.name
        )) {
        that[version + 'Url'] = baseUrl + version + '/' +
        encodeURIComponent(that.name);
      }
    });
  }
};

UploadHandler.prototype.post = function () {
  var handler = this,
    form = new formidable.IncomingForm(),
    tmpFiles = [],
    files = [],
    map = {},
    counter = 1,
    redirect,
    finish = function () {
      counter -= 1;
      if (!counter) {
        files.forEach(function (fileInfo) {
          fileInfo.initUrls(handler.req, form);
        });
        handler.callback({files: files}, redirect);
      }
    };
  form.uploadDir = options.tmpDir;
  form.on('fileBegin', function (name, file) {
    tmpFiles.push(file.path);
    var fileInfo = new FileInfo(file, handler.req, true);
    fileInfo.safeName();
    map[path.basename(file.path)] = fileInfo;
    files.push(fileInfo);
  }).on('field', function (name, value) {
    if (name === 'redirect') {
      redirect = value;
    }
    // remember all the form fields
    if (this.formFields == null) {
      this.formFields = {};
    }
    this.formFields[name] = value;
  }).on('file', function (name, file) {
    var fileInfo = map[path.basename(file.path)];
    fileInfo.size = file.size;
    if (!fileInfo.validate()) {
      fs.unlink(file.path);
      return;
    }

    // we can store files in subdirectories
    var folder = options.getDirectory(fileInfo.name, this.formFields);
    // check if directory exists, if not, create all the directories
    var subFolders = folder.split('/');
    var currentFolder = options.uploadDir;
    for (var i = 0; i < subFolders.length; i++) {
      currentFolder += '/' + subFolders[i];

      if (!fs.existsSync(currentFolder)) {
        fs.mkdirSync(currentFolder);
      }
    }

    // possibly rename file if needed;
    var newFileName = options.getFileName(fileInfo.name, this.formFields);

    // set the file name
    fileInfo.path = folder + "/" + newFileName;

    fs.renameSync(file.path, currentFolder + "/" + newFileName);

    if (options.imageTypes.test(fileInfo.name)) {
      Object.keys(options.imageVersions).forEach(function (version) {
        counter += 1;
        var opts = options.imageVersions[version];

        // check if version directory exists
        if (!fs.existsSync(currentFolder + '/' + version)) {
          fs.mkdirSync(currentFolder + '/' + version);
        }

        imageMagick.resize({
          width: opts.width,
          height: opts.height,
          srcPath: currentFolder + '/' + newFileName,
          dstPath: currentFolder + '/' + version + '/' + newFileName
        }, finish);
      });
    }

    // call the feedback
    options.finished(newFileName, folder, this.formFields);
  }).on('aborted', function () {
    tmpFiles.forEach(function (file) {
      fs.unlink(file);
    });
  }).on('error', function (e) {
    console.log(e);
  }).on('progress', function (bytesReceived, bytesExpected) {
    if (bytesReceived > options.maxPostSize) {
      handler.req.connection.destroy();
    }
  }).on('end', finish).parse(handler.req);
};

UploadHandler.prototype.destroy = function () {
  var handler = this,
    fileName;
  if (handler.req.url.slice(0, options.uploadUrl.length) === options.uploadUrl) {
    fileName = path.basename(decodeURIComponent(handler.req.url));
    if (fileName[0] !== '.') {
      fs.unlink(options.uploadDir + '/' + fileName, function (ex) {
        Object.keys(options.imageVersions).forEach(function (version) {
          fs.unlink(options.uploadDir + '/' + version + '/' + fileName);
        });
        handler.callback({success: !ex});
      });
      return;
    }
  }
  handler.callback({success: false});
};

// declare routes

RoutePolicy.declare(options.uploadUrl, 'network');
WebApp.connectHandlers.use(options.uploadUrl, UploadServer.serve);

<|endoftext|>


<commit_before>export const posthtml = [
  'posthtml-extend',
  'posthtml-modules',
  'posthtml-posthtml-modules',
  'posthtml-include',
  'posthtml-bem',
  'posthtml-custom-elements',
  'posthtml-inline-assets',
  'posthtml-inline-css',
  'posthtml-remove-tags',
  'posthtml-class-to-css-module',
  'posthtml-remove-attributes',
  'posthtml-css-modules',
  'posthtml-beautify',
  'posthtml-spaceless',
  'posthtml-reporter',
  'posthtml-nanohtml'
];

export const postcss = [
  'postcss-devtools',
  'postcss-easy-import',
  'postcss-import',
  'postcss-math',
  'postcss-each',
  'postcss-mixins',
  'postcss-at-rules-variables',
  'postcss-custom-properties',
  'postcss-for',
  'postcss-conditionals',
  'postcss-nested',
  'postcss-sorting',
  'postcss-style-guide',
  'postcss-discard-comments',
  'postcss-csso'
];

export const reshape = {};
<commit_msg>perf: add export default<commit_after>export const posthtml = [
  'posthtml-extend',
  'posthtml-modules',
  'posthtml-posthtml-modules',
  'posthtml-include',
  'posthtml-bem',
  'posthtml-custom-elements',
  'posthtml-inline-assets',
  'posthtml-inline-css',
  'posthtml-remove-tags',
  'posthtml-class-to-css-module',
  'posthtml-remove-attributes',
  'posthtml-css-modules',
  'posthtml-beautify',
  'posthtml-spaceless',
  'posthtml-reporter',
  'posthtml-nanohtml'
];

export const postcss = [
  'postcss-devtools',
  'postcss-easy-import',
  'postcss-import',
  'postcss-math',
  'postcss-each',
  'postcss-mixins',
  'postcss-at-rules-variables',
  'postcss-custom-properties',
  'postcss-for',
  'postcss-conditionals',
  'postcss-nested',
  'postcss-sorting',
  'postcss-style-guide',
  'postcss-discard-comments',
  'postcss-csso'
];

export const reshape = {};

export default {posthtml, postcss, reshape}
<|endoftext|>


<commit_before>// # Graceful
// Encapsulates everything needed to shut down a worker process without interrupting
// anything important, even active requests on an http server.

// [strict mode](http://mzl.la/1fRhnam)
'use strict';

var cluster = require('cluster');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

var winston = require('winston');

var localUtil = require('./util');

/*
The `constructor` has no required parameters. Optional parameters:

+ `pollInterval` - how often to attempt to take down process by first checking all
registered 'ready to stop process' check functions
+ `timeout` - how long to wait for the 'ready to stop process' checks before taking the
process down forcefully
+ `messenger` - a `function(err, options, cb)` that gets the information supplied to the
`shutdown()` method. Defaults to `thehelp-last-ditch`.
+ `log` - an object that looks like `winston`, allowing you to use your own logging
system: `info`, `warn` and `error` keys with the signature `function(string)`.

_Note: it's recommended to create an instance of this class once per process, since it
listens for a number of process-level events, making itself the handler for them. See
`_setupListeners()` below._
*/
function Graceful(options) {
  /*jshint maxcomplexity: 8 */

  options = options || {};

  this.closed = false;

  this.pollInterval = options.pollInterval || 250;
  this.timeout = options.timeout || 5 * 1000;
  this.messenger = options.messenger || require('thehelp-last-ditch');

  this.checks = [];
  this.sending = false;

  var _this = this;
  this.addCheck(function areWeSending() {
    return _this.sending === false;
  });

  this.process = options.process || process;
  this.cluster = options.cluster || cluster;
  this.log = options.log || winston;

  this.logPrefix = this._getLogPrefix();
  this._setupListeners();

  Graceful.instance = this;
}

util.inherits(Graceful, EventEmitter);

module.exports = Graceful;

/*
`shutdown` is the key method for interacting with this class. When something goes
wrong, call this method with the error and any additional information (like the `url`
being serviced). The http server will be stopped, the error will be saved/sent via
`this.messenger` and we'll start the process of checking to see if we can take down the
process via `this._exit()`.

_Note: to be notified when this method is called, register for the 'shutdown' event
(`Graceful` is an `EventEmitter`):_

```
graceful.on('shutdown', function() {
  // do stuff to prepare for shutdown
})
```
*/
Graceful.prototype.shutdown = function shutdown(err, info) {
  if (!this.closed) {
    this.closed = true;
    this.error = err;

    this.log.warn(this.logPrefix + ' gracefully shutting down!');
    this._sendError(err, info);
    this.emit('shutdown');
    this._exit();
  }
};

// `addCheck` allows you to provide a callback you can use to delay `process.exit()` until
// you are finished doing something.
Graceful.prototype.addCheck = function addCheck(check) {
  this.checks.push(check);
};

// Helper methods
// ========

// `_sendError` uses `this.messenger` to save/send the error provided to `shutdown()`. It
// it sets `this.sending` to `true` so we won't take the process down before the call is
// complete.
Graceful.prototype._sendError = function _sendError(err, info) {
  var _this = this;

  if (err) {
    this.sending = true;

    this.messenger(err, info, function() {
      _this.sending = false;
    });
  }
};

// `_check` returns true if all registered check methods returned true.
Graceful.prototype._check = function _check() {
  if (!this.checks || !this.checks.length) {
    return true;
  }

  for (var i = 0, max = this.checks.length; i < max; i += 1) {
    var check = this.checks[i];

    if (!check()) {
      return false;
    }
  }

  return true;
};

// `_exit` _exits if `check()` returns true. Otherwise it sets an interval to continue
// trying. It also sets a timer - if we never get a successful `check()` call, we
// take down the process anyway.
Graceful.prototype._exit = function _exit() {
  var _this = this;

  this.log.info(this.logPrefix + ' calling all provided pre-exit check functions...');

  if (this.closed && this._check()) {
    _this._clearTimers();
    _this._finalLog('info', this.logPrefix + ' passed all checks! Shutting down!');
  }
  else if (!this.interval) {
    this.interval = setInterval(function() {
      _this._exit();
    }, this.pollInterval);

    this.timeout = setTimeout(function() {
      _this._clearTimers();
      _this._finalLog('warn', this.logPrefix + ' checks took too long. Killing process!');
    }, this.timeout);
  }
};

// `_clearTimers` properly gets rid of the timers created in `_exit()`
Graceful.prototype._clearTimers = function _clearTimers() {
  if (this.interval) {
    clearInterval(this.interval);
    this.interval = null;
  }
  if (this.timeout) {
    clearTimeout(this.timeout);
    this.timeout = null;
  }
};

/*
`_finalLog` makes a final winston log, and takes down the process when winston tells us
that the log is complete.

Unfortunately, because sometimes winston gets a bit messed up after unhandled exceptions,
we also set a timer to make sure to take process down even if winston doesn't call the
callback.
*/
Graceful.prototype._finalLog = function _finalLog(type, message) {
  var _this = this;

  var die = localUtil.once(function() {
    _this._die();
  });

  this.log[type](message, function(err, level, msg, meta) {
    /*jshint unused: false */
    die();
  });

  setTimeout(die, 250);
};

// `_die` calls `process._exit()` with the right error code based on `this.error` (set in
// `shutdown()`).
Graceful.prototype._die = function _die() {
  var code = this.error ? this.error.code || 1 : 0;
  this.process.exit(code);
};

// `_getLogPrefix` helps differentiate between the various master/worker processes.
Graceful.prototype._getLogPrefix = function _getLogPrefix() {
  var cluster = this.cluster;

  if (cluster.worker) {
    var id = cluster.worker.id;
    return 'Worker #' + id;
  }
  else {
    return 'Master';
  }
};

// `_setupListeners` sets up some event wireups. We start the shutdown process when the
// process gets a 'SIGTERM' signal, or when the master worker disconnects. And we log
// on process exit.
Graceful.prototype._setupListeners = function _setupListeners() {
  var _this = this;
  var cluster = this.cluster;
  var process = this.process;

  process.on('SIGTERM', function gracefulShutdown() {
    _this.shutdown();
  });

  process.on('exit', function(code) {
    _this.log.warn(_this.logPrefix + ' about to exit with code', code);
  });

  if (cluster.worker) {
    cluster.worker.on('disconnect', function gracefulShutdown() {
      _this.shutdown();
    });
  }
};
<commit_msg>Graceful: fix logging, and too-long line<commit_after>// # Graceful
// Encapsulates everything needed to shut down a worker process without interrupting
// anything important, even active requests on an http server.

// [strict mode](http://mzl.la/1fRhnam)
'use strict';

var cluster = require('cluster');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

var winston = require('winston');

var localUtil = require('./util');

/*
The `constructor` has no required parameters. Optional parameters:

+ `pollInterval` - how often to attempt to take down process by first checking all
registered 'ready to stop process' check functions
+ `timeout` - how long to wait for the 'ready to stop process' checks before taking the
process down forcefully
+ `messenger` - a `function(err, options, cb)` that gets the information supplied to the
`shutdown()` method. Defaults to `thehelp-last-ditch`.
+ `log` - an object that looks like `winston`, allowing you to use your own logging
system: `info`, `warn` and `error` keys with the signature `function(string)`.

_Note: it's recommended to create an instance of this class once per process, since it
listens for a number of process-level events, making itself the handler for them. See
`_setupListeners()` below._
*/
function Graceful(options) {
  /*jshint maxcomplexity: 8 */

  options = options || {};

  this.closed = false;

  this.pollInterval = options.pollInterval || 250;
  this.timeout = options.timeout || 5 * 1000;
  this.messenger = options.messenger || require('thehelp-last-ditch');

  this.checks = [];
  this.sending = false;

  var _this = this;
  this.addCheck(function areWeSending() {
    return _this.sending === false;
  });

  this.process = options.process || process;
  this.cluster = options.cluster || cluster;
  this.log = options.log || winston;

  this.logPrefix = this._getLogPrefix();
  this._setupListeners();

  Graceful.instance = this;
}

util.inherits(Graceful, EventEmitter);

module.exports = Graceful;

/*
`shutdown` is the key method for interacting with this class. When something goes
wrong, call this method with the error and any additional information (like the `url`
being serviced). The http server will be stopped, the error will be saved/sent via
`this.messenger` and we'll start the process of checking to see if we can take down the
process via `this._exit()`.

_Note: to be notified when this method is called, register for the 'shutdown' event
(`Graceful` is an `EventEmitter`):_

```
graceful.on('shutdown', function() {
  // do stuff to prepare for shutdown
})
```
*/
Graceful.prototype.shutdown = function shutdown(err, info) {
  if (!this.closed) {
    this.closed = true;
    this.error = err;

    this.log.warn(this.logPrefix + ' gracefully shutting down!');
    this._sendError(err, info);
    this.emit('shutdown');
    this._exit();
  }
};

// `addCheck` allows you to provide a callback you can use to delay `process.exit()` until
// you are finished doing something.
Graceful.prototype.addCheck = function addCheck(check) {
  this.checks.push(check);
};

// Helper methods
// ========

// `_sendError` uses `this.messenger` to save/send the error provided to `shutdown()`. It
// it sets `this.sending` to `true` so we won't take the process down before the call is
// complete.
Graceful.prototype._sendError = function _sendError(err, info) {
  var _this = this;

  if (err) {
    this.sending = true;

    this.messenger(err, info, function() {
      _this.sending = false;
    });
  }
};

// `_check` returns true if all registered check methods returned true.
Graceful.prototype._check = function _check() {
  if (!this.checks || !this.checks.length) {
    return true;
  }

  for (var i = 0, max = this.checks.length; i < max; i += 1) {
    var check = this.checks[i];

    if (!check()) {
      return false;
    }
  }

  return true;
};

// `_exit` _exits if `check()` returns true. Otherwise it sets an interval to continue
// trying. It also sets a timer - if we never get a successful `check()` call, we
// take down the process anyway.
Graceful.prototype._exit = function _exit() {
  var _this = this;

  this.log.info(this.logPrefix + ' calling all provided pre-exit check functions...');

  if (this.closed && this._check()) {
    _this._clearTimers();
    _this._finalLog('info', this.logPrefix + ' passed all checks! Shutting down!');
  }
  else if (!this.interval) {
    this.interval = setInterval(function() {
      _this._exit();
    }, this.pollInterval);

    this.timeout = setTimeout(function() {
      _this._clearTimers();
      _this._finalLog('warn', _this.logPrefix + ' checks took too long. ' +
        'Killing process now!');

    }, this.timeout);
  }
};

// `_clearTimers` properly gets rid of the timers created in `_exit()`
Graceful.prototype._clearTimers = function _clearTimers() {
  if (this.interval) {
    clearInterval(this.interval);
    this.interval = null;
  }
  if (this.timeout) {
    clearTimeout(this.timeout);
    this.timeout = null;
  }
};

/*
`_finalLog` makes a final winston log, and takes down the process when winston tells us
that the log is complete.

Unfortunately, because sometimes winston gets a bit messed up after unhandled exceptions,
we also set a timer to make sure to take process down even if winston doesn't call the
callback.
*/
Graceful.prototype._finalLog = function _finalLog(type, message) {
  var _this = this;

  var die = localUtil.once(function() {
    _this._die();
  });

  this.log[type](message, function(err, level, msg, meta) {
    /*jshint unused: false */
    die();
  });

  setTimeout(die, 250);
};

// `_die` calls `process._exit()` with the right error code based on `this.error` (set in
// `shutdown()`).
Graceful.prototype._die = function _die() {
  var code = this.error ? this.error.code || 1 : 0;
  this.process.exit(code);
};

// `_getLogPrefix` helps differentiate between the various master/worker processes.
Graceful.prototype._getLogPrefix = function _getLogPrefix() {
  var cluster = this.cluster;

  if (cluster.worker) {
    var id = cluster.worker.id;
    return 'Worker #' + id;
  }
  else {
    return 'Master';
  }
};

// `_setupListeners` sets up some event wireups. We start the shutdown process when the
// process gets a 'SIGTERM' signal, or when the master worker disconnects. And we log
// on process exit.
Graceful.prototype._setupListeners = function _setupListeners() {
  var _this = this;
  var cluster = this.cluster;
  var process = this.process;

  process.on('SIGTERM', function gracefulShutdown() {
    _this.shutdown();
  });

  process.on('exit', function(code) {
    _this.log.warn(_this.logPrefix + ' about to exit with code', code);
  });

  if (cluster.worker) {
    cluster.worker.on('disconnect', function gracefulShutdown() {
      _this.shutdown();
    });
  }
};
<|endoftext|>


<commit_before>
Services = new Mongo.Collection("services");

FlowRouter.route('/gig/:id', {
	action: function(params) {
		var service = Services.findOne({_id: params.id});
        console.log("Flowrouter.route");

		console.log(service);
        //Will either bring in the service or have an empty one so there are no errors when bringing up service.employer
        service = service || {}
        //used for displaying the apply button for employees (see servicelistingpage.html)
        service.isUserEmployer = service.employer === Meteor.userId();

        //debug logs
        console.log(this);
        console.log(Meteor.userId());

        // get employer user object
        var user = Meteor.users.findOne({_id: service.employer});
        console.log(user);
        console.log(service.employer);

		BlazeLayout.render("layout", {
			area: "serviceListingPage",
			params: params,
			service: service,
			employer: user,
			employerName: user.emails[0].address
		});
	}
});

FlowRouter.route('/postgig', {
	action: function(params) {
		BlazeLayout.render("layout", {area: "servicePostForm"});
	}
});

if (Meteor.isServer) {

	Meteor.publish("services", function () {
		return Services.find({});
	});

	Meteor.methods({
		"createService": function(serviceObj) {

            if (!this.userId) {
				console.log("not logged in");
				return false;
			}

			if (!serviceObj) {
				console.log("no service obj");
				return false;
			}

			if (typeof(serviceObj.title) != "string" || serviceObj.title.length == 0) {
				console.log("no title");
				return false;
			}

			if (typeof(serviceObj.description) != "string" || serviceObj.description.length == 0) {
				console.log("no descript");
				return false;
			}

			if ( Number.isNaN(parseInt(serviceObj.wage, 10)) ) {
				console.log("no wage");
				return false;
			}

			var newService = Services.insert({
				employer: this.userId,
				title: serviceObj.title,
				description: serviceObj.description,
				wage: serviceObj.wage,
			});

			return newService;
		}
	});

}

if (Meteor.isClient) {

	Meteor.subscribe("services");
    Template.serviceListingPage.helpers({
       isLoggedIn: function() {
        return Meteor.userId() !== null;
       }
    });
    Template.serviceListingPage.events({
        "click #apply": function(event) {
            event.target.innerHTML = "Thank you for applying";
        }
    });

	Template.servicePostForm.events({
		"input #serviceWage": function(event) {
			event.target.value = event.target.value.replace(/\D/g, "");
		},

		"submit .servicePostForm": function() {
			event.preventDefault();

			Meteor.call("createService", {
				title: event.target.serviceTitle.value,
				description: event.target.serviceDescription.value,
				wage: parseFloat(event.target.serviceWage.value)
			}, function(err, id) {
				if (id) {
					FlowRouter.go("/gig/" + id);
				} else {
					console.log("Failure! Handle this!");
				}
			});

		}
	});

}
/*
Services = new Mongo.Collection("services");

FlowRouter.route('/gig/:id', {
	action: function(params) {
		var service = Services.findOne({_id: params.id});

		console.log(service);

		BlazeLayout.render("layout", {
			area: "serviceListingPage",
			params: params,
			service: service
		});
	}
});

FlowRouter.route('/postgig', {
	action: function(params) {
		BlazeLayout.render("layout", {area: "servicePostForm"});
	}
});

if (Meteor.isServer) {

	Meteor.publish("services", function () {
		return Services.find({});
	});

	Meteor.methods({
		"createService": function(serviceObj) {
			if (!this.userId) {
				console.log("not logged in");
				return false;
			}

			if (!serviceObj) {
				console.log("no service obj");
				return false;
			}

			if (typeof(serviceObj.title) != "string" || serviceObj.title.length == 0) {
				console.log("no title");
				return false;
			}

			if (typeof(serviceObj.description) != "string" || serviceObj.description.length == 0) {
				console.log("no descript");
				return false;
			}

			if (typeof(serviceObj.wage) != "number" || (new String(serviceObj.wage)).length == 0) {
				console.log("no wage");
				return false;
			}

			var newService = Services.insert({
				employer: this.userId,
				title: serviceObj.title,
				description: serviceObj.description,
				wage: serviceObj.wage,
			});

			return newService;
		}
	});

}

if (Meteor.isClient) {

	Meteor.subscribe("services");

	Template.servicePostForm.events({
		"input #serviceWage": function(event) {
			event.target.value = event.target.value.replace(/[^0-9.]+/g, "");
		},

		"submit .servicePostForm": function() {
			event.preventDefault();

			Meteor.call("createService", {
				title: event.target.serviceTitle.value,
				description: event.target.serviceDescription.value,
				wage: parseFloat(event.target.serviceWage.value)
			}, function(err, id) {
				if (id) {
					FlowRouter.go("/gig/" + id);
				} else {
					console.log("Failure! Handle this!");
				}
			});

		}
	});

}
*/
<commit_msg>Display user for facebook<commit_after>
Services = new Mongo.Collection("services");

FlowRouter.route('/gig/:id', {
	action: function(params) {
		var service = Services.findOne({_id: params.id});
        console.log("Flowrouter.route");

		console.log(service);
        //Will either bring in the service or have an empty one so there are no errors when bringing up service.employer
        service = service || {}
        //used for displaying the apply button for employees (see servicelistingpage.html)
        service.isUserEmployer = service.employer === Meteor.userId();

        //debug logs
        console.log(this);
        console.log(Meteor.userId());

        // get employer user object
        var user = Meteor.users.findOne({_id: service.employer});
        console.log(user);
        console.log(service.employer);

        var userName;

        if (user.emails && user.emails[0].address) {
        	userName = user.emails[0].address;
        } else if (user.profile && user.profile.name) {
        	userName = user.profile.name;
        } else {
        	userName = "UNKNOWN";
        }

		BlazeLayout.render("layout", {
			area: "serviceListingPage",
			params: params,
			service: service,
			employer: user,
			employerName: userName
		});
	}
});

FlowRouter.route('/postgig', {
	action: function(params) {
		BlazeLayout.render("layout", {area: "servicePostForm"});
	}
});

if (Meteor.isServer) {

	Meteor.publish("services", function () {
		return Services.find({});
	});

	Meteor.methods({
		"createService": function(serviceObj) {

            if (!this.userId) {
				console.log("not logged in");
				return false;
			}

			if (!serviceObj) {
				console.log("no service obj");
				return false;
			}

			if (typeof(serviceObj.title) != "string" || serviceObj.title.length == 0) {
				console.log("no title");
				return false;
			}

			if (typeof(serviceObj.description) != "string" || serviceObj.description.length == 0) {
				console.log("no descript");
				return false;
			}

			if ( Number.isNaN(parseInt(serviceObj.wage, 10)) ) {
				console.log("no wage");
				return false;
			}

			var newService = Services.insert({
				employer: this.userId,
				title: serviceObj.title,
				description: serviceObj.description,
				wage: serviceObj.wage,
			});

			return newService;
		}
	});

}

if (Meteor.isClient) {

	Meteor.subscribe("services");
    Template.serviceListingPage.helpers({
       isLoggedIn: function() {
        return Meteor.userId() !== null;
       }
    });
    Template.serviceListingPage.events({
        "click #apply": function(event) {
            event.target.innerHTML = "Thank you for applying";
        }
    });

	Template.servicePostForm.events({
		"input #serviceWage": function(event) {
			event.target.value = event.target.value.replace(/\D/g, "");
		},

		"submit .servicePostForm": function() {
			event.preventDefault();

			Meteor.call("createService", {
				title: event.target.serviceTitle.value,
				description: event.target.serviceDescription.value,
				wage: parseFloat(event.target.serviceWage.value)
			}, function(err, id) {
				if (id) {
					FlowRouter.go("/gig/" + id);
				} else {
					console.log("Failure! Handle this!");
				}
			});

		}
	});

}
/*
Services = new Mongo.Collection("services");

FlowRouter.route('/gig/:id', {
	action: function(params) {
		var service = Services.findOne({_id: params.id});

		console.log(service);

		BlazeLayout.render("layout", {
			area: "serviceListingPage",
			params: params,
			service: service
		});
	}
});

FlowRouter.route('/postgig', {
	action: function(params) {
		BlazeLayout.render("layout", {area: "servicePostForm"});
	}
});

if (Meteor.isServer) {

	Meteor.publish("services", function () {
		return Services.find({});
	});

	Meteor.methods({
		"createService": function(serviceObj) {
			if (!this.userId) {
				console.log("not logged in");
				return false;
			}

			if (!serviceObj) {
				console.log("no service obj");
				return false;
			}

			if (typeof(serviceObj.title) != "string" || serviceObj.title.length == 0) {
				console.log("no title");
				return false;
			}

			if (typeof(serviceObj.description) != "string" || serviceObj.description.length == 0) {
				console.log("no descript");
				return false;
			}

			if (typeof(serviceObj.wage) != "number" || (new String(serviceObj.wage)).length == 0) {
				console.log("no wage");
				return false;
			}

			var newService = Services.insert({
				employer: this.userId,
				title: serviceObj.title,
				description: serviceObj.description,
				wage: serviceObj.wage,
			});

			return newService;
		}
	});

}

if (Meteor.isClient) {

	Meteor.subscribe("services");

	Template.servicePostForm.events({
		"input #serviceWage": function(event) {
			event.target.value = event.target.value.replace(/[^0-9.]+/g, "");
		},

		"submit .servicePostForm": function() {
			event.preventDefault();

			Meteor.call("createService", {
				title: event.target.serviceTitle.value,
				description: event.target.serviceDescription.value,
				wage: parseFloat(event.target.serviceWage.value)
			}, function(err, id) {
				if (id) {
					FlowRouter.go("/gig/" + id);
				} else {
					console.log("Failure! Handle this!");
				}
			});

		}
	});

}
*/
<|endoftext|>


<commit_before>console.log( "=== simpread storage load ===" )

/**
 * Read and Write Chrome storage
 *
 * @class
 */

/**
 * Storage mode, include `focus` `read` `option`
 */
const mode = {
        focus  : "focus",
        read   : "read",
        option : "option",
    },
    name = "simpread";

let site  = {
        exclude   : [],
        include   : "",
    },
    current = {
        bgcolor: "",
        opacity: "",
        url    : "",
        site   : site,
    },
    focus = {
        version   : "2016-12-29",
        bgcolor   : "rgba( 235, 235, 235, 0.9 )",
        opacity   : 90,
        shortcuts : "A S",
        sites     : [],    // e.g. [ "<url>", site ]
    },
    simpread = {
        focus,
        read   : {},
        option : {},
    },
    origin  = {};

class Storage {

    /**
     * Get simpread.focus data structure
     *
     * @return {object} simpread["focus"]
     */
    get focus() {
        return simpread[ mode.focus ];
    }

    /**
     * Get current data structure
     *
     * @return {object} current
     */
    get current() {
        return current;
    }

    /**
     * Restore simpread[key]
     *
     * @param {string} @see mode
     */
    Restore( key ) {
        simpread[key] = clone( origin[key] );
        this.Setcur( key );
    }

    /**
     * Set current object
     *
     * @param {string} @see mode
     */
    Setcur( key ) {
        const [ url, sites ] = [getURI(), new Map( simpread[key].sites )];
        current.site = sites.get( url );
        current.url  = url;
        while( !current.site ) {
            sites.set( url, clone( site ));
            current.site = sites.get( url );
            simpread[key].sites.push([ url, sites.get(url) ]);
        }
        swap( simpread[key], current );
        console.log( "current site object is ", current )
        return current;
    }

    /**
     * Save simpread to chrome storage
     *
     * @param {string} @see mode
     */
    Set( key ) {
        swap( current, simpread[key] );
        chrome.storage.local.set( { [name] : simpread }, function(){
            console.log( "save chrome storage success!", simpread );
            origin   = clone( simpread );
        });
    }

    /**
     * Get simpread object from chrome storage
     *
     * @param {function} callback
     */
    Get ( callback ) {
        chrome.storage.local.get( [name], function( result ) {
            if ( result && !$.isEmptyObject( result )) {
                simpread = result[name];
            }
            origin   = clone( simpread );
            callback();
            console.log( "simpread storage result is", result, simpread, origin );
        });
    }
}

/**
 * Swap source and target
 *
 * @param {object} source origin
 * @param {object} target origin
 */
function swap( source, target ) {
    target.bgcolor   = source.bgcolor;
    target.shortcuts = source.shortcuts;
    target.opacity   = source.opacity;
}

/**
 * Get URI
 *
 * @return {string} e.g. current site url is http://www.cnbeta.com/articles/1234.html return http://www.cnbeta.com/articles/
 */
function getURI() {
    const pathname = window.location.pathname.replace( /\/$/g, "" ),
          arr      = pathname.split( "/" ),
          end      = arr.pop(),
          str      = arr.join( "" ) === "" ? arr.join( "" ) : arr.join( "" ) + "/";
    return `${ window.location.protocol }//${ window.location.hostname }/${ str }`;
}

/**
 * Deep clone object
 *
 * @param  {object} target object
 * @return {object} new target object
 */
function clone( target ) {
    return $.extend( true, {}, target );
}

const storage = new Storage();
export { storage, mode as STORAGE_MODE };<commit_msg>Optimize getURL() logic.<commit_after>console.log( "=== simpread storage load ===" )

/**
 * Read and Write Chrome storage
 *
 * @class
 */

/**
 * Storage mode, include `focus` `read` `option`
 */
const mode = {
        focus  : "focus",
        read   : "read",
        option : "option",
    },
    name = "simpread";

let site  = {
        exclude   : [],
        include   : "",
    },
    current = {
        bgcolor: "",
        opacity: "",
        url    : "",
        site   : site,
    },
    focus = {
        version   : "2016-12-29",
        bgcolor   : "rgba( 235, 235, 235, 0.9 )",
        opacity   : 90,
        shortcuts : "A S",
        sites     : [],    // e.g. [ "<url>", site ]
    },
    simpread = {
        focus,
        read   : {},
        option : {},
    },
    origin  = {};

class Storage {

    /**
     * Get simpread.focus data structure
     *
     * @return {object} simpread["focus"]
     */
    get focus() {
        return simpread[ mode.focus ];
    }

    /**
     * Get current data structure
     *
     * @return {object} current
     */
    get current() {
        return current;
    }

    /**
     * Restore simpread[key]
     *
     * @param {string} @see mode
     */
    Restore( key ) {
        simpread[key] = clone( origin[key] );
        this.Setcur( key );
    }

    /**
     * Set current object
     *
     * @param {string} @see mode
     */
    Setcur( key ) {
        const [ url, sites ] = [getURI(), new Map( simpread[key].sites )];
        current.site = sites.get( url );
        current.url  = url;
        while( !current.site ) {
            sites.set( url, clone( site ));
            current.site = sites.get( url );
            simpread[key].sites.push([ url, sites.get(url) ]);
        }
        swap( simpread[key], current );
        console.log( "current site object is ", current )
        return current;
    }

    /**
     * Save simpread to chrome storage
     *
     * @param {string} @see mode
     */
    Set( key ) {
        swap( current, simpread[key] );
        chrome.storage.local.set( { [name] : simpread }, function(){
            console.log( "save chrome storage success!", simpread );
            origin   = clone( simpread );
        });
    }

    /**
     * Get simpread object from chrome storage
     *
     * @param {function} callback
     */
    Get ( callback ) {
        chrome.storage.local.get( [name], function( result ) {
            if ( result && !$.isEmptyObject( result )) {
                simpread = result[name];
            }
            origin   = clone( simpread );
            callback();
            console.log( "simpread storage result is", result, simpread, origin );
        });
    }
}

/**
 * Swap source and target
 *
 * @param {object} source origin
 * @param {object} target origin
 */
function swap( source, target ) {
    target.bgcolor   = source.bgcolor;
    target.shortcuts = source.shortcuts;
    target.opacity   = source.opacity;
}

/**
 * Get URI
 *
 * @return {string} e.g. current site url is http://www.cnbeta.com/articles/1234.html return http://www.cnbeta.com/articles/
 */
function getURI() {
    let arr = window.location.pathname.match( /[^\/]+/g );
    if ( arr == null ) arr = [];
    arr.pop();
    const str = arr.join( "" ) === "" ? arr.join( "" ) : arr.join( "" ) + "/";
    return `${ window.location.protocol }//${ window.location.hostname }/${ str }`;
}

/**
 * Deep clone object
 *
 * @param  {object} target object
 * @return {object} new target object
 */
function clone( target ) {
    return $.extend( true, {}, target );
}

const storage = new Storage();
export { storage, mode as STORAGE_MODE };<|endoftext|>


<commit_before>console.log( "=== simpread storage load ===" )

import "babel-polyfill";
import * as st   from 'site';
import {browser} from 'browser';

/**
 * Read and Write Chrome storage
 *
 * @class
 */

const name = "simpread",
    remote = "http://ojec5ddd5.bkt.clouddn.com/website_list.json",
    local  = browser.extension.getURL( "website_list.json" ),
    mode   = {
        focus     : "focus",
        read      : "read",
        option    : "option",
    },
    site   = {
        name      : "",   // only read mode
        title     : "",   // only read mode
        desc      : "",   // only read mode
        exclude   : [],
        include   : "",
    },
    focus  = {
        version   : "2016-12-29",
        bgcolor   : "rgba( 235, 235, 235, 0.9 )",
        opacity   : 90,
        shortcuts : "A S",
        sites     : [],    // e.g. [ "<url>", site ]
    },
    read   = {
        version   : "2017-03-16",
        shortcuts : "A A",
        theme     : "github",
        fontfamily: "default",
        fontsize  : "",  // default 62.5%
        layout    : "",  // default 20%
        sites     : []   // e.g. [ "<url>", site ]
    },
    option = {
        version   : "2017-04-03",
        create    : "",
        update    : "",
        focus     : 0,
        read      : 0,
    },
    unread = {
        idx       : 0,
        create    : "",
        url       : "",
        title     : "",
        desc      : "",
    };

let current  = {},
    curori   = {},
    origin   = {},
    sync     = {},
    simpread = {
        option,
        focus,
        read,
        unrdist : [],
        sites   : [],
    },
    rdstcode = -1;

class Storage {

    /**
     * Get simpread.option data structure
     *
     * @return {object} simpread["option"]
     */
    get option() {
        return simpread[ mode.option ];
    }

    /**
     * Get simpread.focus data structure
     *
     * @return {object} simpread["focus"]
     */
    get focus() {
        return simpread[ mode.focus ];
    }

    /**
     * Get simpread.read data structure
     *
     * @return {object} simpread["read"]
     */
    get read() {
        return simpread[ mode.read ];
    }

    /**
     * Get current data structure
     *
     * @return {object} current
     */
    get current() {
        return current;
    }

    /**
     * Get read site code
     *
     * @return {int} @see setCode
     */
    get rdstcode() {
        return rdstcode;
    }

    /**
     * Get unread list
     *
     * @return {array} unread list
     */
    get unrdist() {
        return simpread.unrdist;
    }

    /**
     * Verify simpread data structure
     *
     * @param  {object} verify simpread data structure, when undefined, verify self
     * @return {object} option: { code: 0|-1|-2, keys: [ "bgcolor", "layout" ] }
     *         code: 0: valid success; -1: field name failed; -2: site field name failed;
     */
    Verify( data = undefined ) {
        const pendding = data ? { ...data } : { simpread },
              valid    = ( value, source ) => {
            let result = { code: 0, keys: [] }, target = pendding[ value ];
            if ( Object.keys( target ).length !== Object.keys( source ).length ) {
                result.code = -1;
            } else {
                Object.keys( target ).forEach( key => {
                    if ( !Object.keys( source ).includes( key ) ||
                       ( key != "sites" && value != "option" && target[key] == "" )) {
                        result.keys.push( key );
                    }
                    if ( key == "sites" ) {
                        target.sites.forEach( items => {
                            if ( Object.keys( items[1] ).length != Object.keys( site ).length ) {
                                result.code = -2;
                            } else {
                                Object.keys( items[1] ).forEach( key => {
                                    ( !Object.keys( site ).includes( key ) ) && result.keys.push( `site::${key}` );
                                });
                            }
                        });
                    }
                });
                result.keys.length > 0 && result.code == 0 && ( result.code = -3 );
            }
            return result;
        }

        let opt  = valid( "option", option ),
            focu = valid( "focus",  focus ),
            rd   = valid( "read",   read );

        console.log( "storage.Verify() result ", opt, focu, rd )
        return { option: opt, focus: focu, read: rd };
    }

    /**
     * Restore simpread[key]
     *
     * @param {string} @see mode
     */
    Restore( key ) {
        simpread[key] = clone( origin[key] );
        this.Setcur( key );
    }

    /**
     * Set current object, current object structure include:
     *
     * focus mode: { url, mode, site, shortcuts, bgcolor, opacity }
     * read  mode: { url, mode, site, shortcuts, theme, fontsize, fontfamily, layout }
     *
     * @param {string} @see mode
     */
    Setcur( key ) {
        const [ url, sites ] = [ st.GetURI(), new Map( simpread[key].sites )];
        current      = swap( simpread[key], {} );
        current.url  = url;
        current.mode = key;
        let arr = st.Getsite( new Map( simpread[key].sites ), url );
        arr  ? setCode( key, 0 ) : setCode( key, 1 );
        !arr && ( arr = st.Getsite( new Map( simpread.sites ), url ));
        if ( arr ) {
            current.site = arr[0];
            current.url  = arr[1];
        } else {
            sites.set( url, clone( site ));
            current.site = sites.get( url );
            setCode( key, -1 );
        }
        curori      = { ...current };
        curori.site = { ...current.site };
        console.log( "current site object is ", current )
    }

    /**
     * Save simpread to chrome storage
     *
     * @param {string} @see mode
     */
    Set( key ) {
        const { code } = compare();
        if ( code != 0 ) {
            if ( [ 2, 3 ].includes( code ) ) {
                let idx = simpread[key].sites.findIndex( item => item[0] == current.url );
                idx == -1 && ( idx = simpread[key].sites.length );
                simpread[key].sites.splice( idx, 1, [ current.url, current.site ] );
            }
            swap( current, simpread[key] );
            save();
        }
        return code;
    }

    /**
     * Sync simpread data structure
     *
     * @param {string} include: set, get
     * @param {function} callback
     */
    Sync( state, callback ) {
        if ( state == "set" ) {
            sync = { ...simpread };
            sync.option.update = now();
            delete sync.sites;
            browser.storage.sync.set( { [name] : sync }, () => {
                console.log( "chrome storage sync[set] success!" )
                simpread.option.update = sync.option.update;
                save( callback( sync.option.update ));
            });
        } else {
            browser.storage.sync.get( [name] , result => {
                console.log( "chrome storage sync[get] success!", result, simpread )
                let success = false;
                if ( result && !$.isEmptyObject( result )) {
                    success = true;
                    Object.keys( mode ).forEach( key => {
                        simpread[ key ] = result[ name ][ key ];
                    });
                }
                callback( success );
            });
        }
    }

    /**
     * Get simpread object from chrome storage
     *
     * @param {function} callback
     */
    Get ( callback ) {
        browser.storage.local.get( [name], function( result ) {
            let firstload = true;
            if ( result && !$.isEmptyObject( result )) {
                simpread  = result[name];
                firstload = false;
            }
            origin = clone( simpread );
            callback( firstload );
            console.log( "chrome storage read success!", simpread, origin, result );
        });
    }

    /**
     * Set simpread object to chrome storage
     *
     * @param {function} callback
     * @param {object}   new simpread data structure
     */
    Write( callback, new_val = undefined ) {
        new_val && Object.keys( new_val ).forEach( key => simpread[ key ] = new_val[key] );
        save( callback );
    }

    /**
     * Get local/remote JSON usage async
     *
     * @param {string}    url, e.g. chrome-extension://xxxx/website_list.json or http://xxxx.xx/website_list.json
     * @return {function} callback, param1: object; param2: error
     */
    async GetNewsites( type, callback ) {
        try {
            const url    = type === "remote" ? remote : local,
                response = await fetch( url + "?_=" + Math.round(+new Date()) ),
                sites    = await response.json(),
                len      = simpread.sites.length;
            let [ count, forced ] = [ 0, 0 ];
            if ( len == 0 ) {
                simpread.sites = formatSites( sites );
                count          = simpread.sites.length;
                save();
            }
            else if ( { count, forced } = addsites( formatSites( sites )), count > 0 || forced > 0 ) {
                save();
            }
            callback && callback( { count, forced }, undefined );
        } catch ( error ) {
            console.error( error );
            callback && callback( undefined, error );
        }
    }

    /**
     * Clear simpread data structure
     *
     * @param {string}   include: local remote all
     * @param {function} callback
     */
    Clear( state, callback ) {
        let code = 2;
        state == "local"  && ( code = 0 );
        state == "remote" && ( code = 1 );
        ( code == 0 || code == 2 ) && browser.storage.local.clear( callback );
        ( code == 1 || code == 2 ) && browser.storage.sync.clear( callback );
    }

    /**
     * Verity current changed
     *
     * @param {string} @see mode
     */
    VerifyCur( type ) {
        return ( current.mode && current.mode != type ) ||
               ( current.url  && current.url != st.GetURI() ) ||
               $.isEmptyObject( current );
    }

    /**
     * Statistics simpread same info
     *
     * @param {string} include: create, focus, read
     */
    Statistics( type ) {
        if ( type == "create" ) {
            simpread.option.create = now();
        } else {
            simpread.option[ type ] = simpread.option[ type ] + 1;
        }
        save();
    }

    /**
     * Unread list
     *
     * @param {type} include: add remove
     * @param {any} include: object( @see unread ) or index
     * @param {function} callback
     */
    UnRead( type, args, callback ) {
        let success = true;
        switch ( type ) {
            case "add":
                const len = simpread.unrdist.length;
                args.create = now();
                args.idx = len > 0 ? simpread.unrdist[0].idx + 1 : 0;
                simpread.unrdist.findIndex( item => item.url == args.url ) == -1 ?
                    simpread.unrdist.splice( 0, 0, args ) : success = false;
                break;
            case "remove":
                const idx = simpread.unrdist.findIndex( item => item.idx == args );
                idx != -1 && simpread.unrdist.splice( idx, 1 );
                idx == -1 && ( success = false );
                break;
        }
        callback && save( callback( success ) );
    }
}

/**
 * Swap source and target property
 *
 * @param {object} source origin
 * @param {object} target origin
 */
function swap( source, target ) {
    for ( const key of Object.keys( source ) ) {
        if ( ![ "site", "sites", "version", "url", "mode" ].includes( key ) ) {
            target[key] = source[key];
        }
    }
    return target;
}

/**
 * Deep clone object
 *
 * @param  {object} target object
 * @return {object} new target object
 */
function clone( target ) {
    return $.extend( true, {}, target );
}

/**
 * Format sites object from local or remote json file
 *
 * @param  {object} sites.[array]
 * @return {array} foramat e.g. [[ <url>, object ],[ <url>, object ]]
 */
function formatSites( result ) {
    const format = new Map();
    for ( let site of result.sites ) {
        const url = site.url;
        delete site.url;
        format.set( url, site );
    }
    return [ ...format ];
}

/**
 * Add new sites to old sites
 *
 * @param  {array}  new sites from local or remote
 * @return {object} count: new sites; forced: update sites
 */
function addsites( sites ) {
    const update   = new Map( simpread.sites ),
          urls     = [ ...update.keys() ];
    let   [ count, forced ] = [ 0, 0 ];
    sites.map( ( site ) => {
        if ( !urls.includes( site[0] ) ) {
            simpread.sites.push([ site[0], site[1] ]);
            count++;
        } else if ( urls.includes( site[0] ) && site[1].override ) {
            update.set( site[0], site[1] );
            simpread.sites = [ ...update ];
            forced++;
        }
    });
    return { count, forced };
}

/**
 * Call chrome storage set
 */
function save( callback ) {
    browser.storage.local.set( { [name] : simpread }, function() {
        console.log( "chrome storage save success!", simpread );
        origin      = clone( simpread );
        curori      = { ...current };
        curori.site = { ...current.site };
        callback && callback();
    });
}

/**
 * Set read site code
 *
 * @param {string} mode type
 * @param {int}    -1: not found; 0: simpread.read.sites; 1: simpread.sites
 */
function setCode( type, value ) {
    if ( type == mode.read ) rdstcode = value;
}

/**
 * Compare current and curori( origin_current )
 *
 * return {object}
 *      code    {number} include 0: equal； 1: option changed; 2: site changed; 3: all changed
 *      changed {array}  changed key, e.g. [ "theme", "title", "exclude" ]
 */
function compare() {
    let key, code = 0, changed = [], site_changed = false;
    for ( key of Object.keys( curori ) ) {
        if ( typeof curori[key] == "string" && curori[key] != current[key] ) {
            changed.push( key );
            code = 1;
        }
    }
    for ( key of Object.keys( curori.site ) ) {
        if ( ( typeof curori.site[key] == "object" && curori.site[key].join( "" ) != current.site[key].join( "" ) ) || curori.site[key] != current.site[key] ) {
            changed.push( key );
            site_changed = true;
        }
    }
    site_changed && ( code = code + 2 );
    console.log( "current changed state is ", code, changed );
    return { code, changed };
}

/**
 * Get now time
 *
 * @return {string} return now, e.g. 2017年04月03日 11:43:53
 */
function now() {
    const date   = new Date(),
          format = value => value = value < 10 ? "0" + value : value;
    return date.getFullYear() + "年" + format( date.getUTCMonth() + 1 ) + "月" + format( date.getUTCDate() ) + "日 " + format( date.getHours() ) + ":" + format( date.getMinutes() ) + ":" + format( date.getSeconds() );
}

const storage = new Storage();

export {
    storage,
    mode  as STORAGE_MODE,
    clone as Clone,
    now   as Now,
};<commit_msg>Add 'unrdist' to mode.<commit_after>console.log( "=== simpread storage load ===" )

import "babel-polyfill";
import * as st   from 'site';
import {browser} from 'browser';

/**
 * Read and Write Chrome storage
 *
 * @class
 */

const name = "simpread",
    remote = "http://ojec5ddd5.bkt.clouddn.com/website_list.json",
    local  = browser.extension.getURL( "website_list.json" ),
    mode   = {
        focus     : "focus",
        read      : "read",
        option    : "option",
        unrdist   : "unrdist",
    },
    site   = {
        name      : "",   // only read mode
        title     : "",   // only read mode
        desc      : "",   // only read mode
        exclude   : [],
        include   : "",
    },
    focus  = {
        version   : "2016-12-29",
        bgcolor   : "rgba( 235, 235, 235, 0.9 )",
        opacity   : 90,
        shortcuts : "A S",
        sites     : [],    // e.g. [ "<url>", site ]
    },
    read   = {
        version   : "2017-03-16",
        shortcuts : "A A",
        theme     : "github",
        fontfamily: "default",
        fontsize  : "",  // default 62.5%
        layout    : "",  // default 20%
        sites     : []   // e.g. [ "<url>", site ]
    },
    option = {
        version   : "2017-04-03",
        create    : "",
        update    : "",
        focus     : 0,
        read      : 0,
    },
    unread = {
        idx       : 0,
        create    : "",
        url       : "",
        title     : "",
        desc      : "",
    };

let current  = {},
    curori   = {},
    origin   = {},
    sync     = {},
    simpread = {
        option,
        focus,
        read,
        unrdist : [],
        sites   : [],
    },
    rdstcode = -1;

class Storage {

    /**
     * Get simpread.option data structure
     *
     * @return {object} simpread["option"]
     */
    get option() {
        return simpread[ mode.option ];
    }

    /**
     * Get simpread.focus data structure
     *
     * @return {object} simpread["focus"]
     */
    get focus() {
        return simpread[ mode.focus ];
    }

    /**
     * Get simpread.read data structure
     *
     * @return {object} simpread["read"]
     */
    get read() {
        return simpread[ mode.read ];
    }

    /**
     * Get current data structure
     *
     * @return {object} current
     */
    get current() {
        return current;
    }

    /**
     * Get read site code
     *
     * @return {int} @see setCode
     */
    get rdstcode() {
        return rdstcode;
    }

    /**
     * Get unread list
     *
     * @return {array} unread list
     */
    get unrdist() {
        return simpread[ mode.unrdist ];
    }

    /**
     * Verify simpread data structure
     *
     * @param  {object} verify simpread data structure, when undefined, verify self
     * @return {object} option: { code: 0|-1|-2, keys: [ "bgcolor", "layout" ] }
     *         code: 0: valid success; -1: field name failed; -2: site field name failed;
     */
    Verify( data = undefined ) {
        const pendding = data ? { ...data } : { simpread },
              valid    = ( value, source ) => {
            let result = { code: 0, keys: [] }, target = pendding[ value ];
            if ( Object.keys( target ).length !== Object.keys( source ).length ) {
                result.code = -1;
            } else {
                Object.keys( target ).forEach( key => {
                    if ( !Object.keys( source ).includes( key ) ||
                       ( key != "sites" && value != "option" && target[key] == "" )) {
                        result.keys.push( key );
                    }
                    if ( key == "sites" ) {
                        target.sites.forEach( items => {
                            if ( Object.keys( items[1] ).length != Object.keys( site ).length ) {
                                result.code = -2;
                            } else {
                                Object.keys( items[1] ).forEach( key => {
                                    ( !Object.keys( site ).includes( key ) ) && result.keys.push( `site::${key}` );
                                });
                            }
                        });
                    }
                });
                result.keys.length > 0 && result.code == 0 && ( result.code = -3 );
            }
            return result;
        }

        let opt  = valid( "option", option ),
            focu = valid( "focus",  focus ),
            rd   = valid( "read",   read );

        console.log( "storage.Verify() result ", opt, focu, rd )
        return { option: opt, focus: focu, read: rd };
    }

    /**
     * Restore simpread[key]
     *
     * @param {string} @see mode
     */
    Restore( key ) {
        simpread[key] = clone( origin[key] );
        this.Setcur( key );
    }

    /**
     * Set current object, current object structure include:
     *
     * focus mode: { url, mode, site, shortcuts, bgcolor, opacity }
     * read  mode: { url, mode, site, shortcuts, theme, fontsize, fontfamily, layout }
     *
     * @param {string} @see mode
     */
    Setcur( key ) {
        const [ url, sites ] = [ st.GetURI(), new Map( simpread[key].sites )];
        current      = swap( simpread[key], {} );
        current.url  = url;
        current.mode = key;
        let arr = st.Getsite( new Map( simpread[key].sites ), url );
        arr  ? setCode( key, 0 ) : setCode( key, 1 );
        !arr && ( arr = st.Getsite( new Map( simpread.sites ), url ));
        if ( arr ) {
            current.site = arr[0];
            current.url  = arr[1];
        } else {
            sites.set( url, clone( site ));
            current.site = sites.get( url );
            setCode( key, -1 );
        }
        curori      = { ...current };
        curori.site = { ...current.site };
        console.log( "current site object is ", current )
    }

    /**
     * Save simpread to chrome storage
     *
     * @param {string} @see mode
     */
    Set( key ) {
        const { code } = compare();
        if ( code != 0 ) {
            if ( [ 2, 3 ].includes( code ) ) {
                let idx = simpread[key].sites.findIndex( item => item[0] == current.url );
                idx == -1 && ( idx = simpread[key].sites.length );
                simpread[key].sites.splice( idx, 1, [ current.url, current.site ] );
            }
            swap( current, simpread[key] );
            save();
        }
        return code;
    }

    /**
     * Sync simpread data structure
     *
     * @param {string} include: set, get
     * @param {function} callback
     */
    Sync( state, callback ) {
        if ( state == "set" ) {
            sync = { ...simpread };
            sync.option.update = now();
            delete sync.sites;
            browser.storage.sync.set( { [name] : sync }, () => {
                console.log( "chrome storage sync[set] success!" )
                simpread.option.update = sync.option.update;
                save( callback( sync.option.update ));
            });
        } else {
            browser.storage.sync.get( [name] , result => {
                console.log( "chrome storage sync[get] success!", result, simpread )
                let success = false;
                if ( result && !$.isEmptyObject( result )) {
                    success = true;
                    Object.keys( mode ).forEach( key => {
                        simpread[ key ] = result[ name ][ key ];
                    });
                }
                callback( success );
            });
        }
    }

    /**
     * Get simpread object from chrome storage
     *
     * @param {function} callback
     */
    Get ( callback ) {
        browser.storage.local.get( [name], function( result ) {
            let firstload = true;
            if ( result && !$.isEmptyObject( result )) {
                simpread  = result[name];
                firstload = false;
            }
            origin = clone( simpread );
            callback( firstload );
            console.log( "chrome storage read success!", simpread, origin, result );
        });
    }

    /**
     * Set simpread object to chrome storage
     *
     * @param {function} callback
     * @param {object}   new simpread data structure
     */
    Write( callback, new_val = undefined ) {
        new_val && Object.keys( new_val ).forEach( key => simpread[ key ] = new_val[key] );
        save( callback );
    }

    /**
     * Get local/remote JSON usage async
     *
     * @param {string}    url, e.g. chrome-extension://xxxx/website_list.json or http://xxxx.xx/website_list.json
     * @return {function} callback, param1: object; param2: error
     */
    async GetNewsites( type, callback ) {
        try {
            const url    = type === "remote" ? remote : local,
                response = await fetch( url + "?_=" + Math.round(+new Date()) ),
                sites    = await response.json(),
                len      = simpread.sites.length;
            let [ count, forced ] = [ 0, 0 ];
            if ( len == 0 ) {
                simpread.sites = formatSites( sites );
                count          = simpread.sites.length;
                save();
            }
            else if ( { count, forced } = addsites( formatSites( sites )), count > 0 || forced > 0 ) {
                save();
            }
            callback && callback( { count, forced }, undefined );
        } catch ( error ) {
            console.error( error );
            callback && callback( undefined, error );
        }
    }

    /**
     * Clear simpread data structure
     *
     * @param {string}   include: local remote all
     * @param {function} callback
     */
    Clear( state, callback ) {
        let code = 2;
        state == "local"  && ( code = 0 );
        state == "remote" && ( code = 1 );
        ( code == 0 || code == 2 ) && browser.storage.local.clear( callback );
        //( code == 1 || code == 2 ) && browser.storage.sync.clear( callback );
    }

    /**
     * Verity current changed
     *
     * @param {string} @see mode
     */
    VerifyCur( type ) {
        return ( current.mode && current.mode != type ) ||
               ( current.url  && current.url != st.GetURI() ) ||
               $.isEmptyObject( current );
    }

    /**
     * Statistics simpread same info
     *
     * @param {string} include: create, focus, read
     */
    Statistics( type ) {
        if ( type == "create" ) {
            simpread.option.create = now();
        } else {
            simpread.option[ type ] = simpread.option[ type ] + 1;
        }
        save();
    }

    /**
     * Unread list
     *
     * @param {type} include: add remove
     * @param {any} include: object( @see unread ) or index
     * @param {function} callback
     */
    UnRead( type, args, callback ) {
        let success = true;
        switch ( type ) {
            case "add":
                const len = simpread.unrdist.length;
                args.create = now();
                args.idx = len > 0 ? simpread.unrdist[0].idx + 1 : 0;
                simpread.unrdist.findIndex( item => item.url == args.url ) == -1 ?
                    simpread.unrdist.splice( 0, 0, args ) : success = false;
                break;
            case "remove":
                const idx = simpread.unrdist.findIndex( item => item.idx == args );
                idx != -1 && simpread.unrdist.splice( idx, 1 );
                idx == -1 && ( success = false );
                break;
        }
        callback && save( callback( success ) );
    }
}

/**
 * Swap source and target property
 *
 * @param {object} source origin
 * @param {object} target origin
 */
function swap( source, target ) {
    for ( const key of Object.keys( source ) ) {
        if ( ![ "site", "sites", "version", "url", "mode" ].includes( key ) ) {
            target[key] = source[key];
        }
    }
    return target;
}

/**
 * Deep clone object
 *
 * @param  {object} target object
 * @return {object} new target object
 */
function clone( target ) {
    return $.extend( true, {}, target );
}

/**
 * Format sites object from local or remote json file
 *
 * @param  {object} sites.[array]
 * @return {array} foramat e.g. [[ <url>, object ],[ <url>, object ]]
 */
function formatSites( result ) {
    const format = new Map();
    for ( let site of result.sites ) {
        const url = site.url;
        delete site.url;
        format.set( url, site );
    }
    return [ ...format ];
}

/**
 * Add new sites to old sites
 *
 * @param  {array}  new sites from local or remote
 * @return {object} count: new sites; forced: update sites
 */
function addsites( sites ) {
    const update   = new Map( simpread.sites ),
          urls     = [ ...update.keys() ];
    let   [ count, forced ] = [ 0, 0 ];
    sites.map( ( site ) => {
        if ( !urls.includes( site[0] ) ) {
            simpread.sites.push([ site[0], site[1] ]);
            count++;
        } else if ( urls.includes( site[0] ) && site[1].override ) {
            update.set( site[0], site[1] );
            simpread.sites = [ ...update ];
            forced++;
        }
    });
    return { count, forced };
}

/**
 * Call chrome storage set
 */
function save( callback ) {
    browser.storage.local.set( { [name] : simpread }, function() {
        console.log( "chrome storage save success!", simpread );
        origin      = clone( simpread );
        curori      = { ...current };
        curori.site = { ...current.site };
        callback && callback();
    });
}

/**
 * Set read site code
 *
 * @param {string} mode type
 * @param {int}    -1: not found; 0: simpread.read.sites; 1: simpread.sites
 */
function setCode( type, value ) {
    if ( type == mode.read ) rdstcode = value;
}

/**
 * Compare current and curori( origin_current )
 *
 * return {object}
 *      code    {number} include 0: equal； 1: option changed; 2: site changed; 3: all changed
 *      changed {array}  changed key, e.g. [ "theme", "title", "exclude" ]
 */
function compare() {
    let key, code = 0, changed = [], site_changed = false;
    for ( key of Object.keys( curori ) ) {
        if ( typeof curori[key] == "string" && curori[key] != current[key] ) {
            changed.push( key );
            code = 1;
        }
    }
    for ( key of Object.keys( curori.site ) ) {
        if ( ( typeof curori.site[key] == "object" && curori.site[key].join( "" ) != current.site[key].join( "" ) ) || curori.site[key] != current.site[key] ) {
            changed.push( key );
            site_changed = true;
        }
    }
    site_changed && ( code = code + 2 );
    console.log( "current changed state is ", code, changed );
    return { code, changed };
}

/**
 * Get now time
 *
 * @return {string} return now, e.g. 2017年04月03日 11:43:53
 */
function now() {
    const date   = new Date(),
          format = value => value = value < 10 ? "0" + value : value;
    return date.getFullYear() + "年" + format( date.getUTCMonth() + 1 ) + "月" + format( date.getUTCDate() ) + "日 " + format( date.getHours() ) + ":" + format( date.getMinutes() ) + ":" + format( date.getSeconds() );
}

const storage = new Storage();

export {
    storage,
    mode  as STORAGE_MODE,
    clone as Clone,
    now   as Now,
};<|endoftext|>


<commit_before>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 Dan "Ducky" Little
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/** Select Service.
 *
 *  Used to select a set of features using a given geometry.
 *
 */
function SelectService(Application, options) {
    /** Define the title of the service. */
    this.title = options.title ? options.title : 'Select';

    /** Title to show at the top of the results. */
    this.resultsTitle = options.resultsTitle ? options.resultsTitle : 'Select Results';

    /** Header template for rendering before features. */
    this.headerTemplate = options.headerTemplate ? options.headerTemplate : '@select-header';

    /** Template to use for rendering returned features. */
    this.template = options.template ? options.template : '@select';

    /** Footer template for rendering before features. */
    this.footerTemplate = options.footerTemplate ? options.footerTemplate : '@select-footer';

    /** Name will be set by the application when the service is registered. */
    this.name = '';

    /** Define the highlight layer */
    this.highlightPath = options.highlightPath ? options.highlightPath : 'highlight/highlight';

    /** Limit the number of selection tools available */
    this.tools = {
        'Point': true,
        'MultiPoint': true,
        'Polygon': true,
        'LineString': true,
        'Select': true,
        'Modify': true,
        'default': 'Polygon',
        'buffer': true
    };

    /** autoGo = true instructs the service to query whenever
     *                the geometry has changed.
     */
    this.autoGo = false;

    /** keepAlive = true will keep the service in 'query mode'
     *                   in the background, until it is explictly turned off.
     */
    this.keepAlive = false;

    /** User input fields, select allows choosing a layer */
    this.fields = [{
        type: 'select',
        name: 'layer',
        label: 'Query Layer',
        default: options.queryLayers ? options.queryLayers[0].value : '',
        options: options.queryLayers ? options.queryLayers : []
    }];

    /** Alow shapes to be buffered. */
    this.bufferAvailable = true;

    /** This function is called everytime there is an select query.
     *
     *  @param selection contains a GeoJSON feature describing the
     *                   geography to be used for the query.
     *
     *  @param fields    is an array containing any user-input
     *                   given to the service.
     */
    this.query = function(selection, fields) {
        if(typeof(selection) === 'undefined') {
            // throw up this handy dialog.
            var msg = 'A selection geometry is required for this query.';
            var service_name = this.name;
            var on_close = function() {
                Application.startService(service_name);
            };

            Application.alert('selection-required', msg, on_close);
        } else {
            // get the query layer.
            var query_layer = fields[0].value;
            // dispatch the query against on the query layer!
            Application.dispatchQuery(this.name, selection, [], [query_layer], [this.template]);
        }
    }

    /** resultsAsHtml is the function used to populate the Service Tab
     *                after the service has finished querying.
     */
    this.resultsAsHtml = function(queryId, query) {
        // initialize empty html content.
        var html = '';

        // iterate through each layer that was queried by the service.
        for(var i = 0, ii = query.layers.length; i < ii; i++) {
            // short-handing the item in the loop.
            var path = query.layers[i];
                html += Application.renderTemplate(path, this.headerTemplate, query);

            // check to see that the layer has results and features were returned.
            if(query.results[path] && !query.results[path].failed) {
                html += Application.renderFeaturesWithTemplate(query, path, this.template);
            }
            html += Application.renderTemplate(path, this.footerTemplate, query);
        }

        // return the html for rendering.
        return html;
    }

    /** hasRendered is an object which tells renderQueryResults to ignore
     *              queries which have already been rendered.
     */
    this.hasRendered = {};

    /** renderQueryResults is the function called to let the service
     *                     run basically any code it needs to execute after
     *                     the query has been set to finish.
     *
     *  WARNING! This will be called multiple times. It is best to ensure
     *           there is some sort of flag to prevent multiple renderings.
     */
    this.renderQueryResults = function(queryId, query) {
        // This is an ugly short circuit.
        if(this.hasRendered[queryId]) {
            // do nothing.
            return;
        }
        // flag the query as rendered even though code below
        //  this line has not finished, this prevents an accidental
        //  double-rendering.
        this.hasRendered[queryId] = true;
    }
}

if(typeof(module) !== 'undefined') { module.exports = SelectService; }
<commit_msg>Fix for indent err<commit_after>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016-2017 Dan "Ducky" Little
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/** Select Service.
 *
 *  Used to select a set of features using a given geometry.
 *
 */
function SelectService(Application, options) {
    /** Define the title of the service. */
    this.title = options.title ? options.title : 'Select';

    /** Title to show at the top of the results. */
    this.resultsTitle = options.resultsTitle ? options.resultsTitle : 'Select Results';

    /** Header template for rendering before features. */
    this.headerTemplate = options.headerTemplate ? options.headerTemplate : '@select-header';

    /** Template to use for rendering returned features. */
    this.template = options.template ? options.template : '@select';

    /** Footer template for rendering before features. */
    this.footerTemplate = options.footerTemplate ? options.footerTemplate : '@select-footer';

    /** Name will be set by the application when the service is registered. */
    this.name = '';

    /** Define the highlight layer */
    this.highlightPath = options.highlightPath ? options.highlightPath : 'highlight/highlight';

    /** Limit the number of selection tools available */
    this.tools = {
        'Point': true,
        'MultiPoint': true,
        'Polygon': true,
        'LineString': true,
        'Select': true,
        'Modify': true,
        'default': 'Polygon',
        'buffer': true
    };

    /** autoGo = true instructs the service to query whenever
     *                the geometry has changed.
     */
    this.autoGo = false;

    /** keepAlive = true will keep the service in 'query mode'
     *                   in the background, until it is explictly turned off.
     */
    this.keepAlive = false;

    /** User input fields, select allows choosing a layer */
    this.fields = [{
        type: 'select',
        name: 'layer',
        label: 'Query Layer',
        default: options.queryLayers ? options.queryLayers[0].value : '',
        options: options.queryLayers ? options.queryLayers : []
    }];

    /** Alow shapes to be buffered. */
    this.bufferAvailable = true;

    /** This function is called everytime there is an select query.
     *
     *  @param selection contains a GeoJSON feature describing the
     *                   geography to be used for the query.
     *
     *  @param fields    is an array containing any user-input
     *                   given to the service.
     */
    this.query = function(selection, fields) {
        if(typeof(selection) === 'undefined') {
            // throw up this handy dialog.
            var msg = 'A selection geometry is required for this query.';
            var service_name = this.name;
            var on_close = function() {
                Application.startService(service_name);
            };

            Application.alert('selection-required', msg, on_close);
        } else {
            // get the query layer.
            var query_layer = fields[0].value;
            // dispatch the query against on the query layer!
            Application.dispatchQuery(this.name, selection, [], [query_layer], [this.template]);
        }
    }

    /** resultsAsHtml is the function used to populate the Service Tab
     *                after the service has finished querying.
     */
    this.resultsAsHtml = function(queryId, query) {
        // initialize empty html content.
        var html = '';

        // iterate through each layer that was queried by the service.
        for(var i = 0, ii = query.layers.length; i < ii; i++) {
            // short-handing the item in the loop.
            var path = query.layers[i];

            // add the header contents
            html += Application.renderTemplate(path, this.headerTemplate, query);

            // check to see that the layer has results and features were returned.
            if(query.results[path] && !query.results[path].failed) {
                html += Application.renderFeaturesWithTemplate(query, path, this.template);
            }

            // and footer contents.
            html += Application.renderTemplate(path, this.footerTemplate, query);
        }

        // return the html for rendering.
        return html;
    }

    /** hasRendered is an object which tells renderQueryResults to ignore
     *              queries which have already been rendered.
     */
    this.hasRendered = {};

    /** renderQueryResults is the function called to let the service
     *                     run basically any code it needs to execute after
     *                     the query has been set to finish.
     *
     *  WARNING! This will be called multiple times. It is best to ensure
     *           there is some sort of flag to prevent multiple renderings.
     */
    this.renderQueryResults = function(queryId, query) {
        // This is an ugly short circuit.
        if(this.hasRendered[queryId]) {
            // do nothing.
            return;
        }
        // flag the query as rendered even though code below
        //  this line has not finished, this prevents an accidental
        //  double-rendering.
        this.hasRendered[queryId] = true;
    }
}

if(typeof(module) !== 'undefined') { module.exports = SelectService; }
<|endoftext|>