Initial commit

This commit is contained in:
John F
2015-04-08 19:13:41 -04:00
commit ae520c0a33
44 changed files with 2853 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.DS_Store
node_modules
bower_components
public/stylesheets
public/scripts

72
app.js Normal file
View File

@@ -0,0 +1,72 @@
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var torrents = require('./routes/torrents');
var client = require('./routes/client');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(function(req, res, next) {
req.socket.on("error", function(err) {
console.log(err);
});
res.socket.on("error", function(err) {
console.log(err);
});
next();
});
app.use('/', routes);
app.use('/torrents', torrents);
app.use('/client', client);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;

90
bin/www Executable file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('flood:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

84
gulpfile.js Normal file
View File

@@ -0,0 +1,84 @@
var gulp = require('gulp'),
sass = require('gulp-sass'),
autoprefixer = require('gulp-autoprefixer'),
watch = require('gulp-watch'),
notify = require('gulp-notify'),
browserSync = require('browser-sync'),
browserify = require('browserify'),
watchify = require('watchify'),
reactify = require('reactify'),
source = require('vinyl-source-stream');
var supportedBrowsers = ['last 2 versions', '> 1%', 'ie >= 8', 'Firefox ESR', 'Opera >= 12'],
jsFiles = [];
var sourceDir = './source/',
destDir = './public/';
var reload = browserSync.reload;
function handleErrors() {
var args = Array.prototype.slice.call(arguments);
notify.onError({
title: "Compile Error",
message: "<%= error.message %>"
}).apply(this, args);
this.emit('end');
}
gulp.task('browser-sync', function() {
return browserSync.init({
port: 3001
});
});
gulp.task('styles', function() {
return gulp.src(sourceDir + 'sass/style.scss')
.pipe(sass({
errLogToConsole: true
}))
.pipe(autoprefixer({
browsers: supportedBrowsers,
map: true
}))
.pipe(browserSync.reload({
stream: true
}))
.pipe(gulp.dest(destDir + 'stylesheets'));
});
gulp.task('scripts', function() {
var bundler = watchify(browserify({
entries: [sourceDir + '/scripts/app.js'],
cache: {},
packageCache: {},
fullPaths: true
}));
bundler.transform(reactify);
function rebundle() {
var stream = bundler.bundle();
return stream.on('error', handleErrors)
.pipe(source('app.js'))
.pipe(gulp.dest(destDir + 'scripts/'));
}
bundler.on('update', function() {
rebundle();
});
return rebundle();
});
gulp.task('watch', function () {
gulp.watch(sourceDir + 'scripts/**/*.js', ['scripts', reload]);
gulp.watch(sourceDir + 'sass/**/*.scss', ['styles', reload]);
});
gulp.task('default', ['scripts', 'styles', 'watch', 'browser-sync']);

194
models/client.js Normal file
View File

@@ -0,0 +1,194 @@
var rTorrent = require('./rtorrent');
var util = require('util');
function client() {
if((this instanceof client) === false) {
return new client();
}
};
var defaults = {
torrentProperties: [
'hash',
'name',
'state',
'stateChanged',
'isActive',
'uploadRate',
'uploadTotal',
'downloadRate',
'downloadTotal',
'ratio',
'bytesDone',
'sizeBytes',
'chunkSize',
'chunksCompleted',
'peersAccounted',
'peersComplete',
'peerExchange',
'peersNotConnected',
'trackerFocus',
'basePath',
'creationDate',
'customSeedingTime',
'customAddTime'
],
torrentPropertyMethods: [
'main',
'd.get_hash=',
'd.get_name=',
'd.get_state=',
'd.get_state_changed=',
'd.is_active=',
'd.get_up_rate=',
'd.get_up_total=',
'd.get_down_rate=',
'd.get_down_total=',
'd.get_ratio=',
'd.get_bytes_done=',
'd.get_size_bytes=',
'd.get_chunk_size=',
'd.get_completed_chunks=',
'd.get_peers_accounted=',
'd.get_peers_complete=',
'd.get_peer_exchange=',
'd.get_peers_not_connected=',
'd.get_tracker_focus=',
'd.get_base_path=',
'd.get_creation_date=',
'd.get_custom=seedingtime',
'd.get_custom=addtime'
],
clientProperties: [
'uploadRate',
'uploadTotal',
'downloadRate',
'downloadTotal'
],
clientPropertyMethods: [
'get_up_rate',
'get_up_total',
'get_down_rate',
'get_down_total'
]
};
var mapProps = function(props, data) {
var mappedObject = [];
if (data[0].length === 1) {
mappedObject = {};
for (i = 0, len = data.length; i < len; i++) {
mappedObject[props[i]] = data[i][0];
}
} else {
for (i = 0, lenI = data.length; i < lenI; i++) {
mappedObject[i] = {};
for (a = 0, lenA = props.length; a < lenA; a++) {
mappedObject[i][props[a]] = data[i][a];
}
}
}
return mappedObject;
};
var createMulticallRequest = function(data) {
var methodCall = [];
for (i = 0, len = data.length; i < len; i++) {
methodCall.push({
'methodName': data[i],
'params': []
});
}
return methodCall;
}
client.prototype.getTorrentList = function(callback) {
try {
rTorrent.get('d.multicall', defaults.torrentPropertyMethods)
.then(function(data) {
callback(null, mapProps(defaults.torrentProperties, data));
}, function(error) {
callback(error, null)
});
} catch (error) {
console.log(error);
}
};
client.prototype.stopTorrent = function(hash, callback) {
if (!util.isArray(hash)) {
hash = [hash];
}
rTorrent.get('d.stop', hash).then(function(data) {
callback(null, data);
}, function(error) {
console.log(error);
callback(error, null);
});
};
client.prototype.startTorrent = function(hash, callback) {
if (!util.isArray(hash)) {
hash = [hash];
}
rTorrent.get('d.start', hash).then(function(data) {
callback(null, data);
}, function(error) {
console.log(error);
callback(error, null);
});
};
client.prototype.getClientStats = function(callback) {
rTorrent.get('system.multicall', [createMulticallRequest(defaults.clientPropertyMethods)])
.then(function(data) {
callback(null, mapProps(defaults.clientProperties, data));
}, function(error) {
callback(error, null);
});
}
module.exports = client;

15
models/clientStats.js Normal file
View File

@@ -0,0 +1,15 @@
var client = require('./client')();
function clientStats() {
if((this instanceof clientStats) === false) {
return new clientStats();
}
};
clientStats.prototype.getStats = function(callback) {
client.getClientStats(callback);
};
module.exports = clientStats;

188
models/date_formatter.js Normal file
View File

@@ -0,0 +1,188 @@
/**
* @class DateFormatter
* The DateFormatter supports decoding from and encoding to
* ISO8601 formatted strings. Accepts formats with and without
* hyphen/colon separators and correctly parses zoning info.
*/
var DateFormatter = function (opts) {
this.opts = {}
this.setOpts(opts)
}
/**
* Default options for DateFormatter
* @static
* @see DateFormatter#setOpts
*/
DateFormatter.DEFAULT_OPTIONS = {
colons: true
, hyphens: false
, local: true
, ms: false
, offset: false
}
/**
* Regular Expression that disects ISO 8601 formatted strings into
* an array of parts.
* @static
*/
DateFormatter.ISO8601 = new RegExp(
'([0-9]{4})([-]?([0-9]{2}))([-]?([0-9]{2}))'
+ '(T([0-9]{2})(((:?([0-9]{2}))?((:?([0-9]{2}))?(\.([0-9]+))?))?)'
+ '(Z|([+-]([0-9]{2}(:?([0-9]{2}))?)))?)?'
)
/**
* Sets options for encoding Date objects to ISO8601 strings.
* Omitting the 'opts' argument will reset all options to the default.
*
* @param {Object} opts - Options (optional)
* @param {Boolean} opts.colons - Enable/disable formatting the time portion
* with a colon as separator (default: true)
* @param {Boolean} opts.hyphens - Enable/disable formatting the date portion
* with a hyphen as separator (default: false)
* @param {Boolean} opts.local - Encode as local time instead of UTC
* (default: true)
* @param {Boolean} opts.ms - Enable/Disable output of milliseconds
* (default: false)
* @param {Boolean} opts.offset - Enable/Disable output of UTC offset
* (default: false)
*/
DateFormatter.prototype.setOpts = function (opts) {
if (!opts) opts = DateFormatter.DEFAULT_OPTIONS
var ctx = this;
Object.keys(DateFormatter.DEFAULT_OPTIONS).forEach(function (k) {
ctx.opts[k] = opts.hasOwnProperty(k) ?
opts[k] : DateFormatter.DEFAULT_OPTIONS[k]
})
}
/**
* Converts a date time stamp following the ISO8601 format to a JavaScript Date
* object.
*
* @param {String} time - String representation of timestamp.
* @return {Date} - Date object from timestamp.
*/
DateFormatter.prototype.decodeIso8601 = function(time) {
var dateParts = time.toString().match(DateFormatter.ISO8601)
if (!dateParts) {
throw new Error('Expected a ISO8601 datetime but got \'' + time + '\'')
}
var date = [
[dateParts[1], dateParts[3] || '01', dateParts[5] || '01'].join('-')
, 'T'
, [
dateParts[7] || '00'
, dateParts[11] || '00'
, dateParts[14] || '00'
].join(':')
, '.'
, dateParts[16] || '000'
].join('')
date += (dateParts[17] !== undefined) ?
dateParts[17] +
((dateParts[19] && dateParts[20] === undefined) ? '00' : '') :
DateFormatter.formatCurrentOffset(new Date(date))
return new Date(date)
}
/**
* Converts a JavaScript Date object to an ISO8601 timestamp.
*
* @param {Date} date - Date object.
* @return {String} - String representation of timestamp.
*/
DateFormatter.prototype.encodeIso8601 = function(date) {
var parts = this.opts.local ?
DateFormatter.getLocalDateParts(date) :
DateFormatter.getUTCDateParts(date)
return [
[parts[0],parts[1],parts[2]].join(this.opts.hyphens ? '-' : '')
, 'T'
, [parts[3],parts[4],parts[5]].join(this.opts.colons ? ':' : '')
, (this.opts.ms) ? '.' + parts[6] : ''
, (this.opts.local) ? ((this.opts.offset) ?
DateFormatter.formatCurrentOffset(date) : '') : 'Z'
].join('')
}
/**
* Helper function to get an array of zero-padded date parts,
* in UTC
*
* @param {Date} date - Date Object
* @return {String[]}
*/
DateFormatter.getUTCDateParts = function (date) {
return [
date.getUTCFullYear()
, DateFormatter.zeroPad(date.getUTCMonth()+1,2)
, DateFormatter.zeroPad(date.getUTCDate(),2)
, DateFormatter.zeroPad(date.getUTCHours(), 2)
, DateFormatter.zeroPad(date.getUTCMinutes(), 2)
, DateFormatter.zeroPad(date.getUTCSeconds(), 2)
, DateFormatter.zeroPad(date.getUTCMilliseconds(), 3)]
}
/**
* Helper function to get an array of zero-padded date parts,
* in the local time zone
*
* @param {Date} date - Date Object
* @return {String[]}
*/
DateFormatter.getLocalDateParts = function (date) {
return [
date.getFullYear()
, DateFormatter.zeroPad(date.getMonth()+1,2)
, DateFormatter.zeroPad(date.getDate(),2)
, DateFormatter.zeroPad(date.getHours(), 2)
, DateFormatter.zeroPad(date.getMinutes(), 2)
, DateFormatter.zeroPad(date.getSeconds(), 2)
, DateFormatter.zeroPad(date.getMilliseconds(), 3)]
}
/**
* Helper function to pad the digits with 0s to meet date formatting
* requirements.
*
* @param {Number} digit - The number to pad.
* @param {Number} length - Length of digit string, prefix with 0s if not
* already length.
* @return {String} - String with the padded digit
*/
DateFormatter.zeroPad = function (digit, length) {
var padded = '' + digit
while (padded.length < length) {
padded = '0' + padded
}
return padded
}
/**
* Helper function to get the current timezone to default decoding to
* rather than UTC. (for backward compatibility)
*
* @return {String} - in the format /Z|[+-]\d{2}:\d{2}/
*/
DateFormatter.formatCurrentOffset = function (d) {
var offset = (d || new Date()).getTimezoneOffset()
return (offset === 0) ? 'Z' : [
(offset < 0) ? '+' : '-'
, DateFormatter.zeroPad(Math.abs(Math.floor(offset/60)),2)
, ':'
, DateFormatter.zeroPad(Math.abs(offset%60),2)
].join('')
}
// export an instance of DateFormatter only.
module.exports = new DateFormatter()

323
models/deserializer.js Normal file
View File

@@ -0,0 +1,323 @@
var sax = require('sax')
, dateFormatter = require('./date_formatter')
var Deserializer = function(encoding) {
this.type = null
this.responseType = null
this.stack = []
this.marks = []
this.data = []
this.methodname = null
this.encoding = encoding || 'utf8'
this.value = false
this.callback = null
this.error = null
this.parser = sax.createStream()
this.parser.on('opentag', this.onOpentag.bind(this))
this.parser.on('closetag', this.onClosetag.bind(this))
this.parser.on('text', this.onText.bind(this))
this.parser.on('cdata', this.onCDATA.bind(this))
this.parser.on('end', this.onDone.bind(this))
this.parser.on('error', this.onError.bind(this))
}
Deserializer.prototype.deserializeMethodResponse = function(stream, callback) {
var that = this
this.callback = function(error, result) {
if (error) {
callback(error)
}
else if (result.length > 1) {
callback(new Error('Response has more than one param'))
}
else if (that.type !== 'methodresponse') {
callback(new Error('Not a method response'))
}
else if (!that.responseType) {
callback(new Error('Invalid method response'))
}
else {
callback(null, result[0])
}
}
stream.setEncoding(this.encoding)
stream.on('error', this.onError.bind(this))
stream.pipe(this.parser)
}
Deserializer.prototype.deserializeMethodCall = function(stream, callback) {
var that = this
this.callback = function(error, result) {
if (error) {
callback(error)
}
else if (that.type !== 'methodcall') {
callback(new Error('Not a method call'))
}
else if (!that.methodname) {
callback(new Error('Method call did not contain a method name'))
}
else {
callback(null, that.methodname, result)
}
}
stream.setEncoding(this.encoding)
stream.on('error', this.onError.bind(this))
stream.pipe(this.parser)
}
Deserializer.prototype.onDone = function() {
var that = this
if (!this.error) {
if (this.type === null || this.marks.length) {
this.callback(new Error('Invalid XML-RPC message'))
}
else if (this.responseType === 'fault') {
var createFault = function(fault) {
var error = new Error('XML-RPC fault' + (fault.faultString ? ': ' + fault.faultString : ''))
error.code = fault.faultCode
error.faultCode = fault.faultCode
error.faultString = fault.faultString
return error
}
this.callback(createFault(this.stack[0]))
}
else {
this.callback(undefined, this.stack)
}
}
}
// TODO:
// Error handling needs a little thinking. There are two different kinds of
// errors:
// 1. Low level errors like network, stream or xml errors. These don't
// require special treatment. They only need to be forwarded. The IO
// is already stopped in these cases.
// 2. Protocol errors: Invalid tags, invalid values &c. These happen in
// our code and we should tear down the IO and stop parsing.
// Currently all errors end here. Guess I'll split it up.
Deserializer.prototype.onError = function(msg) {
if (!this.error) {
if (typeof msg === 'string') {
this.error = new Error(msg)
}
else {
this.error = msg
}
this.callback(this.error)
}
}
Deserializer.prototype.push = function(value) {
this.stack.push(value)
}
//==============================================================================
// SAX Handlers
//==============================================================================
Deserializer.prototype.onOpentag = function(node) {
if (node.name === 'ARRAY' || node.name === 'STRUCT') {
this.marks.push(this.stack.length)
}
this.data = []
this.value = (node.name === 'VALUE')
}
Deserializer.prototype.onText = function(text) {
this.data.push(text)
}
Deserializer.prototype.onCDATA = function(cdata) {
this.data.push(cdata)
}
Deserializer.prototype.onClosetag = function(el) {
var data = this.data.join('')
try {
switch(el) {
case 'BOOLEAN':
this.endBoolean(data)
break
case 'INT':
case 'I4':
this.endInt(data)
break
case 'I8':
this.endI8(data)
break
case 'DOUBLE':
this.endDouble(data)
break
case 'STRING':
case 'NAME':
this.endString(data)
break
case 'ARRAY':
this.endArray(data)
break
case 'STRUCT':
this.endStruct(data)
break
case 'BASE64':
this.endBase64(data)
break
case 'DATETIME.ISO8601':
this.endDateTime(data)
break
case 'VALUE':
this.endValue(data)
break
case 'PARAMS':
this.endParams(data)
break
case 'FAULT':
this.endFault(data)
break
case 'METHODRESPONSE':
this.endMethodResponse(data)
break
case 'METHODNAME':
this.endMethodName(data)
break
case 'METHODCALL':
this.endMethodCall(data)
break
case 'NIL':
this.endNil(data)
break
case 'DATA':
case 'PARAM':
case 'MEMBER':
// Ignored by design
break
default:
this.onError('Unknown XML-RPC tag \'' + el + '\'')
break
}
}
catch (e) {
this.onError(e)
}
}
Deserializer.prototype.endNil = function(data) {
this.push(null)
this.value = false
}
Deserializer.prototype.endBoolean = function(data) {
if (data === '1') {
this.push(true)
}
else if (data === '0') {
this.push(false)
}
else {
throw new Error('Illegal boolean value \'' + data + '\'')
}
this.value = false
}
Deserializer.prototype.endInt = function(data) {
var value = parseInt(data, 10)
if (isNaN(value)) {
throw new Error('Expected an integer but got \'' + data + '\'')
}
else {
this.push(value)
this.value = false
}
}
Deserializer.prototype.endDouble = function(data) {
var value = parseFloat(data)
if (isNaN(value)) {
throw new Error('Expected a double but got \'' + data + '\'')
}
else {
this.push(value)
this.value = false
}
}
Deserializer.prototype.endString = function(data) {
this.push(data)
this.value = false
}
Deserializer.prototype.endArray = function(data) {
var mark = this.marks.pop()
this.stack.splice(mark, this.stack.length - mark, this.stack.slice(mark))
this.value = false
}
Deserializer.prototype.endStruct = function(data) {
var mark = this.marks.pop()
, struct = {}
, items = this.stack.slice(mark)
, i = 0
for (; i < items.length; i += 2) {
struct[items[i]] = items[i + 1]
}
this.stack.splice(mark, this.stack.length - mark, struct)
this.value = false
}
Deserializer.prototype.endBase64 = function(data) {
var buffer = new Buffer(data, 'base64')
this.push(buffer)
this.value = false
}
Deserializer.prototype.endDateTime = function(data) {
var date = dateFormatter.decodeIso8601(data)
this.push(date)
this.value = false
}
var isInteger = /^-?\d+$/
Deserializer.prototype.endI8 = function(data) {
if (!isInteger.test(data)) {
throw new Error('Expected integer (I8) value but got \'' + data + '\'')
}
else {
this.endString(data)
}
}
Deserializer.prototype.endValue = function(data) {
if (this.value) {
this.endString(data)
}
}
Deserializer.prototype.endParams = function(data) {
this.responseType = 'params'
}
Deserializer.prototype.endFault = function(data) {
this.responseType = 'fault'
}
Deserializer.prototype.endMethodResponse = function(data) {
this.type = 'methodresponse'
}
Deserializer.prototype.endMethodName = function(data) {
this.methodname = data
}
Deserializer.prototype.endMethodCall = function(data) {
this.type = 'methodcall'
}
module.exports = Deserializer

720
models/rtorrent.js Normal file
View File

@@ -0,0 +1,720 @@
var xmlrpc = require('xmlrpc')
// var fs = require('fs');
// var path = require('path');
// var request = require('request');
// var portscanner = require('portscanner');
// var readTorrent = require('read-torrent');
// var logger = require('winston');
var Q = require('q');
// var nconf = require('nconf');
var net = require('net');
// var rimraf = require('rimraf');
var Deserializer = require('./deserializer');
var Serializer = require('./serializer');
// function htmlspecialchars(str) {
// return str.replace(/\&/ig,'&amp;').replace(/\'/ig,'&quot;').replace(/\'/ig,'&#039;').replace(/\</ig,'&lt;').replace(/\>/ig,'&gt;');
// }
// if (!(nconf.get('rtorrent:option') === 'scgi' || nconf.get('rtorrent:option') === 'xmlrpc')) {
// var err = new Error('Config for rtorrent option is not valid. Please check config.json rtorrent.option property.');
// logger.error(err.message);
// throw err;
// }
// logger.info('Connect to rtorrent via', nconf.get('rtorrent:option'));
// need something to test connection to rtorrent first...
var rtorrent = {};
rtorrent.get = function(api, array) {
var stream = net.connect(5000, 'localhost');
var deferred = Q.defer();
var xml;
var length = 0;
stream.setEncoding('UTF8');
try {
xml = Serializer.serializeMethodCall(api, array);
} catch (error) {
console.trace(error);
}
var head = [
'CONTENT_LENGTH' + String.fromCharCode(0) + xml.length + String.fromCharCode(0),
'SCGI' + String.fromCharCode(0) + '1' + String.fromCharCode(0)
];
head.forEach(function (item) {
length += item.length;
});
stream.write(length + ':');
head.forEach(function (item) {
stream.write(item);
});
stream.write(',');
stream.write(xml);
var deserializer = new Deserializer('utf8');
deserializer.deserializeMethodResponse(stream, function (err, data) {
if (err) {
return deferred.reject(err);
}
return deferred.resolve(data);
});
return deferred.promise;
}
// function methodCall (api, array) {
//
// if (nconf.get('rtorrent:option') === 'xmlrpc') {
// return xmlrpcMethodCall(api, array);
// }
//
// if (nconf.get('rtorrent:option') === 'scgi') {
// return scgiMethodCall(api, array);
// }
// }
//
// rtorrent.init = function () {
// return createThrottleSettings()
// .then(function () {
// logger.info('Finished creating throttle settings.');
// }, function (err) {
// if (err.code == 'ECONNREFUSED') {
// throw new Error('Unable to connect to rtorrent.');
// }
// });
// }
//
// function createThrottleSettings () {
// logger.info('Creating throttle settings.');
// var upload_throttles = [];
// var download_throttles = [];
// var throttle_settings = [];
// var createThrottleSettingList = [];
//
// var throttleSpeed = 16;
// for (var i = 5 - 1; i >= 0; i--) {
// upload_throttles.push({
// display: 'Up_' + throttleSpeed,
// name: 'up_' + i,
// up: throttleSpeed,
// down: 0,
// direction: 'up'
// });
// throttleSpeed = throttleSpeed * 2
// }
//
// throttleSpeed = 16;
// for (var i = 5 - 1; i >= 0; i--) {
// download_throttles.push({
// display: 'Down_' + throttleSpeed,
// name: 'down_' + i,
// up: 0,
// down: throttleSpeed,
// direction: 'down'
// });
// throttleSpeed = throttleSpeed * 2
// }
//
// throttle_settings = upload_throttles.concat(download_throttles);
//
// for (var i = throttle_settings.length - 1; i >= 0; i--) {
// createThrottleSettingList.push(createThrottleSetting(throttle_settings[i]));
// }
//
// return Q.all(createThrottleSettingList);
// }
//
// function createThrottleSetting (throttleSetting) {
// switch(throttleSetting.direction) {
// case 'up':
// return rtorrent.throttleUp('' + throttleSetting.name, '' + throttleSetting.up);
// break;
// case 'down':
// return rtorrent.throttleDown('' + throttleSetting.name, '' + throttleSetting.up);
// break;
// }
// }
//
// rtorrent.throttleUp = function (name, value) {
// return methodCall('throttle_up', [name, value]);
// }
//
// rtorrent.throttleDown = function (name, value) {
// return methodCall('throttle_down', [name, value]);
// }
//
// rtorrent.setThrottle = function (hash, throttle_name) {
// return rtorrent.pauseTorrent(hash)
// .then(function () {
// return rtorrent.setThrottleName(hash, throttle_name);
// })
// .then(function () {
// return rtorrent.startTorrent(hash);
// });
// }
//
// rtorrent.setThrottleName = function (hash, throttle_name) {
// return methodCall('d.set_throttle_name', [hash, throttle_name]);
// }
//
// // get_complete, is_open, is_hash_checking, get_state
// // need to figure out better way of getting the status
// function getStatus (value) {
// if (value[0] === '1' && value[1] === '1' && value[2] === '0' && value[3] === '1') {
// return 'seeding';
// } else if (value[0] === '1' && value[1] === '0' && value[2] === '0' && value[3] === '0') {
// return 'finished';
// } else if (value[0] === '0' && value[1] === '1' && value[2] === '0' && value[3] === '1') {
// return 'downloading';
// } else if (value[0] === '0' && value[1] === '0' && value[2] === '0' && value[3] === '1') {
// // stopped in the middle
// return 'stopped';
// } else if (value[0] === '0' && value[1] === '0' && value[2] === '0' && value[3] === '0') {
// // i dont know stopped
// return 'stopped';
// } else if (value[0] === '0' && value[1] === '1' && value[2] === '0' && value[3] === '0') {
// return 'paused';
// } else if (value[0] === '1' && value[1] === '1' && value[2] === '0' && value[3] === '0') {
// // seeding pause
// return 'paused';
// } else if (value[0] === '1' && value[1] === '0' && value[2] === '0' && value[3] === '1') {
// return 'finished';
// } else if (value[2] === '1') {
// return 'checking';
// }
// }
//
// function adaptTorrentArray (torrent) {
// return {
// name: torrent[0],
// hash: torrent[1],
// id: torrent[1],
// size: parseInt(torrent[2], 10),
// downloaded: parseInt(torrent[3], 10),
// uploaded: parseInt(torrent[12], 10),
// dl_speed: parseInt(torrent[4], 10),
// ul_speed: parseInt(torrent[5], 10),
// percent_downloaded: (torrent[3] / torrent[2]).toFixed(4),
// time_remaining: (torrent[2] - torrent[3]) / torrent[4] | 0,
// status: getStatus(torrent.slice(6, 10)),
// seeds: parseInt(torrent[10], 10),
// peers: parseInt(torrent[11], 10),
// total_peers: 0,
// total_seeds: 0
// }
// }
//
// rtorrent.getTorrents = function () {
// return methodCall('d.multicall', ['main', 'd.name=', 'd.hash=', 'd.size_bytes=', 'd.bytes_done=', 'd.get_down_rate=', 'd.get_up_rate=', 'd.get_complete=', 'd.is_open=', 'd.is_hash_checking=', 'd.get_state=', 'd.get_peers_complete=', 'd.get_peers_accounted=', 'd.get_up_total='])
// .then(function (data) {
//
// // If array is empty, return empty array
// if (data.length === 0) {
// return [];
// }
//
// // Adapt array from rtorrent properly for consumption by client
// var torrents = data.map(function (torrent) {
// return adaptTorrentArray(torrent);
// });
//
// // Declare multical array specifically for getting torrent data
// var systemMultiCallArray = [];
//
// // Loop through torrents from main call and push method call to get peers and seeds
// // Note: The order in which it is pushed matters. The returned array from rtorrent will be
// // an array of array of array of array of values
// torrents.forEach(function (torrent) {
//
// // Push peers first for the torrent
// systemMultiCallArray.push({
// methodName: 't.multicall',
// params: [torrent.hash, 'd.get_hash=', 't.get_scrape_incomplete=']
// });
//
// // Push seeds second for the torrent
// systemMultiCallArray.push({
// methodName: 't.multicall',
// params: [torrent.hash, 'd.get_hash=', 't.get_scrape_complete=']
// });
// });
//
// // Do the system.multicall and return promise
// // Inside the resolve function, we loop through the array
// return methodCall('system.multicall', [systemMultiCallArray]).then(function(data) {
// var numberArray = [];
//
// // The length of data should be equal to the length of systemMultiCallArray
// data.forEach(function(item) {
// // Each item in the array has an array of arrays
// item.forEach(function(itemagain) {
// // Map and reduce the array to get the number
// var number = itemagain.map(function (value) {
// return parseInt(value, 10);
// })
// .reduce(function (a, b) {
// return a + b;
// }, 0);
// // Push the number to a clean array so that we can place it correctly back into
// // the torrent object to return to client
// numberArray.push(number);
// });
// });
//
// // Map torrents and shift from numberArray to get the correct order
// // Peers is first, followed by seeds.
// // Return each torrent and finally return torrents back to caller.
// return torrents.map(function (torrent) {
// torrent.total_peers = numberArray.shift();
// torrent.total_seeds = numberArray.shift();
// return torrent;
// });
// });
// });
// }
//
// rtorrent.loadTorrentFile = function (filepath) {
// return methodCall('load', [filepath, 'd.set_custom=x-filename']);
// }
//
// rtorrent.loadTorrentStart = function (url) {
// return methodCall('load_start', [url]);
// }
//
//
// rtorrent.getTorrentMetaData = function (torrent) {
// var deferred = Q.defer();
//
// readTorrent(torrent.url, {}, function (err, data) {
// if (err) {
// deferred.reject(err);
// }
//
// deferred.resolve(data);
// });
//
// return deferred.promise;
// }
//
// rtorrent.getHash = function (hash) {
// return methodCall('d.get_hash', [hash]);
// }
//
// function checkPathExists (path) {
// var deferred = Q.defer();
// fs.exists(path, function (exists) {
// if (exists) {
// deferred.resolve(exists);
// }
//
// deferred.reject(new Error('Path does not exist.'));
// })
// return deferred.promise;
// }
//
//
// rtorrent.loadTorrent = function (torrent) {
// return rtorrent.getTorrentMetaData(torrent)
// .then(function (data) {
// var hash = data.infoHash.toUpperCase();
// logger.info('Retrieved hash from torrent', hash);
//
// // Check if torrent path is passed as a parameter
// if (torrent.path) {
// // Check if path exists
// logger.info('Checking if path exists.');
//
// return checkPathExists(torrent.path)
// .then(function (data) {
//
// logger.info('Directory exists.');
//
// // Load torrent but do not start
// return methodCall('load', [torrent.url])
// .then(function () {
// return Q.delay(500)
// .then(function () {
// // Get torrent hash
// return rtorrent.getHash(hash)
// .then(function () {
// return rtorrent.setTorrentDirectory(hash, torrent.path)
// .then(function () {
// return rtorrent.startTorrent(hash);
// });
// });
// })
// });
// }, function () {
//
// logger.info('Directory does not exist.');
//
// var joinedPath = path.join('/', torrent.path);
//
// return Q.nfcall(fs.mkdir, joinedPath)
// .then(function () {
// logger.info('Created directory', joinedPath);
//
// logger.info('Setting directory of torrent to', joinedPath);
//
// // Load torrent but do not start
// return methodCall('load', [torrent.url])
// .then(function () {
// return Q.delay(500)
// .then(function () {
// // Get torrent hash
// return rtorrent.getHash(hash)
// .then(function () {
// return rtorrent.setTorrentDirectory(hash, joinedPath)
// .then(function () {
// return rtorrent.startTorrent(hash);
// });
// });
// });
// });
//
// }, function (err) {
//
// if (err.code == 'EACCES') {
// throw new Error('Unable to create directory for torrent due to permissions.', hash);
// }
//
// // THrow error if not EACESS
// throw err;
// });
// });
// }
//
// // Start torrent if no path is passed
// return rtorrent.loadTorrentStart(torrent.url);
// });
// }
//
// rtorrent.setTorrentDirectory = function (hash, path) {
// return methodCall('d.set_directory', [hash, path]);
// }
//
// rtorrent.startTorrent = function (hash) {
// return methodCall('d.start', [hash])
// .then(function () {
// return methodCall('d.resume', [hash]);
// });
// }
//
// rtorrent.stopTorrent = function (hash) {
// return methodCall('d.close', [hash]);
// }
//
// rtorrent.pauseTorrent = function (hash) {
// return methodCall('d.stop', [hash]);
// }
//
// rtorrent.removeTorrent = function (hash) {
// return methodCall('d.erase', [hash]);
// }
//
// rtorrent.deleteTorrentData = function (hash) {
// return rtorrent.stopTorrent(hash).then(function() {
// return rtorrent.isMultiFile(hash).then(function(data) {
// return rtorrent.getTorrentDirectory(hash).then(function(dir) {
// if (data === '1') {
// logger.info(hash, 'is a multifile torrent.');
// logger.info('Deleting directory path/file', dir);
// return deleteData(dir).then(function(data) {
// return rtorrent.removeTorrent(hash);
// });
// } else {
// logger.info(hash, 'is a single file torrent.');
// return rtorrent.getTorrentName(hash).then(function(name) {
// logger.info('Deleting directory path/file', dir + '/' + name)
// return deleteData(dir + '/' + name).then(function(data) {
// return rtorrent.removeTorrent(hash);
// });
// });
// }
//
// });
// });
// });
// }
//
// // function deleteData (path) {
// // var deferred = Q.defer();
// //
// // rimraf(path, function(err, results) {
// // if (err) {
// // deferred.reject(err)
// // }
// //
// // deferred.resolve(results);
// // });
// //
// // return deferred.promise;
// // }
//
// rtorrent.getTorrentName = function (hash) {
// return methodCall('d.get_name', [hash]);
// }
//
// rtorrent.getBasePath = function (hash) {
// return methodCall('d.get_base_path', [hash]);
// }
//
// rtorrent.isMultiFile = function (hash) {
// return methodCall('d.is_multi_file', [hash]);
// }
//
// rtorrent.getTorrentDirectory = function (hash) {
// return methodCall('d.get_directory', [hash]);
// }
//
// rtorrent.getNetworkListenPort = function () {
// return methodCall('network.listen.port', []);
// }
//
// rtorrent.setPriority = function (priority) {
// return methodCall('d.set_priority', [hash, priority]);
// }
//
// // change to use nconf
// rtorrent.getPortStatus = function (port) {
// var deferred = Q.defer();
//
// portscanner.checkPortStatus(port, 'localhost', function (err, data) {
// if (err) {
// return deferred.reject(err);
// }
//
// return deferred.resolve(data);
// });
//
// return deferred.promise;
// }
//
// rtorrent.getTotalPeers = function (hash) {
// return rtorrent.getScrapeIncomplete(hash)
// .then(function (data) {
// return data.map(function (value) {
// return parseInt(value, 10);
// })
// .reduce(function (a, b) {
// return a + b;
// }, 0);
// });
// }
//
// rtorrent.getTotalSeeds = function (hash) {
// return rtorrent.getScrapeComplete(hash)
// .then(function (data) {
// return data.map(function (value) {
// return parseInt(value, 10);
// }).reduce(function (a, b) {
// return a + b;
// }, 0);
// });
// }
//
// rtorrent.getScrapeIncomplete = function (hash) {
// return methodCall('t.multicall', [hash, 'd.get_hash=', 't.get_scrape_incomplete=']);
// }
//
// rtorrent.getScrapeComplete = function (hash) {
// return methodCall('t.multicall', [hash, 'd.get_hash=', 't.get_scrape_complete=']);
// }
//
// // get_port_range
// // returns string of port range
// rtorrent.getPortRange = function () {
// return methodCall('get_port_range', [])
// .then(function (data) {
// return {
// 'port_range': data
// }
// });
// }
//
// rtorrent.setPortRange = function (value) {
// return methodCall('set_port_range', [value]);
// }
//
// // get_port_open
// // returns 1 or 0
// // Opens listening port
// rtorrent.getPortOpen = function () {
// return methodCall('get_port_open', [])
// .then(function (data) {
// return {
// 'port_open': data == 1 ? true : false
// }
// });
// }
//
// rtorrent.setPortOpen = function (value) {
// return methodCall('set_port_open', [value]);
// }
//
// rtorrent.getUploadSlots = function () {
// return methodCall('get_max_uploads', [])
// .then(function (data) {
// return {
// 'max_uploads': data
// }
// });
// }
//
// rtorrent.setUploadSlots = function (value) {
// return methodCall('set_max_uploads', [value]);
// }
//
// rtorrent.getUploadSlotsGlobal = function () {
// return methodCall('get_max_uploads_global', [])
// .then(function (data) {
// return {
// 'max_uploads_global': data
// }
// });
// }
//
// rtorrent.setUploadSlotsGlobal = function (value) {
// return methodCall('set_max_uploads_global', [value]);
// }
//
// rtorrent.getDownloadSlotsGlobal = function () {
// return methodCall('get_max_downloads_global', [])
// .then(function (data) {
// return {
// 'max_downloads_global': data
// }
// });
// }
//
// rtorrent.setDownloadSlotsGlobal = function (value) {
// return methodCall('set_max_downloads_global', [value]);
// }
//
// // get_port_random
// // returns 1 or 0
// // Randomize port each time rTorrent starts
// rtorrent.getPortRandom = function () {
// return methodCall('get_port_random', [])
// .then(function (data) {
// return {
// 'port_random': data == 1 ? true : false
// }
// });
// }
//
// rtorrent.setPortRandom = function (value) {
// return methodCall('set_port_random', [value]);
// }
//
// // get_download_rate
// // returns value in bytes
// rtorrent.getGlobalMaximumDownloadRate = function () {
// return methodCall('get_download_rate', [])
// .then(function (data) {
// return {
// 'global_max_download_rate': data
// }
// });
// }
//
// // set_download_rate
// // requires value in bytes
// rtorrent.setGlobalMaximumDownloadRate = function (value) {
// return methodCall('set_download_rate', [value]);
// }
//
// // get_upload_rate
// // returns value in bytes
// rtorrent.getGlobalMaximumUploadRate = function () {
// return methodCall('get_upload_rate', [])
// .then(function (data) {
// return {
// 'global_max_upload_rate': data
// }
// });
// }
//
// rtorrent.getMinNumberPeers = function () {
// return methodCall('get_min_peers', [])
// .then(function (data) {
// return {
// 'min_peers': data
// }
// });
// }
//
// rtorrent.setMinNumberPeers = function (value) {
// return methodCall('set_min_peers', [value]);
// }
//
// rtorrent.getMinNumberSeeds = function () {
// return methodCall('get_min_peers_seed', [])
// .then(function (data) {
// return {
// 'min_seeds': data
// }
// });
// }
//
// rtorrent.setMinNumberSeeds = function (value) {
// return methodCall('set_min_peers_seed', [value]);
// }
//
// rtorrent.getMaxNumberPeers = function () {
// return methodCall('get_max_peers', [])
// .then(function (data) {
// return {
// 'max_peers': data
// }
// });
// }
//
// rtorrent.setMaxNumberPeers = function (value) {
// return methodCall('set_max_peers', [value]);
// }
//
// rtorrent.getMaxNumberSeeds = function () {
// return methodCall('get_max_peers_seed', [])
// .then(function (data) {
// return {
// 'max_seeds': data
// }
// });
// }
//
// rtorrent.setMaxNumberSeeds = function (value) {
// return methodCall('set_max_peers_seed', [value]);
// }
//
// // set_upload_rate
// // requires value in bytes
// rtorrent.setGlobalMaximumUploadRate = function (value) {
// return methodCall('set_upload_rate', [value]);
// }
//
// rtorrent.getDirectory = function () {
// return methodCall('get_directory', [])
// .then(function (data) {
// return {
// 'download_directory': data
// }
// });
// }
//
// rtorrent.setDirectory = function (value) {
// return methodCall('set_directory', [value]);
// }
module.exports = rtorrent;

196
models/serializer.js Normal file
View File

@@ -0,0 +1,196 @@
var xmlBuilder = require('xmlbuilder')
, dateFormatter = require('./date_formatter')
/**
* Creates the XML for an XML-RPC method call.
*
* @param {String} method - The method name.
* @param {Array} params - Params to pass in the call.
* @param {Function} callback - function (error, xml) { ... }
* - {Object|null} error - Any errors that occurred while building the XML,
* otherwise null.
* - {String} xml - The method call XML.
*/
exports.serializeMethodCall = function(method, params) {
var params = params || [];
var xml = xmlBuilder
.create('methodCall')
.ele('methodName')
.txt(method)
.up()
.ele('params');
params.forEach(function(param) {
serializeValue(param, xml.ele('param'))
});
// Includes the <?xml ...> declaration
var xmlString = xml.doc().toString()
return xmlString;
}
/**
* Creates the XML for an XML-RPC method response.
*
* @param {mixed} value - The value to pass in the response.
* @param {Function} callback - function (error, xml) { ... }
* - {Object|null} error - Any errors that occurred while building the XML,
* otherwise null.
* - {String} xml - The method response XML.
*/
exports.serializeMethodResponse = function(result) {
var xml = xmlBuilder.create()
. begin('methodResponse', { version: '1.0' })
. ele('params')
. ele('param')
serializeValue(result, xml)
// Includes the <?xml ...> declaration
return xml.doc().toString()
}
exports.serializeFault = function(fault) {
var xml = xmlBuilder.create()
. begin('methodResponse', { version: '1.0' })
. ele('fault')
serializeValue(fault, xml)
// Includes the <?xml ...> declaration
return xml.doc().toString()
}
function serializeValue(value, xml) {
var stack = [ { value: value, xml: xml } ]
, current = null
, valueNode = null
, next = null
while (stack.length > 0) {
current = stack[stack.length - 1]
if (current.index !== undefined) {
// Iterating a compound
next = getNextItemsFrame(current)
if (next) {
stack.push(next)
}
else {
stack.pop()
}
}
else {
// we're about to add a new value (compound or simple)
valueNode = current.xml.ele('value')
switch(typeof current.value) {
case 'boolean':
appendBoolean(current.value, valueNode)
stack.pop()
break
case 'string':
appendString(current.value, valueNode)
stack.pop()
break
case 'number':
appendNumber(current.value, valueNode)
stack.pop()
break
case 'object':
if (current.value === null) {
valueNode.ele('nil')
stack.pop()
}
else if (current.value instanceof Date) {
appendDatetime(current.value, valueNode)
stack.pop()
}
else if (Buffer.isBuffer(current.value)) {
appendBuffer(current.value, valueNode)
stack.pop()
}
else {
if (Array.isArray(current.value)) {
current.xml = valueNode.ele('array').ele('data')
}
else {
current.xml = valueNode.ele('struct')
current.keys = Object.keys(current.value)
}
current.index = 0
next = getNextItemsFrame(current)
if (next) {
stack.push(next)
}
else {
stack.pop()
}
}
break
default:
stack.pop()
break
}
}
}
}
function getNextItemsFrame(frame) {
var nextFrame = null
if (frame.keys) {
if (frame.index < frame.keys.length) {
var key = frame.keys[frame.index++]
, member = frame.xml.ele('member').ele('name').text(key).up()
nextFrame = {
value: frame.value[key]
, xml: member
}
}
}
else if (frame.index < frame.value.length) {
nextFrame = {
value: frame.value[frame.index]
, xml: frame.xml
}
frame.index++
}
return nextFrame
}
function appendBoolean(value, xml) {
xml.ele('boolean').txt(value ? 1 : 0)
}
var illegalChars = /^(?![^<&]*]]>[^<&]*)[^<&]*$/
function appendString(value, xml) {
if (value.length === 0) {
xml.ele('string')
}
else if (!illegalChars.test(value)) {
xml.ele('string').d(value)
}
else {
xml.ele('string').txt(value)
}
}
function appendNumber(value, xml) {
if (value % 1 == 0) {
xml.ele('int').txt(value)
}
else {
xml.ele('double').txt(value)
}
}
function appendDatetime(value, xml) {
xml.ele('dateTime.iso8601').txt(dateFormatter.encodeIso8601(value))
}
function appendBuffer(value, xml) {
xml.ele('base64').txt(value.toString('base64'))
}

25
models/torrents.js Normal file
View File

@@ -0,0 +1,25 @@
var client = require('./client')();
function torrents() {
if((this instanceof torrents) === false) {
return new torrents();
}
};
torrents.prototype.listTorrents = function(callback) {
client.getTorrentList(callback);
};
torrents.prototype.stopTorrent = function(hash, callback) {
client.stopTorrent(hash, callback);
};
torrents.prototype.startTorrent = function(hash, callback) {
client.startTorrent(hash, callback);
};
module.exports = torrents;

41
package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "flood",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.12.0",
"cookie-parser": "~1.3.4",
"debug": "~2.1.1",
"es6-promise": "^2.0.1",
"express": "~4.12.2",
"flux": "^2.0.1",
"jade": "~1.9.2",
"keymirror": "^0.1.1",
"morgan": "~1.5.1",
"object-assign": "^2.0.0",
"q": "^1.2.0",
"react": "^0.13.1",
"sax": "^0.6.1",
"serve-favicon": "~2.2.0",
"xmlbuilder": "^2.6.2"
},
"devDependencies": {
"browser-sync": "^2.5.3",
"browserify": "^9.0.3",
"flux": "^2.0.1",
"gulp": "^3.8.11",
"gulp-autoprefixer": "^2.1.0",
"gulp-sass": "^1.3.3",
"gulp-watch": "^4.2.1",
"keymirror": "^0.1.1",
"object-assign": "^2.0.0",
"react": "^0.13.1",
"reactify": "^1.1.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^2.6.2",
"xmldom": "^0.1.19"
}
}

18
routes/client.js Normal file
View File

@@ -0,0 +1,18 @@
var express = require('express');
var xmlrpc = require('xmlrpc');
var router = express.Router();
var clientStats = require('../models/clientStats')();
router.get('/', function(req, res, next) {
});
router.get('/stats', function(req, res, next) {
clientStats.getStats(function(error, results) {
res.json(results);
});
});
module.exports = router;

9
routes/index.js Normal file
View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

36
routes/torrents.js Normal file
View File

@@ -0,0 +1,36 @@
var express = require('express');
var xmlrpc = require('xmlrpc');
var router = express.Router();
var torrents = require('../models/torrents')();
router.get('/', function(req, res, next) {
});
router.get('/list', function(req, res, next) {
torrents.listTorrents(function(error, results) {
res.json(results);
});
});
router.get('/:hash/stop', function(req, res, next) {
var hash = req.params.hash;
torrents.stopTorrent(hash, function(error, results) {
res.json(results);
})
});
router.get('/:hash/start', function(req, res, next) {
var hash = req.params.hash;
torrents.startTorrent(hash, function(error, results) {
res.json(results);
})
});
module.exports = router;

View File

@@ -0,0 +1,28 @@
html,
body {
height: 100%;
}
.container {
height: 100%;
width: 100%;
}
.flood {
display: flex;
height: 100%;
}
.filter-bar {
flex: 25%;
min-width: 200px;
max-width: 240px;
}
.torrent {
&__list {
flex: 100%;
}
}

View File

@@ -0,0 +1,7 @@
body {
background: $background;
}
ul {
list-style: none;
}

View File

@@ -0,0 +1,10 @@
body {
color: $foreground;
font-family: $font;
}
.unit {
font-style: normal;
margin-left: 0.3em;
opacity: 0.8;
}

View File

@@ -0,0 +1,27 @@
.client-stats {
padding: 30px 20px;
.speed,
.transferred {
display: block;
font-size: 1.25em;
line-height: 1.2;
}
.speed {
color: $client-stats--primary;
}
.transferred {
color: $client-stats--secondary;
}
}
.client-stat {
& + .client-stat {
margin-top: 20px;
}
}

View File

@@ -0,0 +1,10 @@
.filter-bar {
background: $filter-bar--background;
color: $filter-bar--foreground;
padding: 30px 0;
&__item {
border-bottom: 1px solid $filter-bar--border;
}
}

View File

@@ -0,0 +1,10 @@
.status-filter {
font-size: 0.85em;
padding: 30px 0;
&__item {
cursor: pointer;
padding: 3px 20px;
}
}

View File

@@ -0,0 +1,59 @@
.torrent {
&,
&__header {
display: flex;
padding: 10px 20px;
}
&__header {
background: $torrent--header--background;
border-bottom: 1px solid $torrent--header--border;
border-top: 1px solid $torrent--header--border;
color: $torrent--header--foreground;
display: flex;
font-size: 0.5em;
letter-spacing: 0.1em;
text-transform: uppercase;
}
}
.torrent {
&__details,
&__list {
list-style: none;
}
&__detail {
&--primary,
&--secondary {
flex: 1;
}
&--primary {
.torrent & {
color: $torrent--primary--foreground;
font-size: 1.1em;
}
}
&--secondary {
display: flex;
&--sub {
flex: 1;
}
}
label {
display: block;
}
}
}

18
source/sass/style.scss Normal file
View File

@@ -0,0 +1,18 @@
@import "bower_components/inuit-defaults/settings.defaults";
@import "bower_components/inuit-functions/tools.functions";
@import "bower_components/inuit-mixins/tools.mixins";
@import "bower_components/inuit-normalize/generic.normalize";
@import "bower_components/inuit-reset/generic.reset";
@import "bower_components/inuit-box-sizing/generic.box-sizing";
@import "bower_components/inuit-page/base.page";
@import 'tools/variables';
@import 'base/main';
@import 'base/layout';
@import 'base/typography';
@import 'objects/torrents';
@import 'objects/filter-bar';
@import 'objects/client-stats';
@import 'objects/status-filter';

View File

@@ -0,0 +1,17 @@
$background: #2e2a30;
$foreground: #686469;
$filter-bar--background: #1b191c;
$filter-bar--foreground: #7f7e80;
$filter-bar--border: #161316;
$client-stats--primary: #e4dde6;
$client-stats--secondary: #8b858d;
$client-stats--tertiary: #363337;
$torrent--primary--foreground: #aaa8ab;
$torrent--secondary--foreground: #686469;
$torrent--header--foreground: #5b595c;
$torrent--header--background: #29262b;
$torrent--header--border: #221f24;

View File

@@ -0,0 +1,5 @@
@import 'colors';
@import url(http://fonts.googleapis.com/css?family=Roboto:400italic,700italic,300,700,300italic,400);
$font: 'Roboto', sans-srif;

View File

@@ -0,0 +1,40 @@
var AppDispatcher = require('../dispatcher/AppDispatcher');
var TorrentConstants = require('../constants/TorrentConstants');
var performAction = function(action, hash, success, error) {
$.ajax({
url: '/torrents/' + hash + '/' + action,
dataType: 'json',
success: function(data) {
success(data);
}.bind(this),
error: function(xhr, status, err) {
console.error(torrentsData, status, err.toString());
}.bind(this)
});
};
var TorrentActions = {
stop: function(hash) {
performAction('stop', hash, function(data) {
AppDispatcher.dispatch({
actionType: TorrentConstants.TORRENT_STOP
});
});
},
start: function(hash) {
performAction('start', hash, function(data) {
AppDispatcher.dispatch({
actionType: TorrentConstants.TORRENT_START
});
});
}
}
module.exports = TorrentActions;

5
source/scripts/app.js Normal file
View File

@@ -0,0 +1,5 @@
var React = require('react');
var FloodApp = require('./components/FloodApp');
var mountNode = document.getElementById('app');
React.render(<FloodApp />, mountNode);

View File

@@ -0,0 +1,26 @@
var React = require('react');
var FilterBar = require('./filter-bar/FilterBar');
var TorrentList = require('./torrent-list/TorrentList');
var FloodApp = React.createClass({
getInitialState: function() {
return null;
},
componentDidMount: function() {
},
render: function() {
return (
<div className="flood">
<FilterBar />
<TorrentList />
</div>
);
}
});
module.exports = FloodApp;

View File

@@ -0,0 +1,19 @@
var React = require('react');
var Action = React.createClass({
getInitialState: function() {
return null;
},
render: function() {
var classString = 'action action--' + this.props.slug;
return (
<span className={classString} onClick={this.props.clickHandler}>{this.props.label}</span>
);
}
});
module.exports = Action;

View File

@@ -0,0 +1,29 @@
var React = require('react');
var Action = require('./Action.js');
var FilterBar = React.createClass({
getInitialState: function() {
return null;
},
handleClick: function(event) {
console.log('click ' + event.target);
},
render: function() {
return (
<nav className="filter-bar">
<div className="actions">
<Action label="Add Torrent" slug="add-torrent" clickHandler={this.handleClick} />
<Action label="Remove Torrent" slug="remove-torrent" clickHandler={this.handleClick} />
</div>
</nav>
);
}
});
module.exports = FilterBar;

View File

@@ -0,0 +1,93 @@
var React = require('react');
var ClientStore = require('../../stores/ClientStore');
var format = require('../../helpers/formatData');
var getClientStats = function() {
return {
clientStats: ClientStore.getStats()
}
}
var Speed = React.createClass({
getInitialState: function() {
return null;
},
render: function() {
return (
<span className="speed">
{this.props.value}
<em className="unit">{this.props.unit}</em>
</span>
);
}
});
var DataTransferred = React.createClass({
getInitialState: function() {
return null;
},
render: function() {
return (
<span className="transferred">
{this.props.value}
<em className="unit">{this.props.unit}</em>
</span>
);
}
});
var ClientStats = React.createClass({
getInitialState: function() {
return {
clientStats: {
speed: {
upload: 0,
download: 0
},
transferred: {
upload: 0,
download: 0
}
}
};
},
componentDidMount: function() {
ClientStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
ClientStore.removeChangeListener(this._onChange);
},
render: function() {
var uploadSpeed = format.data(this.state.clientStats.speed.upload, '/s');
var uploadTotal = format.data(this.state.clientStats.transferred.upload);
var downloadSpeed = format.data(this.state.clientStats.speed.download, '/s');
var downloadTotal = format.data(this.state.clientStats.transferred.download);
return (
<div className="client-stats filter-bar__item">
<div className="client-stat client-stat--upload">
<Speed value={uploadSpeed.value} unit={uploadSpeed.unit} />
<DataTransferred value={uploadTotal.value} unit={uploadTotal.unit} />
</div>
<div className="client-stat client-stat--download">
<Speed value={downloadSpeed.value} unit={downloadSpeed.unit} />
<DataTransferred value={downloadTotal.value} unit={downloadTotal.unit} />
</div>
</div>
);
},
_onChange: function() {
this.setState(getClientStats);
}
});
module.exports = ClientStats;

View File

@@ -0,0 +1,28 @@
var React = require('react');
var ClientStats = require('./ClientStats');
var StatusFilter = require('./StatusFilter');
var FilterBar = React.createClass({
getInitialState: function() {
return null;
},
handleClick: function(event) {
console.log('click ' + event.target);
},
render: function() {
return (
<nav className="filter-bar">
<ClientStats />
<StatusFilter />
</nav>
);
}
});
module.exports = FilterBar;

View File

@@ -0,0 +1,40 @@
var React = require('react');
var ClientStats = React.createClass({
getInitialState: function() {
return null;
},
render: function() {
var filters = [
'All',
'Downloading',
'Completed',
'Active',
'Inactive',
'Error'
];
var filterEls = filters.map(function(filter) {
var filterSlug = filter.toLowerCase();
var classString = 'status-filter__item status-filter__item--' + filterSlug;
return (
<li className={classString}>{filter}</li>
);
});
return (
<ul className="status-filter filter-bar__item">
{filterEls}
</ul>
);
}
});
module.exports = ClientStats;

View File

@@ -0,0 +1,65 @@
var React = require('react');
var TorrentActions = require('../../actions/TorrentActions.js');
var format = require('../../helpers/formatData');
var Torrent = React.createClass({
getInitialState: function() {
return null;
},
render: function() {
var torrent = this.props.data;
var uploadRate = format.data(torrent.uploadRate, '/s');
var uploadTotal = format.data(torrent.uploadTotal);
var downloadRate = format.data(torrent.downloadRate, '/s');
var downloadTotal = format.data(torrent.downloadTotal);
return (
<li className="torrent">
<span className="torrent__detail--primary">{torrent.name}</span>
<ul className="torrent__details torrent__detail--secondary">
<li className="torrent__detail--secondary--sub">{torrent.state}</li>
<li className="torrent__detail--secondary--sub">
{uploadRate.value}
<em className="unit">{uploadRate.unit}</em>
</li>
<li className="torrent__detail--secondary--sub">
{uploadTotal.value}
<em className="unit">{uploadTotal.unit}</em>
</li>
<li className="torrent__detail--secondary--sub">
{downloadRate.value}
<em className="unit">{downloadRate.unit}</em>
</li>
<li className="torrent__detail--secondary--sub">
{downloadTotal.value}
<em className="unit">{downloadTotal.unit}</em>
</li>
<li className="torrent__detail--secondary--sub">
{torrent.ratio}
</li>
<li className="torrent__detail--secondary--sub" onClick={this._onStart}>
Start
</li>
<li className="torrent__detail--secondary--sub" onClick={this._onStop}>
Stop
</li>
</ul>
</li>
);
},
_onStop: function() {
TorrentActions.stop(this.props.data.hash);
},
_onStart: function() {
TorrentActions.start(this.props.data.hash);
}
});
module.exports = Torrent;

View File

@@ -0,0 +1,65 @@
var React = require('react');
var Torrent = require('./Torrent');
var TorrentStore = require('../../stores/TorrentStore.js')
var getTorrentList = function() {
return {
allTorrents: TorrentStore.getAll()
}
}
var TorrentList = React.createClass({
getInitialState: function() {
return {
allTorrents: []
};
},
componentDidMount: function() {
TorrentStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
TorrentStore.removeChangeListener(this._onChange);
},
render: function() {
var torrents = this.state.allTorrents;
var torrentList = torrents.map(function(torrent) {
return (
<Torrent key={torrent.hash} data={torrent} />
);
});
return (
<ul className="torrent__list">
<header className="torrent__header">
<span className="torrent__detail--primary">Name</span>
<div className="torrent__detail--secondary">
<span className="torrent__detail--secondary--sub">State</span>
<span className="torrent__detail--secondary--sub">Up</span>
<span className="torrent__detail--secondary--sub">&nbsp;</span>
<span className="torrent__detail--secondary--sub">Down</span>
<span className="torrent__detail--secondary--sub">&nbsp;</span>
<span className="torrent__detail--secondary--sub">Ratio</span>
<span className="torrent__detail--secondary--sub">Start</span>
<span className="torrent__detail--secondary--sub">Stop</span>
</div>
</header>
{torrentList}
</ul>
);
},
_onChange: function() {
this.setState(getTorrentList);
}
});
module.exports = TorrentList;

View File

@@ -0,0 +1,7 @@
var keyMirror = require('keymirror');
module.exports = keyMirror({
ADD_TORRENT: 'client--add-torrent',
REMOVE_TORRENT: 'client--remove-torrent',
CLIENT_STATS_CHANGE: 'client--stats-change'
});

View File

@@ -0,0 +1,7 @@
var keyMirror = require('keymirror');
module.exports = keyMirror({
TORRENT_STOP: 'torrent--stop',
TORRENT_START: 'torrent--start',
TORRENT_LIST_CHANGE: 'torrent-list--change'
});

View File

@@ -0,0 +1,3 @@
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();

View File

@@ -0,0 +1,47 @@
var format = {
data: function(bytes, extraUnits, precision) {
var precision = precision || 2;
var kilobyte = 1024,
megabyte = kilobyte * 1024,
gigabyte = megabyte * 1024,
terabyte = gigabyte * 1024,
value = '',
unit = '';
if ((bytes >= 0) && (bytes < kilobyte)) {
value = bytes;
unit = 'B';
} else if ((bytes >= kilobyte) && (bytes < megabyte)) {
value = (bytes / kilobyte).toFixed(precision);
unit = 'kB';
} else if ((bytes >= megabyte) && (bytes < gigabyte)) {
value = (bytes / megabyte).toFixed(precision);
unit = 'MB';
} else if ((bytes >= gigabyte) && (bytes < terabyte)) {
value = (bytes / gigabyte).toFixed(precision);
unit = 'GB';
} else if (bytes >= terabyte) {
value = (bytes / terabyte).toFixed(precision);
unit = 'TB';
} else {
value = bytes;
unit = 'B';
}
if (extraUnits) {
unit += extraUnits;
}
return {
'value': value,
'unit': unit
};
}
}
module.exports = format;

View File

@@ -0,0 +1,82 @@
var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var ClientConstants = require('../constants/ClientConstants');
var assign = require('object-assign');
var _stats = {};
var ClientStore = assign({}, EventEmitter.prototype, {
getStats: function() {
return _stats;
},
emitChange: function() {
this.emit(ClientConstants.CLIENT_STATS_CHANGE);
},
addChangeListener: function(callback) {
this.on(ClientConstants.CLIENT_STATS_CHANGE, callback);
},
removeChangeListener: function(callback) {
this.removeListener(ClientConstants.CLIENT_STATS_CHANGE, callback);
}
});
var dispatcherIndex = AppDispatcher.register(function(action) {
var text;
switch(action.actionType) {
case ClientConstants.ADD_TORRENT:
getClientStats();
break;
case ClientConstants.REMOVE_TORRENT:
getClientStats();
break;
default:
// nothing
}
});
var getClientStats = function(callback) {
$.ajax({
url: '/client/stats',
dataType: 'json',
success: function(data) {
_stats = {
speed: {
upload: data.uploadRate,
download: data.downloadRate
},
transferred: {
upload: data.uploadTotal,
download: data.downloadTotal
}
};
ClientStore.emitChange();
}.bind(this),
error: function(xhr, status, err) {
console.error('/client/stats', status, err.toString());
}.bind(this)
});
};
getClientStats();
setInterval(getClientStats, 5000);
module.exports = ClientStore;

View File

@@ -0,0 +1,69 @@
var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var TorrentConstants = require('../constants/TorrentConstants');
var assign = require('object-assign');
var _torrents = [];
var getTorrentList = function(callback) {
$.ajax({
url: '/torrents/list',
dataType: 'json',
success: function(data) {
_torrents = data;
TorrentStore.emitChange();
}.bind(this),
error: function(xhr, status, err) {
console.error('/torrents/list', status, err.toString());
}.bind(this)
});
};
getTorrentList();
setInterval(getTorrentList, 5000);
var TorrentStore = assign({}, EventEmitter.prototype, {
getAll: function() {
return _torrents;
},
emitChange: function() {
this.emit(TorrentConstants.TORRENT_LIST_CHANGE);
},
addChangeListener: function(callback) {
this.on(TorrentConstants.TORRENT_LIST_CHANGE, callback);
},
removeChangeListener: function(callback) {
this.removeListener(TorrentConstants.TORRENT_LIST_CHANGE, callback);
}
});
var dispatcherIndex = AppDispatcher.register(function(action) {
var text;
switch(action.actionType) {
case TorrentConstants.TORRENT_STOP:
getTorrentList();
break;
case TorrentConstants.TORRENT_START:
getTorrentList();
break;
default:
// nothing
}
});
module.exports = TorrentStore;

6
views/error.jade Normal file
View File

@@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

4
views/index.jade Normal file
View File

@@ -0,0 +1,4 @@
extends layout
block content
div#app.container

11
views/layout.jade Normal file
View File

@@ -0,0 +1,11 @@
doctype html
html
head
title= title
link(rel='stylesheet' href='/stylesheets/style.css')
script(src='//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js')
body
block content
script(src='/scripts/app.js')
script#__bs_script__.
document.write("<script async src='http://HOST:3001/browser-sync/browser-sync-client.2.5.3.js'><\/script>".replace("HOST", location.hostname));