├── .gitignore ├── api.js ├── cli.js ├── components ├── list.js └── status.js ├── example.config.json ├── frontend.js ├── index.js ├── package.json ├── readme.md ├── request.js ├── router.js ├── server.js └── static ├── dat-data-blank.png ├── example.png ├── loading_icon_small.gif └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bundle.js 3 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | var xhr = require('./request') 2 | 3 | module.exports = { 4 | delete: function (key, cb) { 5 | var opts = { 6 | uri: '/dats', 7 | method: 'DELETE', 8 | json: {key} 9 | } 10 | xhr(opts, cb) 11 | }, 12 | list: function (cb) { 13 | xhr({uri: '/dats', json: true}, function (err, resp, json) { 14 | if (err) return cb(err) 15 | return cb(null, json) 16 | }) 17 | }, 18 | add: function (key, cb) { 19 | var opts = { 20 | method: 'POST', 21 | uri: '/dats', 22 | json: { 23 | key: key 24 | } 25 | } 26 | xhr(opts, cb) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var args = require('minimist')(process.argv.slice(2)) 3 | var server = require('./server.js') 4 | 5 | var port = process.env.PORT || args.PORT || 8080 6 | 7 | server.listen(port, function () { 8 | console.log('http://localhost:' + port) 9 | }) 10 | -------------------------------------------------------------------------------- /components/list.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var ReactDOM = require('react-dom') 3 | var prettyBytes = require('pretty-bytes') 4 | var EditInPlace = require('react-editinplace') 5 | var prettyBytes = require('pretty-bytes') 6 | 7 | var api = require('../api') 8 | var xhr = require('../request.js') 9 | 10 | var DownloadButton = React.createClass({ 11 | render: function () { 12 | var href = '/download/' + this.props.dat 13 | return Download 14 | } 15 | }) 16 | 17 | var DeleteButton = React.createClass({ 18 | delete: function () { 19 | var self = this 20 | api.delete(this.props.dat, function (err, resp, json) { 21 | render() 22 | }) 23 | }, 24 | render: function () { 25 | return Delete 26 | } 27 | }) 28 | 29 | var ListItem = React.createClass({ 30 | render: function () { 31 | var health = this.props.dat.health 32 | return ( 33 |
34 |

{this.props.dat.key}

35 |

36 | {prettyBytes(health.byteLength)} 37 |

38 |

39 | {health.peers.length} peers 40 |

41 |
42 | 43 | 44 |
45 |
46 | ) 47 | } 48 | }) 49 | 50 | var List = React.createClass({ 51 | render: function () { 52 | return ( 53 |
54 | {this.props.dats.map(function (dat) { 55 | return 56 | })} 57 |
58 | ) 59 | } 60 | }) 61 | 62 | module.exports = render 63 | 64 | function render (cb) { 65 | api.list(function (err, dats) { 66 | if (cb) cb(dats) 67 | if (!dats) dats = [] 68 | ReactDOM.render( 69 | , 70 | document.getElementById('app') 71 | ) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /components/status.js: -------------------------------------------------------------------------------- 1 | var $status = document.getElementById('status') 2 | 3 | module.exports = function (message, klass) { 4 | $status.innerHTML = message 5 | $status.classList.add(klass) 6 | $status.style = 'display:block;' 7 | setTimeout(function () { 8 | $status.classList.remove(klass) 9 | $status.style = 'display:none;' 10 | }, 4004) 11 | } 12 | -------------------------------------------------------------------------------- /example.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dir": "./dats", 3 | "title": "My Dat Sever" 4 | } 5 | -------------------------------------------------------------------------------- /frontend.js: -------------------------------------------------------------------------------- 1 | var api = require('./api') 2 | var status = require('./components/status') 3 | var renderList = require('./components/list') 4 | 5 | var $link = document.getElementById('link') 6 | var $loading = document.getElementById('loading') 7 | 8 | document.getElementById('submit').onclick = submit 9 | 10 | function submit (event) { 11 | var key = $link.value.trim() 12 | $link.value = '' 13 | $loading.style = 'display:block;' 14 | api.add(key, function (err) { 15 | if (err) status(err.message) 16 | else status('Dat added successfully.') 17 | renderList() 18 | $loading.style = 'display:none;' 19 | }) 20 | } 21 | 22 | renderList() 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | return ` 3 | 4 | 5 | ${config.title} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 23 |
24 |
25 | 26 | 27 |
28 | 32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | ` 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dat-server", 3 | "version": "1.0.2", 4 | "description": "A web app for backing up dat archives on a remote server.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "npm run watch & node cli.js", 9 | "watch": "watchify -t [ babelify --presets [ react ] ] frontend.js -o static/bundle.js", 10 | "build": "browserify -t [ babelify --presets [ react ] ] frontend.js -o static/bundle.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/karissa/dat-server.git" 15 | }, 16 | "author": "Karissa McKelvey (http://karissamck.com/)", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/karissa/dat-server/issues" 20 | }, 21 | "homepage": "https://github.com/karissa/dat-server#readme", 22 | "dependencies": { 23 | "babel-preset-react": "^6.5.0", 24 | "babelify": "^7.2.0", 25 | "body": "^5.1.0", 26 | "body-parser": "^1.18.2", 27 | "browserify": "^13.0.0", 28 | "dat-encoding": "^4.0.2", 29 | "dat-link-resolve": "^1.1.1", 30 | "express": "^4.15.4", 31 | "getport": "^0.1.0", 32 | "hyperdrive-archiver": "^1.0.0", 33 | "hyperdrive-to-zip-stream": "^2.0.0", 34 | "minimist": "^1.2.0", 35 | "pretty-bytes": "^3.0.1", 36 | "react": "^0.14.7", 37 | "react-dom": "^0.14.7", 38 | "react-editinplace": "^1.0.3", 39 | "rimraf": "^2.5.1", 40 | "run-parallel": "^1.1.6", 41 | "st": "^1.1.0", 42 | "tape": "^4.4.0", 43 | "watchify": "^3.11.0", 44 | "xhr": "^2.2.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # dat-server 2 | 3 | A web ui and http interface for archiving dats. 4 | 5 | ![static/example.png](static/example.png) 6 | 7 | ## Installation 8 | 9 | Clone this repository and install dependencies. 10 | 11 | ``` 12 | git clone https://github.com/karissa/dat-server.git 13 | cd dat-server 14 | npm install 15 | ``` 16 | 17 | ## Create the config 18 | 19 | Copy the example config and change the variables to suit your setup. 20 | 21 | ``` 22 | cp example.config.json config.json 23 | ``` 24 | 25 | * `dir`: Where downloaded dats will be stored. 26 | * `title`: The visible title of the app. 27 | 28 | ## Build assets and run the app. 29 | 30 | ``` 31 | npm start 32 | ``` 33 | 34 | # API 35 | 36 | ### List dats 37 | 38 | GET `/dats` 39 | 40 | A list of currently deployed dats in JSON format. 41 | 42 | ### Download a Dat 43 | 44 | GET `/download/:key` 45 | 46 | Download a dat to zip file, uses a stream. 47 | 48 | ### Dat health 49 | 50 | GET `/health/:key` 51 | 52 | The health of a dat, including it's size, number of peers, and their replication progress. 53 | 54 | ### Add a dat 55 | 56 | POST `/dats` 57 | 58 | with json body: 59 | ``` 60 | {"key": } 61 | ``` 62 | 63 | TODO: Could return download progress. 64 | 65 | ``` 66 | { "progress": 30 } 67 | { "progress": 100 } 68 | { "progress": 403 } 69 | ``` 70 | 71 | ### Delete a dat 72 | 73 | DELETE `/dats` 74 | 75 | with json body: 76 | 77 | ``` 78 | {"key": } 79 | ``` 80 | 81 | Delete a dat, removing all data. They key should be the 64-character string without the `dat://` prefix. 82 | -------------------------------------------------------------------------------- /request.js: -------------------------------------------------------------------------------- 1 | var xhr = require('xhr') 2 | 3 | module.exports = function (opts, cb) { 4 | xhr(opts, function (err, resp, json) { 5 | if (err) return cb(err, resp, json) 6 | if (resp.statusCode === 500) return cb(new Error(json.message), resp, json) 7 | return cb(null, resp, json) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | var parallel = require('run-parallel') 2 | var express = require('express') 3 | var resolve = require('dat-link-resolve') 4 | var bodyParser = require('body-parser') 5 | var fs = require('fs') 6 | var path = require('path') 7 | var encoding = require('dat-encoding') 8 | var archiver = require('hyperdrive-archiver') 9 | var url = require('url') 10 | var toZipStream = require('hyperdrive-to-zip-stream') 11 | 12 | var index = require('./index.js') 13 | 14 | module.exports = createRouter 15 | 16 | function createRouter (config) { 17 | var router = express() 18 | router.use(bodyParser.json()) 19 | var ar = archiver(config) 20 | ar.swarm.on('listening', function () { 21 | console.log('listening') 22 | }) 23 | 24 | function onerror (res, err) { 25 | res.statusCode = err.statusCode || 500 26 | res.end(JSON.stringify({error: true, message: err.message, statusCode: res.statusCode})) 27 | } 28 | 29 | router.get('/', function (req, res) { 30 | res.end(index(config)) 31 | }) 32 | 33 | router.get('/download/:key', function (req, res) { 34 | ar.get(req.params.key, function (err, archive) { 35 | res.attachment(req.params.key) 36 | toZipStream(archive).pipe(res) 37 | }) 38 | }) 39 | 40 | router.get('/health/:key', function (req, res) { 41 | ar.get(req.params.key, function (err, archive) { 42 | res.send(ar.health(archive)) 43 | }) 44 | }) 45 | 46 | router.get('/dats', function (req, res) { 47 | ar.list(function (err, keys) { 48 | if (err) return onerror(res, err) 49 | var dats = [] 50 | var tasks = [] 51 | keys.forEach((key) => tasks.push((done) => { 52 | ar.get(key, function (err, archive) { 53 | if (err) return done(err) 54 | dats.push({ 55 | key: key.toString('hex'), 56 | health: ar.health(archive) 57 | }) 58 | done() 59 | }) 60 | })) 61 | parallel(tasks, function (err) { 62 | if (err) return onerror(res, err) 63 | res.send(dats) 64 | }) 65 | }) 66 | }) 67 | 68 | router.delete('/dats', function (req, res) { 69 | if (!req.body.key) return onerror(res, new Error('key required')) 70 | resolve(req.body.key, function (err, key) { 71 | if (err) return onerror(res, err) 72 | ar.remove(key, function (err) { 73 | if (err) return onerror(res, err) 74 | res.send('ok') 75 | }) 76 | }) 77 | }) 78 | 79 | router.post('/dats', function (req, res) { 80 | if (!req.body.key) return onerror(res, new Error('key required')) 81 | resolve(req.body.key, function (err, key) { 82 | if (err) return onerror(res, err) 83 | key = encoding.encode(key) 84 | ar.add(key, function (err) { 85 | if (err) return onerror(res, err) 86 | res.send('ok') 87 | }) 88 | }) 89 | }) 90 | 91 | return router 92 | } 93 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var st = require('st') 3 | var Router = require('./router.js') 4 | var fs = require('fs') 5 | var path = require('path') 6 | 7 | var config = JSON.parse(fs.readFileSync(path.resolve('config.json')).toString()) 8 | var router = Router(config) 9 | 10 | module.exports = http.createServer(function (req, res) { 11 | if (st({ path: 'static/', url: 'static/' })(req, res)) return 12 | router(req, res) 13 | }) 14 | -------------------------------------------------------------------------------- /static/dat-data-blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okdistribute/dat-server/784357e7d74f011004e4ea0987357a64fe4d5cc9/static/dat-data-blank.png -------------------------------------------------------------------------------- /static/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okdistribute/dat-server/784357e7d74f011004e4ea0987357a64fe4d5cc9/static/example.png -------------------------------------------------------------------------------- /static/loading_icon_small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okdistribute/dat-server/784357e7d74f011004e4ea0987357a64fe4d5cc9/static/loading_icon_small.gif -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | .list-item { 2 | box-shadow: 0px 0px 4px rgba(0,0,0,.3); 3 | background-color: white; 4 | padding: 15px; 5 | margin-bottom: 15px; 6 | } 7 | 8 | nav { 9 | background-color: #eee; 10 | box-shadow: none; 11 | } 12 | 13 | .list-item a { 14 | margin-right: 15px; 15 | } 16 | 17 | body { 18 | background-color: #eee; 19 | } 20 | 21 | main { 22 | margin-top: 40px; 23 | } 24 | 25 | nav ul a { 26 | color: #2a3749; 27 | } 28 | 29 | .centered { 30 | text-align: center; 31 | } 32 | 33 | #status { 34 | position: fixed; 35 | top: 0; 36 | width: 100%; 37 | text-align: center; 38 | padding: 10px; 39 | margin: auto; 40 | display: none; 41 | background-color: white; 42 | box-shadow: 1px 1px 2px rgba(0,0,0,.5); 43 | } 44 | --------------------------------------------------------------------------------