├── .gitattributes ├── .babelrc ├── dist └── .gitignore ├── app ├── styles │ ├── bootstrap-customizations.scss │ ├── style.js │ ├── app.scss │ └── bootstrap-sass.config.js ├── routes │ ├── async.js │ ├── router.js │ └── routes.jsx ├── hot-dev-app.html ├── app.html ├── mainApp.jsx ├── containers │ ├── QueueAllContainer.jsx │ ├── QueueStopContainer.jsx │ ├── QueueCompleteContainer.jsx │ ├── QueueDownloadContainer.jsx │ ├── AppContainer.jsx │ ├── MainContainer.jsx │ ├── PageAboutContainer.jsx │ └── PageSettingContainer.jsx └── components │ ├── Sidebar.jsx │ └── QueuePanel.jsx ├── background ├── lib │ ├── torrent-stream │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── test │ │ │ ├── data │ │ │ │ ├── test.torrent │ │ │ │ └── Lorem ipsum.txt │ │ │ ├── basic.js │ │ │ ├── auto-block.js │ │ │ ├── blocklist.js │ │ │ ├── storage.js │ │ │ └── tracker.js │ │ ├── package.json │ │ ├── lib │ │ │ ├── storage-buffer.js │ │ │ ├── piece.js │ │ │ ├── file-stream.js │ │ │ ├── peer-discovery.js │ │ │ ├── exchange-metadata.js │ │ │ └── storage.js │ │ ├── LICENSE │ │ ├── README.md │ │ └── index.js │ ├── global-state.js │ ├── local-storage.js │ ├── client.js │ └── manager.js └── main.js ├── webpack ├── webpack.config.js ├── webpack-dev-server.config.js ├── webpack-hot-dev-server.config.js ├── webpack.config.production.js ├── loaders-by-extension.js └── make-webpack-config.js ├── icon ├── web_hi_res_512.icns ├── web_hi_res_512.ico ├── web_hi_res_512.png └── res │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ └── mipmap-xxxhdpi │ └── ic_launcher.png ├── test └── example.js ├── .editorconfig ├── .gitignore ├── README.md ├── .eslintrc ├── package.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /app/styles/bootstrap-customizations.scss: -------------------------------------------------------------------------------- 1 | $grid-gutter-width: 0; 2 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/.gitattributes: -------------------------------------------------------------------------------- 1 | test/data/* binary 2 | -------------------------------------------------------------------------------- /app/routes/async.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 'SomePage', 'ReadmePage' ] 2 | -------------------------------------------------------------------------------- /webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./make-webpack-config')({}) 2 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/.gitignore: -------------------------------------------------------------------------------- 1 | torrents 2 | node_modules 3 | example.js 4 | example.html 5 | -------------------------------------------------------------------------------- /icon/web_hi_res_512.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/icon/web_hi_res_512.icns -------------------------------------------------------------------------------- /icon/web_hi_res_512.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/icon/web_hi_res_512.ico -------------------------------------------------------------------------------- /icon/web_hi_res_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/icon/web_hi_res_512.png -------------------------------------------------------------------------------- /icon/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/icon/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /icon/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/icon/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /icon/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/icon/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /icon/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/icon/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /icon/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/icon/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /background/lib/torrent-stream/test/data/test.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeeChSien/nuTorrent/HEAD/background/lib/torrent-stream/test/data/test.torrent -------------------------------------------------------------------------------- /webpack/webpack-dev-server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./make-webpack-config')({ 2 | devServer: true, 3 | devtool: 'eval', 4 | debug: true 5 | }) 6 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | 3 | describe('description', function () { 4 | it('description', function () { 5 | // body... 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /app/styles/style.js: -------------------------------------------------------------------------------- 1 | // Import All Style 2 | 3 | import "bootstrap-sass!./bootstrap-sass.config.js"; 4 | import "react-bootstrap-table/css/react-bootstrap-table.min.css" 5 | import "./app.scss" 6 | -------------------------------------------------------------------------------- /webpack/webpack-hot-dev-server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./make-webpack-config')({ 2 | devServer: true, 3 | hotComponents: true, 4 | devtool: 'eval', 5 | debug: true 6 | }) 7 | -------------------------------------------------------------------------------- /webpack/webpack.config.production.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./make-webpack-config')({ 2 | // commonsChunk: true, 3 | longTermCaching: true, 4 | // separateStylesheet: true, 5 | // minimize: true 6 | // devtool: 'source-map' 7 | }) 8 | -------------------------------------------------------------------------------- /app/routes/router.js: -------------------------------------------------------------------------------- 1 | import routes from './routes' 2 | import Router from 'react-router' 3 | 4 | 5 | // we can create a router before 'running' it 6 | export default Router.create({ 7 | routes: routes, 8 | location: Router.HashLocation 9 | }) 10 | -------------------------------------------------------------------------------- /app/hot-dev-app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | νTorrent 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | νTorrent 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/mainApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AppContainer from './containers/AppContainer' 3 | import router from './routes/router' 4 | 5 | import './styles/style.js' 6 | 7 | window.location.hash = '/' 8 | 9 | router.run(function (Handler) { 10 | React.render(, document.getElementById('react-root')); 11 | }); 12 | -------------------------------------------------------------------------------- /background/lib/global-state.js: -------------------------------------------------------------------------------- 1 | var window = null; 2 | 3 | var GlobalState = function() { 4 | function getWindow() { 5 | return window; 6 | } 7 | 8 | function setWindow(_window) { 9 | window = _window; 10 | } 11 | 12 | return { 13 | getWindow: getWindow, 14 | setWindow: setWindow 15 | } 16 | } 17 | 18 | module.exports = new GlobalState; 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /app/containers/QueueAllContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import QueuePanel from '../components/QueuePanel' 5 | 6 | export default class QueueAllContainer extends React.Component { 7 | 8 | render() { 9 | const queue = 'all'; 10 | 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/containers/QueueStopContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import QueuePanel from '../components/QueuePanel' 5 | 6 | export default class QueueStopContainer extends React.Component { 7 | 8 | render() { 9 | const queue = 'stop'; 10 | 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/containers/QueueCompleteContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import QueuePanel from '../components/QueuePanel' 5 | 6 | export default class QueueCompleteContainer extends React.Component { 7 | 8 | render() { 9 | const queue = 'done'; 10 | 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/containers/QueueDownloadContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import QueuePanel from '../components/QueuePanel' 5 | 6 | export default class QueueDownloadContainer extends React.Component { 7 | 8 | render() { 9 | const queue = 'download'; 10 | 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/containers/AppContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { RouteHandler } from 'react-router' 3 | import { Link } from 'react-router' 4 | 5 | import Sidebar from '../components/Sidebar' 6 | 7 | export default class AppContainer extends React.Component { 8 | 9 | render() { 10 | return ( 11 |
12 | 13 | 14 |
15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/containers/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import ManagerPanel from '../components/ManagerPanel' 5 | 6 | export default class MainContainer extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 | 16 | back Home 17 |
18 | ) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # OSX 30 | .DS_Store 31 | 32 | # App packaged 33 | release 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nuTorrent 2 | ============== 3 | 4 | nuTorrent(νTorrent) is a pure javascript bittorrent client based on Electron, React, torrent-stream. 5 | 6 | OS X Binary (v0.6.0): [download link](https://github.com/LeeChSien/nuTorrent/releases/download/v0.6.0/OSX-nuTorrent-0.6.0.zip) 7 | 8 | Screenshot 9 | ------------ 10 | 11 | ![alt tag](https://cloud.githubusercontent.com/assets/1298784/9428375/d335081c-49de-11e5-9cd0-e812a132d293.png) 12 | 13 | Usage 14 | ------------ 15 | * Open the app 16 | * Add a magnet url or torrent file. 17 | 18 | Build 19 | ------------ 20 | Install dependencies. 21 | 22 | ```bash 23 | $ npm install 24 | ``` 25 | 26 | To start development! 27 | 28 | ```bash 29 | npm run hot-dev-server 30 | npm run start-hot 31 | ``` 32 | License 33 | ------------ 34 | MIT © [LeeChSien](https://github.com/LeeChSien) 35 | -------------------------------------------------------------------------------- /webpack/loaders-by-extension.js: -------------------------------------------------------------------------------- 1 | function extsToRegExp(exts) { 2 | return new RegExp('\\.(' + exts.map(function(ext) { 3 | return ext.replace(/\./g, '\\.') 4 | }).join('|') + ')(\\?.*)?$') 5 | } 6 | 7 | module.exports = function loadersByExtension(obj) { 8 | var loaders = [] 9 | Object.keys(obj).forEach(function(key) { 10 | var exts = key.split('|') 11 | var value = obj[key] 12 | var entry = { 13 | extensions: exts, 14 | test: extsToRegExp(exts) 15 | } 16 | 17 | if (Array.isArray(value)) { 18 | entry.loaders = value 19 | } else if (typeof value === 'string') { 20 | entry.loader = value 21 | } else { 22 | Object.keys(value).forEach(function(valueKey) { 23 | entry[valueKey] = value[valueKey] 24 | }) 25 | } 26 | loaders.push(entry) 27 | }) 28 | return loaders 29 | } 30 | -------------------------------------------------------------------------------- /app/styles/app.scss: -------------------------------------------------------------------------------- 1 | 2 | html { 3 | height: 100%; 4 | overflow: hidden; 5 | } 6 | 7 | body { 8 | height: 100%; 9 | overflow: hidden; 10 | } 11 | 12 | .control-label { 13 | padding-right: 15px; 14 | } 15 | 16 | .app-full-height, #react-root { 17 | height: 100%; 18 | } 19 | 20 | .sidebar { 21 | padding: 10px 10px; 22 | background-color: #f8f8f8; 23 | } 24 | 25 | .page { 26 | padding: 20px 20px; 27 | } 28 | 29 | .queue-panel-nav { 30 | margin-bottom: 0; 31 | 32 | .navbar-btn { 33 | margin-left: 5px; 34 | } 35 | 36 | .btn-group { 37 | margin-left: 40px; 38 | } 39 | } 40 | 41 | .no-task { 42 | margin-top: 100px; 43 | } 44 | 45 | td { 46 | h5, div, a { 47 | margin: 0; 48 | display: inline-block; 49 | width: 100%; 50 | white-space: nowrap; 51 | overflow: hidden; 52 | text-overflow: ellipsis; 53 | } 54 | 55 | .progress { 56 | margin: 0; 57 | } 58 | } 59 | 60 | .brand { 61 | margin-bottom: 10px; 62 | } 63 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torrent-stream", 3 | "version": "0.21.3", 4 | "description": "Low level streaming torrent client that exposes files as node.js streams and downloads pieces based on demand", 5 | "repository": "git://github.com/mafintosh/torrent-stream.git", 6 | "scripts": { 7 | "test": "tap test/*.js" 8 | }, 9 | "dependencies": { 10 | "bitfield": "^0.1.0", 11 | "bittorrent-dht": "^3.0.0", 12 | "bittorrent-tracker": "^2.6.0", 13 | "bncode": "^0.5.2", 14 | "compact2string": "^1.2.0", 15 | "end-of-stream": "^0.1.4", 16 | "hat": "0.0.3", 17 | "ip": "^0.3.0", 18 | "ip-set": "^1.0.0", 19 | "mkdirp": "^0.3.5", 20 | "parse-torrent": "^4.0.0", 21 | "peer-wire-swarm": "^0.12.0", 22 | "random-access-file": "^0.3.1", 23 | "rimraf": "^2.2.5", 24 | "thunky": "^0.1.0" 25 | }, 26 | "devDependencies": { 27 | "tap": "^0.4.8", 28 | "fs-extra": "^0.9.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/lib/storage-buffer.js: -------------------------------------------------------------------------------- 1 | var noop = function() {} 2 | 3 | module.exports = function(storage) { 4 | var that = {}; 5 | var mem = [] 6 | 7 | that.read = function(index, range, cb) { 8 | if (typeof range === 'function') return that.read(index, null, range); 9 | 10 | var offset = (range && range.offset) || 0; 11 | var length = range && range.length; 12 | 13 | if (mem[index]) return cb(null, range ? mem[index].slice(offset, offset+(length || mem[index].length)) : mem[index]); 14 | storage.read(index, range, cb); 15 | }; 16 | 17 | that.write = function(index, buf, cb) { 18 | if (!cb) cb = noop 19 | mem[index] = buf; 20 | storage.write(index, buf, function(err) { 21 | mem[index] = null 22 | cb(err); 23 | }); 24 | }; 25 | 26 | that.close = storage.close && function(cb) { 27 | storage.close(cb || noop); 28 | }; 29 | 30 | that.remove = storage.remove && function(cb) { 31 | storage.remove(cb || noop); 32 | }; 33 | 34 | return that 35 | } 36 | -------------------------------------------------------------------------------- /app/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class Sidebar extends React.Component { 5 | 6 | render() { 7 | return ( 8 |
9 |
10 |
11 | 12 |
13 |
14 | All 15 | Downloading 16 | Stop 17 | Complete 18 |
19 |
20 |
21 | About 22 |
23 |
24 |
25 | ) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "ecmaFeatures": { 7 | "jsx": true, 8 | "modules": true 9 | }, 10 | "env": { 11 | "node": true, 12 | "mocha": true, 13 | "es6": true, 14 | "browser": true 15 | }, 16 | "globals": { 17 | 18 | }, 19 | "rules": { 20 | "quotes": [2, "single"], 21 | "semi": [2, "never"], 22 | "curly": [2, "multi-line"], 23 | "no-underscore-dangle": 0, 24 | 25 | "react/display-name": 0, 26 | "react/jsx-boolean-value": 1, 27 | "react/jsx-quotes": 1, 28 | "react/jsx-no-undef": 1, 29 | "react/jsx-sort-props": 1, 30 | "react/jsx-sort-prop-types": 1, 31 | "react/jsx-uses-react": 1, 32 | "react/jsx-uses-vars": 1, 33 | "react/no-did-mount-set-state": 1, 34 | "react/no-did-update-set-state": 1, 35 | "react/no-multi-comp": 1, 36 | "react/no-unknown-property": 1, 37 | "react/prop-types": 1, 38 | "react/react-in-jsx-scope": 1, 39 | "react/self-closing-comp": 1, 40 | "react/sort-comp": 0, 41 | "react/wrap-multilines": 1 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/containers/PageAboutContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | import sh from 'shell' 5 | 6 | export default class PageAboutContainer extends React.Component { 7 | openUrl(url) { 8 | sh.openExternal(url) 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |
15 |
16 |

About

17 |
18 |
19 |
20 | 21 |
22 |

nuTorrent(νTorrent) is a pure javascript BitTorrent client based on Electron, React, torrent-stream.

23 |
24 |

MIT © LeeChSien

25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Mathias Buus Madsen 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /background/lib/local-storage.js: -------------------------------------------------------------------------------- 1 | var Jsonfile = require('jsonfile') 2 | var Defer = require("node-promise").defer 3 | var App = require('app') 4 | 5 | var CLIENTS_JSON_FILE_PATH = App.getPath('userCache') + '/' + 'clients.json'; 6 | var SETTING_JSON_FILE_PATH = App.getPath('userCache') + '/' + 'setting.json'; 7 | 8 | var LocalStorage = function() { 9 | function saveDefaultPath() { 10 | 11 | } 12 | 13 | function saveSettings() { 14 | 15 | } 16 | 17 | function saveClients(clients) { 18 | var deferred = Defer(); 19 | Jsonfile.writeFile(CLIENTS_JSON_FILE_PATH, clients, function(err) { 20 | deferred.resolve(); 21 | }); 22 | return deferred.promise; 23 | } 24 | 25 | function getClients() { 26 | var deferred = Defer(); 27 | Jsonfile.readFile(CLIENTS_JSON_FILE_PATH, function (err, clients) { 28 | if (!err && clients) { 29 | deferred.resolve(clients); 30 | } 31 | }); 32 | return deferred.promise; 33 | } 34 | 35 | return { 36 | saveClients: saveClients, 37 | getClients: getClients 38 | } 39 | } 40 | 41 | module.exports = new LocalStorage; 42 | -------------------------------------------------------------------------------- /app/routes/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, DefaultRoute } from 'react-router' 3 | 4 | import AppContainer from '../containers/AppContainer' 5 | 6 | import QueueAllContainer from '../containers/QueueAllContainer' 7 | import QueueDownloadContainer from '../containers/QueueDownloadContainer' 8 | import QueueStopContainer from '../containers/QueueStopContainer' 9 | import QueueCompleteContainer from '../containers/QueueCompleteContainer' 10 | 11 | import PageSettingContainer from '../containers/PageSettingContainer' 12 | import PageAboutContainer from '../containers/PageAboutContainer' 13 | 14 | 15 | export default ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /app/containers/PageSettingContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import { Input } from 'react-bootstrap' 4 | 5 | export default class PageSettingContainer extends React.Component { 6 | 7 | render() { 8 | return ( 9 |
10 |
11 |
12 |

Setting

13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 | ) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var torrents = require('../'); 3 | var fs = require('fs'); 4 | 5 | var fixture = torrents('magnet:?xt=urn:btih:ef330b39f4801d25b4245212e75a38634bfc856e', { 6 | tracker: false 7 | }); 8 | 9 | fixture.listen(10000); 10 | 11 | var engine = function() { 12 | var e = torrents('magnet:?xt=urn:btih:ef330b39f4801d25b4245212e75a38634bfc856e', { 13 | dht: false, 14 | tracker: false 15 | }); 16 | 17 | e.connect('127.0.0.1:10000'); 18 | return e; 19 | }; 20 | 21 | test('fixture can connect to the dht', function(t) { 22 | t.plan(1); 23 | fixture.on('ready', t.ok.bind(t, true, 'should be ready')); 24 | }); 25 | 26 | test('destroy engine after ready', function(t) { 27 | t.plan(1); 28 | var e = engine(); 29 | e.on('ready', function() { 30 | e.destroy(t.ok.bind(t, true, 'should be destroyed')); 31 | }); 32 | }); 33 | 34 | test('destroy engine right away', function(t) { 35 | t.plan(1); 36 | var e = engine(); 37 | e.destroy(t.ok.bind(t, true, 'should be destroyed')); 38 | }); 39 | 40 | test('remove fixture and all content', function(t) { 41 | t.plan(1); 42 | fixture.destroy(function() { 43 | fixture.remove(function() { 44 | t.ok(!fs.existsSync(fixture.path)); 45 | }); 46 | }); 47 | }); -------------------------------------------------------------------------------- /app/styles/bootstrap-sass.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bootstrapCustomizations: "./app/styles/bootstrap-customizations.scss", 3 | // mainSass: "./main.scss", // path to your main SASS file (optional) 4 | verbose: true, // print out your custom files used 5 | debug: false, // print out the full generated scss file 6 | styleLoader: "style-loader!css-loader!sass-loader", // see example for the ExtractTextPlugin 7 | scripts: { 8 | // add every bootstrap script you need 9 | // 'transition': true 10 | }, 11 | styles: { 12 | "mixins": true, 13 | 14 | "normalize": true, 15 | "print": true, 16 | "glyphicons": true, 17 | 18 | "scaffolding": true, 19 | "type": true, 20 | "code": true, 21 | "grid": true, 22 | "tables": true, 23 | "forms": true, 24 | "buttons": true, 25 | 26 | "component-animations": true, 27 | "dropdowns": true, 28 | "button-groups": true, 29 | "input-groups": true, 30 | "navs": true, 31 | "navbar": true, 32 | "breadcrumbs": true, 33 | "pagination": true, 34 | "pager": true, 35 | "labels": true, 36 | "badges": true, 37 | "jumbotron": true, 38 | "thumbnails": true, 39 | "alerts": true, 40 | "progress-bars": true, 41 | "media": true, 42 | "list-group": true, 43 | "panels": true, 44 | "wells": true, 45 | "responsive-embed": true, 46 | "close": true, 47 | 48 | "modals": true, 49 | "tooltip": true, 50 | "popovers": true, 51 | "carousel": true, 52 | 53 | "utilities": true, 54 | "responsive-utilities": true 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/test/auto-block.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var torrents = require('../'); 3 | var fs = require('fs-extra'); 4 | var path = require('path'); 5 | 6 | var torrent = fs.readFileSync(path.join(__dirname, 'data', 'test.torrent')); 7 | var tmpPath = path.join(__dirname, '..', 'torrents', 'test'); 8 | fs.removeSync(tmpPath); 9 | fs.copySync(path.join(__dirname, 'data'), tmpPath); 10 | 11 | var fixture = torrents(torrent, { 12 | dht: false, 13 | tracker: false, 14 | path: tmpPath 15 | }); 16 | 17 | fixture.listen(10000); 18 | 19 | test('fixture can verify the torrent', function(t) { 20 | t.plan(2); 21 | fixture.on('ready', function() { 22 | t.ok(true, 'seed should be ready'); 23 | t.deepEqual(fixture.bitfield.buffer.toString('hex'), 'c0', 'should verify all the pieces'); 24 | }); 25 | }); 26 | 27 | test('peer should be blocked on bad piece', function(t) { 28 | t.plan(4); 29 | 30 | fixture.store.write(0, new Buffer(1 << 14), function() { 31 | t.ok(true, 'bad piece should be written'); 32 | 33 | var engine = torrents(torrent, { 34 | dht: false, 35 | tracker: false, 36 | tmp: tmpPath 37 | }); 38 | 39 | engine.on('blocking', function(addr) { 40 | t.equal(addr, '127.0.0.1:10000'); 41 | engine.destroy(t.ok.bind(t, true, 'peer should be destroyed')); 42 | }); 43 | 44 | engine.connect('127.0.0.1:10000'); 45 | 46 | engine.swarm.once('wire', function() { 47 | fixture.swarm.wires[0].unchoke(); 48 | }); 49 | 50 | engine.on('ready', function() { 51 | t.ok(true, 'peer should be ready'); 52 | engine.files[0].select(); 53 | }); 54 | }); 55 | }); 56 | 57 | test('cleanup', function(t) { 58 | t.plan(1); 59 | fixture.destroy(t.ok.bind(t, true, 'seed should be destroyed')); 60 | }); -------------------------------------------------------------------------------- /background/lib/torrent-stream/test/blocklist.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var torrents = require('../'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var rimraf = require('rimraf'); 6 | var tracker = require('bittorrent-tracker'); 7 | var server = new tracker.Server(); 8 | 9 | var torrent = fs.readFileSync(path.join(__dirname, 'data', 'test.torrent')); 10 | var tmpPath = path.join(__dirname, '..', 'torrents', 'test'); 11 | rimraf.sync(tmpPath); 12 | 13 | var seed; 14 | 15 | server.on('error', function() { 16 | }); 17 | 18 | test('seed should connect to the tracker', function(t) { 19 | t.plan(3); 20 | 21 | server.once('listening', function() { 22 | t.ok(true, 'tracker should be listening'); 23 | seed = torrents(torrent, { 24 | dht: false, 25 | path: path.join(__dirname, 'data') 26 | }); 27 | seed.listen(6882); 28 | seed.once('ready', t.ok.bind(t, true, 'should be ready')); 29 | }); 30 | server.once('start', function(addr) { 31 | t.equal(addr, '127.0.0.1:6882'); 32 | }); 33 | server.listen(12345); 34 | }); 35 | 36 | test('peer should block the seed via blocklist', function(t) { 37 | t.plan(3); 38 | var peer = torrents(torrent, { 39 | dht: false, 40 | blocklist: [ 41 | { start: '127.0.0.1', end: '127.0.0.1' } 42 | ] 43 | }); 44 | peer.once('ready', function() { 45 | t.ok(true, 'peer should be ready'); 46 | peer.once('blocked-peer', function(addr) { 47 | t.equal(addr, '127.0.0.1:6882'); 48 | peer.destroy(t.ok.bind(t, true, 'peer should be destroyed')); 49 | }); 50 | }); 51 | }); 52 | 53 | test('peer should block the seed via explicit block', function(t) { 54 | t.plan(3); 55 | var peer = torrents(torrent, { dht: false }); 56 | peer.block('127.0.0.1:6882'); 57 | peer.once('ready', function() { 58 | t.ok(true, 'peer should be ready'); 59 | peer.once('blocked-peer', function(addr) { 60 | t.equal(addr, '127.0.0.1:6882'); 61 | peer.destroy(t.ok.bind(t, true, 'peer should be destroyed')); 62 | }); 63 | }); 64 | }); 65 | 66 | test('cleanup', function(t) { 67 | t.plan(2); 68 | seed.destroy(t.ok.bind(t, true, 'seed should be destroyed')); 69 | server.close(t.ok.bind(t, true, 'tracker should be closed')); 70 | }); -------------------------------------------------------------------------------- /background/lib/torrent-stream/lib/piece.js: -------------------------------------------------------------------------------- 1 | var BLOCK_SIZE = 1 << 14; 2 | 3 | var PieceBuffer = function(length) { 4 | if (!(this instanceof PieceBuffer)) return new PieceBuffer(length); 5 | this.parts = Math.ceil(length / BLOCK_SIZE); 6 | this.remainder = (length % BLOCK_SIZE) || BLOCK_SIZE; 7 | this.length = length; 8 | this.missing = length; 9 | this.buffered = 0; 10 | this.buffer = null; 11 | this.cancellations = null; 12 | this.reservations = 0; 13 | this.sources = null; 14 | this.flushed = false; 15 | }; 16 | 17 | PieceBuffer.BLOCK_SIZE = BLOCK_SIZE; 18 | 19 | PieceBuffer.prototype.size = function(i) { 20 | return i === this.parts-1 ? this.remainder : BLOCK_SIZE; 21 | }; 22 | 23 | PieceBuffer.prototype.offset = function(i) { 24 | return i * BLOCK_SIZE; 25 | }; 26 | 27 | PieceBuffer.prototype.reserve = function() { 28 | if (!this.init()) return -1; 29 | if (this.cancellations.length) return this.cancellations.pop(); 30 | if (this.reservations < this.parts) return this.reservations++; 31 | return -1; 32 | }; 33 | 34 | PieceBuffer.prototype.cancel = function(i) { 35 | if (!this.init()) return; 36 | this.cancellations.push(i); 37 | }; 38 | 39 | PieceBuffer.prototype.get = function(i) { 40 | if (!this.init()) return null; 41 | return this.buffer[i]; 42 | }; 43 | 44 | PieceBuffer.prototype.set = function(i, data, source) { 45 | if (!this.init()) return false; 46 | if (!this.buffer[i]) { 47 | this.buffered++; 48 | this.buffer[i] = data; 49 | this.missing -= data.length; 50 | if (this.sources.indexOf(source) === -1) { 51 | this.sources.push(source); 52 | } 53 | } 54 | return this.buffered === this.parts; 55 | }; 56 | 57 | PieceBuffer.prototype.flush = function() { 58 | if (!this.buffer || this.parts !== this.buffered) return null; 59 | var buffer = Buffer.concat(this.buffer, this.length); 60 | this.buffer = null; 61 | this.cancellations = null; 62 | this.sources = null; 63 | this.flushed = true; 64 | return buffer; 65 | }; 66 | 67 | PieceBuffer.prototype.init = function() { 68 | if (this.flushed) return false; 69 | if (this.buffer) return true; 70 | this.buffer = new Array(this.parts); 71 | this.cancellations = []; 72 | this.sources = []; 73 | return true; 74 | }; 75 | 76 | module.exports = PieceBuffer; -------------------------------------------------------------------------------- /background/lib/torrent-stream/lib/file-stream.js: -------------------------------------------------------------------------------- 1 | var stream = require('stream'); 2 | var util = require('util'); 3 | 4 | var FileStream = function(engine, file, opts) { 5 | if (!(this instanceof FileStream)) return new FileStream(engine, file, opts); 6 | stream.Readable.call(this); 7 | 8 | if (!opts) opts = {}; 9 | if (!opts.start) opts.start = 0; 10 | if (!opts.end && typeof opts.end !== 'number') opts.end = file.length-1; 11 | 12 | var offset = opts.start + file.offset; 13 | var pieceLength = engine.torrent.pieceLength; 14 | 15 | this.length = opts.end - opts.start + 1; 16 | this.startPiece = (offset / pieceLength) | 0; 17 | this.endPiece = ((opts.end + file.offset) / pieceLength) | 0; 18 | this._destroyed = false; 19 | this._engine = engine; 20 | this._piece = this.startPiece; 21 | this._missing = this.length; 22 | this._reading = false; 23 | this._notifying = false; 24 | this._critical = Math.min(1024 * 1024 / pieceLength, 2) | 0; 25 | this._offset = offset - this.startPiece * pieceLength; 26 | }; 27 | 28 | util.inherits(FileStream, stream.Readable); 29 | 30 | FileStream.prototype._read = function() { 31 | if (this._reading) return; 32 | this._reading = true; 33 | this.notify(); 34 | }; 35 | 36 | FileStream.prototype.notify = function() { 37 | if (!this._reading || !this._missing) return; 38 | if (!this._engine.bitfield.get(this._piece)) return this._engine.critical(this._piece, this._critical); 39 | 40 | var self = this; 41 | 42 | if (this._notifying) return; 43 | this._notifying = true; 44 | this._engine.store.read(this._piece++, function(err, buffer) { 45 | self._notifying = false; 46 | 47 | if (self._destroyed || !self._reading) return; 48 | 49 | if (err) return self.destroy(err); 50 | 51 | if (self._offset) { 52 | buffer = buffer.slice(self._offset); 53 | self._offset = 0; 54 | } 55 | 56 | if (self._missing < buffer.length) buffer = buffer.slice(0, self._missing); 57 | 58 | self._missing -= buffer.length; 59 | 60 | if (!self._missing) { 61 | self.push(buffer); 62 | self.push(null); 63 | return; 64 | } 65 | 66 | self._reading = false; 67 | self.push(buffer); 68 | }); 69 | }; 70 | 71 | FileStream.prototype.destroy = function() { 72 | if (this._destroyed) return; 73 | this._destroyed = true; 74 | this.emit('close'); 75 | }; 76 | 77 | module.exports = FileStream; -------------------------------------------------------------------------------- /background/lib/torrent-stream/lib/peer-discovery.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var dht = require('bittorrent-dht'); 3 | var tracker = require('bittorrent-tracker'); 4 | 5 | var DEFAULT_PORT = 6881; 6 | 7 | module.exports = function(torrent, opts) { 8 | if (typeof opts !== 'object') { 9 | opts = torrent; 10 | torrent = null; 11 | } 12 | 13 | var port = opts.port || DEFAULT_PORT; 14 | 15 | var discovery = new events.EventEmitter(); 16 | 17 | discovery.dht = null; 18 | discovery.tracker = null; 19 | 20 | var onpeer = function(addr) { 21 | discovery.emit('peer', addr); 22 | }; 23 | 24 | var createDHT = function(infoHash) { 25 | if (opts.dht === false) return; 26 | 27 | var table = dht(); 28 | 29 | table.on('peer', onpeer); 30 | table.on('ready', function() { 31 | table.lookup(infoHash); 32 | }); 33 | table.listen(); 34 | 35 | return table; 36 | }; 37 | 38 | var createTracker = function(torrent) { 39 | if (opts.trackers) { 40 | torrent = Object.create(torrent); 41 | var trackers = (opts.tracker !== false) && torrent.announce ? torrent.announce : []; 42 | torrent.announce = trackers.concat(opts.trackers); 43 | } else if (opts.tracker === false) { 44 | return; 45 | } 46 | 47 | if (!torrent.announce || !torrent.announce.length) return; 48 | 49 | var tr = new tracker.Client(new Buffer(opts.id), port, torrent); 50 | 51 | tr.on('peer', onpeer); 52 | tr.on('error', function() { /* noop */ }); 53 | 54 | tr.start(); 55 | return tr; 56 | }; 57 | 58 | discovery.setTorrent = function(t) { 59 | torrent = t; 60 | 61 | if (discovery.tracker) { 62 | // If we have tracker then it had probably been created before we got infoDictionary. 63 | // So client do not know torrent length and can not report right information about uploads 64 | discovery.tracker.torrentLength = torrent.length; 65 | } else { 66 | process.nextTick(function() { 67 | if (!discovery.dht) discovery.dht = createDHT(torrent.infoHash); 68 | if (!discovery.tracker) discovery.tracker = createTracker(torrent); 69 | }); 70 | } 71 | }; 72 | 73 | discovery.updatePort = function(p) { 74 | if (port === p) return; 75 | port = p; 76 | if (discovery.tracker) discovery.tracker.stop(); 77 | if (discovery.dht) discovery.dht.announce(torrent.infoHash, port); 78 | if (torrent) discovery.tracker = createTracker(torrent); 79 | }; 80 | 81 | discovery.stop = function() { 82 | if (discovery.tracker) discovery.tracker.stop(); 83 | if (discovery.dht) discovery.dht.destroy(); 84 | }; 85 | 86 | if (torrent) discovery.setTorrent(torrent); 87 | 88 | return discovery; 89 | }; 90 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/test/storage.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var torrents = require('../'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | var torrent = fs.readFileSync(path.join(__dirname, 'data', 'test.torrent')); 7 | 8 | var fixture = torrents(torrent, { 9 | dht: false, 10 | tracker: false, 11 | path: path.join(__dirname, 'data') 12 | }); 13 | 14 | test('fixture can verify the torrent', function(t) { 15 | t.plan(2); 16 | fixture.once('ready', function() { 17 | t.ok(true, 'should be ready'); 18 | t.deepEqual(fixture.bitfield.buffer.toString('hex'), 'c0', 'should verify all the pieces'); 19 | }); 20 | }); 21 | 22 | test('fixture can read the file contents', function(t) { 23 | t.equal(fixture.files.length, 1, 'should have one file'); 24 | var file = fixture.files[0]; 25 | t.test('can read from stream', function(t) { 26 | var stream = file.createReadStream(); 27 | stream.setEncoding('ascii'); 28 | t.plan(1); 29 | stream.once('readable', function() { 30 | t.equal(stream.read(11), 'Lorem ipsum'); 31 | }); 32 | }); 33 | t.test('can read from stream with offset', function(t) { 34 | var stream = file.createReadStream({start: 36109}); 35 | stream.setEncoding('ascii'); 36 | t.plan(1); 37 | stream.once('readable', function() { 38 | t.equal(stream.read(6), 'amet. '); 39 | }); 40 | }); 41 | t.test('can read from storage', function(t) { 42 | t.plan(6); 43 | fixture.store.read(0, function(err, buffer) { 44 | t.equal(buffer.length, 32768); 45 | t.equal(buffer.toString('ascii', 0, 11), 'Lorem ipsum'); 46 | }); 47 | fixture.store.read(0, function(err, buffer) { 48 | t.equal(buffer.length, 32768); 49 | t.equal(buffer.toString('ascii', 588, 598), 'Vestibulum'); 50 | }); 51 | fixture.store.read(1, function(err, buffer) { 52 | t.equal(buffer.length, 3347); 53 | t.equal(buffer.toString('ascii', 3341), 'amet. '); 54 | }); 55 | }); 56 | t.test('can read from storage with offset', function(t) { 57 | t.plan(6); 58 | fixture.store.read(0, {length: 11}, function(err, buffer) { 59 | t.equal(buffer.length, 11); 60 | t.equal(buffer.toString('ascii'), 'Lorem ipsum'); 61 | }); 62 | fixture.store.read(0, {offset: 588, length: 10}, function(err, buffer) { 63 | t.equal(buffer.length, 10); 64 | t.equal(buffer.toString('ascii'), 'Vestibulum'); 65 | }); 66 | fixture.store.read(1, {offset: 3341}, function(err, buffer) { 67 | t.equal(buffer.length, 6); 68 | t.equal(buffer.toString('ascii'), 'amet. '); 69 | }); 70 | }); 71 | t.end(); 72 | }); 73 | 74 | test('cleanup', function(t) { 75 | t.plan(1); 76 | fixture.destroy(t.ok.bind(t, true, 'should be destroyed')); 77 | }); -------------------------------------------------------------------------------- /background/lib/torrent-stream/lib/exchange-metadata.js: -------------------------------------------------------------------------------- 1 | var bncode = require('bncode'); 2 | var crypto = require('crypto'); 3 | 4 | var METADATA_BLOCK_SIZE = 1 << 14; 5 | var METADATA_MAX_SIZE = 1 << 22; 6 | var EXTENSIONS = { 7 | m: { 8 | ut_metadata: 1 9 | } 10 | }; 11 | 12 | var sha1 = function(data) { 13 | return crypto.createHash('sha1').update(data).digest('hex'); 14 | }; 15 | 16 | module.exports = function(engine, callback) { 17 | var metadataPieces = []; 18 | 19 | return function(wire) { 20 | var metadata = engine.metadata; 21 | wire.once('extended', function(id, handshake) { 22 | try { 23 | handshake = bncode.decode(handshake); 24 | } catch (err) { 25 | return; 26 | } 27 | 28 | if (id || !handshake.m || handshake.m.ut_metadata === undefined) return; 29 | 30 | var channel = handshake.m.ut_metadata; 31 | var size = handshake.metadata_size; 32 | 33 | wire.on('extended', function(id, ext) { 34 | if (id !== EXTENSIONS.m.ut_metadata) return; 35 | 36 | var metadata = engine.metadata; 37 | var delimiter, message, piece; 38 | try { 39 | delimiter = ext.toString('ascii').indexOf('ee'); 40 | message = bncode.decode(ext.slice(0, delimiter === -1 ? ext.length : delimiter+2)); 41 | piece = message.piece; 42 | } catch (err) { 43 | return; 44 | } 45 | 46 | if (piece < 0) return; 47 | if (message.msg_type === 2) return; 48 | 49 | if (message.msg_type === 0) { 50 | if (!metadata) return wire.extended(channel, {msg_type:2, piece:piece}); 51 | var offset = piece * METADATA_BLOCK_SIZE; 52 | var buf = metadata.slice(offset, offset + METADATA_BLOCK_SIZE); 53 | wire.extended(channel, Buffer.concat([bncode.encode({msg_type:1, piece:piece}), buf])); 54 | return; 55 | } 56 | 57 | if (message.msg_type === 1 && !metadata) { 58 | metadataPieces[piece] = ext.slice(delimiter+2); 59 | for (var i = 0; i * METADATA_BLOCK_SIZE < size; i++) { 60 | if (!metadataPieces[i]) return; 61 | } 62 | 63 | metadata = Buffer.concat(metadataPieces); 64 | 65 | if (engine.infoHash !== sha1(metadata)) { 66 | metadataPieces = []; 67 | metadata = null; 68 | return; 69 | } 70 | 71 | callback(engine.metadata = metadata); 72 | } 73 | }); 74 | 75 | if (size > METADATA_MAX_SIZE) return; 76 | if (!size || metadata) return; 77 | 78 | for (var i = 0; i * METADATA_BLOCK_SIZE < size; i++) { 79 | if (metadataPieces[i]) continue; 80 | wire.extended(channel, {msg_type:0, piece:i}); 81 | } 82 | }); 83 | 84 | if (!wire.peerExtensions.extended) return; 85 | wire.extended(0, metadata ? {m:{ut_metadata:1}, metadata_size:metadata.length} : {m:{ut_metadata:1}}); 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | var os = require('os') 2 | var webpack = require('webpack') 3 | var cfg = require('./webpack/webpack.config.production.js') 4 | var packager = require('electron-packager') 5 | var assign = require('object-assign') 6 | var del = require('del') 7 | var latest = require('github-latest-release') 8 | var argv = require('minimist')(process.argv.slice(2)) 9 | var devDeps = Object.keys(require('./package.json').devDependencies) 10 | var packageJson = require('./package.json') 11 | 12 | var appName = argv.name || argv.n || 'ElectronReact' 13 | var shouldUseAsar = argv.asar || argv.a || false 14 | var shouldBuildAll = argv.all || false 15 | 16 | 17 | var DEFAULT_OPTS = { 18 | 'app-version': packageJson.version, 19 | dir: './', 20 | name: appName, 21 | asar: shouldUseAsar, 22 | ignore: [ 23 | '/test($|/)', 24 | '/tools($|/)', 25 | '/release($|/)' 26 | ].concat(devDeps.map(function(name) { return '/node_modules/' + name + '($|/)' })) 27 | } 28 | 29 | var icon = argv.icon || argv.i 30 | 31 | if (icon) { 32 | DEFAULT_OPTS.icon = icon 33 | } 34 | 35 | var version = argv.version || argv.v 36 | 37 | if (version) { 38 | DEFAULT_OPTS.version = version 39 | startPack() 40 | } else { 41 | latest('atom', 'electron', function(err, res) { 42 | if (err) { 43 | DEFAULT_OPTS.version = '0.28.3' 44 | } else { 45 | DEFAULT_OPTS.version = res.name.split('v')[1] 46 | } 47 | startPack() 48 | }) 49 | } 50 | 51 | 52 | function startPack() { 53 | console.log('start pack...') 54 | webpack(cfg, function(err, stats) { 55 | if (err) return console.error(err) 56 | del('release', function(err, paths) { 57 | if (err) return console.error(err) 58 | if (shouldBuildAll) { 59 | // build for all platforms 60 | var archs = ['ia32', 'x64'] 61 | var platforms = ['linux', 'win32', 'darwin'] 62 | 63 | platforms.forEach(function (plat) { 64 | archs.forEach(function (arch) { 65 | pack(plat, arch, log(plat, arch)) 66 | }) 67 | }) 68 | } else { 69 | // build for current platform only 70 | pack(os.platform(), os.arch(), log(os.platform(), os.arch())) 71 | } 72 | }) 73 | }) 74 | } 75 | 76 | function pack(plat, arch, cb) { 77 | // there is no darwin ia32 electron 78 | if (plat === 'darwin' && arch === 'ia32') return 79 | 80 | var opts = assign({}, DEFAULT_OPTS, { 81 | platform: plat, 82 | arch: arch, 83 | out: 'release/' + plat + '-' + arch 84 | }) 85 | 86 | packager(opts, cb) 87 | } 88 | 89 | function log(plat, arch) { 90 | return function(err, filepath) { 91 | if (err) return console.error(err) 92 | console.log(plat + '-' + arch + ' finished!') 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuTorrent", 3 | "version": "0.6.0", 4 | "description": "Pure Javascript BitTorrent Client", 5 | "main": "background/main.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "dev-server": "webpack-dev-server --config webpack/webpack-dev-server.config.js --progress --colors --port 2992 --inline", 9 | "hot-dev-server": "webpack-dev-server --config webpack/webpack-hot-dev-server.config.js --hot --progress --colors --port 2992 --inline", 10 | "build": "webpack --config webpack/webpack.config.production.js --progress --profile --colors", 11 | "start": "electron .", 12 | "start-dev": "NODE_ENV=development electron .", 13 | "start-hot": "HOT=1 NODE_ENV=development electron .", 14 | "package": "node package.js --name='nuTorrent' --icon=./icon/web_hi_res_512.icns", 15 | "package-all": "node package.js --name='nuTorrent' --icon=icon/web_hi_res_512.icns --all" 16 | }, 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/LeeChSien/nuTorrent" 21 | }, 22 | "author": { 23 | "name": "Jason Lee", 24 | "email": "chsienlee@gmail.com", 25 | "url": "https://github.com/LeeChSien" 26 | }, 27 | "devDependencies": { 28 | "asar": "^0.6.1", 29 | "babel-core": "^5.4.2", 30 | "babel-eslint": "^3.1.14", 31 | "babel-loader": "^5.1.2", 32 | "chai": "^2.3.0", 33 | "css-loader": "^0.12.1", 34 | "del": "^1.2.0", 35 | "electron-packager": "^4.1.0", 36 | "electron-prebuilt": "^0.30.4", 37 | "electron-rebuild": "^0.2.1", 38 | "eslint": "^0.22.1", 39 | "eslint-plugin-react": "^2.5.0", 40 | "extract-text-webpack-plugin": "^0.8.0", 41 | "file-loader": "^0.8.1", 42 | "github-latest-release": "^0.1.1", 43 | "html-loader": "^0.3.0", 44 | "json-loader": "^0.5.1", 45 | "markdown-loader": "^0.1.2", 46 | "minimist": "^1.1.1", 47 | "mocha": "^2.2.5", 48 | "node-libs-browser": ">= 0.4.0 <=0.6.0", 49 | "node-sass": "^3.2.0", 50 | "proxyquire": "^1.4.0", 51 | "raw-loader": "^0.5.1", 52 | "react-hot-loader": "^1.2.7", 53 | "react-proxy-loader": "^0.3.4", 54 | "sass-loader": "^2.0.1", 55 | "sinon": "^1.14.1", 56 | "stats-webpack-plugin": "^0.1.0", 57 | "style-loader": "^0.12.2", 58 | "url-loader": "^0.5.5", 59 | "webpack": "^1.9.7", 60 | "webpack-dev-server": "^1.9.0" 61 | }, 62 | "dependencies": { 63 | "bootstrap-sass": "^3.3.5", 64 | "bootstrap-sass-loader": "^1.0.8", 65 | "debug": "^2.2.0", 66 | "electron-debug": "^0.1.1", 67 | "ipc-promise": "^0.1.1", 68 | "javascript-state-machine": "^2.3.5", 69 | "jsonfile": "^2.2.1", 70 | "keymirror": "^0.1.1", 71 | "md5": "^2.0.0", 72 | "node-promise": "^0.5.12", 73 | "object-assign": "^2.1.1", 74 | "open": "0.0.5", 75 | "parse-torrent": "^5.3.0", 76 | "react": "^0.13.3", 77 | "react-bootstrap": "^0.24.5", 78 | "react-bootstrap-table": "^0.9.9", 79 | "react-router": "^0.13.3" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/test/tracker.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var torrents = require('../'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var rimraf = require('rimraf'); 6 | var tracker = require('bittorrent-tracker'); 7 | var server = new tracker.Server(); 8 | 9 | var torrent = fs.readFileSync(path.join(__dirname, 'data', 'test.torrent')); 10 | var tmpPath = path.join(__dirname, '..', 'torrents', 'test'); 11 | rimraf.sync(tmpPath); 12 | 13 | var fixture; 14 | 15 | server.on('error', function() { 16 | }); 17 | 18 | test('seed should connect to the tracker', function(t) { 19 | t.plan(3); 20 | 21 | server.once('listening', function() { 22 | t.ok(true, 'tracker should be listening'); 23 | fixture = torrents(torrent, { 24 | dht: false, 25 | path: path.join(__dirname, 'data') 26 | }); 27 | fixture.listen(6882); 28 | fixture.once('ready', t.ok.bind(t, true, 'should be ready')); 29 | }); 30 | server.once('start', function(addr) { 31 | t.equal(addr, '127.0.0.1:6882'); 32 | }); 33 | server.listen(12345); 34 | }); 35 | 36 | test('peer should connect to the swarm using .torrent file', function(t) { 37 | t.plan(4); 38 | var engine = torrents(torrent, { dht: false }); 39 | engine.once('ready', function() { 40 | t.ok(true, 'should be ready'); 41 | engine.destroy(function() { 42 | engine.remove(t.ok.bind(t, true, 'should be destroyed')); 43 | }); 44 | }); 45 | server.once('start', function(addr) { 46 | t.equal(addr, '127.0.0.1:6881'); 47 | }); 48 | server.once('stop', function(addr) { 49 | t.equal(addr, '127.0.0.1:6881'); 50 | }); 51 | }); 52 | 53 | test('peer should connect to the swarm using magnet link', function(t) { 54 | t.plan(4); 55 | var engine = torrents('magnet:?xt=urn:btih:1cb9681dccbe6ef86ac797ab93840a4f0c4ccae8' + 56 | '&tr=http%3A%2F%2F127.0.0.1%3A12345%2Fannounce', { dht: false, tmp: tmpPath }); 57 | engine.once('ready', function() { 58 | t.ok(true, 'should be ready'); 59 | engine.destroy(function() { 60 | engine.remove(t.ok.bind(t, true, 'should be destroyed')); 61 | }); 62 | }); 63 | server.once('start', function(addr) { 64 | t.equal(addr, '127.0.0.1:6881'); 65 | }); 66 | server.once('stop', function(addr) { 67 | t.equal(addr, '127.0.0.1:6881'); 68 | }); 69 | }); 70 | 71 | test('peer should connect to the swarm using magnet link and trackers', function(t) { 72 | t.plan(4); 73 | var engine = torrents('magnet:?xt=urn:btih:1cb9681dccbe6ef86ac797ab93840a4f0c4ccae8', 74 | { dht: false, tmp: tmpPath, trackers: ['http://127.0.0.1:12345/announce'] }); 75 | engine.once('ready', function() { 76 | t.ok(true, 'should be ready'); 77 | engine.destroy(function() { 78 | engine.remove(t.ok.bind(t, true, 'should be destroyed')); 79 | }); 80 | }); 81 | server.once('start', function(addr) { 82 | t.equal(addr, '127.0.0.1:6881'); 83 | }); 84 | server.once('stop', function(addr) { 85 | t.equal(addr, '127.0.0.1:6881'); 86 | }); 87 | }); 88 | 89 | test('peer should connect to an alternate tracker', function(t) { 90 | t.plan(5); 91 | var server = new tracker.Server(); 92 | server.once('listening', function() { 93 | t.ok(true, 'tracker should be listening'); 94 | }); 95 | server.once('start', function(addr) { 96 | t.equal(addr, '127.0.0.1:6881'); 97 | }); 98 | server.listen(54321); 99 | 100 | var engine = torrents(torrent, { dht: false, trackers: ['http://127.0.0.1:54321/announce'] }); 101 | engine.once('ready', function() { 102 | t.ok(true, 'should be ready'); 103 | engine.destroy(t.ok.bind(t, true, 'should be destroyed')); 104 | server.close(t.ok.bind(t, true, 'tracker should be closed')); 105 | }); 106 | }); 107 | 108 | test('cleanup', function(t) { 109 | t.plan(2); 110 | fixture.destroy(t.ok.bind(t, true, 'should be destroyed')); 111 | server.close(t.ok.bind(t, true, 'tracker should be closed')); 112 | }); 113 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/lib/storage.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var raf = require('random-access-file'); 4 | var mkdirp = require('mkdirp'); 5 | var rimraf = require('rimraf'); 6 | var thunky = require('thunky'); 7 | 8 | var noop = function() {}; 9 | 10 | module.exports = function(folder) { 11 | return function(torrent) { 12 | var that = {}; 13 | 14 | var destroyed = false; 15 | var piecesMap = []; 16 | var pieceLength = torrent.pieceLength; 17 | var files = []; 18 | 19 | var nonExistent = new Error('Non-existent file'); 20 | 21 | torrent.files.forEach(function(file) { 22 | var fileStart = file.offset; 23 | var fileEnd = file.offset + file.length; 24 | 25 | var firstPiece = Math.floor(fileStart / pieceLength); 26 | var lastPiece = Math.floor((fileEnd - 1) / pieceLength); 27 | 28 | var filePath = path.join(folder, file.path); 29 | 30 | var openWrite = thunky(function(cb) { 31 | mkdirp(path.dirname(filePath), function(err) { 32 | if (err) return cb(err); 33 | if (destroyed) return cb(new Error('Storage destroyed')); 34 | 35 | var f = raf(filePath); 36 | files.push(f); 37 | cb(null, f); 38 | }); 39 | }); 40 | 41 | var openRead = thunky(function(cb) { 42 | fs.exists(filePath, function(exists) { 43 | if (exists) return openWrite(cb); 44 | cb(nonExistent); 45 | }); 46 | }); 47 | 48 | for (var p = firstPiece; p <= lastPiece; ++p) { 49 | var pieceStart = p * pieceLength; 50 | var pieceEnd = pieceStart + pieceLength; 51 | 52 | var from = (fileStart < pieceStart) ? 0 : fileStart - pieceStart; 53 | var to = (fileEnd > pieceEnd) ? pieceLength : fileEnd - pieceStart; 54 | var offset = (fileStart > pieceStart) ? 0 : pieceStart - fileStart; 55 | 56 | if (!piecesMap[p]) piecesMap[p] = []; 57 | 58 | piecesMap[p].push({ 59 | from: from, 60 | to: to, 61 | offset: offset, 62 | openWrite: openWrite, 63 | openRead: openRead 64 | }); 65 | } 66 | }); 67 | 68 | that.read = function(index, range, cb) { 69 | if (typeof range === 'function') { 70 | cb = range; 71 | range = false; 72 | } 73 | 74 | if (range) { 75 | var rangeFrom = range.offset || 0; 76 | var rangeTo = range.length ? rangeFrom + range.length : pieceLength; 77 | if (rangeFrom === rangeTo) return cb(null, new Buffer(0)); 78 | } 79 | 80 | var targets = piecesMap[index]; 81 | if (range) { 82 | targets = targets.filter(function(target) { 83 | return (target.to > rangeFrom && target.from < rangeTo); 84 | }); 85 | 86 | if (!targets.length) return cb(new Error('no file matching the requested range?')); 87 | } 88 | 89 | if (!targets) return cb(new Error('no targets for the given index?')); 90 | 91 | var buffers = []; 92 | var i = 0; 93 | var end = targets.length; 94 | 95 | var next = function(err, buffer) { 96 | if (err) return cb(err); 97 | if (buffer) buffers.push(buffer); 98 | if (i >= end) return cb(null, Buffer.concat(buffers)); 99 | 100 | var target = targets[i++]; 101 | 102 | var from = target.from; 103 | var to = target.to; 104 | var offset = target.offset; 105 | 106 | if (range) { 107 | if (to > rangeTo) to = rangeTo; 108 | if (from < rangeFrom) { 109 | offset += rangeFrom - from; 110 | from = rangeFrom; 111 | } 112 | } 113 | 114 | target.openRead(function(err, file) { 115 | if (err) return (err === nonExistent) ? cb(null, new Buffer(0)) : cb(err); 116 | file.read(offset, to - from, next); 117 | }); 118 | }; 119 | 120 | next(); 121 | }; 122 | 123 | that.write = function(index, buffer, cb) { 124 | if (!cb) cb = noop; 125 | 126 | var targets = piecesMap[index]; 127 | var i = 0; 128 | var end = targets.length; 129 | 130 | var next = function(err) { 131 | if (err) return cb(err); 132 | if (i >= end) return cb(); 133 | 134 | var target = targets[i++]; 135 | target.openWrite(function(err, file) { 136 | if (err) return cb(err); 137 | file.write(target.offset, buffer.slice(target.from, target.to), next); 138 | }); 139 | }; 140 | 141 | next(); 142 | }; 143 | 144 | that.remove = function(cb) { 145 | if (!cb) cb = noop; 146 | if (!torrent.files.length) return cb(); 147 | 148 | that.close(function(err) { 149 | if (err) return cb(err); 150 | var root = torrent.files[0].path.split(path.sep)[0]; 151 | rimraf(path.join(folder, root), { maxBusyTries: 60 }, cb); 152 | }); 153 | }; 154 | 155 | that.close = function(cb) { 156 | if (!cb) cb = noop; 157 | if (destroyed) return cb(); 158 | destroyed = true; 159 | 160 | var i = 0; 161 | var loop = function(err) { 162 | if (i >= files.length) return cb(); 163 | if (err) return cb(err); 164 | var next = files[i++]; 165 | if (!next) return process.nextTick(loop); 166 | next.close(loop); 167 | }; 168 | 169 | process.nextTick(loop); 170 | }; 171 | 172 | return that; 173 | }; 174 | }; 175 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/README.md: -------------------------------------------------------------------------------- 1 | # torrent-stream 2 | 3 | The streaming torrent engine that [peerflix](https://github.com/mafintosh/peerflix) uses 4 | 5 | npm install torrent-stream 6 | 7 | ## How can I help? 8 | 9 | 1. Open issues on things that are broken 10 | 2. Fix open issues by sending PRs 11 | 3. Add documentation 12 | 13 | ## Usage 14 | 15 | torrent-stream is a node module that allows you to access files inside a torrent as node streams. 16 | 17 | ``` js 18 | var torrentStream = require('torrent-stream'); 19 | 20 | var engine = torrentStream('magnet:my-magnet-link'); 21 | 22 | engine.on('ready', function() { 23 | engine.files.forEach(function(file) { 24 | console.log('filename:', file.name); 25 | var stream = file.createReadStream(); 26 | // stream is readable stream to containing the file content 27 | }); 28 | }); 29 | ``` 30 | 31 | You can pass `start` and `end` options to stream to slice the file 32 | 33 | ``` js 34 | // get a stream containing bytes 10-100 inclusive. 35 | var stream = file.createReadStream({ 36 | start: 10, 37 | end: 100 38 | }); 39 | ``` 40 | 41 | Per default no files are downloaded unless you create a stream to them. 42 | If you want to fetch a file without creating a stream you should use the `file.select` and `file.deselect` methods. 43 | 44 | When you start torrent-stream it will connect to the torrent dht 45 | and fetch pieces according to the streams you create. 46 | 47 | ## Full API 48 | 49 | #### `engine = torrentStream(magnet_link_or_buffer, opts)` 50 | 51 | Create a new engine instance. Options can contain the following 52 | 53 | ``` js 54 | { 55 | connections: 100, // Max amount of peers to be connected to. 56 | uploads: 10, // Number of upload slots. 57 | tmp: '/tmp', // Root folder for the files storage. 58 | // Defaults to '/tmp' or temp folder specific to your OS. 59 | // Each torrent will be placed into a separate folder under /tmp/torrent-stream/{infoHash} 60 | path: '/tmp/my-file', // Where to save the files. Overrides `tmp`. 61 | verify: true, // Verify previously stored data before starting 62 | // Defaults to true 63 | dht: true, // Whether or not to use DHT to initialize the swarm. 64 | // Defaults to true 65 | tracker: true, // Whether or not to use trackers from torrent file or magnet link 66 | // Defaults to true 67 | trackers: [ 68 | 'udp://tracker.openbittorrent.com:80', 69 | 'udp://tracker.ccc.de:80' 70 | ], 71 | // Allows to declare additional custom trackers to use 72 | // Defaults to empty 73 | storage: myStorage() // Use a custom storage backend rather than the default disk-backed one 74 | } 75 | ``` 76 | 77 | #### `engine.on('ready', fn)` 78 | 79 | Emitted when the engine is ready to be used. 80 | The files array will be empty until this event is emitted 81 | 82 | #### `engine.on('download', [piece-index])` 83 | 84 | Emitted everytime a piece has been downloaded and verified. 85 | 86 | #### `engine.on('upload', [piece-index, offset, length])` 87 | 88 | Emitted everytime a piece is uploaded. 89 | 90 | #### `engine.files[...]` 91 | 92 | An array of all files in the torrent. See the file section for more info on what methods the file has 93 | 94 | #### `engine.destroy(cb)` 95 | 96 | Destroy the engine. Destroys all connections to peers 97 | 98 | #### `engine.connect('127.0.0.0:6881')` 99 | 100 | Connect to a peer manually 101 | 102 | #### `engine.disconnect('127.0.0.1:6881')` 103 | 104 | Disconnect from a peer manually 105 | 106 | #### `engine.block('127.0.0.1:6881')` 107 | 108 | Disconnect from a peer and add it to the blocklist, preventing any other connection to it 109 | 110 | #### `engine.remove([keep-pieces], cb)` 111 | 112 | Completely remove all saved data for this torrent. 113 | Optionally, only remove cache and temporary data but keep downloaded pieces 114 | 115 | #### `engine.listen([port], cb)` 116 | 117 | Listen for incoming peers on the specified port. Port defaults to `6881` 118 | 119 | #### `engine.swarm` 120 | 121 | The attached [peer-wire-swarm](https://github.com/mafintosh/peer-wire-swarm) instance 122 | 123 | #### `file = engine.files[...]` 124 | 125 | A file in the torrent. They contains the following data 126 | 127 | ``` js 128 | { 129 | name: 'my-filename.txt', 130 | path: 'my-folder/my-filename.txt', 131 | length: 424242 132 | } 133 | ``` 134 | 135 | #### `file.select()` 136 | 137 | Selects the file to be downloaded, but at a lower priority than streams. 138 | Useful if you know you need the file at a later stage. 139 | 140 | #### `file.deselect()` 141 | 142 | Deselects the file which means it won't be downloaded unless someone creates a stream to it 143 | 144 | #### `stream = file.createReadStream(opts)` 145 | 146 | Create a readable stream to the file. Pieces needed by the stream will be prioritized highly. 147 | Options can contain the following 148 | 149 | ``` js 150 | { 151 | start: startByte, 152 | end: endByte 153 | } 154 | ``` 155 | 156 | Both `start` and `end` are inclusive 157 | 158 | ## License 159 | 160 | MIT 161 | -------------------------------------------------------------------------------- /webpack/make-webpack-config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var path = require('path') 3 | var fs = require('fs') 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | var StatsPlugin = require('stats-webpack-plugin') 6 | var loadersByExtension = require('./loaders-by-extension') 7 | 8 | var projectRoot = path.join(__dirname, '..') 9 | var appRoot = path.join(projectRoot, 'app') 10 | 11 | module.exports = function(opts) { 12 | 13 | var entry = { 14 | main: opts.prerender ? path.join(appRoot, 'mainApp') : path.join(appRoot, 'mainApp') 15 | } 16 | 17 | var loaders = { 18 | 'jsx': opts.hotComponents ? [ 'react-hot-loader', 'babel-loader' ] : 'babel-loader', 19 | 'js': { 20 | loader: 'babel-loader', 21 | include: appRoot 22 | }, 23 | 'json': 'json-loader', 24 | 'txt': 'raw-loader', 25 | 'png|jpg|jpeg|gif|svg': 'url-loader?limit=10000', 26 | 'woff|woff2': 'url-loader?limit=100000', 27 | 'ttf|eot': 'file-loader', 28 | 'wav|mp3': 'file-loader', 29 | 'html': 'html-loader', 30 | 'md|markdown': [ 'html-loader', 'markdown-loader' ] 31 | } 32 | 33 | var cssLoader = opts.minimize ? 'css-loader' : 'css-loader?localIdentName=[path][name]---[local]---[hash:base64:5]' 34 | 35 | var stylesheetLoaders = { 36 | 'css': cssLoader, 37 | 'less': [ cssLoader, 'less-loader' ], 38 | 'styl': [ cssLoader, 'stylus-loader' ], 39 | 'scss|sass': [ cssLoader, 'sass-loader' ] 40 | } 41 | 42 | var additionalLoaders = [ 43 | // { test: /some-reg-exp$/, loader: 'any-loader' } 44 | ] 45 | 46 | var alias = { 47 | 48 | } 49 | 50 | var aliasLoader = { 51 | 52 | } 53 | 54 | var externals = [ 55 | 56 | ] 57 | 58 | var modulesDirectories = [ 'node_modules' ] 59 | 60 | var extensions = [ '', '.js', '.jsx', '.json', '.node' ] 61 | 62 | var publicPath = opts.devServer 63 | ? 'http://localhost:2992/dist/' 64 | : '/dist/' 65 | 66 | 67 | var output = { 68 | path: projectRoot + '/dist/', 69 | filename: 'bundle.js', 70 | publicPath: publicPath, 71 | contentBase: projectRoot + '/public/', 72 | libraryTarget: 'commonjs2' 73 | } 74 | 75 | var excludeFromStats = [ 76 | /node_modules[\\\/]react(-router)?[\\\/]/ 77 | ] 78 | 79 | 80 | var plugins = [ 81 | new webpack.PrefetchPlugin('react'), 82 | new webpack.PrefetchPlugin('react/lib/ReactComponentBrowserEnvironment') 83 | ] 84 | 85 | if (opts.prerender) { 86 | plugins.push(new StatsPlugin(path.join(projectRoot, 'dist', 'stats.prerender.json'), { 87 | chunkModules: true, 88 | exclude: excludeFromStats 89 | })) 90 | aliasLoader['react-proxy$'] = 'react-proxy/unavailable' 91 | aliasLoader['react-proxy-loader$'] = 'react-proxy-loader/unavailable' 92 | externals.push( 93 | /^react(\/.*)?$/, 94 | /^reflux(\/.*)?$/, 95 | 'superagent', 96 | 'async' 97 | ) 98 | plugins.push(new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })) 99 | } else { 100 | plugins.push(new StatsPlugin(path.join(projectRoot, 'dist', 'stats.json'), { 101 | chunkModules: true, 102 | exclude: excludeFromStats 103 | })); 104 | } 105 | 106 | if (opts.commonsChunk) { 107 | plugins.push(new webpack.optimize.CommonsChunkPlugin('commons', 'commons.js' + (opts.longTermCaching && !opts.prerender ? '?[chunkhash]' : ''))) 108 | } 109 | 110 | var asyncLoader = { 111 | test: require('../app/routes/async').map(function(name) { 112 | return path.join(appRoot, 'routes', name) 113 | }), 114 | loader: opts.prerender ? 'react-proxy-loader/unavailable' : 'react-proxy-loader' 115 | } 116 | 117 | Object.keys(stylesheetLoaders).forEach(function(ext) { 118 | var stylesheetLoader = stylesheetLoaders[ext] 119 | if (Array.isArray(stylesheetLoader)) stylesheetLoader = stylesheetLoader.join('!') 120 | if (opts.prerender) { 121 | stylesheetLoaders[ext] = stylesheetLoader.replace(/^css-loader/, 'css-loader/locals') 122 | } else if (opts.separateStylesheet) { 123 | stylesheetLoaders[ext] = ExtractTextPlugin.extract('style-loader', stylesheetLoader) 124 | } else { 125 | stylesheetLoaders[ext] = 'style-loader!' + stylesheetLoader 126 | } 127 | }) 128 | 129 | if (opts.separateStylesheet && !opts.prerender) { 130 | plugins.push(new ExtractTextPlugin('[name].css' + (opts.longTermCaching ? '?[contenthash]' : ''))) 131 | } 132 | 133 | if (opts.minimize && !opts.prerender) { 134 | plugins.push( 135 | new webpack.optimize.UglifyJsPlugin({ 136 | compressor: { 137 | warnings: false 138 | } 139 | }), 140 | new webpack.optimize.DedupePlugin() 141 | ) 142 | } 143 | 144 | if (opts.minimize) { 145 | plugins.push( 146 | new webpack.DefinePlugin({ 147 | 'process.env': { 148 | NODE_ENV: "'production'" 149 | } 150 | }), 151 | new webpack.NoErrorsPlugin() 152 | ) 153 | } 154 | 155 | var nodeModules = fs.readdirSync('node_modules').filter(function(x) { return x !== '.bin' }) 156 | 157 | return { 158 | entry: entry, 159 | output: output, 160 | target: 'atom', 161 | externals: nodeModules, 162 | module: { 163 | loaders: [asyncLoader].concat(loadersByExtension(loaders)).concat(loadersByExtension(stylesheetLoaders)).concat(additionalLoaders) 164 | }, 165 | devtool: opts.devtool, 166 | debug: opts.debug, 167 | resolve: { 168 | root: appRoot, 169 | modulesDirectories: modulesDirectories, 170 | extensions: extensions, 171 | alias: alias 172 | }, 173 | plugins: plugins, 174 | devServer: { 175 | stats: { 176 | cached: false, 177 | exclude: excludeFromStats 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /background/lib/client.js: -------------------------------------------------------------------------------- 1 | var Util = require('util') 2 | var Open = require("open") 3 | 4 | var TorrentStream = require('./torrent-stream/index') 5 | var StateMachine = require('javascript-state-machine') 6 | 7 | var Ipc = require('ipc') 8 | var GlobalState = require('./global-state') 9 | 10 | 11 | var PROGRESS_CHECKER_INTERVAL = 1000; 12 | 13 | var Client = function() { 14 | // Private 15 | this._setting = {}; 16 | this._engine = null; 17 | this._progressIntervalChecker = null; 18 | this._lastSwarm = null; 19 | 20 | this._name = null; 21 | this._progress = 0; 22 | this._size = 0; 23 | this._torrentHash = null; 24 | this._currentDataRate = { 25 | download: 0, 26 | upload: 0 27 | }; 28 | 29 | // Public 30 | this.controlHash = ''; 31 | this.magnet = ''; 32 | }; 33 | 34 | Client.prototype = { 35 | // State transition callbacks 36 | onenterrestore: function(event, from, to, attributes) { 37 | var self = this, clonedSetting; 38 | 39 | this.controlHash = attributes.controlHash; 40 | this.magnet = attributes.magnet; 41 | this._progress = attributes.progress; 42 | this._setting = attributes.setting; 43 | 44 | clonedSetting = JSON.parse(JSON.stringify(this._setting)); 45 | 46 | self._engine = TorrentStream(this.magnet, clonedSetting); 47 | self._engine.on('ready', function() { 48 | self._name = self._engine.torrent.name; 49 | self._size = self._engine.torrent.length; 50 | self._torrentHash = self._engine.torrent.infoHash; 51 | 52 | if (attributes.state == 'stop') { 53 | self.stop(); 54 | } else { 55 | self.ready(); 56 | } 57 | 58 | }); 59 | }, 60 | 61 | onenterresume: function(event, from, to) { 62 | this._updateView(); 63 | this.setup(); 64 | }, 65 | 66 | onentersetup: function(event, from, to, controlHash, magnet, setting) { 67 | var self = this, clonedSetting; 68 | 69 | if (controlHash && magnet && setting) { 70 | this._setting = setting; 71 | this.controlHash = controlHash; 72 | this.magnet = magnet; 73 | } 74 | 75 | clonedSetting = JSON.parse(JSON.stringify(this._setting)); 76 | 77 | self._engine = TorrentStream(this.magnet, clonedSetting); 78 | self._engine.on('ready', function() { 79 | self._name = self._engine.torrent.name; 80 | self._size = self._engine.torrent.length; 81 | self._torrentHash = self._engine.torrent.infoHash; 82 | 83 | self.ready(); 84 | }); 85 | }, 86 | 87 | onenterready: function(event, from, to) { 88 | this.download(); 89 | }, 90 | 91 | onenterdownload: function(event, from, to) { 92 | this._engine.files.forEach(function(file) { 93 | file.createReadStream(); 94 | }); 95 | 96 | this._allocateIntervelProgressChecker(); 97 | }, 98 | 99 | onenterstop: function(event, from, to) { 100 | this.teardown(); 101 | }, 102 | 103 | onenterdone: function(event, from, to) { 104 | this.teardown(); 105 | }, 106 | 107 | // Private function 108 | _caculateDataRate: function() { 109 | if (this._lastSwarm) { 110 | this._currentDataRate = { 111 | download: (this._engine.swarm.downloaded - this._lastSwarm.downloaded) / 112 | (PROGRESS_CHECKER_INTERVAL / 1000), 113 | upload: (this._engine.swarm.uploaded - this._lastSwarm.uploaded) / 114 | (PROGRESS_CHECKER_INTERVAL / 1000) 115 | }; 116 | } 117 | 118 | this._lastSwarm = { 119 | downloaded: this._engine.swarm.downloaded, 120 | uploaded: this._engine.swarm.uploaded, 121 | }; 122 | }, 123 | 124 | _updateView: function() { 125 | var Window = GlobalState.getWindow(); 126 | if (Window) {Window.webContents.send('client-refresh', this.getAttributes()); } 127 | }, 128 | 129 | _intervelProgressChecker: function() { 130 | // Check progress 131 | this._progress = this._engine.getProgress(); 132 | 133 | // Calculate Data Rate 134 | this._caculateDataRate(); 135 | 136 | // Update View 137 | this._updateView(); 138 | 139 | // Check if complete 140 | if (this._progress == 1) { this.complete(); } 141 | }, 142 | 143 | _allocateIntervelProgressChecker: function() { 144 | this._progressIntervalChecker = 145 | !this._progressIntervalChecker ? 146 | setInterval(this._intervelProgressChecker.bind(this), PROGRESS_CHECKER_INTERVAL) : 147 | this._progressIntervalChecker; 148 | }, 149 | 150 | _releaseIntervelProgressChecker: function() { 151 | if (this._progressIntervalChecker) { 152 | clearInterval(this._progressIntervalChecker); 153 | this._progressIntervalChecker = null 154 | } 155 | }, 156 | 157 | // Public function 158 | getAttributes: function() { 159 | return { 160 | state: this.current, 161 | name: this._name, 162 | progress: this._progress, 163 | size: this._size, 164 | downlink: this._currentDataRate.download, 165 | uplink: this._currentDataRate.upload, 166 | torrentHash: this._torrentHash, 167 | controlHash: this.controlHash, 168 | magnet: this.magnet, 169 | setting: this._setting 170 | }; 171 | }, 172 | 173 | openPath: function() { 174 | // TODO test _setting will not be clean 175 | Open(this._setting.path); 176 | }, 177 | 178 | teardown: function() { 179 | this._releaseIntervelProgressChecker(); 180 | if (this._engine) { this._engine.destroy(); } 181 | this._lastSwarm = null; 182 | 183 | this._currentDataRate = { 184 | download: 0, 185 | upload: 0 186 | }; 187 | 188 | this._updateView(); 189 | } 190 | }; 191 | 192 | StateMachine.create({ 193 | target: Client.prototype, 194 | events: [ 195 | { name: 'restore', from: 'none', to: 'restore' }, 196 | { name: 'setup', from: ['none', 'resume'], to: 'setup' }, 197 | { name: 'ready', from: ['setup', 'restore'], to: 'ready' }, 198 | { name: 'download', from: 'ready', to: 'download' }, 199 | { name: 'stop', from: ['download', 'restore'], to: 'stop' }, 200 | { name: 'resume', from: 'stop', to: 'resume' }, 201 | { name: 'complete', from: 'download', to: 'done' }, 202 | ]}); 203 | 204 | module.exports = Client; 205 | -------------------------------------------------------------------------------- /background/lib/manager.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs") 2 | var parseTorrent = require('parse-torrent') 3 | var Md5 = require('md5') 4 | var Defer = require("node-promise").defer 5 | 6 | var App = require('app') 7 | var Ipc = require('ipc') 8 | var Dialog = require('dialog') 9 | 10 | var Client = require('./client') 11 | var LocalStorage = require('./local-storage') 12 | 13 | var GlobalState = require('./global-state') 14 | 15 | var clients = []; 16 | 17 | 18 | // Array utils 19 | 20 | function removeItemBy(array, key, value){ 21 | for (var i in array) { 22 | if (array[i][key] == value) { 23 | array.splice(i,1); 24 | break; 25 | } 26 | } 27 | } 28 | 29 | function getItemBy(array, key, value){ 30 | for (var i in array) { 31 | if (array[i][key] == value) { 32 | return array[i]; 33 | } 34 | } 35 | return null; 36 | } 37 | 38 | 39 | var Manager = function() { 40 | // Private 41 | 42 | function addClient(magnet, path) { 43 | var controlHash = Md5(magnet + (new Date().getTime())), 44 | setting = { 45 | verify: true, 46 | path: path ? path : null, 47 | tmp: App.getPath('userCache'), 48 | connections: 30 49 | }, 50 | client = new Client(); 51 | 52 | client.setup(controlHash, magnet, setting); 53 | clients.push(client); 54 | LocalStorage.saveClients(getClients()); 55 | } 56 | 57 | function removeClient(controlHash) { 58 | var _client = getItemBy(clients, 'controlHash', controlHash); 59 | if (_client) { 60 | _client.teardown(); 61 | removeItemBy(clients, 'controlHash', controlHash); 62 | 63 | LocalStorage.saveClients(getClients()); 64 | } 65 | } 66 | 67 | function restoreClients() { 68 | LocalStorage.getClients().then(function(existClients) { 69 | existClients.forEach(function(clientAttributes) { 70 | var client = new Client(); 71 | client.restore(clientAttributes); 72 | clients.push(client); 73 | }); 74 | }); 75 | } 76 | 77 | function getClients() { 78 | return clients.map(function(client) { 79 | return client.getAttributes(); 80 | }); 81 | } 82 | 83 | function getClient(index) { 84 | return clients[index]; 85 | } 86 | 87 | // Public 88 | 89 | function restore() { 90 | restoreClients() 91 | } 92 | 93 | function quit() { 94 | var deferred = Defer(); 95 | LocalStorage.saveClients(getClients()).then(function() { 96 | deferred.resolve(); 97 | }); 98 | return deferred.promise; 99 | } 100 | 101 | function openUrl(magnet) { 102 | if (getItemBy(clients , 'magnet', magnet)) { 103 | Dialog.showMessageBox({ 104 | type: 'error', 105 | buttons: ['ok'], 106 | title: 'Duplicate torrent', 107 | message: 'Duplicate torrent', 108 | detail: 'This torrent is already exist in the queue.' 109 | }, function() { }); 110 | return; 111 | } 112 | 113 | Dialog.showOpenDialog({ 114 | title: 'Select download path', 115 | properties: ['openDirectory'] 116 | }, function (paths) { 117 | if (paths) { 118 | addClient(magnet, paths[0]); 119 | 120 | var Window = GlobalState.getWindow(); 121 | if (Window) {Window.webContents.send('client-list-refresh', getClients()); } 122 | } 123 | }); 124 | } 125 | 126 | function openFile(file) { 127 | var result = parseTorrent(fs.readFileSync(file)), 128 | magnet = parseTorrent.toMagnetURI(result); 129 | 130 | if (getItemBy(clients , 'magnet', magnet)) { 131 | Dialog.showMessageBox({ 132 | type: 'error', 133 | buttons: ['ok'], 134 | title: 'Duplicate torrent', 135 | message: 'Duplicate torrent', 136 | detail: 'This torrent is already exist in the queue.' 137 | }, function() { }); 138 | return; 139 | } 140 | 141 | Dialog.showOpenDialog({ 142 | title: 'Select download path', 143 | properties: ['openDirectory'] 144 | }, function (paths) { 145 | if (paths) { 146 | addClient(magnet, paths[0]); 147 | 148 | var Window = GlobalState.getWindow(); 149 | if (Window) {Window.webContents.send('client-list-refresh', getClients()); } 150 | } 151 | }); 152 | } 153 | 154 | function bindIpc() { 155 | Ipc.on('add-client-from-magnet', function(event, magnet) { 156 | openUrl(magnet); 157 | }); 158 | 159 | Ipc.on('add-client-from-torrent', function(event) { 160 | Dialog.showOpenDialog({ 161 | title: 'Open torrent', 162 | properties: ['openFile'], 163 | filters: [ 164 | { name: 'torrent', extensions: ['torrent'] } 165 | ] 166 | }, function (torrentPaths) { 167 | if (torrentPaths) { 168 | openFile(torrentPaths[0]); 169 | } 170 | }); 171 | }); 172 | 173 | 174 | Ipc.on('client-remove', function(event, controlHashes) { 175 | controlHashes.forEach(function(controlHash) { 176 | removeClient(controlHash); 177 | }); 178 | event.sender.send('client-list-refresh', getClients()); 179 | }); 180 | 181 | Ipc.on('client-stop', function(event, controlHashes) { 182 | controlHashes.forEach(function(controlHash) { 183 | var c = getItemBy(clients , 'controlHash', controlHash) 184 | if (c && c.can('stop')) { c.stop(); } 185 | }); 186 | }); 187 | 188 | Ipc.on('client-resume', function(event, controlHashes) { 189 | controlHashes.forEach(function(controlHash) { 190 | var c = getItemBy(clients , 'controlHash', controlHash) 191 | if (c && c.can('resume')) { c.resume(); } 192 | }); 193 | }); 194 | 195 | Ipc.on('client-open-path', function(event, controlHashes) { 196 | controlHashes.forEach(function(controlHash) { 197 | var c = getItemBy(clients , 'controlHash', controlHash) 198 | if (c) { c.openPath(); } 199 | }); 200 | }); 201 | 202 | Ipc.on('request-client-list-refresh', function(event, controlHash) { 203 | event.sender.send('client-list-refresh', getClients()); 204 | }); 205 | } 206 | 207 | return { 208 | bindIpc: bindIpc, 209 | restore: restore, 210 | openFile: openFile, 211 | openUrl: openUrl, 212 | quit: quit 213 | }; 214 | } 215 | 216 | module.exports = new Manager; 217 | -------------------------------------------------------------------------------- /background/main.js: -------------------------------------------------------------------------------- 1 | var app = require('app') 2 | var BrowserWindow = require('browser-window') 3 | var Menu = require('menu') 4 | var menu, template 5 | 6 | var TorrentManager = require('./lib/manager') 7 | var GlobalState = require('./lib/global-state') 8 | 9 | require('electron-debug')() 10 | require('crash-reporter').start() 11 | 12 | var mainWindow = null 13 | 14 | app.on('window-all-closed', function() { 15 | TorrentManager.quit().then(function() { 16 | app.quit(); 17 | }); 18 | }) 19 | 20 | app.on('open-url', function(event, url) { 21 | if (GlobalState.getWindow()) { 22 | TorrentManager.openUrl(url); 23 | } else { 24 | app.on('preopen-url', function() { 25 | TorrentManager.openUrl(url); 26 | }); 27 | } 28 | }) 29 | 30 | app.on('open-file', function(event, path) { 31 | if (GlobalState.getWindow()) { 32 | TorrentManager.openFile(path); 33 | } else { 34 | app.on('preopen-url', function() { 35 | TorrentManager.openFile(path); 36 | }); 37 | } 38 | }) 39 | 40 | app.on('ready', function() { 41 | mainWindow = new BrowserWindow({ width: 1024, height: 600, resizable: false }); 42 | 43 | GlobalState.setWindow(mainWindow); 44 | TorrentManager.bindIpc(); 45 | TorrentManager.restore(); 46 | 47 | app.emit('preopen-url'); 48 | app.emit('preopen-file'); 49 | 50 | if (process.env.HOT) { 51 | mainWindow.loadUrl('file://' + __dirname + '/../app/hot-dev-app.html') 52 | } else { 53 | mainWindow.loadUrl('file://' + __dirname + '/../app/app.html') 54 | } 55 | 56 | mainWindow.on('closed', function() { 57 | mainWindow = null 58 | }) 59 | 60 | if (process.env.NODE_ENV === 'development') { 61 | mainWindow.openDevTools() 62 | } 63 | 64 | if (process.platform === 'darwin') { 65 | template = [{ 66 | label: 'Electron', 67 | submenu: [{ 68 | label: 'About ElectronReact', 69 | selector: 'orderFrontStandardAboutPanel:' 70 | }, { 71 | type: 'separator' 72 | }, { 73 | label: 'Services', 74 | submenu: [] 75 | }, { 76 | type: 'separator' 77 | }, { 78 | label: 'Hide ElectronReact', 79 | accelerator: 'Command+H', 80 | selector: 'hide:' 81 | }, { 82 | label: 'Hide Others', 83 | accelerator: 'Command+Shift+H', 84 | selector: 'hideOtherApplications:' 85 | }, { 86 | label: 'Show All', 87 | selector: 'unhideAllApplications:' 88 | }, { 89 | type: 'separator' 90 | }, { 91 | label: 'Quit', 92 | accelerator: 'Command+Q', 93 | click: function() { 94 | app.quit() 95 | } 96 | }] 97 | }, { 98 | label: 'Edit', 99 | submenu: [{ 100 | label: 'Undo', 101 | accelerator: 'Command+Z', 102 | selector: 'undo:' 103 | }, { 104 | label: 'Redo', 105 | accelerator: 'Shift+Command+Z', 106 | selector: 'redo:' 107 | }, { 108 | type: 'separator' 109 | }, { 110 | label: 'Cut', 111 | accelerator: 'Command+X', 112 | selector: 'cut:' 113 | }, { 114 | label: 'Copy', 115 | accelerator: 'Command+C', 116 | selector: 'copy:' 117 | }, { 118 | label: 'Paste', 119 | accelerator: 'Command+V', 120 | selector: 'paste:' 121 | }, { 122 | label: 'Select All', 123 | accelerator: 'Command+A', 124 | selector: 'selectAll:' 125 | }] 126 | }, { 127 | label: 'View', 128 | submenu: [{ 129 | label: 'Reload', 130 | accelerator: 'Command+R', 131 | click: function() { 132 | mainWindow.restart() 133 | } 134 | }, { 135 | label: 'Toggle Full Screen', 136 | accelerator: 'Ctrl+Command+F', 137 | click: function() { 138 | mainWindow.setFullScreen(!mainWindow.isFullScreen()) 139 | } 140 | }, { 141 | label: 'Toggle Developer Tools', 142 | accelerator: 'Alt+Command+I', 143 | click: function() { 144 | mainWindow.toggleDevTools() 145 | } 146 | }] 147 | }, { 148 | label: 'Window', 149 | submenu: [{ 150 | label: 'Minimize', 151 | accelerator: 'Command+M', 152 | selector: 'performMiniaturize:' 153 | }, { 154 | label: 'Close', 155 | accelerator: 'Command+W', 156 | selector: 'performClose:' 157 | }, { 158 | type: 'separator' 159 | }, { 160 | label: 'Bring All to Front', 161 | selector: 'arrangeInFront:' 162 | }] 163 | }, { 164 | label: 'Help', 165 | submenu: [{ 166 | label: 'Learn More', 167 | click: function() { 168 | require('shell').openExternal('http://electron.atom.io') 169 | } 170 | }, { 171 | label: 'Documentation', 172 | click: function() { 173 | require('shell').openExternal('https://github.com/atom/electron/tree/master/docs#readme') 174 | } 175 | }, { 176 | label: 'Community Discussions', 177 | click: function() { 178 | require('shell').openExternal('https://discuss.atom.io/c/electron') 179 | } 180 | }, { 181 | label: 'Search Issues', 182 | click: function() { 183 | require('shell').openExternal('https://github.com/atom/electron/issues') 184 | } 185 | }] 186 | }] 187 | 188 | menu = Menu.buildFromTemplate(template) 189 | Menu.setApplicationMenu(menu) 190 | } else { 191 | template = [{ 192 | label: '&File', 193 | submenu: [{ 194 | label: '&Open', 195 | accelerator: 'Ctrl+O' 196 | }, { 197 | label: '&Close', 198 | accelerator: 'Ctrl+W', 199 | click: function() { 200 | mainWindow.close() 201 | } 202 | }] 203 | }, { 204 | label: '&View', 205 | submenu: [{ 206 | label: '&Reload', 207 | accelerator: 'Ctrl+R', 208 | click: function() { 209 | mainWindow.restart() 210 | } 211 | }, { 212 | label: 'Toggle &Full Screen', 213 | accelerator: 'F11', 214 | click: function() { 215 | mainWindow.setFullScreen(!mainWindow.isFullScreen()) 216 | } 217 | }, { 218 | label: 'Toggle &Developer Tools', 219 | accelerator: 'Alt+Ctrl+I', 220 | click: function() { 221 | mainWindow.toggleDevTools() 222 | } 223 | }] 224 | }, { 225 | label: 'Help', 226 | submenu: [{ 227 | label: 'Learn More', 228 | click: function() { 229 | require('shell').openExternal('http://electron.atom.io') 230 | } 231 | }, { 232 | label: 'Documentation', 233 | click: function() { 234 | require('shell').openExternal('https://github.com/atom/electron/tree/master/docs#readme') 235 | } 236 | }, { 237 | label: 'Community Discussions', 238 | click: function() { 239 | require('shell').openExternal('https://discuss.atom.io/c/electron') 240 | } 241 | }, { 242 | label: 'Search Issues', 243 | click: function() { 244 | require('shell').openExternal('https://github.com/atom/electron/issues') 245 | } 246 | }] 247 | }] 248 | menu = Menu.buildFromTemplate(template) 249 | mainWindow.setMenu(menu) 250 | } 251 | }) 252 | -------------------------------------------------------------------------------- /app/components/QueuePanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons' 2 | import { ProgressBar, Tooltip, OverlayTrigger, Modal, Button } from 'react-bootstrap' 3 | import { BootstrapTable, TableHeaderColumn, TableDataSet } from 'react-bootstrap-table'; 4 | 5 | import Ipc from 'ipc' 6 | 7 | export default class QueuePanel extends React.Component { 8 | static defaultProps = { 9 | clients: [], 10 | selectedClients: {} 11 | } 12 | 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | showMagnetModal: false, 18 | magnetUrl: '', 19 | invalidMagnetUrl: false, 20 | 21 | showRemoveModal: false, 22 | 23 | clientsLength: 0 24 | }; 25 | 26 | this.props.clients = []; 27 | this.clients = new TableDataSet(this.props.clients); 28 | 29 | // Clear previos events 30 | Ipc.removeAllListeners('client-list-refresh'); 31 | Ipc.removeAllListeners('client-refresh'); 32 | 33 | // Bind events 34 | Ipc.on('client-list-refresh', (clients) => { 35 | if (this.props.queue != 'all') { 36 | clients.forEach((client) => { 37 | if (client.state == this.props.queue) { this.props.clients.push(client); } 38 | }); 39 | } else { 40 | this.props.clients = clients; 41 | } 42 | 43 | this.clients.setData(this.props.clients); 44 | this.setState({clientsLength: this.props.clients.length}); 45 | }); 46 | 47 | Ipc.on('client-refresh', (client) => { 48 | for (var i = 0; i < this.props.clients.length; i++) { 49 | if (this.props.clients[i]['controlHash'] == client['controlHash']) { 50 | this.props.clients[i] = client; 51 | this.clients.setData(this.props.clients); 52 | break; 53 | } 54 | } 55 | }); 56 | 57 | Ipc.send('request-client-list-refresh'); 58 | } 59 | 60 | onCliensSelect(row, isSelected){ 61 | this.props.selectedClients[row['controlHash']] = (isSelected) ? true : false; 62 | } 63 | 64 | onSelectAllClient(isSelected){ 65 | for (var i = 0; i < this.props.clients.length; i++) { 66 | this.props.selectedClients[this.props.clients[i]['controlHash']] = (isSelected) ? true : false; 67 | } 68 | } 69 | 70 | closeMagnetModal() { 71 | this.setState({ 72 | showMagnetModal: false, 73 | magnetUrl: '', 74 | invalidMagnetUrl: false 75 | }); 76 | } 77 | 78 | openMagnetModal() { 79 | this.setState({ showMagnetModal: true }); 80 | } 81 | 82 | handleMagnetUrlChange(event) { 83 | this.setState({ magnetUrl: event.target.value }); 84 | } 85 | 86 | openRemoveModal() { 87 | if (this.getSelectedClients().length > 0) { this.setState({ showRemoveModal: true }); } 88 | } 89 | 90 | closeRemoveModal() { 91 | this.setState({ showRemoveModal: false }); 92 | } 93 | 94 | addMagnetUrl() { 95 | if (this.state.magnetUrl.match(/magnet:\?xt=urn:[a-z0-9]+:[a-z0-9]{32}/i) != null) { 96 | Ipc.send('add-client-from-magnet', this.state.magnetUrl); 97 | this.closeMagnetModal(); 98 | } else { 99 | this.setState({ invalidMagnetUrl: true }); 100 | } 101 | } 102 | 103 | addTorrent() { 104 | Ipc.send('add-client-from-torrent'); 105 | } 106 | 107 | getSelectedClients() { 108 | var controlHashes = []; 109 | for (var controlHash in this.props.selectedClients) { 110 | if (this.props.selectedClients[controlHash]) { 111 | controlHashes.push(controlHash) 112 | } 113 | } 114 | return controlHashes; 115 | } 116 | 117 | resume() { 118 | Ipc.send('client-resume', this.getSelectedClients()); 119 | } 120 | 121 | stop() { 122 | Ipc.send('client-stop', this.getSelectedClients()); 123 | } 124 | 125 | remove() { 126 | Ipc.send('client-remove', this.getSelectedClients()); 127 | this.closeRemoveModal(); 128 | } 129 | 130 | openPath() { 131 | Ipc.send('client-open-path', this.getSelectedClients()); 132 | } 133 | 134 | // Formatter 135 | 136 | dataRateStringFormatter(dataRate) { 137 | if (!dataRate) { return 0; } 138 | 139 | var KByte = dataRate/1024, 140 | MByte = KByte/1024; 141 | 142 | return (KByte > 1000) ? 143 | ((MByte).toFixed(2) + ' MB/s') : 144 | ((KByte).toFixed() + ' KB/s'); 145 | } 146 | 147 | sizeStringFormatter(size) { 148 | if (!size) { return 0; } 149 | 150 | var KByte = size/1024, 151 | MByte = KByte/1024, 152 | GByte = MByte/1024; 153 | 154 | return (MByte > 1000) ? 155 | ((GByte).toFixed(2) + ' GB') : 156 | ((KByte > 1000) ? 157 | ((MByte).toFixed(2) + ' MB') : 158 | ((KByte).toFixed() + ' KB')); 159 | } 160 | 161 | nameFormatter(cell, row) { 162 | const nameTooltip = ( 163 | {cell} 164 | ); 165 | 166 | return cell ? 167 | (
168 | 169 | {cell} 170 | 171 |
) : 172 | (
173 | Initializing... 174 |
); 175 | } 176 | 177 | progressFormatter(cell, row) { 178 | let progress = (cell * 100).toFixed(1), 179 | progressBarActive = (row.state == 'download') ? true : false, 180 | progressBarStyle = (row.state == 'done') ? 181 | 'success' : ((row.state == 'stop') ? 182 | 'warning' : 'info'); 183 | 184 | return (); 186 | } 187 | 188 | sizeFormatter(cell, row) { 189 | return this.sizeStringFormatter(cell); 190 | } 191 | 192 | downlinkFormatter(cell, row) { 193 | let downlink = this.dataRateStringFormatter(cell); 194 | 195 | return ( 196 | 197 | {downlink} 198 | 199 | ); 200 | } 201 | 202 | uplinkFormatter(cell, row) { 203 | let uplink = this.dataRateStringFormatter(cell); 204 | 205 | return ( 206 | 207 | {uplink} 208 | 209 | ); 210 | } 211 | 212 | render() { 213 | let selectRowProp = { 214 | mode: "checkbox", 215 | clickToSelect: true, 216 | bgColor: "#E0FFFF", 217 | onSelect: this.onCliensSelect.bind(this), 218 | onSelectAll: this.onSelectAllClient.bind(this) 219 | }; 220 | 221 | return ( 222 |
223 | 246 | 247 |
248 | 249 | 250 | 251 | Name 252 | Progress 253 | Size 254 | Downlink 255 | Uplink 256 | 257 |
258 | 259 | { (this.state.clientsLength == 0) ?

No task.

: null } 260 | 261 | 262 | 263 | Add magnet url 264 | 265 | 266 |
267 |
268 | 270 | 271 | 272 | 273 |
274 |
275 |
276 | { this.state.invalidMagnetUrl ?
Invalid magnet url
: null } 277 |
278 |
279 | 280 | 281 | 282 | Remove 283 | 284 | 285 | Are you sure? 286 | 287 | 288 | 289 | 290 | 291 | 292 |
293 | ) 294 | } 295 | 296 | } 297 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/index.js: -------------------------------------------------------------------------------- 1 | var hat = require('hat'); 2 | var pws = require('peer-wire-swarm'); 3 | var bncode = require('bncode'); 4 | var crypto = require('crypto'); 5 | var bitfield = require('bitfield'); 6 | var parseTorrent = require('parse-torrent'); 7 | var mkdirp = require('mkdirp'); 8 | var events = require('events'); 9 | var path = require('path'); 10 | var fs = require('fs'); 11 | var os = require('os'); 12 | var eos = require('end-of-stream'); 13 | 14 | var peerDiscovery = require('./lib/peer-discovery'); 15 | var blocklist = require('ip-set'); 16 | var exchangeMetadata = require('./lib/exchange-metadata'); 17 | var storage = require('./lib/storage'); 18 | var storageBuffer = require('./lib/storage-buffer'); 19 | var fileStream = require('./lib/file-stream'); 20 | var piece = require('./lib/piece'); 21 | 22 | var MAX_REQUESTS = 5; 23 | var CHOKE_TIMEOUT = 5000; 24 | var REQUEST_TIMEOUT = 30000; 25 | var SPEED_THRESHOLD = 3 * piece.BLOCK_SIZE; 26 | var DEFAULT_PORT = 6881; 27 | 28 | var BAD_PIECE_STRIKES_MAX = 3; 29 | var BAD_PIECE_STRIKES_DURATION = 120000; // 2 minutes 30 | 31 | var RECHOKE_INTERVAL = 10000; 32 | var RECHOKE_OPTIMISTIC_DURATION = 2; 33 | 34 | var TMP = fs.existsSync('/tmp') ? '/tmp' : os.tmpDir(); 35 | 36 | var noop = function() {}; 37 | 38 | var sha1 = function(data) { 39 | return crypto.createHash('sha1').update(data).digest('hex'); 40 | }; 41 | 42 | var thruthy = function() { 43 | return true; 44 | }; 45 | 46 | var falsy = function() { 47 | return false; 48 | }; 49 | 50 | var toNumber = function(val) { 51 | return val === true ? 1 : (val || 0); 52 | }; 53 | 54 | var torrentStream = function(link, opts, cb) { 55 | if (typeof opts === 'function') return torrentStream(link, null, opts); 56 | 57 | link = parseTorrent(link); 58 | var metadata = link.infoBuffer || null; 59 | var infoHash = link.infoHash; 60 | 61 | if (!opts) opts = {}; 62 | if (!opts.id) opts.id = '-TS0008-'+hat(48); 63 | if (!opts.tmp) opts.tmp = TMP; 64 | if (!opts.name) opts.name = 'torrent-stream'; 65 | if (!opts.flood) opts.flood = 0; // Pulse defaults: 66 | if (!opts.pulse) opts.pulse = Number.MAX_SAFE_INTEGER; // Do not pulse 67 | 68 | var usingTmp = false; 69 | var destroyed = false; 70 | 71 | if (!opts.path) { 72 | usingTmp = true; 73 | opts.path = path.join(opts.tmp, opts.name, infoHash); 74 | } 75 | 76 | var engine = new events.EventEmitter(); 77 | var swarm = pws(infoHash, opts.id, { size: (opts.connections || opts.size), speed: 10 }); 78 | var torrentPath = path.join(opts.tmp, opts.name, infoHash + '.torrent'); 79 | 80 | if (cb) engine.on('ready', cb.bind(null, engine)); 81 | 82 | engine.ready = function (cb) { 83 | if (engine.torrent) cb(); 84 | else engine.once('ready', cb); 85 | } 86 | 87 | var wires = swarm.wires; 88 | var critical = []; 89 | var refresh = noop; 90 | 91 | var rechokeSlots = (opts.uploads === false || opts.uploads === 0) ? 0 : (+opts.uploads || 10); 92 | var rechokeOptimistic = null; 93 | var rechokeOptimisticTime = 0; 94 | var rechokeIntervalId; 95 | 96 | engine.infoHash = infoHash; 97 | engine.metadata = metadata; 98 | engine.path = opts.path; 99 | engine.files = []; 100 | engine.selection = []; 101 | engine.torrent = null; 102 | engine.bitfield = null; 103 | engine.amInterested = false; 104 | engine.store = null; 105 | engine.swarm = swarm; 106 | engine._flood = opts.flood; 107 | engine._pulse = opts.pulse; 108 | 109 | var discovery = peerDiscovery(opts); 110 | var blocked = blocklist(opts.blocklist); 111 | 112 | discovery.on('peer', function(addr) { 113 | if (blocked.contains(addr.split(':')[0])) { 114 | engine.emit('blocked-peer', addr); 115 | } else { 116 | engine.emit('peer', addr); 117 | engine.connect(addr); 118 | } 119 | }); 120 | 121 | var ontorrent = function(torrent) { 122 | engine.store = storageBuffer((opts.storage || storage(opts.path))(torrent, opts)); 123 | engine.torrent = torrent; 124 | engine.bitfield = bitfield(torrent.pieces.length); 125 | 126 | var pieceLength = torrent.pieceLength; 127 | var pieceRemainder = (torrent.length % pieceLength) || pieceLength; 128 | 129 | var pieces = torrent.pieces.map(function(hash, i) { 130 | return piece(i === torrent.pieces.length-1 ? pieceRemainder : pieceLength); 131 | }); 132 | var reservations = torrent.pieces.map(function() { 133 | return []; 134 | }); 135 | 136 | discovery.setTorrent(torrent); 137 | 138 | 139 | // Customize Total Progress API 140 | engine.getProgress = function() { 141 | var completePiecesLength = 0; 142 | 143 | pieces.forEach(function(piece) { 144 | if (!piece) 145 | completePiecesLength++; 146 | }); 147 | 148 | return completePiecesLength / pieces.length; 149 | }; 150 | 151 | // Customize File Progress API 152 | engine.torrent.files.forEach(function(file) { 153 | file.getProgress = function() { 154 | var fileStart = file.offset; 155 | var fileEnd = file.offset + file.length; 156 | 157 | var firstPiece = Math.floor(fileStart / pieceLength); 158 | var lastPiece = Math.floor((fileEnd - 1) / pieceLength); 159 | 160 | var completePiecesLength = 0; 161 | var filePieceLength = lastPiece - firstPiece + 1; 162 | 163 | for (var i = firstPiece; i <= lastPiece; i++) { 164 | if (!pieces[i]) 165 | completePiecesLength++; 166 | } 167 | 168 | return completePiecesLength / filePieceLength; 169 | } 170 | }); 171 | 172 | engine.files = torrent.files.map(function(file) { 173 | file = Object.create(file); 174 | var offsetPiece = (file.offset / torrent.pieceLength) | 0; 175 | var endPiece = ((file.offset+file.length-1) / torrent.pieceLength) | 0; 176 | 177 | file.deselect = function() { 178 | engine.deselect(offsetPiece, endPiece, false); 179 | }; 180 | 181 | file.select = function() { 182 | engine.select(offsetPiece, endPiece, false); 183 | }; 184 | 185 | file.createReadStream = function(opts) { 186 | var stream = fileStream(engine, file, opts); 187 | 188 | var notify = stream.notify.bind(stream); 189 | engine.select(stream.startPiece, stream.endPiece, true, notify); 190 | eos(stream, function() { 191 | engine.deselect(stream.startPiece, stream.endPiece, true, notify); 192 | }); 193 | 194 | return stream; 195 | }; 196 | 197 | return file; 198 | }); 199 | 200 | // Customize Total Progress API 201 | engine.getProgress = function() { 202 | var completePiecesLength = 0; 203 | 204 | pieces.forEach(function(piece) { 205 | if (!piece) 206 | completePiecesLength++; 207 | }); 208 | 209 | return completePiecesLength / pieces.length; 210 | }; 211 | 212 | // Customize File Progress API 213 | engine.files.forEach(function(file) { 214 | file.getProgress = function() { 215 | var fileStart = file.offset; 216 | var fileEnd = file.offset + file.length; 217 | 218 | var firstPiece = Math.floor(fileStart / pieceLength); 219 | var lastPiece = Math.floor((fileEnd - 1) / pieceLength); 220 | 221 | var completePiecesLength = 0; 222 | var filePieceLength = lastPiece - firstPiece + 1; 223 | 224 | for (var i = firstPiece; i <= lastPiece; i++) { 225 | if (!pieces[i]) 226 | completePiecesLength++; 227 | } 228 | 229 | return completePiecesLength / filePieceLength; 230 | } 231 | }); 232 | 233 | var oninterestchange = function() { 234 | var prev = engine.amInterested; 235 | engine.amInterested = !!engine.selection.length; 236 | 237 | wires.forEach(function(wire) { 238 | if (engine.amInterested) wire.interested(); 239 | else wire.uninterested(); 240 | }); 241 | 242 | if (prev === engine.amInterested) return; 243 | if (engine.amInterested) engine.emit('interested'); 244 | else engine.emit('uninterested'); 245 | }; 246 | 247 | var gc = function() { 248 | for (var i = 0; i < engine.selection.length; i++) { 249 | var s = engine.selection[i]; 250 | var oldOffset = s.offset; 251 | 252 | while (!pieces[s.from+s.offset] && s.from+s.offset < s.to) s.offset++; 253 | 254 | if (oldOffset !== s.offset) s.notify(); 255 | if (s.to !== s.from+s.offset) continue; 256 | if (pieces[s.from+s.offset]) continue; 257 | 258 | engine.selection.splice(i, 1); 259 | i--; // -1 to offset splice 260 | s.notify(); 261 | oninterestchange(); 262 | } 263 | 264 | if (!engine.selection.length) engine.emit('idle'); 265 | }; 266 | 267 | var onpiececomplete = function(index, buffer) { 268 | if (!pieces[index]) return; 269 | 270 | pieces[index] = null; 271 | reservations[index] = null; 272 | engine.bitfield.set(index, true); 273 | 274 | for (var i = 0; i < wires.length; i++) wires[i].have(index); 275 | 276 | engine.emit('verify', index); 277 | engine.emit('download', index, buffer); 278 | 279 | engine.store.write(index, buffer); 280 | gc(); 281 | }; 282 | 283 | var onhotswap = opts.hotswap === false ? falsy : function(wire, index) { 284 | var speed = wire.downloadSpeed(); 285 | if (speed < piece.BLOCK_SIZE) return; 286 | if (!reservations[index] || !pieces[index]) return; 287 | 288 | var r = reservations[index]; 289 | var minSpeed = Infinity; 290 | var min; 291 | 292 | for (var i = 0; i < r.length; i++) { 293 | var other = r[i]; 294 | if (!other || other === wire) continue; 295 | 296 | var otherSpeed = other.downloadSpeed(); 297 | if (otherSpeed >= SPEED_THRESHOLD) continue; 298 | if (2 * otherSpeed > speed || otherSpeed > minSpeed) continue; 299 | 300 | min = other; 301 | minSpeed = otherSpeed; 302 | } 303 | 304 | if (!min) return false; 305 | 306 | for (i = 0; i < r.length; i++) { 307 | if (r[i] === min) r[i] = null; 308 | } 309 | 310 | for (i = 0; i < min.requests.length; i++) { 311 | var req = min.requests[i]; 312 | if (req.piece !== index) continue; 313 | pieces[index].cancel((req.offset / piece.BLOCK_SIZE) | 0); 314 | } 315 | 316 | engine.emit('hotswap', min, wire, index); 317 | return true; 318 | }; 319 | 320 | var onupdatetick = function() { 321 | process.nextTick(onupdate); 322 | }; 323 | 324 | var onrequest = function(wire, index, hotswap) { 325 | if (!pieces[index]) return false; 326 | 327 | var p = pieces[index]; 328 | var reservation = p.reserve(); 329 | 330 | if (reservation === -1 && hotswap && onhotswap(wire, index)) reservation = p.reserve(); 331 | if (reservation === -1) return false; 332 | 333 | var r = reservations[index] || []; 334 | var offset = p.offset(reservation); 335 | var size = p.size(reservation); 336 | 337 | var i = r.indexOf(null); 338 | if (i === -1) i = r.length; 339 | r[i] = wire; 340 | 341 | wire.request(index, offset, size, function(err, block) { 342 | if (r[i] === wire) r[i] = null; 343 | 344 | if (p !== pieces[index]) return onupdatetick(); 345 | 346 | if (err) { 347 | p.cancel(reservation); 348 | onupdatetick(); 349 | return; 350 | } 351 | 352 | if (!p.set(reservation, block, wire)) return onupdatetick(); 353 | 354 | var sources = p.sources; 355 | var buffer = p.flush(); 356 | 357 | if (sha1(buffer) !== torrent.pieces[index]) { 358 | pieces[index] = piece(p.length); 359 | engine.emit('invalid-piece', index, buffer); 360 | onupdatetick(); 361 | 362 | sources.forEach(function(wire) { 363 | var now = Date.now(); 364 | 365 | wire.badPieceStrikes = wire.badPieceStrikes.filter(function(strike) { 366 | return (now - strike) < BAD_PIECE_STRIKES_DURATION; 367 | }); 368 | 369 | wire.badPieceStrikes.push(now); 370 | 371 | if (wire.badPieceStrikes.length > BAD_PIECE_STRIKES_MAX) { 372 | engine.block(wire.peerAddress); 373 | } 374 | }); 375 | 376 | return; 377 | } 378 | 379 | onpiececomplete(index, buffer); 380 | onupdatetick(); 381 | }); 382 | 383 | return true; 384 | }; 385 | 386 | var onvalidatewire = function(wire) { 387 | if (wire.requests.length) return; 388 | 389 | for (var i = engine.selection.length-1; i >= 0; i--) { 390 | var next = engine.selection[i]; 391 | for (var j = next.to; j >= next.from + next.offset; j--) { 392 | if (!wire.peerPieces[j]) continue; 393 | if (onrequest(wire, j, false)) return; 394 | } 395 | } 396 | }; 397 | 398 | var speedRanker = function(wire) { 399 | var speed = wire.downloadSpeed() || 1; 400 | if (speed > SPEED_THRESHOLD) return thruthy; 401 | 402 | var secs = MAX_REQUESTS * piece.BLOCK_SIZE / speed; 403 | var tries = 10; 404 | var ptr = 0; 405 | 406 | return function(index) { 407 | if (!tries || !pieces[index]) return true; 408 | 409 | var missing = pieces[index].missing; 410 | for (; ptr < wires.length; ptr++) { 411 | var other = wires[ptr]; 412 | var otherSpeed = other.downloadSpeed(); 413 | 414 | if (otherSpeed < SPEED_THRESHOLD) continue; 415 | if (otherSpeed <= speed || !other.peerPieces[index]) continue; 416 | if ((missing -= otherSpeed * secs) > 0) continue; 417 | 418 | tries--; 419 | return false; 420 | } 421 | 422 | return true; 423 | }; 424 | }; 425 | 426 | var shufflePriority = function(i) { 427 | var last = i; 428 | for (var j = i; j < engine.selection.length && engine.selection[j].priority; j++) { 429 | last = j; 430 | } 431 | var tmp = engine.selection[i]; 432 | engine.selection[i] = engine.selection[last]; 433 | engine.selection[last] = tmp; 434 | }; 435 | 436 | var select = function(wire, hotswap) { 437 | if (wire.requests.length >= MAX_REQUESTS) return true; 438 | 439 | // Pulse, or flood (default) 440 | if (swarm.downloaded > engine._flood && swarm.downloadSpeed() > engine._pulse) 441 | return true; 442 | 443 | var rank = speedRanker(wire); 444 | 445 | for (var i = 0; i < engine.selection.length; i++) { 446 | var next = engine.selection[i]; 447 | for (var j = next.from + next.offset; j <= next.to; j++) { 448 | if (!wire.peerPieces[j] || !rank(j)) continue; 449 | while (wire.requests.length < MAX_REQUESTS && onrequest(wire, j, critical[j] || hotswap)); 450 | if (wire.requests.length < MAX_REQUESTS) continue; 451 | if (next.priority) shufflePriority(i); 452 | return true; 453 | } 454 | } 455 | 456 | return false; 457 | }; 458 | 459 | var onupdatewire = function(wire) { 460 | if (wire.peerChoking) return; 461 | if (!wire.downloaded) return onvalidatewire(wire); 462 | select(wire, false) || select(wire, true); 463 | }; 464 | 465 | var onupdate = function() { 466 | wires.forEach(onupdatewire); 467 | }; 468 | 469 | var onwire = function(wire) { 470 | wire.setTimeout(opts.timeout || REQUEST_TIMEOUT, function() { 471 | engine.emit('timeout', wire); 472 | wire.destroy(); 473 | }); 474 | 475 | if (engine.selection.length) wire.interested(); 476 | 477 | var timeout = CHOKE_TIMEOUT; 478 | var id; 479 | 480 | var onchoketimeout = function() { 481 | if (swarm.queued > 2 * (swarm.size - swarm.wires.length) && wire.amInterested) return wire.destroy(); 482 | id = setTimeout(onchoketimeout, timeout); 483 | }; 484 | 485 | wire.on('close', function() { 486 | clearTimeout(id); 487 | }); 488 | 489 | wire.on('choke', function() { 490 | clearTimeout(id); 491 | id = setTimeout(onchoketimeout, timeout); 492 | }); 493 | 494 | wire.on('unchoke', function() { 495 | clearTimeout(id); 496 | }); 497 | 498 | wire.on('request', function(index, offset, length, cb) { 499 | if (pieces[index]) return; 500 | engine.store.read(index, { offset: offset, length: length }, function(err, buffer) { 501 | if (err) return cb(err); 502 | engine.emit('upload', index, offset, length); 503 | cb(null, buffer); 504 | }); 505 | }); 506 | 507 | wire.on('unchoke', onupdate); 508 | wire.on('bitfield', onupdate); 509 | wire.on('have', onupdate); 510 | 511 | wire.isSeeder = false; 512 | 513 | var i = 0; 514 | var checkseeder = function() { 515 | if (wire.peerPieces.length !== torrent.pieces.length) return; 516 | for (; i < torrent.pieces.length; ++i) { 517 | if (!wire.peerPieces[i]) return; 518 | } 519 | wire.isSeeder = true; 520 | }; 521 | 522 | wire.on('bitfield', checkseeder); 523 | wire.on('have', checkseeder); 524 | checkseeder(); 525 | 526 | wire.badPieceStrikes = []; 527 | 528 | id = setTimeout(onchoketimeout, timeout); 529 | }; 530 | 531 | var rechokeSort = function(a, b) { 532 | // Prefer higher download speed 533 | if (a.downSpeed !== b.downSpeed) return a.downSpeed > b.downSpeed ? -1 : 1; 534 | // Prefer higher upload speed 535 | if (a.upSpeed !== b.upSpeed) return a.upSpeed > b.upSpeed ? -1 : 1; 536 | // Prefer unchoked 537 | if (a.wasChoked !== b.wasChoked) return a.wasChoked ? 1 : -1; 538 | // Random order 539 | return a.salt - b.salt; 540 | }; 541 | 542 | var onrechoke = function() { 543 | if (rechokeOptimisticTime > 0) --rechokeOptimisticTime; 544 | else rechokeOptimistic = null; 545 | 546 | var peers = []; 547 | 548 | wires.forEach(function(wire) { 549 | if (wire.isSeeder) { 550 | if (!wire.amChoking) wire.choke(); 551 | } else if (wire !== rechokeOptimistic) { 552 | peers.push({ 553 | wire: wire, 554 | downSpeed: wire.downloadSpeed(), 555 | upSpeed: wire.uploadSpeed(), 556 | salt: Math.random(), 557 | interested: wire.peerInterested, 558 | wasChoked: wire.amChoking, 559 | isChoked: true 560 | }); 561 | } 562 | }); 563 | 564 | peers.sort(rechokeSort); 565 | 566 | var i = 0; 567 | var unchokeInterested = 0; 568 | for (; i < peers.length && unchokeInterested < rechokeSlots; ++i) { 569 | peers[i].isChoked = false; 570 | if (peers[i].interested) ++unchokeInterested; 571 | } 572 | 573 | if (!rechokeOptimistic && i < peers.length && rechokeSlots) { 574 | var candidates = peers.slice(i).filter(function(peer) { return peer.interested; }); 575 | var optimistic = candidates[(Math.random() * candidates.length) | 0]; 576 | 577 | if (optimistic) { 578 | optimistic.isChoked = false; 579 | rechokeOptimistic = optimistic.wire; 580 | rechokeOptimisticTime = RECHOKE_OPTIMISTIC_DURATION; 581 | } 582 | } 583 | 584 | peers.forEach(function(peer) { 585 | if (peer.wasChoked !== peer.isChoked) { 586 | if (peer.isChoked) peer.wire.choke(); 587 | else peer.wire.unchoke(); 588 | } 589 | }); 590 | }; 591 | 592 | var onready = function() { 593 | swarm.on('wire', onwire); 594 | swarm.wires.forEach(onwire); 595 | 596 | refresh = function() { 597 | process.nextTick(gc); 598 | oninterestchange(); 599 | onupdate(); 600 | }; 601 | 602 | rechokeIntervalId = setInterval(onrechoke, RECHOKE_INTERVAL); 603 | 604 | engine.emit('ready'); 605 | refresh(); 606 | }; 607 | 608 | if (opts.verify === false) return onready(); 609 | 610 | engine.emit('verifying'); 611 | 612 | var loop = function(i) { 613 | if (i >= torrent.pieces.length) return onready(); 614 | engine.store.read(i, function(_, buf) { 615 | if (!buf || sha1(buf) !== torrent.pieces[i] || !pieces[i]) return loop(i+1); 616 | pieces[i] = null; 617 | engine.bitfield.set(i, true); 618 | engine.emit('verify', i); 619 | loop(i+1); 620 | }); 621 | }; 622 | 623 | loop(0); 624 | }; 625 | 626 | var exchange = exchangeMetadata(engine, function(metadata) { 627 | var buf = bncode.encode({ 628 | info: bncode.decode(metadata), 629 | 'announce-list': [] 630 | }); 631 | 632 | ontorrent(parseTorrent(buf)); 633 | 634 | mkdirp(path.dirname(torrentPath), function(err) { 635 | if (err) return engine.emit('error', err); 636 | fs.writeFile(torrentPath, buf, function(err) { 637 | if (err) engine.emit('error', err); 638 | }); 639 | }); 640 | }); 641 | 642 | swarm.on('wire', function(wire) { 643 | engine.emit('wire', wire); 644 | exchange(wire); 645 | if (engine.bitfield) wire.bitfield(engine.bitfield); 646 | }); 647 | 648 | swarm.pause(); 649 | 650 | if (link.files && engine.metadata) { 651 | swarm.resume(); 652 | ontorrent(link); 653 | } else { 654 | fs.readFile(torrentPath, function(_, buf) { 655 | if (destroyed) return; 656 | swarm.resume(); 657 | 658 | // We know only infoHash here, not full infoDictionary. 659 | // But infoHash is enough to connect to trackers and get peers. 660 | if (!buf) return discovery.setTorrent(link); 661 | 662 | var torrent = parseTorrent(buf); 663 | 664 | // Bad cache file - fetch it again 665 | if (torrent.infoHash !== infoHash) return discovery.setTorrent(link); 666 | 667 | if (!torrent.announce || !torrent.announce.length) { 668 | opts.trackers = [].concat(opts.trackers || []).concat(link.announce || []); 669 | } 670 | 671 | engine.metadata = torrent.infoBuffer; 672 | ontorrent(torrent); 673 | }); 674 | } 675 | 676 | engine.critical = function(piece, width) { 677 | for (var i = 0; i < (width || 1); i++) critical[piece+i] = true; 678 | }; 679 | 680 | engine.select = function(from, to, priority, notify) { 681 | engine.selection.push({ 682 | from:from, 683 | to:to, 684 | offset:0, 685 | priority: toNumber(priority), 686 | notify: notify || noop 687 | }); 688 | 689 | engine.selection.sort(function(a, b) { 690 | return b.priority - a.priority; 691 | }); 692 | 693 | refresh(); 694 | }; 695 | 696 | engine.deselect = function(from, to, priority, notify) { 697 | notify = notify || noop; 698 | for (var i = 0; i < engine.selection.length; i++) { 699 | var s = engine.selection[i]; 700 | if (s.from !== from || s.to !== to) continue; 701 | if (s.priority !== toNumber(priority)) continue; 702 | if (s.notify !== notify) continue; 703 | engine.selection.splice(i, 1); 704 | i--; 705 | break; 706 | } 707 | 708 | refresh(); 709 | }; 710 | 711 | engine.setPulse = function(bps) { 712 | // Set minimum byte/second pulse starting now (dynamic) 713 | // Eg. Start pulsing at minimum 312 KBps: 714 | // engine.setPulse(312*1024); 715 | 716 | engine._pulse = bps; 717 | }; 718 | 719 | engine.setFlood = function(b) { 720 | // Set bytes to flood starting now (dynamic) 721 | // Eg. Start flooding for next 10 MB: 722 | // engine.setFlood(10*1024*1024) 723 | 724 | engine._flood = b + swarm.downloaded; 725 | }; 726 | 727 | engine.setFloodedPulse = function(b, bps) { 728 | // Set bytes to flood before starting a minimum byte/second pulse (dynamic) 729 | // Eg. Start flooding for next 10 MB, then start pulsing at minimum 312 KBps: 730 | // engine.setFloodedPulse(10*1024*1024, 312*1024); 731 | 732 | engine.setFlood(b); 733 | engine.setPulse(bps); 734 | }; 735 | 736 | engine.flood = function() { 737 | // Reset flood/pulse values to default (dynamic) 738 | // Eg. Flood the network starting now: 739 | // engine.flood(); 740 | 741 | engine._flood = 0; 742 | engine._pulse = Number.MAX_SAFE_INTEGER; 743 | }; 744 | 745 | engine.connect = function(addr) { 746 | swarm.add(addr); 747 | }; 748 | 749 | engine.disconnect = function(addr) { 750 | swarm.remove(addr); 751 | }; 752 | 753 | engine.block = function(addr) { 754 | blocked.add(addr.split(':')[0]); 755 | engine.disconnect(addr); 756 | engine.emit('blocking', addr); 757 | }; 758 | 759 | var removeTorrent = function(cb) { 760 | fs.unlink(torrentPath, function(err) { 761 | if (err) return cb(err); 762 | fs.rmdir(path.dirname(torrentPath), function(err) { 763 | if (err && err.code !== 'ENOTEMPTY') return cb(err); 764 | cb(); 765 | }); 766 | }); 767 | }; 768 | 769 | var removeTmp = function(cb) { 770 | if (!usingTmp) return removeTorrent(cb); 771 | fs.rmdir(opts.path, function(err) { 772 | if (err) return cb(err); 773 | removeTorrent(cb); 774 | }); 775 | }; 776 | 777 | engine.remove = function(keepPieces, cb) { 778 | if (typeof keepPieces === 'function') { 779 | cb = keepPieces; 780 | keepPieces = false; 781 | } 782 | 783 | if (keepPieces || !engine.store || !engine.store.remove) return removeTmp(cb); 784 | 785 | engine.store.remove(function(err) { 786 | if (err) return cb(err); 787 | removeTmp(cb); 788 | }); 789 | }; 790 | 791 | engine.destroy = function(cb) { 792 | destroyed = true; 793 | swarm.destroy(); 794 | clearInterval(rechokeIntervalId); 795 | discovery.stop(); 796 | if (engine.store && engine.store.remove) { 797 | engine.store.close(cb); 798 | } else if (cb) { 799 | process.nextTick(cb); 800 | } 801 | }; 802 | 803 | var findPort = function(def, cb) { 804 | var net = require('net') 805 | var s = net.createServer() 806 | 807 | s.on('error', function() { 808 | findPort(0, cb) 809 | }) 810 | 811 | s.listen(def, function() { 812 | var port = s.address().port 813 | s.close(function() { 814 | engine.listen(port, cb) 815 | }) 816 | }) 817 | } 818 | 819 | engine.listen = function(port, cb) { 820 | if (typeof port === 'function') return engine.listen(0, port); 821 | if (!port) return findPort(opts.port || DEFAULT_PORT, cb); 822 | engine.port = port 823 | swarm.listen(engine.port, cb); 824 | discovery.updatePort(engine.port); 825 | }; 826 | 827 | return engine; 828 | }; 829 | 830 | module.exports = torrentStream; 831 | -------------------------------------------------------------------------------- /background/lib/torrent-stream/test/data/Lorem ipsum.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam et iaculis neque. Nam sodales volutpat turpis, at adipiscing sem ornare sed. Nullam molestie ac lorem sit amet viverra. Etiam blandit nibh nisi, quis sodales lectus rhoncus fringilla. Nam velit orci, auctor vel mollis et, lobortis in quam. Vivamus vehicula commodo dolor. Quisque nec nunc vitae nisl ornare tempor. Donec semper nisl ac nisl ullamcorper volutpat. Donec in iaculis nisl, vel volutpat tellus. Curabitur sit amet ultricies libero, in tempus nulla. Quisque id nunc sed sapien eleifend faucibus eget vitae neque. 2 | 3 | Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum purus risus, accumsan sit amet fermentum quis, pellentesque non ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam eu neque consectetur, consectetur velit non, vehicula orci. Donec elit massa, dapibus ut tellus in, bibendum auctor arcu. Curabitur ullamcorper arcu et arcu placerat, nec adipiscing arcu sodales. Proin mattis bibendum tempor. Nunc auctor, turpis id hendrerit venenatis, augue urna vehicula arcu, sit amet vehicula nisi justo et sem. Curabitur porta, odio ut pellentesque adipiscing, justo nisl laoreet eros, ut auctor urna mi vel nulla. Morbi vel tortor vel leo sollicitudin volutpat. Duis blandit lacinia nibh nec euismod. 4 | 5 | Etiam tincidunt imperdiet eros, quis sollicitudin mi blandit et. Duis facilisis, dui ut varius feugiat, leo arcu dapibus ipsum, eu fringilla elit sapien et odio. Phasellus a tortor mi. Curabitur eget fermentum eros. Cras eget purus vel lectus porttitor congue. Etiam diam lorem, eleifend nec dictum ac, dapibus non risus. Vestibulum convallis molestie purus, vitae mattis nisi aliquet ac. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis id purus ut ipsum sodales fermentum. Aliquam vitae ante mauris. Sed dapibus sem in turpis elementum tempor. Aliquam lorem erat, convallis sed ante vitae, interdum pellentesque tortor. Phasellus lobortis quis massa nec rutrum. Curabitur in nibh a arcu varius vehicula. Donec arcu dolor, imperdiet at nisi at, vehicula molestie nunc. Phasellus nunc dolor, blandit non dolor in, bibendum pellentesque urna. 6 | 7 | Vivamus eu ante a purus dapibus accumsan ut nec eros. In luctus quam nibh, id dapibus leo condimentum scelerisque. Nulla aliquam tellus erat, quis consequat libero pellentesque eget. Duis lacinia elementum tempus. In elementum elementum leo eu feugiat. Sed ac suscipit diam. Duis consectetur quam at turpis aliquam ullamcorper. Pellentesque sit amet viverra erat, id faucibus purus. 8 | 9 | Nullam vel turpis tempus, elementum risus vel, eleifend purus. Maecenas laoreet turpis in mattis iaculis. Ut pharetra sed ipsum et bibendum. Proin porta nec nisl a tincidunt. Pellentesque iaculis cursus dui, eget cursus diam aliquet vel. Fusce sapien mi, placerat vitae eros semper, semper varius nisi. Suspendisse semper sit amet mauris sit amet rutrum. Duis viverra in velit convallis pellentesque. Integer convallis metus vel pharetra tristique. 10 | 11 | Proin interdum tortor id iaculis interdum. Praesent non elit nec tortor condimentum venenatis quis eu lectus. Suspendisse dictum felis at malesuada viverra. Nam metus erat, sagittis ut adipiscing non, euismod vitae odio. Sed sed odio non diam hendrerit fermentum nec vel arcu. Quisque cursus diam in urna egestas elementum. Maecenas sapien nunc, ullamcorper eget felis sed, faucibus tristique dolor. Pellentesque pellentesque porta magna, non sodales est hendrerit quis. Curabitur sodales nibh quis tincidunt varius. Aenean eu augue mauris. Vestibulum vel euismod magna. In adipiscing ultricies erat, id pretium nisi mattis facilisis. Praesent eget mi et enim aliquam ullamcorper. 12 | 13 | Proin risus urna, pulvinar a ultricies sed, blandit lobortis augue. Etiam ac gravida tortor, vel tincidunt risus. Sed a augue magna. Donec aliquet consectetur porta. Curabitur sit amet viverra quam. Nunc et tincidunt elit, ut ultrices augue. Phasellus sodales, purus vel mattis fringilla, arcu felis tincidunt erat, non eleifend urna turpis quis massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. 14 | 15 | Donec semper est vitae nisi fermentum ornare. Vivamus congue ultrices rhoncus. Duis in aliquet augue. Nullam ac adipiscing neque. Praesent vel velit et diam accumsan bibendum ut at eros. Pellentesque congue dolor dignissim porta eleifend. Vestibulum ac ultrices eros, sed auctor libero. Nulla facilisi. Nunc mattis lorem lectus, at mollis enim fermentum ac. Donec varius cursus massa ut iaculis. Ut convallis ante id felis hendrerit faucibus. Suspendisse laoreet tristique mi sit amet imperdiet. Aliquam molestie, nisi volutpat molestie iaculis, purus neque porttitor eros, ac pretium massa lacus commodo risus. Nullam luctus, tellus a suscipit egestas, mauris nibh faucibus orci, eget ornare risus libero id lacus. Donec ullamcorper at urna ac feugiat. 16 | 17 | Donec fringilla varius felis a porttitor. Duis diam elit, sollicitudin vel risus ac, tempor volutpat augue. Nullam volutpat, dui in tristique tristique, orci nulla ornare ante, ac elementum augue felis vitae ligula. Nam metus velit, lobortis ut dui at, condimentum facilisis sem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam porttitor faucibus sem eget consectetur. Donec blandit diam ac metus pellentesque convallis. Vestibulum eu dignissim nunc, ut accumsan nibh. Ut enim tellus, molestie rutrum cursus in, condimentum eu risus. 18 | 19 | Suspendisse pharetra vitae nibh sed eleifend. In rhoncus dapibus elit, id aliquet velit. Aliquam ac orci ligula. Mauris nec mattis metus. Ut gravida scelerisque mauris id varius. Integer gravida tristique mattis. Ut blandit suscipit ligula et porttitor. Vestibulum congue lorem ut nisl sagittis sollicitudin. Maecenas vulputate luctus scelerisque. Sed vestibulum molestie tellus a vehicula. Sed hendrerit varius nisl. Donec pellentesque, ipsum ut laoreet auctor, quam turpis porttitor felis, eu vulputate purus quam ac sapien. Mauris sit amet diam ac nibh feugiat semper nec a massa. 20 | 21 | Quisque vitae dictum neque. Proin fermentum ornare sapien ac consequat. Nullam nulla purus, pretium ut accumsan at, auctor eu mi. Phasellus at urna justo. Donec elit quam, dictum vitae elementum et, volutpat ut leo. Maecenas felis purus, commodo non arcu nec, commodo placerat elit. Praesent sit amet tincidunt ipsum. Phasellus risus velit, sagittis sed augue ac, accumsan iaculis nunc. Aliquam erat volutpat. Nam egestas diam eu euismod pulvinar. Nulla at neque ut nibh rhoncus euismod quis at mi. Vestibulum tristique in tortor ultrices venenatis. Quisque eu velit orci. Proin blandit metus dolor, quis luctus mi pretium sit amet. Cras vel neque in nisl scelerisque ullamcorper vel vel magna. 22 | 23 | Donec non libero venenatis, ullamcorper eros non, tristique sem. Mauris tincidunt nisl erat. Quisque cursus odio massa, at tempor nunc tincidunt id. Vestibulum lacinia eget purus et viverra. Donec lobortis mi et justo lobortis, ac placerat metus dapibus. In ut bibendum nunc, nec bibendum eros. Curabitur suscipit sodales luctus. Quisque luctus interdum libero, nec cursus lorem auctor quis. 24 | 25 | Ut placerat est eu nisi imperdiet fringilla. Nullam felis purus, gravida sit amet dapibus vitae, tincidunt vitae sem. Maecenas eu tellus condimentum, vulputate purus convallis, interdum justo. Donec molestie ultrices porta. Suspendisse et diam sem. Aenean vel lacus et augue fringilla sollicitudin ut sit amet nulla. Donec nec tincidunt massa. Cras blandit consectetur augue ac fermentum. Fusce feugiat, orci a convallis luctus, turpis libero semper lacus, sed pretium massa magna quis eros. Etiam lacinia felis id auctor fringilla. Donec posuere eros facilisis pulvinar ultrices. Vestibulum interdum in enim at volutpat. Vivamus eu pharetra odio, vitae accumsan neque. Phasellus a ligula sed augue luctus pulvinar. 26 | 27 | Fusce ut tellus a elit eleifend pulvinar in eu orci. Sed ut magna felis. Integer non suscipit nibh. Maecenas vitae tellus consequat, imperdiet lectus eu, mattis quam. Etiam id tincidunt eros. Fusce risus sem, pellentesque in ullamcorper id, molestie ut mi. Donec eget lectus mollis, gravida est sed, porttitor lorem. Nullam blandit tristique ullamcorper. Nulla auctor et tellus quis auctor. Morbi eu dui aliquet, vulputate odio a, tincidunt neque. Sed mattis pharetra nunc non cursus. 28 | 29 | Vestibulum hendrerit at ligula ac pharetra. Etiam sagittis suscipit pulvinar. Donec luctus diam turpis, id tempus metus porta at. Donec nec lacus consectetur, aliquam nisl ut, ullamcorper dolor. Sed porttitor est quis felis molestie, et adipiscing erat vestibulum. Vestibulum ac ipsum auctor, blandit eros facilisis, blandit mi. Sed non magna elit. Aenean risus augue, gravida dapibus mauris ac, convallis pellentesque ligula. Integer eros tortor, scelerisque id eros ac, posuere volutpat ipsum. Quisque accumsan volutpat tellus ac sagittis. Praesent pellentesque turpis quis libero tempor mattis. Duis at mi laoreet, aliquet ante vel, consectetur orci. Maecenas vehicula ultrices nisl sed pharetra. Vestibulum hendrerit dignissim arcu ut venenatis. 30 | 31 | In nec pulvinar eros. Vivamus ligula tellus, dignissim in malesuada in, malesuada ut ipsum. Curabitur a laoreet magna, a tristique nulla. Pellentesque congue dui velit, et aliquam augue auctor scelerisque. Donec ullamcorper elit dui, in posuere magna viverra vitae. Donec bibendum urna sit amet suscipit mattis. Phasellus sed vulputate libero. Maecenas pellentesque, velit laoreet ornare facilisis, neque eros aliquet elit, non semper velit tortor id massa. Aliquam laoreet tempus erat vitae dictum. Aenean ut semper sem. 32 | 33 | Ut condimentum tempor turpis id porttitor. Nullam ultrices dui eget accumsan lacinia. Ut cursus pharetra arcu, non porta nisi iaculis at. Aenean eget felis eu leo rutrum ullamcorper et in urna. Etiam vitae ullamcorper erat. Etiam bibendum lacus libero. Nunc porta lacus vitae egestas sagittis. Ut pellentesque leo nulla, et gravida lectus eleifend at. Curabitur feugiat metus ut vestibulum varius. 34 | 35 | Aliquam massa magna, lacinia sed hendrerit quis, lobortis nec nunc. Etiam ultrices velit ut magna tristique, non placerat mauris sagittis. Mauris lacinia ultricies tincidunt. Integer aliquet sodales urna, sed faucibus erat dictum sit amet. Fusce id risus scelerisque, suscipit nisi vel, vestibulum mi. Nam tempus suscipit luctus. Suspendisse rutrum tincidunt odio et dictum. Mauris ligula nibh, pharetra eu augue sed, varius aliquet erat. Vivamus aliquet, ante eu porta blandit, felis nisl bibendum tellus, fermentum ornare arcu nisi et leo. Ut scelerisque neque diam, tincidunt tempus tellus viverra sed. Mauris tristique nec odio sed condimentum. Fusce feugiat tempor augue, vel consequat purus molestie quis. Quisque et gravida diam. Sed ornare diam consequat mauris placerat vulputate. Donec libero neque, eleifend a vulputate quis, elementum et nibh. Pellentesque id dapibus metus. 36 | 37 | Nullam condimentum, nibh sed condimentum sagittis, ante justo tincidunt quam, vitae dignissim turpis mi non dui. Nunc sit amet ullamcorper orci, at hendrerit nulla. Pellentesque interdum viverra congue. Nunc sit amet nulla ut eros malesuada posuere. Nunc rutrum mauris at tempor pretium. Proin porta nisi at faucibus placerat. Fusce sodales, lectus id posuere scelerisque, massa mauris fermentum libero, eget eleifend nisl odio id sapien. 38 | 39 | Sed ac sem eget ligula fringilla vulputate ut quis quam. Sed elit nisi, egestas sit amet adipiscing nec, dapibus a augue. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas dignissim auctor libero vel dignissim. Quisque porta a sapien nec eleifend. Morbi eu augue augue. Aliquam erat volutpat. Sed gravida ut neque at fermentum. Praesent metus est, convallis mattis viverra nec, imperdiet at augue. Donec id dolor mollis, congue purus ac, auctor risus. Vestibulum eget ornare nibh. 40 | 41 | Nunc sem lorem, tincidunt quis venenatis et, malesuada eget eros. Nullam libero felis, tristique at laoreet ut, vulputate eget nisl. Aliquam vitae elit tellus. Donec in turpis non nibh fringilla facilisis sed non urna. Maecenas lorem sem, rhoncus vel fermentum at, molestie in velit. Proin vitae euismod urna. Pellentesque vitae purus elementum, ultrices libero vitae, egestas lectus. Vestibulum ultricies diam nec libero dictum tristique. Morbi auctor enim arcu, quis semper turpis pretium ut. Donec consectetur sodales nulla in dapibus. Nullam commodo magna velit, eu egestas urna posuere et. Phasellus vestibulum erat ac vehicula vehicula. Morbi ullamcorper eu orci in malesuada. Aliquam condimentum turpis sed suscipit pellentesque. 42 | 43 | In velit dolor, molestie sit amet quam at, rutrum convallis enim. Vivamus viverra mi tincidunt ultricies feugiat. Vivamus pulvinar odio ut blandit auctor. Ut eleifend at tellus id varius. Duis placerat vitae elit eu imperdiet. Curabitur tortor mauris, dapibus quis orci eget, iaculis vulputate urna. Nulla pellentesque augue quis tincidunt tincidunt. Sed pharetra porta suscipit. Phasellus tristique lectus neque, id adipiscing dolor semper nec. Nunc et faucibus risus. Sed condimentum faucibus ante, at dignissim lectus fringilla nec. Aenean posuere eleifend enim, ac fringilla nulla scelerisque volutpat. Curabitur sollicitudin varius purus quis placerat. Etiam metus urna, pretium mattis urna venenatis, dapibus mollis augue. Nulla viverra, sem accumsan vestibulum sagittis, sapien nulla vehicula metus, vel ornare lacus sem sit amet neque. 44 | 45 | Nullam dui purus, fermentum nec dolor id, rhoncus posuere purus. Sed nunc enim, condimentum non tempus ac, posuere quis urna. Vestibulum vestibulum tempus metus, ac laoreet sem tincidunt at. Fusce nec nisl scelerisque, tincidunt tortor a, vestibulum risus. In vitae sollicitudin nibh. In vulputate, nunc vel ultricies rutrum, nibh ante pharetra velit, vitae pharetra sapien tortor sed orci. Fusce lacinia bibendum ornare. Pellentesque et imperdiet nibh. Ut nisi erat, volutpat in commodo sed, vulputate sit amet ligula. Nullam gravida nisl diam, et imperdiet libero vehicula eu. Nullam eget mauris at turpis mollis vulputate. Quisque mauris felis, gravida nec tortor quis, vulputate consequat sem. Fusce imperdiet in nisi sed egestas. Curabitur rhoncus condimentum ligula eu eleifend. Morbi massa sem, bibendum sed enim id, tempus imperdiet leo. 46 | 47 | Aliquam dolor mauris, laoreet non lacus et, posuere vehicula augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Maecenas id urna consectetur, dapibus lacus id, mattis enim. Fusce felis turpis, sodales aliquet pharetra sit amet, imperdiet nec mi. Nullam facilisis ante ac arcu pellentesque, eu dignissim turpis convallis. Fusce scelerisque lacinia interdum. Proin placerat felis et elit luctus, eget laoreet lacus ullamcorper. Vestibulum suscipit luctus sollicitudin. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. 48 | 49 | In eget felis pharetra, posuere libero facilisis, consequat orci. Donec aliquam leo non libero placerat volutpat. Maecenas cursus sodales ultrices. Nunc in rutrum justo, eu fermentum leo. Pellentesque semper dui varius nibh commodo lacinia. Praesent feugiat neque sed gravida ultrices. Mauris posuere lobortis ante, nec varius nisl pretium ut. Aliquam rutrum placerat gravida. Maecenas laoreet, turpis aliquet tempor ultrices, velit risus consequat neque, eget ullamcorper leo libero non justo. Curabitur gravida turpis eu nulla mollis ultrices. Nunc enim eros, pharetra vitae quam vitae, dapibus luctus enim. 50 | 51 | Donec sollicitudin accumsan nibh et auctor. Donec fermentum felis a sem suscipit pellentesque. Sed aliquam dui nec varius dignissim. Vivamus egestas dui eu sapien euismod rhoncus. Nam nisl lorem, tempus ac egestas eu, auctor ac lectus. Quisque adipiscing justo nunc, a sodales ante consectetur non. Curabitur aliquet nulla vitae lobortis pharetra. Aenean volutpat tincidunt risus sed rutrum. In hac habitasse platea dictumst. Duis lectus elit, malesuada adipiscing porta eget, posuere ut urna. Duis aliquet at odio id mattis. Nulla facilisi. Nullam accumsan diam eu erat rutrum gravida. Cras tempus quam vel libero sollicitudin bibendum. 52 | 53 | Interdum et malesuada fames ac ante ipsum primis in faucibus. In pharetra dolor vitae vehicula semper. Curabitur at nisl ac sem placerat tincidunt vitae ut erat. Maecenas ullamcorper, justo non consequat condimentum, augue erat condimentum ipsum, vitae sollicitudin lorem dolor vitae massa. Suspendisse vitae tortor a dui tristique accumsan. Integer sagittis mollis eros sit amet semper. Praesent rutrum, dui sed sollicitudin gravida, ligula ante elementum tortor, eget euismod risus ipsum et elit. Vestibulum interdum sapien quis felis tempor aliquet. Curabitur eu tincidunt arcu, quis lacinia diam. 54 | 55 | Etiam pretium ante quis est hendrerit, in tempor diam gravida. Morbi dictum neque interdum metus congue, in tincidunt sapien elementum. Duis nibh tellus, vehicula in mollis nec, egestas sollicitudin felis. Aliquam quis consectetur orci, eget blandit augue. Mauris pharetra, nunc eget rutrum tempor, purus dolor vestibulum velit, vel rutrum nulla diam sit amet arcu. In faucibus commodo nunc, eget facilisis nisl facilisis vitae. In tempor viverra lorem, eu malesuada orci porta at. Proin sit amet ornare tortor. 56 | 57 | Duis tempus quis eros fringilla porttitor. Cras id nisi ante. Aliquam eleifend quam vitae imperdiet tempor. Morbi egestas ultricies dui et varius. In hac habitasse platea dictumst. Nulla quis tortor quis diam condimentum dapibus ut nec metus. Nulla at pulvinar diam. Sed mollis turpis ac tortor imperdiet, sed porttitor magna fringilla. Sed mauris justo, pretium eget dapibus id, condimentum id ante. Integer et arcu luctus, aliquam diam non, hendrerit sapien. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed eu ante dui. Quisque sit amet malesuada mi, et vulputate massa. Nulla in nisi eget nisi accumsan dapibus. Nulla convallis sollicitudin mauris quis ultricies. 58 | 59 | Suspendisse tincidunt aliquet lacus, et interdum justo malesuada id. Praesent laoreet velit quis purus eleifend semper. Donec adipiscing tristique elit, ac condimentum diam interdum a. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis ullamcorper consequat neque et pharetra. Nulla rhoncus nulla nec sagittis condimentum. Maecenas hendrerit dignissim vestibulum. Proin dignissim quis risus at bibendum. 60 | 61 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi tincidunt justo sit amet mi feugiat commodo. Nulla ultrices dui enim, non tempus sapien accumsan at. Quisque adipiscing egestas metus egestas rutrum. Donec ultrices mi ac lectus egestas, a egestas elit eleifend. Aenean at magna condimentum, sagittis nibh posuere, dapibus lacus. Duis porttitor turpis urna, vulputate ornare nibh hendrerit ut. Curabitur ut enim lacus. Sed scelerisque felis eu felis luctus, ut tincidunt libero pulvinar. Vestibulum ac enim commodo, vulputate purus in, viverra velit. Duis interdum libero molestie sem tincidunt cursus. 62 | 63 | Nullam semper massa urna, vel euismod tortor molestie eu. Suspendisse potenti. Mauris eros arcu, auctor sed mi vel, interdum gravida nulla. Sed interdum porttitor semper. Phasellus tempus congue ante sed tempor. Pellentesque quis velit sed libero aliquam iaculis non venenatis turpis. Integer mattis, nisl a blandit iaculis, mauris nibh suscipit libero, ut tempus nunc lectus at arcu. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent accumsan tincidunt mauris, et iaculis enim tincidunt quis. In lacinia, augue ac convallis sollicitudin, mauris justo iaculis ante, a dictum dui nunc quis arcu. Quisque eget dapibus lorem. Aliquam eget rhoncus lorem. 64 | 65 | Nunc commodo tincidunt pharetra. Cras ultricies velit sed libero porta posuere. Sed posuere commodo faucibus. Suspendisse sodales purus non massa lobortis, sit amet auctor quam lobortis. Curabitur sit amet ultricies est, id lobortis enim. Quisque malesuada dolor mauris. Quisque tincidunt, ipsum ut viverra adipiscing, nisi orci vulputate purus, sit amet egestas augue mauris eu nunc. Sed consequat nunc eu enim laoreet, id pulvinar nisi porta. Sed eu ante scelerisque, pharetra dui a, congue diam. Curabitur sapien quam, posuere vitae facilisis quis, tincidunt lobortis turpis. Nam ac orci eu sapien ultrices sollicitudin. Curabitur eget eros purus. Phasellus pretium et quam non accumsan. Suspendisse tempor lacus a dolor luctus, ut viverra nibh bibendum. Aliquam sagittis ornare massa sit amet placerat. 66 | 67 | Curabitur mattis dui ac augue viverra, eu hendrerit ipsum dictum. Aenean ullamcorper placerat sem, eget feugiat ipsum laoreet nec. In varius ullamcorper ligula in pulvinar. Suspendisse eget nisi vitae urna aliquet vestibulum. Vestibulum ipsum erat, malesuada at tristique non, posuere id dui. In blandit interdum luctus. Sed ac dui ut purus aliquam mollis. Cras at adipiscing tellus, at interdum nulla. 68 | 69 | Mauris vitae justo congue, elementum risus nec, lacinia augue. Maecenas sit amet magna fermentum, tempus neque ut, tincidunt tellus. Donec pharetra venenatis pulvinar. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec dignissim leo non varius congue. Vivamus ultricies scelerisque arcu, sed lobortis nisl euismod id. Nulla pellentesque consequat magna, consequat scelerisque diam fringilla blandit. 70 | 71 | Donec posuere tincidunt erat, vitae luctus erat gravida vel. Sed pretium, nisi a luctus porttitor, lacus magna placerat ipsum, aliquam faucibus leo purus vitae sem. Curabitur convallis sem et consequat porttitor. Vestibulum odio turpis, porttitor non facilisis a, accumsan ut velit. Nullam interdum ipsum erat, at porta enim varius quis. Sed molestie velit sed metus egestas tincidunt. Curabitur mattis felis sed mauris lacinia tempor. Curabitur et tellus risus. Nunc nisl nisi, faucibus feugiat orci eu, pulvinar cursus risus. Etiam volutpat velit magna, nec malesuada nisi ornare et. Mauris porttitor rhoncus nulla ac mollis. Phasellus faucibus, felis varius dictum volutpat, elit est aliquet elit, ut venenatis sapien nisi eget leo. Nullam convallis velit a odio consequat aliquet. 72 | 73 | Maecenas convallis metus eget lorem varius fermentum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam justo nibh, mollis sit amet imperdiet non, facilisis id massa. Maecenas tincidunt leo elit, sit amet pretium libero cursus vel. Fusce et tincidunt urna. Donec in tellus malesuada, dignissim nisl vel, rutrum erat. In imperdiet, purus quis viverra egestas, lorem ipsum auctor nisl, nec aliquet tellus enim et mauris. Donec et orci imperdiet arcu ultricies blandit tristique quis velit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec erat lorem, eleifend eget nisi a, tincidunt adipiscing nulla. Mauris suscipit in lacus in elementum. Maecenas pharetra lobortis dolor in tempus. Nam a libero diam. Donec semper elit a neque molestie dignissim. 74 | 75 | Suspendisse in nisi ut eros tincidunt laoreet eu eu nunc. Fusce venenatis suscipit dolor nec consectetur. Curabitur vel luctus purus. Aenean sem magna, porttitor ultricies iaculis ac, pellentesque eu mi. Nunc risus tellus, laoreet non risus nec, imperdiet tempus risus. Praesent condimentum ipsum vel magna pharetra, et viverra eros iaculis. Quisque bibendum facilisis orci. Praesent varius eget urna at hendrerit. 76 | 77 | Nam gravida auctor fringilla. Nam lobortis ante eget rutrum iaculis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam erat volutpat. Pellentesque et adipiscing turpis. Aenean sed ultricies ante, interdum accumsan felis. Quisque rutrum pulvinar nisl vehicula dictum. 78 | 79 | Duis mauris mi, auctor ac mauris id, venenatis bibendum libero. Pellentesque ac eleifend quam. Nulla turpis orci, fermentum aliquam tortor sed, egestas scelerisque nisl. In convallis a velit a vulputate. Suspendisse iaculis nec mauris eget convallis. Duis ante sem, viverra id velit nec, scelerisque dapibus tellus. Ut ultricies, mi in rutrum lobortis, risus ante condimentum erat, eu ornare ipsum augue sed mi. Donec mi turpis, elementum in est in, elementum mattis orci. 80 | 81 | Aliquam pulvinar convallis congue. Sed non turpis id lorem tincidunt hendrerit eget vel justo. Aliquam tempus nulla quis metus viverra, at elementum sem dictum. Etiam condimentum velit eu nisi suscipit suscipit. Cras quis quam non purus mattis rhoncus non in leo. Proin tincidunt condimentum urna tempus cursus. Sed placerat elementum tortor, id aliquam metus. Etiam ac lorem eget erat auctor aliquam. Etiam in dictum leo, sed sagittis erat. Phasellus laoreet velit eget velit porta, quis blandit eros luctus. Donec suscipit, felis eu dignissim pulvinar, risus velit adipiscing urna, et semper tortor mauris sit amet ipsum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur rhoncus, justo eget pretium aliquet, arcu sem dictum tortor, sed ultricies odio leo quis erat. Suspendisse vitae aliquam ipsum. Sed adipiscing, nisi quis aliquet euismod, velit ante placerat risus, vel gravida tellus elit et orci. Integer nibh elit, adipiscing sed ipsum non, viverra facilisis tellus. 82 | 83 | Vivamus malesuada lorem in felis auctor, vitae lacinia augue condimentum. Nam tortor mauris, condimentum vel fermentum nec, malesuada eget purus. Fusce ultricies lectus justo, ac cursus erat ultricies eget. Quisque aliquet sem ut lacinia tincidunt. Praesent quis quam quis leo ultrices auctor eu at sem. Mauris pulvinar ac risus vitae ullamcorper. Pellentesque ut placerat est. Ut nisl massa, fringilla a nisi in, porta varius purus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Quisque fringilla ullamcorper condimentum. Aliquam non porttitor diam. Suspendisse sodales, velit ut laoreet gravida, odio nunc volutpat purus, vel feugiat leo magna sit amet sapien. Donec nec tellus cursus lectus molestie viverra. Maecenas mollis scelerisque massa, vitae dictum orci vestibulum et. Sed viverra ultricies dui vitae sagittis. Nunc sed justo id urna consequat fermentum. 84 | 85 | Cras convallis odio vitae lobortis sodales. Fusce condimentum posuere tellus, eget semper urna tincidunt vel. Nullam porta nisi et ligula blandit ullamcorper. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas eleifend, felis sit amet pretium dictum, urna erat rutrum massa, dapibus tempus purus nisi id felis. Curabitur aliquet enim id ornare fringilla. Nullam luctus dignissim mi, nec vehicula quam vehicula eget. Nunc sed nulla vitae nunc vehicula molestie. Maecenas sollicitudin, neque sed cursus facilisis, tortor ipsum congue eros, eu aliquam ante magna sit amet magna. 86 | 87 | Nulla convallis et tellus ac posuere. Aliquam in nulla quis lacus euismod tempus. Aenean a orci in purus blandit aliquet. Sed ac tortor massa. Suspendisse potenti. Maecenas imperdiet massa a sapien elementum, ut vestibulum lectus sagittis. Vivamus ac ligula et leo aliquet tempus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque sed turpis commodo, sollicitudin dolor eu, fermentum massa. Suspendisse et orci nunc. Sed convallis hendrerit ligula, in blandit mi tincidunt in. Proin mattis erat neque, vel porttitor ipsum congue quis. Phasellus dictum metus lorem, ac fringilla augue mollis tristique. Suspendisse potenti. Fusce neque urna, vestibulum sed sapien at, molestie mattis turpis. Nunc sapien elit, vulputate nec tortor a, ornare volutpat ipsum. 88 | 89 | Suspendisse vestibulum volutpat ante sed suscipit. Etiam eu leo vel ante faucibus commodo a sodales enim. Praesent et lorem venenatis, rutrum augue at, eleifend nulla. Proin ornare molestie lacus, in semper neque imperdiet consequat. Aliquam iaculis elit eu lorem elementum mattis. Donec consectetur lacus et ligula iaculis, sit amet consequat diam dictum. Praesent congue ipsum id nisl elementum, id lobortis tortor laoreet. Integer at mauris semper, tempus ipsum sed, interdum lectus. Vestibulum sed augue nec orci dignissim consequat. Quisque malesuada dignissim diam, sed semper risus convallis a. Praesent sed vulputate metus. 90 | 91 | Nunc porttitor eros eu euismod elementum. Quisque eget magna ornare, tempor nisi at, tempus mauris. Sed tincidunt nec ligula a rhoncus. Suspendisse a volutpat nisl. Fusce rutrum orci neque, non iaculis purus suscipit sed. Donec eros tortor, dapibus eget justo id, euismod dignissim ipsum. Vestibulum id vulputate quam. Donec faucibus nulla in erat blandit elementum. Sed bibendum malesuada diam vitae accumsan. Nullam congue venenatis dui a vestibulum. Aliquam gravida nec erat vel molestie. In posuere sollicitudin turpis sit amet bibendum. Fusce adipiscing adipiscing eros, et luctus leo pharetra mattis. Sed a pretium mauris. Maecenas ante urna, adipiscing nec suscipit sit amet, condimentum sit amet diam. Vivamus commodo sem at ante mattis, eget elementum sapien posuere. 92 | 93 | Vestibulum tristique, magna ut pulvinar facilisis, lectus felis malesuada augue, quis porttitor leo purus a mauris. Sed in scelerisque tortor. Mauris molestie tempus leo, vitae tempus odio viverra ac. Ut augue massa, convallis eu molestie vitae, tristique sed arcu. Donec sagittis purus eros, in consectetur tortor ultrices id. Sed pulvinar faucibus velit, non tempor felis aliquam a. Donec imperdiet nisi orci, eu ultrices lorem adipiscing vitae. Praesent egestas quam suscipit, faucibus nisl a, scelerisque turpis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. 94 | 95 | Aenean enim sem, vulputate id ligula quis, vehicula adipiscing odio. Cras nec iaculis massa, eu ultricies leo. Nunc pellentesque purus sem, ut elementum turpis pellentesque non. Etiam rhoncus venenatis urna at congue. Nullam quis rutrum urna, vitae dignissim risus. Ut ac lacus sed ipsum feugiat lacinia a non sapien. In pharetra turpis id sagittis sagittis. Suspendisse scelerisque auctor tortor id pellentesque. Ut viverra malesuada libero sed laoreet. 96 | 97 | Nulla mauris felis, ornare quis dapibus sed, bibendum at nisl. Nam sed odio ipsum. Nunc ac lorem libero. Vivamus tincidunt risus sit amet quam rutrum, vitae luctus enim consectetur. Nam vitae purus vitae lectus euismod faucibus. Nulla facilisi. Nam dictum nunc vitae ullamcorper gravida. Ut sit amet ante sed elit fermentum tristique in id urna. Nam dictum tincidunt risus, id blandit justo tristique eget. Aliquam erat volutpat. 98 | 99 | Nullam vel turpis convallis, rhoncus mauris vitae, feugiat sapien. Sed libero sapien, hendrerit non augue vel, facilisis porttitor sapien. Praesent convallis aliquet mi, a vulputate neque commodo id. Sed non ullamcorper lorem, in eleifend tortor. Nunc sagittis faucibus sem, quis fringilla nisl elementum sagittis. Etiam gravida sed justo id malesuada. Proin et ultricies arcu. Curabitur nisl diam, molestie ac nibh non, lobortis dictum erat. Nunc eget scelerisque dolor, vitae consectetur sapien. Nulla convallis mi massa, quis dictum tortor lobortis eu. Vivamus lorem elit, auctor pellentesque condimentum in, consequat nec enim. Duis non ultrices magna, nec suscipit dui. Ut porta, erat sit amet volutpat convallis, nulla magna hendrerit felis, id semper ante lectus id augue. Aenean euismod sem quis tincidunt hendrerit. 100 | 101 | Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus erat nisi, viverra nec egestas vel, mattis et diam. Morbi quam leo, commodo id sapien ac, aliquet vehicula mauris. Vestibulum rhoncus nec ipsum at eleifend. Sed vel turpis congue, dapibus turpis at, suscipit neque. Curabitur feugiat odio quis tristique fringilla. Nunc fermentum elementum ligula volutpat dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nam suscipit ante sit amet odio mattis, ut condimentum erat posuere. In placerat, purus vitae congue bibendum, elit purus pretium eros, a consectetur nisl erat eget augue. Fusce eleifend et mi vel viverra. Mauris nec fringilla mi. Nulla enim magna, fringilla eget lorem at, consequat pharetra augue. Fusce at tincidunt sapien, sit amet laoreet risus. Pellentesque sollicitudin commodo nulla sit amet sagittis. Interdum et malesuada fames ac ante ipsum primis in faucibus. 102 | 103 | Nulla ac diam leo. Cras et elit vitae erat mattis volutpat. Quisque adipiscing luctus lacinia. Aenean eget ipsum convallis, tristique quam non, ultrices nulla. Mauris nibh nisl, suscipit ac elit at, molestie ornare sem. Fusce rutrum tristique nulla in suscipit. Quisque tincidunt ultricies dolor nec euismod. Morbi eros diam, molestie a semper quis, tincidunt sed quam. 104 | 105 | Suspendisse porta, felis a imperdiet sodales, nisl ipsum fermentum lorem, a bibendum nisi dui ut turpis. In quis purus rhoncus, sodales est sed, aliquam mauris. Quisque eu nisi vestibulum, feugiat odio quis, malesuada velit. Integer purus quam, pretium a lectus sed, pharetra porta turpis. In elementum nisi quis ipsum sollicitudin bibendum. Integer pharetra dolor sit amet lacus placerat, ac placerat justo suscipit. Aliquam quis lacus quis odio gravida dapibus gravida id turpis. Donec bibendum, mi eget pretium pretium, erat velit hendrerit nibh, non eleifend nulla lectus et lorem. Sed suscipit ut ligula et rhoncus. Duis egestas fringilla tortor sit amet ultrices. Nulla luctus lectus quis consectetur lacinia. Aliquam erat volutpat. Etiam velit enim, tincidunt vitae aliquet eu, elementum molestie libero. Quisque tincidunt mi urna, consequat tempor mauris cursus eu. Ut pretium sagittis pharetra. Duis id magna vitae sem blandit tempor. 106 | 107 | In ut faucibus odio. Nunc nibh orci, gravida non nibh eu, sodales lobortis quam. Curabitur euismod varius est, in facilisis odio gravida suscipit. Suspendisse potenti. Donec porttitor fringilla nisi nec semper. In sagittis accumsan risus, id aliquet tortor consectetur cursus. Maecenas sed vulputate orci, id fermentum dolor. 108 | 109 | Sed vitae posuere tellus, id interdum sem. Ut lobortis lacinia lectus, ut pulvinar ligula dictum sit amet. Mauris rhoncus, nisl quis molestie commodo, quam tellus eleifend quam, sit amet suscipit odio lorem ac nulla. Ut id pretium dolor. Cras nec felis sit amet justo condimentum ornare. Aliquam luctus libero eget tortor convallis cursus. Aliquam varius, justo vel tincidunt tincidunt, eros dolor placerat sem, a laoreet magna velit ut est. Vivamus tristique eget orci vel tincidunt. Praesent faucibus lacus ornare lacus accumsan, a molestie dolor feugiat. Nullam mollis massa quis sollicitudin malesuada. 110 | 111 | Integer id porta leo. In pulvinar nulla non lacus tincidunt, sed molestie dolor congue. Etiam sollicitudin, nunc at scelerisque placerat, nibh lectus viverra lacus, sed elementum nibh ante quis augue. Nulla nisl massa, aliquam et velit vitae, euismod dictum neque. Suspendisse at sapien pretium nibh rhoncus aliquet vel id sem. Donec quis erat lorem. Proin consectetur velit a dui adipiscing, et venenatis erat tempus. Mauris sodales at nunc lacinia condimentum. In consectetur tristique lorem non porttitor. 112 | 113 | Nunc vitae erat iaculis, lobortis orci eget, fringilla risus. Cras lorem odio, mollis ut lorem ac, vestibulum accumsan diam. Proin viverra iaculis est, blandit cursus nulla blandit quis. Vivamus a ligula facilisis, tempor metus ut, commodo dui. Duis semper dapibus metus, sit amet semper purus. Etiam fermentum aliquet elementum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent feugiat a velit a placerat. Donec ultricies nec erat a sollicitudin. Fusce tempus tortor nec sapien vehicula blandit. Maecenas mauris velit, sodales quis consectetur id, bibendum at magna. Pellentesque rutrum nibh vel nibh pretium rutrum. Sed pellentesque sollicitudin velit. Sed rutrum scelerisque imperdiet. Sed id posuere felis, eu mattis sem. 114 | 115 | Nunc blandit vitae lorem sit amet faucibus. Vivamus dapibus, augue nec rhoncus porttitor, turpis urna sollicitudin dui, vel scelerisque sem dolor eget enim. Nunc et accumsan sapien, vitae varius orci. In hac habitasse cras amet. --------------------------------------------------------------------------------