diff --git a/.gitignore b/.gitignore index ad13999d..fdedeae8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules npm-debug.log server/db/users.js +server/db/history diff --git a/package.json b/package.json index b8d9489f..263832fd 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "keymirror": "^0.1.1", "lodash": "^3.10.1", "morgan": "~1.5.1", + "nedb": "^1.7.2", "object-assign": "^2.0.0", "passport": "^0.3.2", "passport-http": "^0.3.0", diff --git a/server/models/HistoryEra.js b/server/models/HistoryEra.js new file mode 100644 index 00000000..4ae5f399 --- /dev/null +++ b/server/models/HistoryEra.js @@ -0,0 +1,141 @@ +'use strict'; + +let Datastore = require('nedb'); + +let stringUtil = require('./util/stringUtil'); + +const FILE_PATH = './server/db/history/'; +const MAX_CLEANUP_INTERVAL = 1000 * 60 * 60; // 1 hour +const MAX_NEXT_ERA_UPDATE_INTERVAL = 1000 * 60 * 60 * 12; // 12 hours +const REQUIRED_FIELDS = ['interval', 'maxTime', 'name']; + +class HistoryEra { + constructor(opts) { + opts = opts || {}; + + this.ready = false; + + if (!this.hasRequiredFields(opts)) { + return; + } + + this.data = []; + this.opts = opts; + this.startedAt = Date.now(); + + this.db = this.loadDatabase(this.opts.name); + this.removeOutdatedData(this.db); + + let cleanupInterval = this.opts.maxTime; + let nextEraUpdateInterval = this.opts.nextEraUpdateInterval; + + if (cleanupInterval === 0 || cleanupInterval > MAX_CLEANUP_INTERVAL) { + cleanupInterval = MAX_CLEANUP_INTERVAL; + } + + if (nextEraUpdateInterval && nextEraUpdateInterval > MAX_NEXT_ERA_UPDATE_INTERVAL) { + nextEraUpdateInterval = MAX_NEXT_ERA_UPDATE_INTERVAL; + } + + this.startAutoCleanup(cleanupInterval, this.db); + + if (nextEraUpdateInterval) { + this.startNextEraUpdate(nextEraUpdateInterval, this.db); + } + } + + addData(data) { + if (!this.ready) { + console.warn('database is not ready'); + return; + } + + console.log(`insert data in ${this.opts.name}`); + + this.db.insert({ + ts: Date.now(), + up: data.upload, + dn: data.download + }); + } + + cleanup(db) { + this.removeOutdatedData(db); + db.persistence.compactDatafile(); + } + + hasRequiredFields(opts) { + let requirementsMet = true; + + REQUIRED_FIELDS.forEach(function (field) { + if (opts[field] == null) { + console.warn(`historyEra requires ${field}`); + requirementsMet = false; + } + }); + + return requirementsMet; + } + + loadDatabase(dbName) { + let db = new Datastore({ + autoload: true, + filename: `${FILE_PATH}${dbName}.db` + }); + + this.ready = true; + + return db; + } + + removeOutdatedData(db) { + if (this.opts.maxTime > 0) { + let minTimestamp = Date.now() - this.opts.maxTime; + db.remove({ts: {$lt: minTimestamp}}, {multi: true}, (err, numRemoved) => { + console.log(`removed ${numRemoved} entries from ${this.opts.name}`) + }); + } + } + + startAutoCleanup(interval, db) { + this.autoCleanupInterval = setInterval( + this.cleanup.bind(this, db), interval + ); + } + + startNextEraUpdate(interval, currentDB, nextDB) { + this.nextEraUpdateInterval = setInterval( + this.updateNextEra.bind(this, currentDB, nextDB), interval + ); + } + + stopAutoCleanup() { + clearInterval(this.autoCleanupInterval); + this.autoCleanupInterval = null; + } + + stopNextEraUpdate(interval, db) { + clearInterval(this.nextEraUpdateInterval); + this.nextEraUpdateInterval = null; + } + + updateNextEra(currentDB, nextDB) { + let minTimestamp = Date.now() - this.opts.nextEraUpdateInterval; + currentDB.find({ts: {$gte: minTimestamp}}, (err, docs) => { + let downTotal = 0; + let upTotal = 0; + + docs.forEach(function (doc) { + downTotal += parseInt(doc.dn); + upTotal += parseInt(doc.up); + }); + + this.opts.nextEra.addData({ + download: (downTotal / docs.length).toFixed(1), + upload: (upTotal / docs.length).toFixed(1) + }); + }); + } +} + +module.exports = HistoryEra; diff --git a/server/models/client.js b/server/models/client.js index 9067ddfb..4e20657b 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -241,25 +241,22 @@ var client = { }, getTransferStats: function(callback) { - try { - var request = clientUtil.createMulticallRequest( - clientUtil.defaults.clientPropertyMethods - ); + var request = clientUtil.createMulticallRequest( + clientUtil.defaults.clientPropertyMethods + ); - request = [request]; + request = [request]; - rTorrent.get('system.multicall', request) - .then(function(data) { - callback(null, clientUtil.mapClientProps( - clientUtil.defaults.clientProperties, - data - )); - }, function(error) { - callback(error, null); - }); - } catch(err) { - console.log(err); - } + rTorrent.get('system.multicall', request) + .then(function(data) { + var parsedData = clientUtil.mapClientProps( + clientUtil.defaults.clientProperties, + data + ); + callback(null, parsedData); + }, function(error) { + callback(error, null); + }); } }; diff --git a/server/models/collectHistory.js b/server/models/collectHistory.js new file mode 100644 index 00000000..79b65b2e --- /dev/null +++ b/server/models/collectHistory.js @@ -0,0 +1,74 @@ +'use strict'; +var Datastore = require('nedb'); + +var client = require('./client'); +var HistoryEra = require('./HistoryEra'); + +let pollInterval = null; + +let yearSnapshot = new HistoryEra({ + interval: 1000 * 60 * 60 * 24 * 7, // 7 days + name: 'yearSnapshot', + maxTime: 0 // 365 days +}); + +let monthSnapshot = new HistoryEra({ + interval: 1000 * 60 * 60 * 12, // 12 hours + maxTime: 1000 * 60 * 60 * 24 * 365, // 365 days + name: 'monthSnapshot', + nextEraUpdateInterval: 1000 * 60 * 60 * 24 * 7, // 7 days + nextEra: yearSnapshot +}); + +let weekSnapshot = new HistoryEra({ + interval: 1000 * 60 * 60 * 4, // 4 hours + maxTime: 1000 * 60 * 60 * 24 * 7 * 24, // 24 weeks + name: 'weekSnapshot', + nextEraUpdateInterval: 1000 * 60 * 60 * 12, // 12 hours + nextEra: monthSnapshot +}); + +let daySnapshot = new HistoryEra({ + interval: 1000 * 60 * 60, // 60 minutes + maxTime: 1000 * 60 * 60 * 24 * 30, // 30 days + name: 'daySnapshot', + nextEraUpdateInterval: 1000 * 60 * 60 * 4, // 4 hours + nextEra: weekSnapshot +}); + +let hourSnapshot = new HistoryEra({ + interval: 1000 * 60 * 15, // 15 minutes + maxTime: 1000 * 60 * 60 * 24, // 24 hours + name: 'hourSnapshot', + nextEraUpdateInterval: 1000 * 60 * 60, // 60 minutes + nextEra: daySnapshot +}); + +let thirtyMinSnapshot = new HistoryEra({ + interval: 1000 * 20, // 20 seconds + maxTime: 1000 * 60 * 30, // 30 minutes + name: 'thirtyMinSnapshot', + nextEraUpdateInterval: 1000 * 60 * 15, // 15 minutes + nextEra: hourSnapshot +}); + +let fiveMinSnapshot = new HistoryEra({ + interval: 1000 * 5, // 5 seconds + maxTime: 1000 * 60 * 5, // 5 minutes + name: 'fiveMinSnapshot', + nextEraUpdateInterval: 1000 * 20, // 20 seconds + nextEra: thirtyMinSnapshot +}); + +pollInterval = setInterval(function() { + client.getTransferStats(function (err, data) { + if (err) { + return; + } + + fiveMinSnapshot.addData({ + upload: data.uploadRate, + download: data.downloadRate + }); + }); +}, 1000 * 5); diff --git a/server/models/util/stringUtil.js b/server/models/util/stringUtil.js new file mode 100644 index 00000000..0f55bb0b --- /dev/null +++ b/server/models/util/stringUtil.js @@ -0,0 +1,9 @@ +'use strict'; + +let stringUtil = { + capitalize: function (string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } +} + +module.exports = stringUtil; diff --git a/server/routes/index.js b/server/routes/index.js index 58916eef..4ec7606c 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -5,6 +5,8 @@ var Strategy = require('passport-http').BasicStrategy; var users = require('../db/users'); +var collectHistory = require('../models/collectHistory'); + passport.use(new Strategy( function(username, password, callback) { users.findByUsername(username, function(err, user) {