├── .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 |
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 | 
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 |
--------------------------------------------------------------------------------